自然言語処理で「王様 – 男性 + 女性 = 女王」という有名な類推が成立するのを知っていますか。単語を適切なベクトル空間に埋め込むと、意味的な関係がベクトルの演算として表現されるのです。これは非常に驚くべきことです。言語という記号的・離散的な体系が、連続的なベクトル空間で幾何学的に表現できるのですから。
コンピュータは本質的に数値しか処理できないため、単語を何らかの数値表現に変換する必要があります。最も素朴な方法はone-hotエンコーディングですが、語彙が3万語なら3万次元のスパースベクトルになり、全ての単語間の距離が等しい(意味の近さが表現できない)という致命的な問題があります。「犬」のone-hotベクトルと「猫」のone-hotベクトルのコサイン類似度は0であり、「犬」と「微分」と同じ距離になってしまいます。
単語埋め込み(word embedding)は、各単語を数百次元の密なベクトルで表現する手法です。意味が近い単語のベクトルは近くに配置され、意味の関係がベクトル空間の幾何学的構造として捉えられます。
たとえるなら、one-hotエンコーディングは「住所録」のようなものです。各住所(単語)には一意の番号が振られていますが、番号の大小は距離と無関係です。一方、単語埋め込みは「地図」のようなものです。近い場所(意味的に近い単語)は地図上でも近くに配置されます。
単語埋め込みを理解すると、以下のことが可能になります。
- NLPモデルの入力設計: Transformerの埋め込み層の仕組みの理解
- 意味的類似度の計算: 単語やフレーズの類似度を定量的に測定
- 転移学習の基盤: 事前学習された埋め込みの活用
- 多言語NLP: 異なる言語の埋め込みを同一空間に配置
単語埋め込みはNLPの歴史において最も重要なブレークスルーの1つであり、現在のBERTやGPTの埋め込み層にもその基本的な思想が受け継がれています。
本記事の内容
- 分散仮説と分散表現
- Word2Vec(Skip-gram, CBOW)の理論
- GloVeの理論 — 共起行列の因子分解
- FastText — サブワード情報の活用
- Pythonでの実装と可視化
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
- トークナイゼーションの理論 — テキストの分割手法
- ニューラルネットワークの基礎 — MLPの構造
分散仮説
「ある単語の意味は、その周囲の単語から決まる」
単語埋め込みの理論的基盤は、言語学者J.R. Firth(1957)の有名な言葉「You shall know a word by the company it keeps(単語はその仲間によって知られる)」に集約されます。これを分散仮説(distributional hypothesis)と呼びます。
たとえば「犬」と「猫」は、「かわいい」「飼う」「ペット」「散歩」といった似た文脈に出現します。逆に「犬」と「微分」は共通の文脈がほとんどありません。この文脈の類似性を捉えれば、単語の意味の近さを計算できるのです。
分散仮説は非常にシンプルなアイデアですが、驚くほど強力です。実際に大量のテキストで統計を取ると、「犬」と「猫」は似た文脈パターンを示し、「犬」と「微分」は全く異なるパターンを示します。この統計的なパターンを低次元のベクトルに圧縮したものが、まさに単語埋め込みなのです。
one-hotから密なベクトルへ
one-hotベクトルの問題は、全ての単語ペアの内積が0(直交)であり、類似度が全く計算できないことです。数式で表すと、語彙サイズ $V$ のone-hotベクトル $\bm{e}_i, \bm{e}_j \in \mathbb{R}^V$ に対して、$i \neq j$ のとき常に $\bm{e}_i^\top \bm{e}_j = 0$ です。
密な低次元ベクトル(数百次元)に埋め込むことで、内積やコサイン類似度で意味の近さを測定できるようになります。コサイン類似度は次のように定義されます。
$$ \text{cos}(\bm{u}, \bm{v}) = \frac{\bm{u}^\top \bm{v}}{\|\bm{u}\| \cdot \|\bm{v}\|} $$
この値は $-1$ から $1$ の間をとり、意味が近い単語ほど1に近くなります。では、このような「意味を反映した」密なベクトルをどのように学習すればよいのでしょうか。次に紹介するWord2Vecがその答えを提示しました。
Word2Vec
Skip-gramモデル
Word2Vec(Mikolov et al., 2013)には2つのモデルがあります。まずSkip-gramモデルを解説します。
Skip-gramの基本的なアイデアは、「ある単語が与えられたとき、その近くに出現する単語を予測できるようなベクトル表現を学習する」というものです。たとえば「The cat sat on the mat」という文で「sat」が中心語なら、「cat」「on」などの周囲の単語を予測するよう学習します。
入力: 中心語 $w_t$ 出力: 文脈語 $w_{t-c}, \ldots, w_{t-1}, w_{t+1}, \ldots, w_{t+c}$($c$ はウィンドウサイズ)
ウィンドウサイズ $c$ は中心語からどこまでの範囲を「文脈」とみなすかを決めるハイパーパラメータです。$c = 2$ なら中心語の前後2単語が文脈となります。一般的に $c$ が小さいと構文的な関係(品詞の類似性)を、$c$ が大きいと意味的な関係(トピックの類似性)を捉えやすくなります。
目的関数は、全ての中心語・文脈語ペアに対する対数尤度の和を最大化するものです。
$$ \begin{equation} J = -\frac{1}{T}\sum_{t=1}^{T}\sum_{\substack{-c \leq j \leq c \\ j \neq 0}} \log P(w_{t+j} | w_t) \end{equation} $$
ここで $T$ はコーパス中の全トークン数です。条件付き確率はSoftmaxで定義されます。
$$ P(w_O | w_I) = \frac{\exp(\bm{v}_{w_O}^{\prime\top} \bm{v}_{w_I})}{\sum_{w=1}^{V}\exp(\bm{v}_w^{\prime\top} \bm{v}_{w_I})} $$
$\bm{v}_w \in \mathbb{R}^d$ は単語 $w$ の入力埋め込み(中心語として使うとき)、$\bm{v}’_w \in \mathbb{R}^d$ は出力埋め込み(文脈語として使うとき)です。分子の $\bm{v}_{w_O}^{\prime\top} \bm{v}_{w_I}$ は中心語と文脈語の「相性」を表す内積で、この値が大きいほど「この2単語は近くに出現しやすい」ことを意味します。
1つの単語に2つの埋め込み(入力・出力)がある点は直感に反するかもしれません。学習後は通常、入力埋め込み $\bm{v}_w$ のみを使うか、$(\bm{v}_w + \bm{v}’_w) / 2$ として両方の平均を使います。
Negative Sampling
Softmaxの分母は語彙全体の和($V$ は数万〜数十万)であり、1回のパラメータ更新で $V$ 回の内積計算が必要になるため、計算コストが高すぎます。Negative Samplingはこの問題を解決する近似手法です。
核となるアイデアは、「全単語に対して正規化する代わりに、正例1つとランダムに選んだ少数の負例だけで学習する」というものです。
正しいペア $(w_I, w_O)$ に対して「正例」の確率を高め、ランダムにサンプルした $k$ 個の「負例」$(w_I, w_\text{neg})$ の確率を低めます。
$$ \mathcal{L} = -\log\sigma(\bm{v}_{w_O}^{\prime\top}\bm{v}_{w_I}) – \sum_{i=1}^{k} E_{w_i \sim P_n(w)}[\log\sigma(-\bm{v}_{w_i}^{\prime\top}\bm{v}_{w_I})] $$
$\sigma(x) = 1/(1+e^{-x})$ はシグモイド関数、$P_n(w)$ はノイズ分布(通常、単語頻度の3/4乗に比例: $P_n(w) \propto f(w)^{3/4}$)です。$k$ は典型的に5〜20です。
なぜ3/4乗なのかという疑問が自然に浮かびます。3/4乗をかけると、高頻度語の確率は相対的に下がり、低頻度語の確率は相対的に上がります。つまり、低頻度語も負例としてある程度サンプルされるようになり、学習のバランスが改善されるのです。
Negative Samplingにより、1回の更新で必要な計算量が $O(V)$ から $O(k)$ に削減され、大規模コーパスでの学習が実用的になりました。
Skip-gramとは逆方向の予測を行うモデルも存在します。それがCBOWです。
CBOWモデル
CBOW(Continuous Bag of Words)はSkip-gramの逆で、文脈語から中心語を予測します。名前の「Bag of Words」が示すように、文脈語の順序は考慮されず、ベクトルの平均が入力として使われます。
たとえば「The cat sat on the mat」で「sat」を予測する場合、CBOWは「The」「cat」「on」「the」の埋め込みの平均ベクトルを入力として「sat」を予測します。
文脈語の埋め込みの平均から中心語を予測する式は次のとおりです。
$$ P(w_t | w_{t-c}, \ldots, w_{t+c}) = \frac{\exp(\bm{v}_{w_t}^{\prime\top} \bar{\bm{v}})}{\sum_{w=1}^{V}\exp(\bm{v}_w^{\prime\top} \bar{\bm{v}})}, \quad \bar{\bm{v}} = \frac{1}{2c}\sum_{\substack{-c \leq j \leq c \\ j \neq 0}} \bm{v}_{w_{t+j}} $$
CBOWはSkip-gramより学習が速いですが、Skip-gramの方が稀な単語に強いとされています。その理由は、Skip-gramでは各中心語に対して複数の文脈語を独立に予測するため、稀な単語でもそれが中心語として現れるたびにパラメータが更新されるからです。一方、CBOWは複数の文脈語を平均してから1回の予測を行うため、個々の単語の影響が薄まります。
Word2Vecはローカルな文脈ウィンドウを使って学習しますが、コーパス全体の統計情報を直接活用する手法も考えられます。それがGloVeのアプローチです。
GloVe
共起行列の因子分解
GloVe(Pennington et al., 2014)は、Word2Vecのローカルな文脈ウィンドウベースのアプローチと、LSA(潜在意味分析)のグローバルな共起統計ベースのアプローチを統合した手法です。名前は Global Vectors の略で、「グローバルな統計からベクトルを生成する」という思想を表しています。
Word2Vecは文脈ウィンドウをスライドさせながらオンラインで学習するため、コーパス全体の統計を直接利用しているわけではありません。一方、LSA(潜在意味分析)は共起行列の特異値分解(SVD)でグローバルな統計を捉えますが、単語間の意味的な関係を十分に反映できないことがあります。GloVeは両方の長所を取り入れた手法です。
まず、コーパス全体の共起行列 $\bm{X}$ を構築します。$X_{ij}$ は単語 $i$ の文脈ウィンドウ内に単語 $j$ が出現した回数です。たとえば「I like cats. I like dogs.」というコーパスでは、$X_{\text{I}, \text{like}} = 2$ となります。
GloVeの目的関数は次のように定義されます。
$$ \begin{equation} J = \sum_{i,j=1}^{V} f(X_{ij})\left(\bm{w}_i^\top \tilde{\bm{w}}_j + b_i + \tilde{b}_j – \log X_{ij}\right)^2 \end{equation} $$
ここで $\bm{w}_i$ と $\tilde{\bm{w}}_j$ はそれぞれ単語 $i$ と $j$ のベクトル、$b_i$ と $\tilde{b}_j$ はバイアス項です。$f(x)$ は重み関数で、高頻度の共起に過度な影響力を与えないように設計されています。
$$ f(x) = \begin{cases} (x/x_\text{max})^\alpha & (x < x_\text{max}) \\ 1 & (x \geq x_\text{max}) \end{cases} $$
通常 $x_\text{max} = 100$, $\alpha = 3/4$ が使われます。$X_{ij} = 0$ の場合は $f(0) = 0$ となるため、共起しない単語ペアは損失に寄与しません。
GloVeの直感的な理解
GloVeの目的関数を直感的に理解しましょう。$\bm{w}_i^\top \tilde{\bm{w}}_j + b_i + \tilde{b}_j \approx \log X_{ij}$ という関係を学習することは、「2つの単語ベクトルの内積が、その共起頻度の対数に近くなる」ようにベクトルを配置することを意味します。共起頻度が高い単語ペアほど内積が大きくなる、つまりベクトルが近い方向を向くのです。
重み関数 $f(x)$ は、「the」と「of」のような超高頻度の共起に引きずられないためのダンパーとして機能します。$x_{\text{max}} = 100$ でキャップすることで、共起回数100回以上は全て同じ重みで扱われます。
GloVeの利点
GloVeはコーパス全体の統計情報を活用するため、大規模コーパスでの性能がWord2Vecと同等以上です。また、損失関数が明示的であるため、最適化の収束が予測しやすいという利点があります。さらに、共起行列は非ゼロ要素のみを保持すればよいため、メモリ効率も優れています。
実際の研究では、Stanfordが公開しているGloVeの事前学習済みベクトル(Wikipedia + Gigawordで学習、300次元)が広く使われています。
Word2VecとGloVeはともに完全な単語を基本単位としていますが、未知語や形態論的な関係を捉えるには限界があります。この問題を解決するのがFastTextです。
FastText
サブワード情報の活用
FastText(Bojanowski et al., 2017)はWord2Vecを拡張し、単語をn-gram(文字のn連続列)の集合として表現します。Word2VecやGloVeの大きな弱点は、訓練コーパスに含まれない単語(OOV: Out-of-Vocabulary)に対して埋め込みを生成できないことです。FastTextはこの問題を、単語を文字n-gramに分解するという巧みな方法で解決しました。
単語 $w$ のn-gramの集合を $G_w$ とすると、単語の埋め込みは各n-gramの埋め込みの和で表されます。
$$ \bm{v}_w = \sum_{g \in G_w} \bm{z}_g $$
ここで $\bm{z}_g$ は文字n-gram $g$ の埋め込みベクトルです。たとえば、「where」(n=3の場合): <wh, whe, her, ere, re>, <where>
< と > は単語の境界を示す特殊記号です。また <where> は単語全体をn-gramとして含めたものです。このように単語全体のn-gramも加えることで、頻出語は事実上Word2Vecと同じように学習されます。
FastTextの利点
OOV対応: 訓練時に見なかった単語でも、n-gramの組み合わせで埋め込みを生成できます。たとえば「chatbot」という単語が訓練コーパスになくても、「cha」「hat」「bot」などのn-gramが学習されていれば、それらの和として合理的な埋め込みを構成できます
形態論的な関係: 「play」「playing」「played」は「pla」「lay」などの共通n-gramを持つため、自然に近いベクトルが得られます。語幹を共有する活用変化形が似た埋め込みを持つことが保証されます
低頻度語: 稀な単語でも、そのn-gramが他の単語で学習されていれば良い表現が得られます。Word2Vecでは出現回数が少ない単語の埋め込みは信頼性が低くなりますが、FastTextではn-gramを共有することで間接的に多くの学習信号を受け取れます
FastTextは特にトルコ語やフィンランド語のような膠着語(接辞を多用する言語)で大きな効果を発揮します。これらの言語では1つの語根から数百の変化形が生まれるため、単語単位の埋め込みでは語彙が爆発しますが、FastTextならn-gramの共有により効率よく表現できます。
ここまで静的な単語埋め込みの主要手法を見てきました。Pythonで実際にSkip-gramを実装し、埋め込みの学習過程を可視化してみましょう。
Pythonでの実装
簡易Skip-gramの実装
以下のコードでは、小規模なコーパスを使ってSkip-gram with Negative Samplingをスクラッチで実装します。学習過程の損失曲線と、学習された埋め込みの2D可視化(PCA)、およびコサイン類似度の計算を行います。
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
# --- 簡易Skip-gram (Negative Sampling) ---
# 訓練コーパス
corpus = [
"the cat sat on the mat",
"the dog sat on the log",
"the cat chased the dog",
"the dog chased the cat",
"the bird sat on the tree",
"the cat climbed the tree",
]
# 語彙の構築
words = set()
for sent in corpus:
words.update(sent.split())
word2idx = {w: i for i, w in enumerate(sorted(words))}
idx2word = {i: w for w, i in word2idx.items()}
V = len(word2idx)
print(f"語彙サイズ: {V}")
print(f"語彙: {sorted(word2idx.keys())}")
# 訓練データの生成(Skip-gram pairs)
window_size = 2
training_pairs = []
for sent in corpus:
tokens = sent.split()
for i, center in enumerate(tokens):
for j in range(max(0, i-window_size), min(len(tokens), i+window_size+1)):
if i != j:
training_pairs.append((word2idx[center], word2idx[tokens[j]]))
print(f"訓練ペア数: {len(training_pairs)}")
# 埋め込みの初期化
d = 10 # 埋め込み次元
W_in = np.random.randn(V, d) * 0.1 # 入力埋め込み
W_out = np.random.randn(V, d) * 0.1 # 出力埋め込み
def sigmoid(x):
return 1 / (1 + np.exp(-np.clip(x, -10, 10)))
# 学習(Negative Sampling)
lr = 0.1
n_neg = 5
n_epochs = 300
# 単語頻度(Negative Samplingの分布)
word_freq = np.zeros(V)
for sent in corpus:
for w in sent.split():
word_freq[word2idx[w]] += 1
noise_dist = word_freq ** 0.75
noise_dist /= noise_dist.sum()
losses = []
for epoch in range(n_epochs):
np.random.shuffle(training_pairs)
epoch_loss = 0
for center, context in training_pairs:
# 正例
score_pos = W_out[context] @ W_in[center]
loss_pos = -np.log(sigmoid(score_pos) + 1e-8)
grad_pos = (sigmoid(score_pos) - 1)
W_in[center] -= lr * grad_pos * W_out[context]
W_out[context] -= lr * grad_pos * W_in[center]
# 負例
neg_samples = np.random.choice(V, n_neg, p=noise_dist)
for neg in neg_samples:
score_neg = W_out[neg] @ W_in[center]
loss_neg = -np.log(sigmoid(-score_neg) + 1e-8)
grad_neg = sigmoid(score_neg)
W_in[center] -= lr * grad_neg * W_out[neg]
W_out[neg] -= lr * grad_neg * W_in[center]
epoch_loss += loss_neg
epoch_loss += loss_pos
losses.append(epoch_loss / len(training_pairs))
# --- 可視化 ---
fig, axes = plt.subplots(1, 2, figsize=(14, 5.5))
# (a) 損失の推移
ax = axes[0]
ax.plot(losses, linewidth=2, color="steelblue")
ax.set_xlabel("Epoch", fontsize=12)
ax.set_ylabel("Loss", fontsize=12)
ax.set_title("Skip-gram Training Loss", fontsize=13)
ax.grid(True, alpha=0.3)
# (b) 単語埋め込みの2D可視化(PCA)
ax = axes[1]
embeddings = W_in # 入力埋め込みを使用
# PCAで2次元に削減
mean = embeddings.mean(axis=0)
centered = embeddings - mean
cov = centered.T @ centered / V
eigenvalues, eigenvectors = np.linalg.eigh(cov)
idx_sorted = np.argsort(eigenvalues)[::-1]
pc = eigenvectors[:, idx_sorted[:2]]
projected = centered @ pc
ax.scatter(projected[:, 0], projected[:, 1], s=100, c="steelblue",
edgecolors="black", linewidth=0.5, zorder=5)
for i, word in idx2word.items():
ax.annotate(word, (projected[i, 0], projected[i, 1]),
fontsize=11, ha="center", va="bottom",
xytext=(0, 5), textcoords="offset points")
ax.set_xlabel("PC1", fontsize=12)
ax.set_ylabel("PC2", fontsize=12)
ax.set_title("Word Embeddings (PCA projection)", fontsize=13)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("word_embeddings.png", dpi=150, bbox_inches="tight")
plt.show()
# コサイン類似度の計算
def cosine_sim(w1, w2):
v1 = W_in[word2idx[w1]]
v2 = W_in[word2idx[w2]]
return v1 @ v2 / (np.linalg.norm(v1) * np.linalg.norm(v2))
print("\n=== コサイン類似度 ===")
pairs = [("cat", "dog"), ("cat", "bird"), ("sat", "chased"),
("cat", "tree"), ("the", "on")]
for w1, w2 in pairs:
sim = cosine_sim(w1, w2)
print(f" sim({w1}, {w2}) = {sim:.3f}")
この実験結果から、Skip-gramの学習が正しく機能していることが確認できます。
-
損失曲線(左図): 損失が学習とともに減少し、モデルが文脈の共起パターンを学習していることがわかります
-
埋め込みの2D可視化(右図): PCAで2次元に射影した結果、意味的に関連する単語が近くに配置されています。「cat」と「dog」(動物)、「sat」と「chased」(動詞)がそれぞれ近くにグループ化される傾向が観察できます。ただし、小さなコーパスでの学習であるため、大規模な実験ほど明確なクラスタリングは見られません
-
コサイン類似度: 共通の文脈で出現する単語ペア(cat-dog等)のコサイン類似度が高く、無関係な単語ペアの類似度が低いことが確認できます。「cat」と「dog」はどちらも「sat」「chased」「the」といった共通の文脈語を持つため、類似した埋め込みが学習されます
なお、このスクラッチ実装は教育目的のものであり、実務ではgensimライブラリのWord2Vecや、torchtextの事前学習済みGloVeベクトルを利用するのが一般的です。大規模コーパス(Wikipedia全文など)で学習された事前学習済み埋め込みは、数百万の単語に対して高品質なベクトルを提供します
文脈依存の埋め込み
静的埋め込みの限界
Word2Vec, GloVe, FastTextは静的な埋め込みです。同じ単語は文脈によらず常に同じベクトルになります。しかし、「bank」は「銀行」と「河岸」の2つの意味を持ち、文脈によって意味が異なります。
具体的に見てみましょう。以下の2文を考えます。
- 「I deposited money in the bank」(銀行にお金を預けた)
- 「I sat on the river bank」(川岸に座った)
静的な埋め込みでは、両方の「bank」に同じベクトルが割り当てられます。このベクトルは「銀行」と「河岸」の意味が混ざった中間的な表現になってしまい、どちらの意味も正確には捉えられません。
文脈依存の埋め込み(contextualized embedding)は、文脈に応じて同じ単語に異なるベクトルを割り当てます。ELMo(Peters et al., 2018)が双方向LSTMで最初にこの問題に取り組み、BERT(双方向Transformer)、GPT(単方向Transformer)がさらに高い性能を達成しました。
現代のNLPでは、Word2VecやGloVeは直接的には使われなくなっていますが、Transformerの埋め込み層の初期化や、概念的な基盤として依然として重要です。Transformerの最初の層にある埋め込み行列は、本質的にはWord2Vecと同じく「各トークンを密なベクトルに変換する」ルックアップテーブルであり、Word2Vecで学んだ概念がそのまま適用されます。
まとめ
本記事では、単語埋め込みの理論をWord2VecからFastTextまで解説しました。
- 分散仮説:「単語の意味はその文脈から決まる」が埋め込みの理論的基盤
- Word2Vec (Skip-gram): 中心語から文脈語を予測。Negative Samplingで効率的に学習
- Word2Vec (CBOW): 文脈語から中心語を予測。Skip-gramより高速だが稀語にやや弱い
- GloVe: コーパス全体の共起統計を因子分解。グローバルな統計情報を活用
- FastText: n-gram情報でOOV対応と形態論的関係を捉える
- 現代のNLPでは文脈依存埋め込み(BERT, GPT)が主流だが、静的埋め込みは概念的基盤として重要
単語埋め込みの歴史は「離散的な記号をどうやって連続的なベクトル空間に配置するか」という問題への挑戦でした。Word2Vecが「文脈予測」というシンプルなアイデアでこの問題を解き、GloVeが統計的な洗練を加え、FastTextが形態論を取り込みました。そしてBERTやGPTが「文脈に応じて埋め込みを動的に変化させる」という次のステージに進みました。この発展の流れを押さえておくことで、NLPの全体像がより明確に見えてくるでしょう。
次のステップとして、以下の記事も参考にしてください。
- BERTとGPTの違い — 文脈依存埋め込みの進化
- トークナイゼーションの理論 — 埋め込みの入力となるトークン化
- Self-Attentionの理論と実装 — 文脈依存表現の計算