word2vec

word2vec は単語を意味空間のベクトルに変換するGoogleの古典的なアルゴリズムである。この空間では king - man + woman ≒ queen が成り立つなどと言われている。これを検証してみよう。

まずは上記のページから学習済みのモデル GoogleNews-vectors-negative300.bin.gz をいただいてくる(約1.6GB)。これは約1000億語からなるGoogle Newsデータセットを学習したもので、300万の単語・熟語を300次元の空間に埋め込む。単語・熟語は大文字・小文字を区別し、熟語の場合はスペースを _ で置き換えてある(例: New_York)。

Pythonで扱うには gensim というパッケージを使う。これは今のところPython 3.12までしか対応していないようだ。pip install --upgrade gensim でインストールすると、gensim-4.3.3 が入り、numpy-1.26.4 scipy-1.13.1 にダウングレードされた。

from gensim.models import KeyedVectors

model = KeyedVectors.load_word2vec_format("GoogleNews-vectors-negative300.bin.gz", binary=True)

query = model.most_similar(
    positive=["king", "woman"],
    negative=["man"],
    topn=10
)

query

これで king + woman - man にコサイン類似度の近い10個の単語・フレーズがリストされる:

[('queen', 0.7118192911148071),
 ('monarch', 0.6189674735069275),
 ('princess', 0.5902431011199951),
 ('crown_prince', 0.5499460697174072),
 ('prince', 0.5377321243286133),
 ('kings', 0.5236844420433044),
 ('Queen_Consort', 0.5235945582389832),
 ('queens', 0.5181134343147278),
 ('sultan', 0.5098593235015869),
 ('monarchy', 0.5087411403656006)]

これを見ると、確かに queen が一番近いようだ。でもこれには罠がある。model.most_similar を使わずにやってみよう:

import numpy as np

vec = model.get_vector("king", norm=True) + model.get_vector("woman", norm=True) - model.get_vector("man", norm=True)
vec /= np.linalg.norm(vec)
sims = model.get_normed_vectors() @ vec
idx = np.argsort(-sims)
[(model.index_to_key[i], sims[i]) for i in idx[:10]]
[('king', 0.7992598),
 ('queen', 0.7118192),
 ('monarch', 0.6189675),
 ('princess', 0.5902431),
 ('crown_prince', 0.54994607),
 ('prince', 0.5377321),
 ('kings', 0.5236844),
 ('Queen_Consort', 0.52359456),
 ('queens', 0.51811343),
 ('sultan', 0.5098593)]

つまり king + woman - man とそれぞれの単語との類似度は、king が一番高く(0.799)、queen は次点(0.712)である。

どうやら model.most_similar() は引数に与えた単語を除外してリストするようだ。そのため、queen が一番近いように見えるが、実は king のほうが近い(king - woman + man ≒ king)。

なお、上のコードは model.most_similar() の結果と合わせるためベクトルを長さ1に正規化してから合計している。理屈からすれば合計する前に正規化するのは変なので、正規化しないで同じことをやってみる。

import numpy as np

vec = model["king"] + model["woman"] - model["man"]
vec /= np.linalg.norm(vec)
sims = model.get_normed_vectors() @ vec
idx = np.argsort(-sims)
[(model.index_to_key[i], sims[i]) for i in idx[:10]]
[('king', 0.8449392),
 ('queen', 0.73005164),
 ('monarch', 0.64546597),
 ('princess', 0.6156251),
 ('crown_prince', 0.5818677),
 ('prince', 0.5777117),
 ('kings', 0.5613663),
 ('sultan', 0.53767765),
 ('Queen_Consort', 0.5344247),
 ('queens', 0.5289887)]

結果はほとんど同じで、やはり king のほうが近い。

さらについでに、ユークリッド距離でも調べてみる:

vec = model["king"] + model["woman"] - model["man"]
dist = np.linalg.norm(model.vectors - vec, axis=1)
idx = np.argsort(dist)
[(model.index_to_key[i], dist[i]) for i in idx[:10]]
[('king', 1.727951),
 ('queen', 2.298658),
 ('Prince_Paras', 2.758862),
 ('Princess_Sikhanyiso', 2.7791162),
 ('Queen_Consort', 2.7801094),
 ('very_pampered_McElhatton', 2.7926178),
 ('King_Bhumipol', 2.8105414),
 ('Savory_aromas_wafted', 2.8125243),
 ('monarch_Gyanendra', 2.821101),
 ('monarch', 2.8295388)]

やはり king のほうが近い。

ついでに主成分分析(PCA)で2次元に落としてプロットする。こちらも正規化しないでやってみる。

from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

WORDS = ["man", "woman", "king", "queen"]
vecs = [model[w] for w in WORDS]
X = np.vstack(vecs)
pca = PCA(n_components=2, random_state=42)
X2 = pca.fit_transform(X)

plt.scatter(X2[:, 0], X2[:, 1])
for (x, y), label in zip(X2, WORDS):
    plt.text(x, y, label)
plt.axis("scaled")
man、woman、king、queenをword2vecで埋め込んだベクトルをPCAで2次元にしたもの

コサイン類似度の行列も求めておく:

from sklearn.metrics.pairwise import cosine_similarity
cosine_similarity(X)
array([[1.0000002 , 0.76640147, 0.22942662, 0.166582  ],
       [0.76640147, 1.0000001 , 0.12847976, 0.31618142],
       [0.22942662, 0.12847976, 0.9999999 , 0.65109575],
       [0.166582  , 0.31618142, 0.65109575, 1.0000002 ]], dtype=float32)

man と woman は非常に近いが、king と queen はそれほど近くなく、king に woman - man を加えても queen まで到達せずむしろ king に近いまま、とも解釈できる。

ほかにも model.similarity("king", "queen") が 0.6510956 なのに model.similarity("king", "King") が 0.5158918 だとか model.similarity("king", "KING") が 0.17314966 だとか、言語感覚に合わない例がいくらでも見つかる。あまり意味空間のベクトルという意味を文字通り捉えないほうがいいかもしれない。