Word2Vecで特定のwordを除外してmost_similarする方法

Word2Vecで特定のwordを除外してmost_similarする方法


レコメンドタスクでgensimのWord2Vecを利用する場合、特定のword(アイテム)を除外した上で類似度上位N件をmost_similarで出力したいことがあると思います。
しかし、現時点のgensimのWord2Vecではmost_similarに特定のwordの除外機能がないので以下の対応が必要です。

  1. topNを増やしてmost_similar後に特定wordを除外する
  2. 学習データで特定wordを除外しておく

1.だと除外対象のwordの出現頻度が高い場合、topNをどの程度増やせば良いのかが職人技になったり、処理が遅くなったりします。
また、2.だと学習時点で意図的に学習データから除外することで本来のデータ表現を損なうので、単語埋め込みの精度が悪化する可能性があります。

このように、どちらの方法もスマートではないので、やはりmost_similarに除外対象のwordを設定できるのが望ましいです。
今回はこのカスタマイズしたmost_similarを実装し、除外対象のwordを除外したうえでmost_similarする方法を紹介します。

実装のアイデア

学習済みモデルのwordの正規化ベクトルをもとに入力wordのベクトルとのcosine類似度を計算し、特定のwordを除外したうえでcosine類似度topNを出力します。
cosine類似度計算と特定wordの除外処理について、それぞれ分けて説明します。

cosine類似度計算

学習済みモデルのwordの正規化ベクトルは以下に格納されています。

gensim.model.Word2Vec.wv.vector_norm

\( N \)を学習済みモデル内のword数、\( M \)を正規化ベクトルの次元数とすると、学習済みモデルのwordについては\( N \times M \)行列で\( X = (x_1,x_2,\dots,x_N) \) 、入力wordについては\( M \)次元の \( y \) と表現できます。
このとき、\( x_1 \)と\( y \)のcosine類似度は以下で計算できます。

$$
cosine\,similarity(x_i,y) = \frac{x_i \cdot y}{\|x_i\| \cdot \|y\|}
$$
ここで、\( x_i \)は正規化ベクトルなので\( \|x_i\| = 1 \)となります。
よってcosine類似度の計算は以下と書き換えることができます。
$$
cosine\,similarity(x_i,y) = \frac{x_i \cdot y}{\|y\|}
$$
さらに、cosine類似度の計算は要素ごとではなく、行列ベクトル積を使った方が処理が高速になります。
したがって、最終的には以下のように行列ベクトル積を使って\( M \)次元のcosine類似度ベクトル\( S \)を算出します。
$$
S = \frac{X \cdot y}{\|y\|}
$$

特定wordの除外処理

numpyを使って処理を高速化するため、most_similarではwordのindexをもとに処理が行われます。
内部ではwordとindexをマッピングするlistが使われていて、そのマッピングは以下に格納されています。

gensim.model.Word2Vec.wv.index2word

したがって、除外対象のwordと入力wordを次の2つから除外した上で、類似度topNを出力すれば良いことになります。

  • 類似度ベクトル\( S \)
  • wordとindexをマッピングするlist

実装

まず、テキトーに学習データを作成し、gensimのWord2Vecで学習します。
ちなみに、gensimのバージョンは3.6.0です。
※今回はカスタマイズしたmost_similarの実装に焦点を当てているので、前処理はかなり手を抜いています。悪しからず。
INPUT:

from gensim.models import Word2Vec
import numpy as np

# テキトーなテキスト文
text = '''
If you are missing much of your data, this can cause several problems. The most apparent problem is that there simply won't be enough data points to run your analyses. The EFA, CFA, and path models require a certain number of data points in order to compute estimates. This number increases with the complexity of your model. If you are missing several values in your data, the analysis just won't run.
Additionally, missing data might represent bias issues. Some people may not have answered particular questions in your survey because of some common issue. For example, if you asked about gender, and females are less likely to report their gender than males, then you will have male-biased data. Perhaps only 50% of the females reported their gender, but 95% of the males reported gender. If you use gender in your causal models, then you will be heavily biased toward males, because you will not end up using the unreported responses.
To find out how many missing values each variable has, in SPSS go to Analyze, then Descriptive Statistics, then Frequencies. Enter the variables in the variables list. Then click OK. The table in the output will show the number of missing values for each variable.
'''

# 文単位の単語リスト(2次元リスト)を作成
train = [line.split(' ') for line in text.split('\n') if line != '']
# 全ての単語が学習済みモデルに含まれるようにする
model = Word2Vec(train, min_count=1)

いったん”missing”、”for”の合成ベクトルに対して類似度topNを出力してみます。
INPUT:

model.wv.most_similar(positive=['missing', 'for'])

OUTPUT:

/usr/local/lib/python3.6/dist-packages/gensim/matutils.py:737: FutureWarning: Conversion of the second argument of issubdtype from `int` to `np.signedinteger` is deprecated. In future, it will be treated as `np.int64 == np.dtype(int).type`.
  if np.issubdtype(vec.dtype, np.int):
[('simply', 0.2573418617248535),
 ('some', 0.2455665022134781),
 ('answered', 0.2347792088985443),
 ('problem', 0.22516906261444092),
 ('if', 0.19684883952140808),
 ('reported', 0.17115947604179382),
 ('The', 0.15902499854564667),
 ('Enter', 0.15506282448768616),
 ('using', 0.15042588114738464),
 ('data', 0.1450893133878708)]

ここで、何らかの事情で”some”、”if”、”reported”を除外してmost_similarのtopNを出力したいとします。
では上記のアイデアをもと関数を実装していきます。

def get_word2index_dict(model):
    """
    wordからindexを取得できる辞書を作成する
    """
    word2index = dict()
    for i in range(len(model.wv.vocab)):
        word2index[model.wv.index2word[i]] = i
    return word2index


def customized_most_similar(model, positive: list, remove_index: list, word2index: dict, topN: int):
    """
    特定のwordを除外してmost_similarを出力する

    Parameters
    ----------
    model : gensim.model.Word2Vec
        word2vec学習済みモデル。
    positive : list
        加算対象のwordのlist。
    remove_index : list
        除外対象のwordのindexのlist。
    word2index : dict
        wordがkey、indexがvalueのdict。
    topN : int
        出力対象のword数。

    Returns
    -------
    result : list
        類似度topNの(word, sim)のtupleのlist。
    """
    
    model.wv.init_sims()
    # 加算対象のwordのベクトルを取得し、合成する
    pos = [model.wv.vectors_norm[word2index[i]] for i in positive]
    pos_sum = np.sum(np.array(pos), axis=0)
    # cosine類似度ベクトルを作成
    sim = np.dot(model.wv.vectors_norm, pos_sum)/np.linalg.norm(pos_sum)
    # 加算対象のwordを除外wordに追加
    positive_index = [word2index[i] for i in positive]
    remove_index = remove_index + positive_index
    # cosine類似度ベクトルから除外対象のwordを除外
    sim = np.delete(sim, remove_index, 0)
    # index2wordからも除外対象のwordを除外
    index2word = np.delete(np.array(model.wv.index2word), remove_index, 0)
    # 類似度topNを取得
    topn = np.argsort(sim)[::-1][:topN]

    return [(index2word[i], sim[i]) for i in topn]

実装した関数をもとにカスタマイズしたmost_similarを実行してみます。
INPUT:

# word2index作成
word2index = get_word2index_dict(model)
# remove_indexを作成
remove_words = ['some', 'if', 'reported']
remove_index = [word2index[i] for i in remove_words]

customized_most_similar(
    model=model,
    positive=['missing', 'for'],
    remove_index=remove_index,
    word2index=word2index,
    topN=10
)

OUTPUT:

[('simply', 0.25734186),
 ('answered', 0.23477921),
 ('problem', 0.22516908),
 ('The', 0.15902501),
 ('Enter', 0.15506281),
 ('using', 0.15042587),
 ('data', 0.1450893),
 ('Then', 0.14496422),
 ('gender', 0.14487101),
 ('out', 0.14111653)]

意図した通り、対象のwordが除外されてtopNが出力されていることを確認できました。
また、本家のmost_similarとcosine類似度が少し異なりますがこれは桁落ちが原因と思われます。
おそらく、合成ベクトルや内積の計算で数値が不安定になっているのでしょう。
とはいえ、類似度の順位は意図した通りなのでこの桁落ちはそこまで影響はないと考えています。

最後に

今回紹介したように、cosine類似度の計算方法だったり、行列ベクトル積への変換ができればmost_similarは簡単に実装できます。
とはいえ、gensimのバージョンアップで特定wordの除外を考慮できるmost_similarが実装されるのを期待したいところです。