Word2Vecとは?Skip-gramとCBOWをわかりやすく解説

Word2Vecは、2013年にGoogleの Tomas Mikolov らが提案した、単語を固定長の実数ベクトルに変換する手法です。大量のテキストコーパスから「単語の意味」を数百次元のベクトル空間に埋め込むことで、単語間の意味的な類似度を計算したり、有名な “king – man + woman = queen” のようなベクトル演算で意味の関係を捉えたりできます。

Word2Vecは自然言語処理(NLP)の歴史において最も影響力のある手法の一つです。これ以降、GloVe、fastText、ELMo、そしてBERT・GPTへと続く単語表現の進化の出発点となりました。現在のTransformerベースのモデルを理解するうえでも、Word2Vecの考え方は欠かせない基礎です。

本記事の内容

  • 分布仮説とOne-hot表現の限界
  • CBOW(Continuous Bag-of-Words)の目的関数
  • Skip-gramの目的関数
  • softmaxの計算コストとNegative Samplingによる効率化
  • Subsampling of Frequent Words
  • 学習されるベクトルの性質(加法構成性)
  • gensimによるPython実装とt-SNEでの可視化

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

分布仮説 — 単語の意味は周囲の単語で決まる

Word2Vecの理論的背景には分布仮説(distributional hypothesis)があります。これは言語学者 J.R. Firth の有名な言葉に集約されます。

“You shall know a word by the company it keeps.”(単語の意味は、その周囲の単語によって決まる)

たとえば、「犬は公園でを追いかけた」と「猫は庭でを追いかけた」という文を考えてみましょう。空欄に入る単語(ボール、蝶など)は似ており、「犬」と「猫」が似た文脈に現れることから、これらが意味的に近いことがわかります。

Word2Vecは、この分布仮説を具体的なアルゴリズムに落とし込み、共起パターンからベクトル表現を学習します。

One-hot表現の限界

自然言語処理で単語をコンピュータに入力するには、何らかの数値表現が必要です。最も素朴な方法がOne-hot表現です。語彙サイズを $V$ とすると、各単語を $V$ 次元のベクトルで表し、対応する位置だけを1、残りを0にします。

$$ \text{“king”} = \begin{pmatrix} 0 \\ 0 \\ 1 \\ 0 \\ \vdots \\ 0 \end{pmatrix}, \quad \text{“queen”} = \begin{pmatrix} 0 \\ 0 \\ 0 \\ 1 \\ \vdots \\ 0 \end{pmatrix} $$

One-hot表現には2つの致命的な問題があります。

1. 次元の呪い: 語彙サイズが $V = 100{,}000$ なら、各単語は10万次元のスパースベクトルになります。計算効率が極めて悪くなります。

2. 意味的な類似度が計算できない: 任意の2つの異なるOne-hotベクトル $\bm{x}_i, \bm{x}_j$ について、内積は常にゼロです。

$$ \bm{x}_i^\top \bm{x}_j = 0 \quad (i \neq j) $$

つまり「king」と「queen」の類似度も、「king」と「apple」の類似度も、どちらもゼロです。意味の近さを全く反映できません。

Word2Vecは、このOne-hot表現を低次元の密なベクトル(例えば300次元)に圧縮し、意味的に近い単語が近いベクトルになるよう学習します。

CBOW — 周辺語から中心語を予測する

Word2Vecには2つのアーキテクチャがあります。まずCBOW(Continuous Bag-of-Words)から解説します。

CBOWの基本的なアイデアは「周辺語(コンテキスト)から中心語(ターゲット)を予測する」ことです。

たとえば、ウィンドウサイズ $m = 2$ で「the cat sat on the」という文を考えると、中心語が “sat” のとき、周辺語は {“the”, “cat”, “on”, “the”} です。

モデルの構造

語彙サイズを $V$、埋め込み次元を $d$ とします。CBOWは2つの重み行列を持ちます。

  • 入力側の重み行列: $\bm{W} \in \mathbb{R}^{V \times d}$ — 各行が単語の入力ベクトル $\bm{v}_w$
  • 出力側の重み行列: $\bm{W’} \in \mathbb{R}^{d \times V}$ — 各列が単語の出力ベクトル $\bm{u}_w$

CBOWでは、まず周辺語のベクトルの平均を計算します。

$$ \hat{\bm{v}} = \frac{1}{2m} \sum_{-m \le j \le m, \, j \neq 0} \bm{v}_{w_{t+j}} $$

ここで $w_{t+j}$ は位置 $t$ の中心語から $j$ だけ離れた周辺語です。

目的関数

中心語 $w_t$ が生成される確率をsoftmaxで定義します。

$$ P(w_t \mid w_{t-m}, \dots, w_{t+m}) = \frac{\exp(\bm{u}_{w_t}^\top \hat{\bm{v}})}{\displaystyle\sum_{w=1}^{V} \exp(\bm{u}_w^\top \hat{\bm{v}})} $$

コーパス全体にわたる対数尤度を最大化することが目標です。

$$ J_{\text{CBOW}} = \frac{1}{T} \sum_{t=1}^{T} \log P(w_t \mid w_{t-m}, \dots, w_{t+m}) $$

ここで $T$ はコーパス中の総単語数です。これを展開すると、

$$ \begin{align} J_{\text{CBOW}} &= \frac{1}{T} \sum_{t=1}^{T} \log \frac{\exp(\bm{u}_{w_t}^\top \hat{\bm{v}})}{\displaystyle\sum_{w=1}^{V} \exp(\bm{u}_w^\top \hat{\bm{v}})} \\ &= \frac{1}{T} \sum_{t=1}^{T} \left[ \bm{u}_{w_t}^\top \hat{\bm{v}} – \log \sum_{w=1}^{V} \exp(\bm{u}_w^\top \hat{\bm{v}}) \right] \end{align} $$

第1項はターゲット単語のスコア、第2項は全単語にわたる正規化定数の対数です。

CBOWは周辺語を平均して使うため、語順の情報は失われます(”bag of words” の名の通り)。一方で、小規模コーパスでも比較的安定して学習できるという利点があります。

Skip-gram — 中心語から周辺語を予測する

Skip-gramはCBOWの逆で、「中心語からその周辺語を予測する」モデルです。Mikolovの原論文では、Skip-gramのほうが特に低頻度語の表現学習に優れるとされています。

モデルの構造

中心語 $w_t$ が与えられたとき、ウィンドウ内の各周辺語 $w_{t+j}$ が独立に生成されると仮定します。

$$ P(w_{t+j} \mid w_t) = \frac{\exp(\bm{u}_{w_{t+j}}^\top \bm{v}_{w_t})}{\displaystyle\sum_{w=1}^{V} \exp(\bm{u}_w^\top \bm{v}_{w_t})} $$

ここで $\bm{v}_{w_t}$ は中心語の入力ベクトル、$\bm{u}_{w_{t+j}}$ は周辺語の出力ベクトルです。

目的関数

Skip-gramの目的関数は、コーパス全体にわたる対数尤度の最大化です。

$$ J_{\text{Skip-gram}} = \frac{1}{T} \sum_{t=1}^{T} \sum_{\substack{-m \le j \le m \\ j \neq 0}} \log P(w_{t+j} \mid w_t) $$

これを展開すると、

$$ \begin{align} J_{\text{Skip-gram}} &= \frac{1}{T} \sum_{t=1}^{T} \sum_{\substack{-m \le j \le m \\ j \neq 0}} \log \frac{\exp(\bm{u}_{w_{t+j}}^\top \bm{v}_{w_t})}{\displaystyle\sum_{w=1}^{V} \exp(\bm{u}_w^\top \bm{v}_{w_t})} \\ &= \frac{1}{T} \sum_{t=1}^{T} \sum_{\substack{-m \le j \le m \\ j \neq 0}} \left[ \bm{u}_{w_{t+j}}^\top \bm{v}_{w_t} – \log \sum_{w=1}^{V} \exp(\bm{u}_w^\top \bm{v}_{w_t}) \right] \end{align} $$

Skip-gramでは各周辺語を個別に予測するため、1つの中心語から $2m$ 個の学習サンプルが生成されます。これにより、低頻度語でも十分な学習機会が得られるのです。

softmaxの計算コスト問題

CBOWとSkip-gramの目的関数には共通して、正規化定数(分母)の計算があります。

$$ \sum_{w=1}^{V} \exp(\bm{u}_w^\top \bm{v}) $$

この和は語彙中の全単語 $V$ について計算する必要があります。典型的な語彙サイズは $V = 10^5 \sim 10^6$ であり、これを全学習ステップで毎回計算するのは現実的ではありません。

パラメータの勾配を考えてみましょう。Skip-gramの場合、$\bm{v}_{w_t}$ に関する勾配は、

$$ \frac{\partial \log P(w_O \mid w_I)}{\partial \bm{v}_{w_I}} = \bm{u}_{w_O} – \sum_{w=1}^{V} P(w \mid w_I) \bm{u}_w $$

となりますが、第2項で全語彙についての期待値を計算しなければなりません。これは計算量 $O(V \cdot d)$ であり、ボトルネックになります。

この問題を解決するために提案されたのが、Hierarchical SoftmaxNegative Samplingです。本記事では、より広く使われているNegative Samplingを詳しく解説します。

Negative Sampling — 効率的な学習の鍵

基本的なアイデア

Negative Samplingの直感的なアイデアは非常にシンプルです。「全語彙にわたるsoftmaxを計算する代わりに、正例1つと少数の負例だけを使って二値分類問題に帰着させる」というものです。

つまり、「(center, context) ペアが実際にコーパスに現れたか否か」を判定するロジスティック回帰に問題を置き換えます。

目的関数の導出

まず、シグモイド関数を定義します。

$$ \sigma(x) = \frac{1}{1 + \exp(-x)} $$

Skip-gram with Negative Sampling(SGNS)の目的関数は以下の通りです。

$$ J_{\text{NEG}} = \log \sigma(\bm{u}_{w_O}^\top \bm{v}_{w_I}) + \sum_{k=1}^{K} \mathbb{E}_{w_k \sim P_n(w)} \left[ \log \sigma(-\bm{u}_{w_k}^\top \bm{v}_{w_I}) \right] $$

各項の意味を確認しましょう。

第1項: 正例(実際に共起する単語ペア)に対して、内積 $\bm{u}_{w_O}^\top \bm{v}_{w_I}$ を大きくします。$\sigma$ が1に近づくので、$\log \sigma$ が0に近づき、目的関数が最大化されます。

第2項: 負例(ランダムにサンプリングした単語 $w_k$)に対して、内積 $\bm{u}_{w_k}^\top \bm{v}_{w_I}$ を小さく(負に)します。$\sigma(-\bm{u}_{w_k}^\top \bm{v}_{w_I})$ が1に近づくので、同様に目的関数が最大化されます。

ここで $K$ は負例の数(原論文では小規模コーパスで $K = 5 \sim 20$、大規模コーパスで $K = 2 \sim 5$ を推奨)、$P_n(w)$ はノイズ分布です。

ノイズ分布

負例のサンプリングには、単語の出現頻度 $f(w)$ の $3/4$ 乗に比例する分布が用いられます。

$$ P_n(w) = \frac{f(w)^{3/4}}{\displaystyle\sum_{w’} f(w’)^{3/4}} $$

なぜ $3/4$ 乗なのでしょうか。生の頻度 $f(w)$ をそのまま使うと、”the”、”a”、”is” のような高頻度語ばかりが負例として選ばれてしまい、学習が偏ります。一方、一様分布にすると低頻度語が負例として多く選ばれすぎます。$3/4$ 乗は、この両極端の間をうまくバランスする経験的に優れた値です。

具体例で確認しましょう。

単語 $f(w)$ $f(w)^{3/4}$ 相対的な変化
“the” 0.10 0.056 減少
“cat” 0.001 0.0018 増加
“axolotl” 0.00001 0.000056 増加

高頻度語のサンプリング確率が下がり、低頻度語のサンプリング確率が相対的に上がることで、より多様な負例が得られます。

計算量の改善

Negative Samplingにより、各学習ステップの計算量は $O(V \cdot d)$ から $O(K \cdot d)$ に削減されます。$K \ll V$ なので、これは劇的な効率化です。例えば $V = 100{,}000$, $K = 5$ とすると、計算量は約2万分の1になります。

Subsampling of Frequent Words

英語のコーパスでは “the”、”a”、”in” のような高頻度語が非常に多く出現します。これらの単語は、

  1. すでに十分な学習サンプルがあり、追加のサンプルで大きく改善しない
  2. 他の単語との共起にあまり情報を持たない(”the” はほぼ全ての名詞と共起する)

そこで、Mikolov et al. (2013) ではsubsamplingを導入しました。学習コーパス中の各単語 $w_i$ を確率 $P_{\text{discard}}(w_i)$ で破棄します。

$$ P_{\text{discard}}(w_i) = 1 – \sqrt{\frac{t}{f(w_i)}} $$

ここで $f(w_i)$ は単語 $w_i$ の出現頻度(全単語に対する割合)、$t$ は閾値パラメータ(典型的には $t = 10^{-5}$)です。

この式の振る舞いを確認しましょう。

  • $f(w_i) \gg t$ のとき(”the” のような超高頻度語): $\sqrt{t / f(w_i)} \ll 1$ なので、$P_{\text{discard}} \approx 1$。ほとんど破棄されます。
  • $f(w_i) \approx t$ のとき: $P_{\text{discard}} \approx 0$。ほぼ保持されます。
  • $f(w_i) \ll t$ のとき: $\sqrt{t / f(w_i)} > 1$ なので、$P_{\text{discard}} < 0$(= 0とみなす)。必ず保持されます。

これにより、高頻度語によるノイズが削減され、低頻度語の相対的な重要度が上がり、学習の質と速度が向上します。

Word2Vecで学習されるベクトルの性質

Word2Vecで学習された単語ベクトルには、興味深い加法構成性(additive compositionality)が現れます。

king – man + woman = queen

最も有名な例がこの関係です。学習済みのベクトル空間において、以下の演算が成り立ちます。

$$ \bm{v}_{\text{king}} – \bm{v}_{\text{man}} + \bm{v}_{\text{woman}} \approx \bm{v}_{\text{queen}} $$

直感的には、$\bm{v}_{\text{king}} – \bm{v}_{\text{man}}$ で「男性性」を除去し、$+ \bm{v}_{\text{woman}}$ で「女性性」を加えると、「女性の王族」= queen になるということです。

これは単なる偶然ではなく、様々な意味関係で成り立ちます。

関係
性別 $\bm{v}_{\text{brother}} – \bm{v}_{\text{man}} + \bm{v}_{\text{woman}} \approx \bm{v}_{\text{sister}}$
国と首都 $\bm{v}_{\text{Paris}} – \bm{v}_{\text{France}} + \bm{v}_{\text{Japan}} \approx \bm{v}_{\text{Tokyo}}$
比較級 $\bm{v}_{\text{bigger}} – \bm{v}_{\text{big}} + \bm{v}_{\text{small}} \approx \bm{v}_{\text{smaller}}$
時制 $\bm{v}_{\text{walking}} – \bm{v}_{\text{walk}} + \bm{v}_{\text{swim}} \approx \bm{v}_{\text{swimming}}$

なぜ加法構成性が生まれるのか

Levy & Goldberg (2014) は、Skip-gram with Negative Sampling が暗黙的に単語の共起行列のPMI(Pointwise Mutual Information)を分解していることを示しました。

$$ \bm{v}_{w}^\top \bm{u}_{c} \approx \text{PMI}(w, c) – \log K $$

ここで PMI は以下のように定義されます。

$$ \text{PMI}(w, c) = \log \frac{P(w, c)}{P(w) P(c)} $$

PMI行列は加法的な構造を持つため、その分解で得られるベクトルにも加法構成性が自然に現れます。

コサイン類似度

2つの単語ベクトル間の意味的な類似度は、コサイン類似度で測定します。

$$ \text{sim}(\bm{v}_a, \bm{v}_b) = \frac{\bm{v}_a^\top \bm{v}_b}{\|\bm{v}_a\| \, \|\bm{v}_b\|} $$

値は $[-1, 1]$ の範囲をとり、1に近いほど意味が似ていることを示します。

Python実装 — gensimで学習しt-SNEで可視化

gensimによるWord2Vecの学習

実際にWord2Vecを学習してみましょう。Pythonの gensim ライブラリを使えば、数行で学習できます。

import gensim.downloader as api
from gensim.models import Word2Vec

# 学習済みコーパスのダウンロード(text8: Wikipedia英語の最初の1億文字)
dataset = api.load("text8")

# Word2Vecモデルの学習(Skip-gram)
model = Word2Vec(
    sentences=dataset,
    vector_size=200,     # 埋め込み次元
    window=5,            # ウィンドウサイズ
    min_count=5,         # 最低出現回数
    sg=1,                # 1: Skip-gram, 0: CBOW
    negative=5,          # 負例の数
    sample=1e-5,         # subsampling閾値
    epochs=5,            # エポック数
    workers=4            # 並列ワーカー数
)

# モデルの保存
model.save("word2vec_text8.model")

類似単語の検索

# "king" に最も近い単語を検索
similar_words = model.wv.most_similar("king", topn=10)
for word, score in similar_words:
    print(f"{word}: {score:.4f}")

アナロジーテスト

# king - man + woman = ?
result = model.wv.most_similar(
    positive=["king", "woman"],
    negative=["man"],
    topn=5
)
print("king - man + woman =")
for word, score in result:
    print(f"  {word}: {score:.4f}")

# Paris - France + Japan = ?
result = model.wv.most_similar(
    positive=["paris", "japan"],
    negative=["france"],
    topn=5
)
print("\nparis - france + japan =")
for word, score in result:
    print(f"  {word}: {score:.4f}")

t-SNEによる可視化

高次元の単語ベクトルを2次元に圧縮し、可視化してみましょう。

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

# 可視化する単語リスト
words = [
    "king", "queen", "prince", "princess",
    "man", "woman", "boy", "girl",
    "dog", "cat", "horse", "bird",
    "car", "bus", "train", "airplane",
    "japan", "china", "france", "germany",
    "tokyo", "beijing", "paris", "berlin",
    "one", "two", "three", "four",
]

# 単語ベクトルの取得
word_vectors = np.array([model.wv[w] for w in words])

# t-SNEで2次元に圧縮
tsne = TSNE(n_components=2, random_state=42, perplexity=10)
vectors_2d = tsne.fit_transform(word_vectors)

# 可視化
plt.figure(figsize=(14, 10))
plt.scatter(vectors_2d[:, 0], vectors_2d[:, 1], c="steelblue", s=50, alpha=0.7)

for i, word in enumerate(words):
    plt.annotate(
        word,
        xy=(vectors_2d[i, 0], vectors_2d[i, 1]),
        fontsize=12,
        ha="center",
        va="bottom",
    )

plt.title("Word2Vec Embeddings Visualized with t-SNE", fontsize=14)
plt.xlabel("t-SNE dim 1")
plt.ylabel("t-SNE dim 2")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("word2vec_tsne.png", dpi=150)
plt.show()

t-SNEの結果を見ると、意味的に近い単語がクラスターを形成していることが確認できます。王族に関する単語(king, queen, prince, princess)、動物(dog, cat, horse, bird)、乗り物(car, bus, train, airplane)、国と首都(japan-tokyo, france-paris など)がそれぞれ近くに配置されます。

CBOWでの学習

CBOWで学習する場合は、sg=0 に変更するだけです。

# CBOWで学習
model_cbow = Word2Vec(
    sentences=dataset,
    vector_size=200,
    window=5,
    min_count=5,
    sg=0,              # 0: CBOW
    negative=5,
    sample=1e-5,
    epochs=5,
    workers=4,
)

一般的に、Skip-gramは低頻度語の表現が優れ、CBOWは学習が高速で高頻度語の表現が安定するとされています。用途に応じて使い分けましょう。

まとめ

本記事では、Word2Vecの理論と実装について解説しました。

  • 分布仮説: 単語の意味はその周囲の単語(文脈)によって決まるという考え方が基盤
  • CBOW: 周辺語から中心語を予測するモデル。周辺語ベクトルの平均を入力とする
  • Skip-gram: 中心語から周辺語を予測するモデル。低頻度語の学習に優れる
  • Negative Sampling: 全語彙にわたるsoftmaxの代わりに、少数の負例との二値分類で近似する。計算量を $O(V \cdot d)$ から $O(K \cdot d)$ に削減
  • Subsampling: 高頻度語を確率的に除外し、学習の質と速度を向上
  • 加法構成性: 学習されたベクトルは “king – man + woman = queen” のような意味的な演算が可能
  • PMIとの関係: SGNSは暗黙的にPMI行列を分解している

Word2Vecは2013年の提案以来、NLPの基盤技術として広く使われ続けています。しかし、Word2Vecには「各単語に固定のベクトルを割り当てる」という制約があり、文脈によって意味が変わる多義語(例: “bank” は「銀行」か「岸」か)を区別できません。

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