目次を表示する

LLM完全解説:スクラッチから理解する大規模言語モデル

単語の数値化 ── 分布仮説と Word2Vec

単語の数値化 ── 分布仮説と Word2Vec

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


単語埋め込み — 分布仮説・Word2Vec・Embedding Layer

この章で何ができるようになるか:「意味が近い単語はベクトル空間でも近い」がどうやって実現されるかを理解し、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 に置き換えられたかを見ていく。