Word2Vec は、2013年に Mikolov らが提案した単語の分散表現(distributed representation)を学習するモデルです。従来の one-hot ベクトルでは表現できなかった「単語間の意味的な距離」を、低次元の密なベクトルとして捉えることに成功し、自然言語処理(NLP)の分野に革命をもたらしました。
Word2Vec 以前、単語をベクトルとして扱う最も単純な方法は one-hot エンコーディングでした。しかし、語彙数が $V$ の場合に $V$ 次元のスパースベクトルとなり、任意の2単語間のコサイン類似度が常に 0 になるという根本的な問題がありました。Word2Vec はこの問題を解決し、「king – man + woman ≈ queen」のような加法的な意味演算を可能にする密なベクトル空間を学習します。
本記事の内容
- one-hot エンコーディングの限界と分散表現の動機
- CBOW(Continuous Bag-of-Words)モデルの構造と目的関数
- Skip-gram モデルの確率的定式化と目的関数の導出
- ソフトマックスの計算量問題と Negative Sampling の導出
- 階層的ソフトマックス(ハフマン木)の概要
- サブサンプリングの理論
- 単語ベクトルの加法性の経験的理解
- Python でのスクラッチ実装と gensim による学習・類似度計算
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
one-hot エンコーディングの限界
one-hot ベクトルの定義
語彙 $\mathcal{V} = \{w_1, w_2, \dots, w_V\}$ が与えられたとき、単語 $w_i$ の one-hot ベクトルは
$$ \bm{e}_i = (0, \dots, 0, \underbrace{1}_{i\text{番目}}, 0, \dots, 0)^\top \in \mathbb{R}^V $$
と定義されます。このとき、任意の異なる2単語 $w_i, w_j$($i \neq j$)について
$$ \bm{e}_i^\top \bm{e}_j = 0 $$
が成り立ちます。つまり、「犬」と「猫」の類似度も、「犬」と「飛行機」の類似度も同じく 0 であり、意味的な距離を一切捉えることができません。
one-hot の問題点まとめ
- 次元の呪い: 語彙数 $V$ が大きいと(典型的には $V \sim 10^4 \text{–} 10^5$)、ベクトルが非常に高次元かつスパースになる
- 意味的類似度の欠如: 全ての単語対が直交しており、意味の近さを反映できない
- 汎化能力の欠如: 新しい文脈での予測に、似た単語の知識を活用できない
分散表現の動機
分散表現(distributed representation)のアイデアは、各単語を低次元の密なベクトル $\bm{v} \in \mathbb{R}^d$(典型的には $d = 50 \text{–} 300$)で表現することです。このベクトルは 分散仮説(distributional hypothesis)に基づいて学習されます。
分散仮説: 「同じ文脈に出現する単語は、類似した意味を持つ」(Firth, 1957)
つまり、「犬を散歩させる」と「猫を散歩させる」のように似た文脈で使われる単語は、ベクトル空間上で近い位置に配置されるべきだという考え方です。
CBOW モデル
モデルの構造
CBOW(Continuous Bag-of-Words)は、文脈(周辺単語)から中心単語を予測するモデルです。ウィンドウサイズを $m$ とすると、位置 $t$ の中心単語 $w_t$ に対して、文脈は $\{w_{t-m}, \dots, w_{t-1}, w_{t+1}, \dots, w_{t+m}\}$ の $2m$ 個の単語です。
モデルのパラメータは以下の2つの行列です。
- 入力側埋め込み行列: $\bm{W} \in \mathbb{R}^{d \times V}$(各列 $\bm{v}_{w}$ が単語 $w$ の入力ベクトル)
- 出力側埋め込み行列: $\bm{W}’ \in \mathbb{R}^{V \times d}$(各行 $\bm{u}_{w}^\top$ が単語 $w$ の出力ベクトル)
CBOW の目的関数
文脈単語の入力ベクトルの平均を
$$ \bm{h} = \frac{1}{2m} \sum_{j \in \mathcal{C}(t)} \bm{v}_{w_j} $$
とします。ここで $\mathcal{C}(t) = \{t-m, \dots, t-1, t+1, \dots, t+m\}$ は文脈のインデックス集合です。
中心単語 $w_t$ が生成される確率は、ソフトマックス関数を用いて
$$ P(w_t \mid \mathcal{C}(t)) = \frac{\exp(\bm{u}_{w_t}^\top \bm{h})}{\sum_{j=1}^{V} \exp(\bm{u}_{w_j}^\top \bm{h})} $$
と定義されます。コーパス全体にわたる目的関数は、対数尤度の最大化です。
$$ J_{\text{CBOW}} = \frac{1}{T} \sum_{t=1}^{T} \log P(w_t \mid \mathcal{C}(t)) $$
ここで $T$ はコーパスの総単語数です。
Skip-gram モデル
モデルの構造
Skip-gram は CBOW の逆で、中心単語から文脈(周辺単語)を予測するモデルです。実際には Skip-gram の方が大規模コーパスでの学習性能が良いことが知られており、Word2Vec の代表的なモデルとされます。
確率の定式化
中心単語 $w_c$ が与えられたとき、文脈単語 $w_o$ が出現する条件付き確率を
$$ P(w_o \mid w_c) = \frac{\exp(\bm{u}_{w_o}^\top \bm{v}_{w_c})}{\sum_{j=1}^{V} \exp(\bm{u}_{w_j}^\top \bm{v}_{w_c})} $$
と定義します。ここで $\bm{v}_{w_c} \in \mathbb{R}^d$ は中心単語の入力ベクトル、$\bm{u}_{w_o} \in \mathbb{R}^d$ は文脈単語の出力ベクトルです。
目的関数の導出
文脈内の各単語の出現を条件付き独立と仮定すると、位置 $t$ での対数尤度は
$$ \log P(\mathcal{C}(t) \mid w_t) = \sum_{\substack{j = t-m \\ j \neq t}}^{t+m} \log P(w_j \mid w_t) $$
コーパス全体では
$$ J_{\text{Skip-gram}} = \frac{1}{T} \sum_{t=1}^{T} \sum_{\substack{j = t-m \\ j \neq t}}^{t+m} \log P(w_j \mid w_t) $$
各項を展開すると
$$ \begin{align} \log P(w_j \mid w_t) &= \log \frac{\exp(\bm{u}_{w_j}^\top \bm{v}_{w_t})}{\sum_{k=1}^{V} \exp(\bm{u}_{w_k}^\top \bm{v}_{w_t})} \\ &= \bm{u}_{w_j}^\top \bm{v}_{w_t} – \log \sum_{k=1}^{V} \exp(\bm{u}_{w_k}^\top \bm{v}_{w_t}) \end{align} $$
ここで問題となるのは、分母の $\sum_{k=1}^{V} \exp(\bm{u}_{w_k}^\top \bm{v}_{w_t})$ の計算です。$V$ が $10^5$ オーダーの場合、全単語について内積と指数関数を計算する必要があり、計算量が $O(V)$ となって非常に重くなります。
ソフトマックスの計算量問題
正規化定数の計算量 $O(V)$ は、パラメータの更新のたびに語彙全体をスキャンする必要があることを意味します。$V = 100{,}000$ とすると、1回の勾配更新に $O(Vd) \approx O(10^7)$ の計算が必要です。これを解決するために、以下の2つの近似手法が提案されました。
- Negative Sampling(負例サンプリング)
- 階層的ソフトマックス
Negative Sampling の導出
ノイズ対比推定(NCE)からの出発
Negative Sampling は、ノイズ対比推定(Noise Contrastive Estimation; NCE)を Word2Vec 向けに簡略化したものです。
NCE の基本アイデアは、「データ分布からのサンプルか、ノイズ分布からのサンプルかを判別する二値分類問題」として密度推定を定式化することです。
データ分布を $P_{\text{data}}$、ノイズ分布を $P_n$ とします。データから引いたサンプルにラベル $D=1$、ノイズからのサンプルにラベル $D=0$ を付けると
$$ P(D=1 \mid w, c) = \frac{P_{\text{data}}(w \mid c)}{P_{\text{data}}(w \mid c) + k \cdot P_n(w)} $$
ここで $k$ はノイズサンプル数とデータサンプル数の比です。
Word2Vec での簡略化
Word2Vec では、$P_{\text{data}}(w \mid c)$ をモデルのパラメータで表し、ノイズ分布 $P_n(w)$ として単語のユニグラム分布の $3/4$ 乗を使います。
$$ P_n(w) = \frac{[\text{count}(w)]^{3/4}}{\sum_{w’} [\text{count}(w’)]^{3/4}} $$
$3/4$ 乗にする理由は、頻度の低い単語のサンプリング確率を相対的に上げ、学習機会を増やすためです。
中心単語 $w_c$ と文脈単語 $w_o$ のペア $(w_o, w_c)$ が実際にコーパスに現れたかどうかを、シグモイド関数
$$ \sigma(x) = \frac{1}{1 + \exp(-x)} $$
を用いて判定します。正例(実際に共起したペア)については
$$ P(D=1 \mid w_o, w_c) = \sigma(\bm{u}_{w_o}^\top \bm{v}_{w_c}) $$
負例(ノイズ分布からサンプルした単語 $w_k$)については
$$ P(D=0 \mid w_k, w_c) = 1 – \sigma(\bm{u}_{w_k}^\top \bm{v}_{w_c}) = \sigma(-\bm{u}_{w_k}^\top \bm{v}_{w_c}) $$
$K$ 個の負例をサンプリングすると、1つの正例ペア $(w_o, w_c)$ に対する目的関数は
$$ \begin{align} \mathcal{L}_{\text{NEG}} &= \log \sigma(\bm{u}_{w_o}^\top \bm{v}_{w_c}) + \sum_{k=1}^{K} \mathbb{E}_{w_k \sim P_n} \left[\log \sigma(-\bm{u}_{w_k}^\top \bm{v}_{w_c})\right] \end{align} $$
実際の実装では、期待値をモンテカルロサンプリングで近似します。
$$ \mathcal{L}_{\text{NEG}} \approx \log \sigma(\bm{u}_{w_o}^\top \bm{v}_{w_c}) + \sum_{k=1}^{K} \log \sigma(-\bm{u}_{w_k}^\top \bm{v}_{w_c}) $$
勾配の導出
$\bm{v}_{w_c}$ に関する勾配を求めます。まず、シグモイド関数の微分の性質
$$ \frac{d\sigma(x)}{dx} = \sigma(x)(1 – \sigma(x)) $$
を使います。正例項の勾配は
$$ \begin{align} \frac{\partial}{\partial \bm{v}_{w_c}} \log \sigma(\bm{u}_{w_o}^\top \bm{v}_{w_c}) &= \frac{\sigma(\bm{u}_{w_o}^\top \bm{v}_{w_c})(1 – \sigma(\bm{u}_{w_o}^\top \bm{v}_{w_c}))}{\sigma(\bm{u}_{w_o}^\top \bm{v}_{w_c})} \bm{u}_{w_o} \\ &= (1 – \sigma(\bm{u}_{w_o}^\top \bm{v}_{w_c})) \bm{u}_{w_o} \end{align} $$
負例項の勾配は($k$ 番目の負例 $w_k$ について)
$$ \begin{align} \frac{\partial}{\partial \bm{v}_{w_c}} \log \sigma(-\bm{u}_{w_k}^\top \bm{v}_{w_c}) &= (1 – \sigma(-\bm{u}_{w_k}^\top \bm{v}_{w_c}))(-\bm{u}_{w_k}) \\ &= -\sigma(\bm{u}_{w_k}^\top \bm{v}_{w_c}) \bm{u}_{w_k} \end{align} $$
ここで $1 – \sigma(-x) = \sigma(x)$ を用いました。
したがって、全体の勾配は
$$ \frac{\partial \mathcal{L}_{\text{NEG}}}{\partial \bm{v}_{w_c}} = (1 – \sigma(\bm{u}_{w_o}^\top \bm{v}_{w_c})) \bm{u}_{w_o} – \sum_{k=1}^{K} \sigma(\bm{u}_{w_k}^\top \bm{v}_{w_c}) \bm{u}_{w_k} $$
同様に $\bm{u}_{w_o}$ に関する勾配は
$$ \frac{\partial \mathcal{L}_{\text{NEG}}}{\partial \bm{u}_{w_o}} = (1 – \sigma(\bm{u}_{w_o}^\top \bm{v}_{w_c})) \bm{v}_{w_c} $$
各負例ベクトル $\bm{u}_{w_k}$ に関する勾配は
$$ \frac{\partial \mathcal{L}_{\text{NEG}}}{\partial \bm{u}_{w_k}} = -\sigma(\bm{u}_{w_k}^\top \bm{v}_{w_c}) \bm{v}_{w_c} $$
計算量は $O((K+1)d)$ であり、$K$ は典型的には 5–20 程度なので、ソフトマックスの $O(Vd)$ に比べて大幅に削減されます。
階層的ソフトマックス
ハフマン木による近似
階層的ソフトマックス(Hierarchical Softmax)は、語彙全体にわたるソフトマックスの計算を、二分木上のパスに沿った $O(\log V)$ 回のシグモイド計算に置き換える手法です。
語彙に含まれる $V$ 個の単語を葉ノードとする二分木(通常はハフマン木)を構築します。ハフマン木を使う理由は、高頻度の単語ほど根に近い浅いノードに配置され、計算回数が少なくなるためです。
根から単語 $w$ の葉ノードへのパスを $(n_0, n_1, \dots, n_L)$ とします($n_0$ は根、$n_L$ が $w$ に対応する葉)。各内部ノード $n_l$ にはパラメータベクトル $\bm{\theta}_{n_l} \in \mathbb{R}^d$ が割り当てられ、左に進む確率を
$$ P(\text{左} \mid n_l) = \sigma(\bm{\theta}_{n_l}^\top \bm{v}_{w_c}) $$
右に進む確率を
$$ P(\text{右} \mid n_l) = 1 – \sigma(\bm{\theta}_{n_l}^\top \bm{v}_{w_c}) $$
と定義します。すると、単語 $w$ の生成確率は
$$ P(w \mid w_c) = \prod_{l=0}^{L-1} \sigma\left([\![d(n_{l+1}) = \text{左}]\!] \cdot \bm{\theta}_{n_l}^\top \bm{v}_{w_c}\right) $$
ここで $[\![d(n_{l+1}) = \text{左}]\!]$ はパスが左に進む場合に $+1$、右に進む場合に $-1$ を返す関数です。計算量は $O(L \cdot d)$ であり、ハフマン木の深さは平均的に $O(\log V)$ なので、全体で $O(d \log V)$ となります。
サブサンプリング
高頻度の単語(”the”, “a”, “is” など)は情報量が少ないにもかかわらず、大量のトレーニングペアを生成します。Mikolov らは、各単語 $w_i$ をサブサンプリング確率
$$ P(\text{keep} \mid w_i) = 1 – \sqrt{\frac{t}{f(w_i)}} $$
で確率的に除外する手法を提案しました。ここで $f(w_i)$ は単語 $w_i$ のコーパス中の相対頻度、$t$ はしきい値パラメータ(典型的には $t = 10^{-5}$)です。
実際の実装では、gensim 等では以下の式が使われます。
$$ P(\text{keep} \mid w_i) = \min\left(1, \sqrt{\frac{t}{f(w_i)}} + \frac{t}{f(w_i)}\right) $$
高頻度の単語ほど除外される確率が高く、低頻度の単語はほぼ確実に保持されます。
単語ベクトルの加法性
Word2Vec で学習されたベクトルには、興味深い代数的性質が観察されます。
$$ \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」に到達するという解釈ができます。
数学的には、Skip-gram with Negative Sampling が暗黙的に単語の PMI(自己相互情報量)行列の低ランク近似を行っていることが Levy & Goldberg (2014) によって示されており、加法性はこの PMI 空間の線形構造に起因するとされています。
Python でのスクラッチ実装
Skip-gram with Negative Sampling
以下では、簡単なコーパスに対して Skip-gram + Negative Sampling をスクラッチで実装します。
import numpy as np
import matplotlib.pyplot as plt
from collections import Counter
# 簡易コーパス
corpus = [
"the cat sat on the mat",
"the dog sat on the rug",
"the cat chased the dog",
"the dog chased the cat",
"the mat is on the floor",
"the rug is on the floor",
"cat and dog are friends",
"the cat is small",
"the dog is big",
"the floor is flat"
]
# トークナイズと語彙構築
sentences = [s.split() for s in corpus]
word_counts = Counter(w for s in sentences for w in s)
vocab = sorted(word_counts.keys())
word2idx = {w: i for i, w in enumerate(vocab)}
idx2word = {i: w for w, i in word2idx.items()}
V = len(vocab)
print(f"語彙数: {V}")
print(f"語彙: {vocab}")
# ノイズ分布(ユニグラム分布の3/4乗)
total = sum(word_counts.values())
freqs = np.array([word_counts[idx2word[i]] for i in range(V)], dtype=np.float64)
noise_dist = freqs ** 0.75
noise_dist /= noise_dist.sum()
# Skip-gram with Negative Sampling
class SkipGramNS:
def __init__(self, vocab_size, embed_dim, lr=0.025):
self.V = vocab_size
self.d = embed_dim
self.lr = lr
# 入力・出力埋め込み行列の初期化
self.W_in = (np.random.randn(vocab_size, embed_dim) * 0.01) # v_w
self.W_out = (np.random.randn(vocab_size, embed_dim) * 0.01) # u_w
def sigmoid(self, x):
# オーバーフロー防止
return np.where(x >= 0,
1 / (1 + np.exp(-x)),
np.exp(x) / (1 + np.exp(x)))
def train_pair(self, center_idx, context_idx, neg_indices):
"""1つの(中心, 文脈)ペアに対して勾配更新"""
v_c = self.W_in[center_idx] # 中心単語の入力ベクトル
u_o = self.W_out[context_idx] # 文脈単語の出力ベクトル
# 正例のスコアと勾配
score_pos = self.sigmoid(np.dot(u_o, v_c))
grad_uo = (score_pos - 1) * v_c # -(1 - sigma) * v_c
grad_vc = (score_pos - 1) * u_o # -(1 - sigma) * u_o
# 負例のスコアと勾配
for neg_idx in neg_indices:
u_k = self.W_out[neg_idx]
score_neg = self.sigmoid(np.dot(u_k, v_c))
grad_uk = score_neg * v_c # sigma * v_c
grad_vc += score_neg * u_k # sigma * u_k を累積
# 出力ベクトル更新
self.W_out[neg_idx] -= self.lr * grad_uk
# パラメータ更新
self.W_out[context_idx] -= self.lr * grad_uo
self.W_in[center_idx] -= self.lr * grad_vc
def compute_loss(self, center_idx, context_idx, neg_indices):
"""Negative Sampling の損失を計算"""
v_c = self.W_in[center_idx]
u_o = self.W_out[context_idx]
# 正例の損失
pos_score = np.dot(u_o, v_c)
loss = -np.log(self.sigmoid(pos_score) + 1e-10)
# 負例の損失
for neg_idx in neg_indices:
u_k = self.W_out[neg_idx]
neg_score = np.dot(u_k, v_c)
loss -= np.log(self.sigmoid(-neg_score) + 1e-10)
return loss
# 学習パラメータ
embed_dim = 20
window_size = 2
num_neg = 5
num_epochs = 100
model = SkipGramNS(V, embed_dim, lr=0.025)
# 学習ループ
losses = []
for epoch in range(num_epochs):
epoch_loss = 0
num_pairs = 0
for sentence in sentences:
indices = [word2idx[w] for w in sentence]
for i, center_idx in enumerate(indices):
# ウィンドウ内の文脈単語
start = max(0, i - window_size)
end = min(len(indices), i + window_size + 1)
for j in range(start, end):
if j == i:
continue
context_idx = indices[j]
# 負例サンプリング
neg_indices = []
while len(neg_indices) < num_neg:
neg = np.random.choice(V, p=noise_dist)
if neg != context_idx:
neg_indices.append(neg)
# 損失の計算
loss = model.compute_loss(center_idx, context_idx, neg_indices)
epoch_loss += loss
num_pairs += 1
# 勾配更新
model.train_pair(center_idx, context_idx, neg_indices)
avg_loss = epoch_loss / max(num_pairs, 1)
losses.append(avg_loss)
if (epoch + 1) % 20 == 0:
print(f"Epoch {epoch+1}/{num_epochs}, Loss: {avg_loss:.4f}")
# 学習曲線の可視化
plt.figure(figsize=(8, 5))
plt.plot(losses)
plt.xlabel("Epoch")
plt.ylabel("Average Loss")
plt.title("Skip-gram with Negative Sampling: Training Loss")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 学習した単語ベクトルの取得(入力ベクトルを使用)
embeddings = model.W_in
# コサイン類似度の計算
def cosine_similarity(v1, v2):
return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) + 1e-10)
# 類似度の確認
print("\n=== 単語間のコサイン類似度 ===")
pairs = [("cat", "dog"), ("cat", "mat"), ("dog", "rug"),
("mat", "rug"), ("mat", "floor"), ("sat", "chased")]
for w1, w2 in pairs:
sim = cosine_similarity(embeddings[word2idx[w1]], embeddings[word2idx[w2]])
print(f"sim({w1}, {w2}) = {sim:.4f}")
学習した埋め込みの可視化
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
# PCA で2次元に次元削減して可視化
pca = PCA(n_components=2)
embeddings_2d = pca.fit_transform(embeddings)
plt.figure(figsize=(10, 8))
for i, word in enumerate(vocab):
plt.scatter(embeddings_2d[i, 0], embeddings_2d[i, 1], c='steelblue', s=50)
plt.annotate(word, (embeddings_2d[i, 0] + 0.01, embeddings_2d[i, 1] + 0.01),
fontsize=12)
plt.xlabel("PC1")
plt.ylabel("PC2")
plt.title("Word2Vec Embeddings (PCA Projection)")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
gensim による Word2Vec 学習
from gensim.models import Word2Vec
# コーパス
corpus = [
"the cat sat on the mat".split(),
"the dog sat on the rug".split(),
"the cat chased the dog".split(),
"the dog chased the cat".split(),
"the mat is on the floor".split(),
"the rug is on the floor".split(),
"cat and dog are friends".split(),
"the cat is small".split(),
"the dog is big".split(),
"the floor is flat".split(),
]
# Word2Vec モデルの学習(Skip-gram)
model_sg = Word2Vec(
sentences=corpus,
vector_size=50, # 埋め込み次元
window=2, # ウィンドウサイズ
min_count=1, # 最低出現頻度
sg=1, # 1=Skip-gram, 0=CBOW
negative=5, # 負例数
epochs=200, # エポック数
seed=42
)
# CBOW モデルの学習
model_cbow = Word2Vec(
sentences=corpus,
vector_size=50,
window=2,
min_count=1,
sg=0, # CBOW
negative=5,
epochs=200,
seed=42
)
# 類似単語の検索
print("=== Skip-gram: 'cat' に類似する単語 ===")
for word, sim in model_sg.wv.most_similar("cat", topn=5):
print(f" {word}: {sim:.4f}")
print("\n=== CBOW: 'cat' に類似する単語 ===")
for word, sim in model_cbow.wv.most_similar("cat", topn=5):
print(f" {word}: {sim:.4f}")
# コサイン類似度の計算
print(f"\n=== コサイン類似度 ===")
print(f"sim(cat, dog) = {model_sg.wv.similarity('cat', 'dog'):.4f}")
print(f"sim(mat, rug) = {model_sg.wv.similarity('mat', 'rug'):.4f}")
print(f"sim(cat, floor) = {model_sg.wv.similarity('cat', 'floor'):.4f}")
CBOW と Skip-gram の比較
| 特性 | CBOW | Skip-gram |
|---|---|---|
| 予測方向 | 文脈 → 中心単語 | 中心単語 → 文脈 |
| 学習速度 | 速い | 遅い |
| 低頻度語 | 不得意 | 得意 |
| 大規模コーパス | 効率的 | 高品質なベクトル |
| 目的関数 | $\sum_t \log P(w_t \mid \mathcal{C}(t))$ | $\sum_t \sum_{j} \log P(w_j \mid w_t)$ |
CBOW は文脈ベクトルを平均化してから中心単語を予測するため、低頻度語の文脈情報が平滑化される傾向があります。一方、Skip-gram は中心単語から各文脈単語を個別に予測するため、低頻度語でも十分な学習ペアが生成されます。
Negative Sampling の個数 $K$ の選び方
Mikolov らの原論文では、以下の目安が示されています。
| データセット規模 | 推奨 $K$ |
|---|---|
| 小規模 | 5–20 |
| 大規模 | 2–5 |
$K$ が大きすぎると、正例の信号が負例に埋もれてしまい、学習が非効率になります。
まとめ
本記事では、Word2Vec の理論と実装について解説しました。
- one-hot エンコーディングの限界: 高次元・スパース・意味的類似度ゼロという問題を確認し、分散表現の動機を理解しました
- CBOW と Skip-gram: 文脈→中心単語(CBOW)と中心単語→文脈(Skip-gram)の2つのモデルを定式化し、それぞれの目的関数を導出しました
- Negative Sampling: NCE からの導出を通じて、ソフトマックスの計算量 $O(V)$ を $O(K)$ に削減する手法の理論と勾配を導出しました
- 階層的ソフトマックス: ハフマン木を用いて $O(\log V)$ に計算量を削減する手法の仕組みを理解しました
- 実装: Python でのスクラッチ実装と gensim ライブラリの使い方を確認しました
次のステップとして、以下の記事も参考にしてください。