単語の分散表現(Word Embedding)とは?理論と実装を解説

単語の分散表現(Word Embedding)とは、単語を固定長の密なベクトルとして表現する手法です。色をRGB値 $(r, g, b)$ のような3次元ベクトルで表現するように、単語も数百次元のベクトル空間に配置することで、単語間の意味的な類似度を計算できるようになります。

分散表現は自然言語処理(NLP)の基盤技術であり、Word2Vec、GloVe、BERTなどの手法が知られています。

本記事の内容

  • 単語表現の問題点(one-hot表現)
  • 共起行列による分散表現
  • Word2Vec(Skip-gram, CBOW)の理論
  • gensimを用いた実装と単語ベクトルの可視化

前提知識

この記事を読む前に、以下の概念を押さえておくと理解が深まります。

  • ニューラルネットワークの基礎
  • softmax関数

単語表現の問題点

One-Hot表現

最も単純な単語表現は one-hot表現です。語彙数 $V$ のとき、各単語を $V$ 次元のベクトルで表現し、該当する位置のみ1、それ以外を0にします。

例: 語彙が {cat, dog, bird} の場合

$$ \text{cat} = [1, 0, 0], \quad \text{dog} = [0, 1, 0], \quad \text{bird} = [0, 0, 1] $$

one-hot表現の問題点:

  • 高次元・スパース: 語彙数が数万〜数十万になると、非常に高次元
  • 意味情報がない: 全ての単語ペアの距離が等しく、「犬」と「猫」の類似性を表現できない

$$ \|\text{cat} – \text{dog}\|_2 = \|\text{cat} – \text{bird}\|_2 = \sqrt{2} $$

共起行列による分散表現

共起行列(Co-occurrence Matrix)は、単語の文脈での共起頻度を記録した行列です。

ウィンドウサイズ $w$ を設定し、各単語ペアがウィンドウ内で何回共起したかを数えます。

$$ C_{ij} = \text{単語 } i \text{ と単語 } j \text{ がウィンドウ内で共起した回数} $$

共起行列に対してSVD(特異値分解)を適用することで、低次元の密なベクトルが得られます。これがカウントベースの分散表現です。

Word2Vec

Word2Vecは2013年にMikolovらによって提案された手法で、ニューラルネットワークを用いて単語ベクトルを学習します。2つのアーキテクチャがあります。

Skip-gram

中心の単語から周囲の文脈語を予測するモデルです。

入力: 中心語 $w_t$、出力: 文脈語 $w_{t+j}$($-c \leq j \leq c$, $j \neq 0$)

目的関数は以下の対数尤度を最大化します。

$$ L = \frac{1}{T}\sum_{t=1}^{T}\sum_{-c \leq j \leq c, j \neq 0} \log p(w_{t+j} | w_t) $$

softmaxによる条件付き確率:

$$ p(w_O | w_I) = \frac{\exp(\bm{v}’_{w_O} \cdot \bm{v}_{w_I})}{\sum_{w=1}^{V} \exp(\bm{v}’_{w} \cdot \bm{v}_{w_I})} $$

ここで $\bm{v}_{w}$ は入力側の単語ベクトル、$\bm{v}’_{w}$ は出力側の単語ベクトルです。

CBOW (Continuous Bag of Words)

文脈語から中心語を予測するモデルです。Skip-gramの逆です。

$$ p(w_t | w_{t-c}, \dots, w_{t+c}) = \frac{\exp(\bm{v}’_{w_t} \cdot \bar{\bm{v}})}{\sum_{w=1}^{V} \exp(\bm{v}’_w \cdot \bar{\bm{v}})} $$

$$ \bar{\bm{v}} = \frac{1}{2c}\sum_{-c \leq j \leq c, j \neq 0} \bm{v}_{w_{t+j}} $$

Negative Sampling

語彙数 $V$ が大きいとsoftmaxの計算が困難です。Negative Samplingは、正例と少数の負例のみで学習する近似手法です。

$$ \log p(w_O | w_I) \approx \log \sigma(\bm{v}’_{w_O} \cdot \bm{v}_{w_I}) + \sum_{k=1}^{K} E_{w_k \sim P_n(w)}[\log \sigma(-\bm{v}’_{w_k} \cdot \bm{v}_{w_I})] $$

ここで $\sigma$ はシグモイド関数、$K$ は負例の数、$P_n(w)$ はノイズ分布です。

gensimを用いた実装

import numpy as np
import matplotlib.pyplot as plt
from gensim.models import Word2Vec
from sklearn.manifold import TSNE

# --- サンプルコーパス ---
sentences = [
    ["king", "queen", "prince", "princess", "castle", "throne", "royal"],
    ["cat", "dog", "bird", "fish", "animal", "pet"],
    ["car", "bus", "train", "plane", "vehicle", "transport"],
    ["python", "java", "code", "program", "software", "developer"],
    ["king", "prince", "royal", "throne", "crown"],
    ["queen", "princess", "royal", "throne", "crown"],
    ["cat", "dog", "pet", "animal", "cute"],
    ["bird", "fish", "animal", "nature", "wild"],
    ["car", "bus", "transport", "road", "drive"],
    ["train", "plane", "transport", "travel", "journey"],
    ["python", "code", "program", "developer", "software"],
    ["java", "code", "program", "developer", "application"],
    ["king", "queen", "castle", "crown", "throne", "royal", "knight"],
    ["cat", "dog", "pet", "animal", "friend", "cute"],
    ["car", "bus", "train", "plane", "transport", "vehicle", "drive"],
    ["python", "java", "code", "program", "software", "developer", "debug"],
]

# 同じ文を繰り返して学習データを増やす
sentences_repeated = sentences * 100

# --- Word2Vecの学習 ---
model = Word2Vec(
    sentences=sentences_repeated,
    vector_size=50,    # 埋め込み次元
    window=3,          # ウィンドウサイズ
    min_count=1,       # 最小出現回数
    sg=1,              # 1=Skip-gram, 0=CBOW
    epochs=100,
    seed=42
)

# --- 単語間の類似度 ---
print("=== 単語間の類似度 ===")
pairs = [("king", "queen"), ("cat", "dog"), ("car", "python"), ("king", "cat")]
for w1, w2 in pairs:
    sim = model.wv.similarity(w1, w2)
    print(f"  similarity({w1}, {w2}) = {sim:.4f}")

# 最も類似する単語
print(f"\n'king'に最も類似する単語: {model.wv.most_similar('king', topn=5)}")
print(f"'cat'に最も類似する単語: {model.wv.most_similar('cat', topn=5)}")

# --- 単語ベクトルの可視化 ---
words = list(model.wv.key_to_index.keys())
vectors = np.array([model.wv[w] for w in words])

# t-SNEで2次元に射影
tsne = TSNE(n_components=2, random_state=42, perplexity=5)
vectors_2d = tsne.fit_transform(vectors)

# カテゴリごとに色分け
categories = {
    'royalty': ['king', 'queen', 'prince', 'princess', 'castle', 'throne', 'royal', 'crown', 'knight'],
    'animals': ['cat', 'dog', 'bird', 'fish', 'animal', 'pet', 'cute'],
    'vehicles': ['car', 'bus', 'train', 'plane', 'vehicle', 'transport', 'drive', 'road', 'travel', 'journey'],
    'programming': ['python', 'java', 'code', 'program', 'software', 'developer', 'debug', 'application'],
}

colors = {'royalty': 'red', 'animals': 'blue', 'vehicles': 'green', 'programming': 'purple'}

fig, ax = plt.subplots(figsize=(10, 8))
for word, vec in zip(words, vectors_2d):
    color = 'gray'
    for cat, cat_words in categories.items():
        if word in cat_words:
            color = colors[cat]
            break
    ax.scatter(vec[0], vec[1], c=color, s=50, alpha=0.7)
    ax.annotate(word, (vec[0], vec[1]), textcoords="offset points",
                xytext=(5, 5), fontsize=9)

# 凡例
for cat, color in colors.items():
    ax.scatter([], [], c=color, label=cat, s=50)
ax.legend(loc='upper right')
ax.set_title('Word Embeddings Visualization (t-SNE)')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

同じカテゴリの単語がベクトル空間上で近くに配置されることが確認できます。意味的に近い単語ほどベクトルの距離が近くなるのが分散表現の特徴です。

まとめ

本記事では、単語の分散表現の理論と実装について解説しました。

  • One-Hot表現は高次元・スパースで意味情報を持たないが、分散表現は密なベクトルで意味的類似度を表現できる
  • Word2VecはSkip-gram(中心語から文脈語を予測)とCBOW(文脈語から中心語を予測)の2つのアーキテクチャがある
  • Negative Samplingにより大規模語彙でも効率的に学習可能
  • 学習された単語ベクトルは意味的に類似した単語が近くに配置される

次のステップとして、以下の記事も参考にしてください。