Word2Vecを使った文章間の類似度算出（簡易版）


文章間の類似度算出にはDoc2Vecなどを使う手もあるんですが、それ用のモデルを一から作ったりしないといけないので、ちょっと面倒。
ある程度の精度を出すだけならWord2Vecのモデルをそのまま使えた方が汎用的で楽かもしれません。

ということで、文章に含まれる単語の特徴ベクトル平均と文章間のコサイン類似度をもとに、文章間の類似度を算出してみました。

## 環境
```
# OS
macOS Sierra

# Python(Anacondaを使用)
Python : Python 3.5.3 :: Anaconda custom (x86_64)
pip : 9.0.1 from /Users/username/anaconda/lib/python3.5/site-packages (python 3.5)
```
python3.6だといろいろとうまく動かなかったので、Anacondaのpython3.5バージョン([Anaconda 4.2.0 for python3](https://repo.continuum.io/archive/Anaconda3-4.2.0-MacOSX-x86_64.pkg))を使用しています。

## 学習済みモデルの取得＆読み込み
コーパスから辞書を生成するには自分のMacBookAirでは時間がかかりすぎたので

[fastTextの学習済みモデルを公開しました](http://qiita.com/Hironsan/items/513b9f93752ecee9e670)

より公開されている学習済みモデルを使用しました。
今回はWikipediaの内容をMeCabのNEologdを使って分かち書きしたテキストをfastTextによって学習させたモデル(model_neologd.vec)を使用します。(次元数:300)


#### 学習済みモデルの読み込み
```python
import gensim
word2vec_model = gensim.models.KeyedVectors.load_word2vec_format('model/model_neologd.vec', binary=False)
```
(ファイルは１GB近くあるので読み込みに数十秒かかります)

このモデルを使うことによって特徴ベクトルを使った単語の意味的計算が行えます。

#### Word2Vecによる言葉の計算（例：女 + 国王 - 男）

```python
import pprint
pprint.pprint(word2vec_model.most_similar(positive=['女', '国王'], negative=['男']))

# => [('王妃', 0.7062159180641174),
# ('王室', 0.6530475616455078),
# ('王族', 0.6122198104858398),
# ('王太子', 0.6098779439926147),
# ('王家', 0.6084121465682983),
# ('王女', 0.6005773544311523),
# ('女王', 0.5964134335517883),
# ('王', 0.593998908996582),
# ('君主', 0.5929002165794373),
# ('王宮', 0.5772185325622559)]
```

#### Word2Vecによる単語間の類似度算出
```python
# 単純な１単語間に類似度を算出したい場合は、model.similarityで計算できる
pprint.pprint(word2vec_model.similarity('国王', '王妃'))
# => 0.74155587641044496
pprint.pprint(word2vec_model.similarity('国王', 'ラーメン'))
# => 0.036460763469822188
```
なんとなくそれっぽい結果になってますね。

## MeCabを読み込み
自然言語を分かち書きに分解するためにMeCabを使用します。
辞書として学習済みモデルの生成でも用いられているmecab-ipadic-neologdを指定し、分かち書きの形式での出力を指定。

```python
import MeCab
mecab = MeCab.Tagger("-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd -Owakati")

mecab.parse("彼は昨日、お腹を壊した")
# => '彼 は 昨日 、 お腹 を 壊し た \n'
```

分かち書きされたテキストはスペース区切りになります。最後に改行が含まれてしまっているので実装時は削除する必要がありそうです。
(ちなみにMeCabはmecab-python3を使用してインストールしました。python3.6系だと正しく動作しないようなのでpython3.5系を使う必要がありました at 2017/5)

## 文章で使用されている単語の特徴ベクトルの平均を算出
今回の方法では文章中で使用されている単語の特徴ベクトル平均を文自体の特徴ベクトルとしてしようするため、そのための関数を定義。

```python
import numpy as np
def avg_feature_vector(sentence, model, num_features):
    words = mecab.parse(sentence).replace(' \n', '').split() # mecabの分かち書きでは最後に改行(\n)が出力されてしまうため、除去
    feature_vec = np.zeros((num_features,), dtype="float32") # 特徴ベクトルの入れ物を初期化
    for word in words:
        feature_vec = np.add(feature_vec, model[word])
    if len(words) > 0:
        feature_vec = np.divide(feature_vec, len(words))
    return feature_vec
```
単語ごとの特徴ベクトルを単に平均してるだけですね。
(学習済みモデルの次元数が300なのでnum_featuresは300を指定)

```python
avg_feature_vector("彼は昨日、お腹を壊した", word2vec_model, 300)
# => array([  6.39975071e-03,  -6.38077855e-02,  -1.41418248e-01,
#       -2.01289997e-01,   1.76049918e-01,   1.99666247e-02,
#             :                 :                 :
#       -7.54096806e-02,  -5.46530560e-02,  -9.14395228e-02,
#       -2.21335635e-01,   3.34903784e-02,   1.81226760e-01], dtype=float32)
```
実行すると300次元の特徴量が出力されるかと思います。

## ２つの文章の類似度を算出
次に上記関数を使って２つの文章間の平均ベクトルのコサイン類似度を算出。

```python
from scipy import spatial
def sentence_similarity(sentence_1, sentence_2):
    # 今回使うWord2Vecのモデルは300次元の特徴ベクトルで生成されているので、num_featuresも300に指定
    num_features=300
    sentence_1_avg_vector = avg_feature_vector(sentence_1, word2vec_model, num_features)
    sentence_2_avg_vector = avg_feature_vector(sentence_2, word2vec_model, num_features)
    # １からベクトル間の距離を引いてあげることで、コサイン類似度を計算
    return 1 - spatial.distance.cosine(sentence_1_avg_vector, sentence_2_avg_vector)
```

この関数を使うことで、簡易的な文章間の類似度算出が行えます。（範囲は０〜１で１に近いほど類似している）

```python
result = sentence_similarity(
    "彼は昨日、激辛ラーメンを食べてお腹を壊した",
    "昨日、僕も激辛の中華料理を食べてお腹を壊した"
)
print(result)
# =>  0.973996032475

result = sentence_similarity(
    "駄目だこいつ…早くなんとかしないと…",
    "厳選した求人情報をお届けします"
)
print(result)
# => 0.608137464334
```
それっぽい数値を算出することができました！

### 手法の問題点①
**長文の場合、類似度が高く出てしまう。**
単語の平均を取って比較しているだけなので、長い文章になると文章間の平均値の差が生まれにくくなってしまい、関係ない文章でも類似性が高くなってしまう。

```python
result = sentence_similarity(
    "それは今とうとうこの話めというのの中が持っませあり。いよいよほかが教育方はけっしてこの邁進ないべきまでを来らてくるますにも誤解つかんだろて、ある程度とは甘んじあっでしょたた。",
    "病気だってまるでかっこう一日はいいものましな。ゴーシュへねずみに考えて来きみ顔がそのドレミファ晩息や次がいのかっこうほどの狸セロをすって行っましこれのちがいはずいぶんしのたろ。"
)
print(result)
# => 0.878950984671
```
できたとしても１０単語の文章同士の比較が限界。

### 手法の問題点②
**未知の単語には対応できない。**
学習済みモデルに登録されていない未知の単語に対しては特徴ベクトルを出せないため、その単語自体を他単語の平均特徴ベクトルで埋めるなどの対応が必要そうです。(ただその場合、未知の単語というのは意味的な特徴を持っていることが多いため、類似度の精度が下がる)

```python
>>> result = sentence_similarity(
...     "リファラル採用が近年流行ってる",
...     "新卒一括採用の時代は終わった"
... )
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
  File "<stdin>", line 5, in sentence_similarity
  File "<stdin>", line 6, in avg_feature_vector
  File "/Users/username/anaconda/lib/python3.5/site-packages/gensim/models/keyedvectors.py", line 574, in __getitem__
    return self.word_vec(words)
  File "/Users/username/anaconda/lib/python3.5/site-packages/gensim/models/keyedvectors.py", line 273, in word_vec
    raise KeyError("word '%s' not in vocabulary" % word)
KeyError: "word 'リファラル' not in vocabulary"
```
この場合、リファラルという単語を見つけきれなくて落ちてますね。

## まとめ
今回の手法は、やり方自体が簡易的なため、使えるケースがかなり限定される手法だと思います。逆に短文にしか対応しなくていいのであれば、本手法でもある程度の精度は出せそうです。
本格的に文章間の類似度を求める際はDoc2Vecのような手法を用い、モデル自体もその用途にあったコーパスを用意して自作するのが正攻法ですかね。。

### 記事で紹介したコード（全部）

```python
import gensim
import MeCab
import numpy as np
from scipy import spatial

word2vec_model = gensim.models.KeyedVectors.load_word2vec_format('model/model_neologd.vec', binary=False)
mecab = MeCab.Tagger("-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd -Owakati")

# 文章で使用されている単語の特徴ベクトルの平均を算出
def avg_feature_vector(sentence, model, num_features):
    words = mecab.parse(sentence).replace(' \n', '').split() # mecabの分かち書きでは最後に改行(\n)が出力されてしまうため、除去
    feature_vec = np.zeros((num_features,), dtype="float32") # 特徴ベクトルの入れ物を初期化
    for word in words:
        feature_vec = np.add(feature_vec, model[word])
    if len(words) > 0:
        feature_vec = np.divide(feature_vec, len(words))
    return feature_vec

# ２つの文章の類似度を算出
def sentence_similarity(sentence_1, sentence_2):
    # 今回使うWord2Vecのモデルは300次元の特徴ベクトルで生成されているので、num_featuresも300に指定
    num_features=300
    sentence_1_avg_vector = avg_feature_vector(sentence_1, word2vec_model, num_features)
    sentence_2_avg_vector = avg_feature_vector(sentence_2, word2vec_model, num_features)
    # １からベクトル間の距離を引いてあげることで、コサイン類似度を計算
    return 1 - spatial.distance.cosine(sentence_1_avg_vector, sentence_2_avg_vector)

result = sentence_similarity(
    "彼は昨日、激辛ラーメンを食べてお腹を壊した",
    "昨日、僕も激辛の中華料理を食べてお腹を壊した"
)
print(result)
# => 0.973996032475
```

## 参考
- [fastTextの学習済みモデルを公開しました](http://qiita.com/Hironsan/items/513b9f93752ecee9e670)
- [Cos類似度とDoc2Vecってどっちが良いの？](http://qiita.com/enta0701/items/87cbe783aeb44ddf41ce)
- [How to calculate the sentence similarity using word2vec model of gensim with python](http://stackoverflow.com/questions/22129943/how-to-calculate-the-sentence-similarity-using-word2vec-model-of-gensim-with-pyt?rq=1)
- [models.word2vec – Deep learning with word2vec](https://radimrehurek.com/gensim/models/word2vec.html)