gensimのword2vecのmost_similarと平均ベクトルを作ったmost_similar_by_vectorの結果の違いを正す

書いてる理由

  • word2vec実践中
  • most_similarと平均ベクトルを作ってmost_similar_by_vectorした時の結果が違うので調べた

参考

無し

概要

gensimのword2vecでmodel.most_similarでの計算結果と、自分で平均ベクトルを作って計算したmost_similar_by_vectorの結果が違う。
この差が何かを調べた。
結論は、most_similarはl2_normを内部で使ってアイテム(単語)ごとのベクトルを単位ベクトル化しているためで、word2vecのベクトルにl2_normをかけると同じ結果が得られる。

詳細

1. most_similarを試す。

# word2vecのモデルをロードする。
model_file = '/path/to/model_file'
model = self.load_model(model_file)

# id1とid2を使って近いアイテムを探す。
id1 = 'hoo'
id2 = 'bar'
result = model.most_similar(positive=[id1, id2], topn=5)
print(result)
> [('id_X1', 0.958929181098938), ('id_X2', 0.9585146903991699), ('id_X3', 0.9560137987136841), ('id_X4', 0.9551813006401062), ('id_X5', 0.9540027379989624)]

モデルをロードして、id1とid2で近いidを計算。

2. 平均ベクトルを作成してmost_similar_by_vectorで結果を取得する。

# id1とid2のベクトルを取得
vector1 = w2v.get_vector(id1)
vector2 = w2v.get_vector(id2)

# 平均ベクトルを作成する
average_vector = (vector1 + vector2) / 2

# 平均ベクトルで近いアイテムを探す。
result = model.most_similar_by_vector(average_vector, 5)
print(result)
> [('id_Y1', 0.9540631771087646), ('id_Y2', 0.9499505758285522), ('id_Y3', 0.9497346878051758), ('id_Y4', 0.9431592226028442), ('id_Y5', 0.942722737789154]

IDはID_X1とかの仮のIDにしているので具体的に結果の比較が出来ないが、1のmost_similarの結果と平均ベクトルを作成してからmost_similar_by_vectorをした結果が異なっている。

3. L2ノームを入れて初期化して比較する

# word2vecのモデルのベクトルを、L2ノームで正則化して置き換える。
normalized = np.sqrt((model.wv.vectors ** 2).sum(-1))[..., np.newaxis]
model.wv.vectors = model.wv.vectors / normalized

# 再度most_similarとmost_sililar_by_vectorで結果を比較
result = model.most_similar(positive=[id1, id2], topn=5)
print(result)
> [('id_Z1', 0.958929181098938), ('id_Z2', 0.9585146903991699), ('id_Z3', 0.9560137987136841), ('id_Z4', 0.9551813006401062), ('id_Z5', 0.9540027379989624)]
result = model.most_similar_by_vector(average_vector, 5)
print(result)
> [('id_Z1', 0.958929181098938), ('id_Z2', 0.9585146903991699), ('id_Z13, 0.9560137987136841), ('id_Z4', 0.9551813006401062), ('id_Z5', 0.9540027379989624)]

完全一致した。

補足

ここを見ると、positiveでの平均ベクトル作成時にuse_normが常にセットされていて、これが_l2_normを実施している。

その中での以下の式を最初に当ててあげることで、外で平均ベクトルを作成しても同じベクトルでの検索が可能となった。
以下のコードはL2ノームの箇所で、mがword2vecのベクトル全体でshapeは[全idの数, 表現しているベクトルの次元]。REALはfloat32。各IDごとに全部の要素を二乗してsumをとり、それをそれぞれに割る形。

dist = sqrt((m ** 2).sum(-1))[..., newaxis]
    if replace:
        m /= dist
        return m
    else:
        return (m / dist).astype(REAL)

いっじょ!