単語の数値化 ── 分布仮説と Word2Vec
LLM との関係:LLM の入力層(Embedding Layer)は Word2Vec の発展形。「単語をベクトルに変換する」思想は Transformer にもそのまま受け継がれている。

この章で何ができるようになるか:「意味が近い単語はベクトル空間でも近い」がどうやって実現されるかを理解し、Embedding Layer の役割を説明できる。
問題:単語をどう数値化するか
ニューラルネットワークは数値しか扱えない。「猫」「犬」「東京」をどう数値にするか。
素朴な方法:One-Hot Encoding
vocab = {"猫": 0, "犬": 1, "東京": 2, "大阪": 3}
# 「猫」 → [1, 0, 0, 0]
# 「犬」 → [0, 1, 0, 0]
# 「東京」→ [0, 0, 1, 0]
# 問題1: 次元が語彙サイズ(5万〜10万)になる → 疎で非効率
# 問題2: 「猫」と「犬」の距離 = 「猫」と「東京」の距離(意味の近さが反映されない)
分布仮説:「周辺の単語で意味が決まる」
“You shall know a word by the company it keeps” — J.R. Firth (1957)
「猫が___で寝ている」→ 「ソファ」「布団」「こたつ」
「犬が___で寝ている」→ 「ソファ」「庭」「ケージ」
猫と犬は似たような文脈で使われる → 意味が近い
猫と経済は全く異なる文脈で使われる → 意味が遠い
この「文脈を共有する単語は意味が近い」を数学的に実現したのが Word2Vec。
Word2Vec:Skip-gram モデル
「中心の単語から周辺の単語を予測する」タスクを解くことで、副産物としてベクトル表現が得られる。
入力テキスト: 「猫が ソファ で 寝ている」
中心語=「ソファ」、ウィンドウサイズ=2 のとき:
訓練データ:
(ソファ, 猫) — 予測すべき
(ソファ, が) — 予測すべき
(ソファ, で) — 予測すべき
(ソファ, 寝ている) — 予測すべき
import torch
import torch.nn as nn
class SkipGram(nn.Module):
def __init__(self, vocab_size: int, embed_dim: int = 100):
super().__init__()
# 2つの埋め込み行列
self.center_embedding = nn.Embedding(vocab_size, embed_dim)
self.context_embedding = nn.Embedding(vocab_size, embed_dim)
def forward(self, center_word, context_word):
# 中心語と文脈語のベクトルを取得
center_vec = self.center_embedding(center_word) # (batch, embed_dim)
context_vec = self.context_embedding(context_word) # (batch, embed_dim)
# 内積 = 「この2つの単語が共起する度合い」のスコア
score = (center_vec * context_vec).sum(dim=1)
return score
# 訓練: 実際の共起ペア(正例)のスコアを高く、
# ランダムな組み合わせ(負例)のスコアを低くする
Negative Sampling
語彙全体のソフトマックスは計算コストが高すぎる(語彙数5万のソフトマックスを毎回計算)。代わりに「正例1つ + ランダムな負例 k 個」だけで学習する。
def negative_sampling_loss(model, center, positive, negatives):
"""
正例: 実際に共起した単語ペア → スコアを高く
負例: ランダムに選んだ単語ペア → スコアを低く
"""
pos_score = model(center, positive)
pos_loss = -torch.log(torch.sigmoid(pos_score))
neg_scores = model(center.expand_as(negatives), negatives)
neg_loss = -torch.log(torch.sigmoid(-neg_scores)).sum()
return pos_loss + neg_loss
Word2Vec の結果:単語の演算
訓練後のベクトルには意味的な構造が現れる。
# 有名な例
king - man + woman ≈ queen
# ベクトル空間上で「性別」の軸に沿った平行移動
tokyo - japan + france ≈ paris
# 「首都」の関係が軸として現れる
walking - walk + swim ≈ swimming
# 「現在進行形」の変換が現れる
なぜこうなるか:
king と queen は同じ文脈(「国の」「統治する」「王座」)で使われるが、
king は man と共通の文脈(「彼は」「男性の」)を持ち、
queen は woman と共通の文脈(「彼女は」「女性の」)を持つ。
この共有パターンが、ベクトル空間上で規則的な構造として現れる。
Embedding Layer:LLM での単語の数値化
Word2Vec は「事前に訓練した固定ベクトル」を使うが、LLM はEmbedding Layer を含むモデル全体を end-to-end で訓練する。
class Embedding(nn.Module):
def __init__(self, vocab_size: int, embed_dim: int):
super().__init__()
# vocab_size × embed_dim の行列(学習可能なパラメータ)
self.weight = nn.Parameter(torch.randn(vocab_size, embed_dim))
def forward(self, token_ids):
# token_ids: (batch, seq_len)
# 単なるルックアップ(one-hot × 行列 = 行の取り出し)
return self.weight[token_ids] # (batch, seq_len, embed_dim)
# GPT-3 の場合:
# vocab_size = 50,257
# embed_dim = 12,288
# Embedding のパラメータ数 = 50,257 × 12,288 ≈ 6.2億
One-Hot × Embedding Matrix = ルックアップ:
one_hot("猫") = [0, 0, 1, 0, 0, ...] (猫のインデックスだけ1)
Embedding Matrix:
[0.1, 0.3, 0.5] ← "の"
[0.2, 0.4, 0.6] ← "犬"
[0.7, 0.8, 0.9] ← "猫" ← この行が取り出される
...
one_hot × Matrix = [0.7, 0.8, 0.9](猫のベクトル)
→ 実装上は行列積ではなくインデックスアクセスで済む(高速)
Word2Vec から Contextual Embeddings へ
Word2Vec の限界:同じ単語は常に同じベクトル。
「bank」= 銀行? 川岸?
Word2Vec: 常に同じベクトル(文脈を考慮しない)
LLM: 「I went to the bank to deposit money」→ 銀行のベクトル
「I sat on the bank of the river」→ 川岸のベクトル
LLM の Embedding は文脈に応じて動的に変わる。これが Transformer の Attention(Ch.7)で実現される。
まとめ
| 手法 | 特徴 | 限界 |
|---|---|---|
| One-Hot | 最も単純、意味なし | 高次元、意味の近さなし |
| Word2Vec | 意味の近さを反映、演算可能 | 文脈を考慮しない(静的) |
| GloVe | 共起統計を行列分解 | 同上(静的) |
| ELMo | 文脈を考慮(BiLSTM) | 双方向だが浅い |
| BERT / GPT | 文脈を深く考慮(Transformer) | 計算コスト大 |
次章では、「系列データ」を扱うための RNN/LSTM と、それがなぜ Transformer に置き換えられたかを見ていく。