Pythonで文章の類似度を計算する方法〜TF-IDFとcos類似度〜
Pythonで、2つの文章の類似度計算をしてみます。
理論編は以下をご覧ください。
今回は例題として、以下の3つの文章について、それぞれの類似度を計算してみます。
文章A「私は犬が好きです。」
文章B「私は犬が嫌いです。」
文章C「私は犬のことがとても好きです。」
形態素解析
文章を単語ごとにバラバラにする「形態素解析」を行うパッケージはMeCab、Janome、など幾つかあります。
いずれも素晴らしいパッケージで、大きく違いは無いのですが、Windows環境ではJanomeが最もスムーズに使えるようです。
(Mac,Linuxではどれでも変わりないように思います。)
ここでは、試しにJanomeを使ってみます。
以下、文字列を入力すると、単語ごとに分断されたものがリストになって返ってくる関数です。
janomeパッケージのインストールを忘れずに。
1 2 3 4 5 6 7 8 |
def wakachi(text): from janome.tokenizer import Tokenizer t = Tokenizer() tokens = t.tokenize(text) docs=[] for token in tokens: docs.append(token.surface) return docs |
tokenにはその品詞や読み方も紐付いていますので、token.surfaceとすると単語だけ抽出できます。
文章のベクトル化
単語を分断したら、次はベクトル化する関数を作ります。
これには、sklearn内のTfidfVectorizerというメソッドが使えます。
TfidfVectorizerを使うと、理論紹介の時の記事のように単純に単語が出たか出ないかでベクトル化するのではなく、ひとつの文章の中に何回単語が出たか(TF値)、データ全体の中の単語の出現回数はどれだけか(IDF値)ということも考慮に入れて計算できます。
関数は以下です。
入力は文章のリスト(今回の場合は3つ)、出力は数値ベクトルの配列です。
この関数の中で、上記wakachi関数を呼んでいます。
1 2 3 4 5 6 |
def vecs_array(documents): from sklearn.feature_extraction.text import TfidfVectorizer docs = np.array(documents) vectorizer = TfidfVectorizer(analyzer=wakachi,binary=True,use_idf=False) vecs = vectorizer.fit_transform(docs) return vecs.toarray() |
TfidfVectorizer関数は、初期設定ではTF値もIDF値も考慮に入れる設定になっています。
TF値を考えたくなければ(ある文章に単語が「出た」「出ない」の2値にしたければ)、引数にbinary=Trueを設定します。
IDF値を考えたくなければ(文章全体の単語の頻度を考慮したくなければ)、引数にuse_idf=Falseを設定します。
類似度の計算
さて、文章からベクトルにする関数が出来たので、最後にコサイン類似度を計算しましょう。
これはsklearnのcosine_similarityという、そのまんまの名称の関数があります。
1 2 |
from sklearn.metrics.pairwise import cosine_similarity cs_array = np.round(cosine_similarity(vecs_array(docs), vecs_array(docs)),3) |
docsには文章がリストで格納されている前提です。
また、numpyのroundメソッドで、小数点以下3桁までで四捨五入しています。
これで、「形態素を分断して、ベクトル化して、類似度を計算する」処理ができそうです。
ソース全体
上記をすべて繋げると以下になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# -*- coding: utf-8 -*- import numpy as np #わかち書き関数 def wakachi(text): from janome.tokenizer import Tokenizer t = Tokenizer() tokens = t.tokenize(text) docs=[] for token in tokens: docs.append(token.surface) return docs #文書ベクトル化関数 def vecs_array(documents): from sklearn.feature_extraction.text import TfidfVectorizer docs = np.array(documents) vectorizer = TfidfVectorizer(analyzer=wakachi,binary=True,use_idf=False) vecs = vectorizer.fit_transform(docs) return vecs.toarray() if __name__ == '__main__': from sklearn.metrics.pairwise import cosine_similarity docs = [ "私は犬が好きです。", "私は犬が嫌いです。", "私は犬のことがとても好きです。"] #類似度行列作成 cs_array = np.round(cosine_similarity(vecs_array(docs), vecs_array(docs)),3) print(cs_array) |
上記プログラムを実行すると、
1 2 3 |
[[1. 0.857 0.837] [0.857 1. 0.717] [0.837 0.717 1. ]] |
上記のように出力されます。
つまり、
「私は犬が好きです。」と「私は犬が嫌いです。」の類似度は約0.857、
「私は犬が好きです。」と「私は犬のことがとても好きです。」の類似度は約0.837、
「私は犬が嫌いです。」と「私は犬のことがとても好きです。」の類似度は約0.717、
と計算されました。
対角線は同じ文章同士の比較なので、すべて類似度=1(完全一致)になります。
こうしてみると、最も類似度の高い文章のペアは「私は犬が好きです。」と「私は犬が嫌いです。」であることになります。
意味としては真逆ですが、使われている単語はほぼ一致しているので類似度が高くなってしまうのですね。
確かにこの方法では、その単語の意味までは解釈できないので、致し方ない結果と言えます。
近年はそういった「意味」まで捉えられる・推定できる方法も色々とあるのですが、今回ご紹介した手法がその根底となっています。
意味まで考慮に入れた自然言語処理の理論にご興味があれば、以下の書籍などがおすすめです。