埋め込み(Embedding)――なぜ単語のID番号を、数千次元の「空間座標」に写すのか?

2026-11-05 / シリーズ「LLM・AIの内臓」第3話 / LLM, Embedding, 埋め込み, ベクトル空間, Transformer, Python, 線形代数

1. 素朴な疑問:なぜ単語の「番号」のまま計算してはいけないのか?

前話(第2話)で、私たちはBPEトークナイザによって、あらゆるテキストが「整数の配列(トークンID)」へと変換される物理を学びました。

"The cat sat on the mat" 
  ↓ トークナイズ
[464, 3797, 3332, 319, 262, 2603]

テキストは綺麗に数字の並びになりました。コンピュータは数字を扱う計算機です。 ならば、この「464」や「3797」という整数を、そのまま足したり掛けたりしてニューラルネットワークの計算を始めればいいのではないでしょうか?

なぜ現代のすべてのLLM(GPT-4、Claude、Llama 3など)は、このせっかく手に入れた整数を捨て、各単語をわざわざ「4096個や8192個もの小数が並んだ巨大なベクトル(配列)」へ引き延ばしてから計算を始めるのでしょうか?

整数IDのままの計算(大破綻) 464('The') 3797('cat') 4261('engine') に化けてしまう! 464 + 3797 = 4261 ??? 整数の大小関係には「意味の近さ」が 1ミリも反映されていない 埋め込みベクトル(高次元空間への写像) ID: 3797('cat') Embedding Lookup [0.12, -0.98, 0.44, …, 0.05] 4096次元の連続的な『意味の座標』 意味が似た単語同士が近くに集まる

結論から言えば、**「トークンID(整数)の大小には、意味の近さが1ミリも反映されていないから」**です。

辞書の中で、単語番号「4523」と「4524」が隣り合っていたとしても、それは単に辞書に並んだ順番が近かっただけで、2つの単語の意味には何の関係もありません(社員番号が連番の2人が、似た性格とは限らないのと同じです)。 整数IDのまま掛け算や足し算を行えば、意味のない数字が掛け合わされ、モデルは一瞬で論理崩壊を起こします。

単語を「計算可能な存在」に変えるには、整数という記号の牢獄から解き放ち、**「意味が似ている単語同士が近くに集まる連続的な多次元空間(ベクトル空間)」**へと写像しなければならないのです。


2. One-Hot表現の絶望:すべての単語が「完全に直交する」という物理限界

ベクトル化と聞いて、初学者が最初に思いつく素朴な手法が**「One-Hot(ワンホット)表現」**です。

語彙数が10万語あるなら、長さ10万の配列を用意し、自分の単語のインデックスだけを 1 にして、残り99,999箇所をすべて 0 にします。

リンゴ : [1, 0, 0, 0, 0, ..., 0]
バナナ : [0, 1, 0, 0, 0, ..., 0]
飛行機 : [0, 0, 0, 0, 1, ..., 0]

この表現は、コンピュータにとって明確です。しかし、機械学習においては2つの致命的な物理的欠陥を抱えています。

リンゴ [1, 0, 0] バナナ [0, 1, 0] 飛行機 [0, 0, 1] 90° 90° 90° どのペアも 内積 = 0(直交) すべての単語間の距離が 完全に等しくなってしまう 「リンゴとバナナが似ている」を 1ビットも表現できない! One-Hot空間:全単語が互いに90°を向いて直立している

① 直交の悲劇(意味の距離がゼロ)

高校数学で習う通り、2つのベクトルの類似度は**「内積(ドット積)」**で計算できます。

$$\text{内積} = \sum (A_i \times B_i)$$

One-Hotベクトルの内積を取ってみてください。 1 が立っている位置が互いに絶対に異なるため、どの単語同士の内積を計算しても、**答えは常に「0」**になります。 幾何学的に言えば、すべての単語が互いに「90度(直交)」を向いて直立している状態です。

「リンゴ」と「バナナ」の類似度もゼロ。「リンゴ」と「超音速旅客機」の類似度もゼロ。 全単語が等間隔に孤立しているため、モデルは「果物という共通点」を空間的に認識することが原理的にできません。

② スパース(疎)の地獄(メモリの浪費)

1つの単語を表すために、99.999%が「0」で埋め尽くされた10万次元の巨大ベクトルをメモリに確保するのは、極めて無駄です。 計算のたびにゼロを掛け算し続けることになり、GPUのレジスタとメモリ帯域を無意味に食い潰します。

この絶望を解決するのが、全要素に有効な小数がギュッと詰まった**「密ベクトル(Dense Vector / 埋め込みベクトル)」です。 10万次元のスカスカな空間を、わずか4096次元(または数千次元)の濃密な連続空間**へとギュッと圧縮するのです。


3. 埋め込み層(Embedding Layer)の正体:実はただの「巨大な引き当て表」

「4096次元の密ベクトルへ変換する」と聞くと、何か高度で複雑な非線形関数やディープな数式を通しているように思えます。 しかし、PyTorchのソースコードを開くと、そこには拍子抜けするほど原始的な物理が書かれています。

PyTorchの nn.Embedding(vocab_size, hidden_dim) とは、一体何者なのでしょうか?

その正体は、**「ただの巨大な2次元の重み行列(Lookup Table:参照テーブル)」**です。

【埋め込み行列 W_emb の実体】
形状(Shape): [Vocab_Size, Hidden_Dim]
例: [100,000語, 4096次元]
🧾 入力:トークンID = 3797('cat')が到着 「3797行目をそのまま抜き出せ!」 🗄️ 埋め込み重み行列(Lookup Table)Shape: [100000, 4096] W_emb[3797, :] をスライスして返却 📤 出力:埋め込みベクトル Shape: [4096] ⚡ 計算処理(足し算・掛け算)は一切なし!ただの配列スライス = O(1)

計算すらしていない:ただ配列の「行」を引っこ抜くだけ

埋め込み処理の正体は、数式で言えば「One-Hotベクトルと重み行列の掛け算」ですが、実装上はそんな無駄な掛け算は行いません。

**「入力されたトークンID(例: 3797)を行番号として、あらかじめメモリ上に確保された行列の 3797 行目を、定数時間 $O(1)$ でそのまま引っこ抜いて(スライスして)くるだけ」**です。

埋め込み層とは、賢いアルゴリズムではなく、**「10万個の単語それぞれに、4096個の浮動小数点が割り当てられた巨大な住所録」**に過ぎないのです。


4. なぜ「意味」が宿るのか?:分布仮説とバックプロパゲーションの魔法

「ただの住所録なら、最初はランダムな乱数が入っているだけではないか? なぜ学習が進むと、似た意味の単語同士が空間上で寄り添うようになるのか?」

ここが深層学習の真骨頂です。 言語学の世界に、J.R.ファース(J.R. Firth)が1957年に提唱した有名な金言があります。

「単語の意味は、その単語の『周辺の仲間(コンテキスト)』によって決まる(分布仮説:Distributional Hypothesis)」

人間が言葉の意味を覚えるプロセスを思い出してください。 「ある日、モフモフした [ ??? ] がニャーと鳴きながら喉をゴロゴロ鳴らした」 この文章を見れば、未知の言葉 [ ??? ] が入っていても、文脈から「猫のような小動物だな」と誰でも分かります。

文脈:「飼い主が [ ??? ] にエサをやり、喉を撫でた」 予測に役立つ方向へ勾配 予測に役立つ方向へ勾配 文脈が違うため遠ざかる 「猫」のベクトル 「犬」のベクトル 「ノートPC」のベクトル 🐈🐕 「猫」と「犬」は空間上で自然と隣人になる!

LLMの事前学習では、第1話で解説した通り、何テラバイトものテキストを使って「次に来る1単語」を当てる過酷なゲームを繰り返します。

何千億回もの反復学習の果てに、「似た文脈で使われる単語同士の4096次元ベクトル」は、磁石が引き合うように高次元空間の同じ領域へと自然に集積していくのです。


5. 高次元空間の幾何学:「王様 − 男 + 女 = 女王」が成立する物理的理由

2013年、トマー・ミコロフ(Tomas Mikolov)らが発表したWord2vecによって、世界中の研究者を驚愕させた数式があります。

$$\vec{v}{\text{King}} - \vec{v}{\text{Man}} + \vec{v}{\text{Woman}} \approx \vec{v}{\text{Queen}}$$

「王様」のベクトルから「男」のベクトルを引き、「女」のベクトルを足すと、空間上で最も近い位置にあるベクトルが「女王」になる。 なぜ、こんなSFのような単語の足し算・引き算が成立するのでしょうか?

その秘密は、高次元空間の中に自発的に形成される**「平行な関係性ベクトル(意味の軸)」**にあります。

王様(King) 女王(Queen) 矢印: 男(Man) 女(Woman) 矢印: パリ(Paris) フランス(France) 矢印: 東京(Tokyo) 日本(Japan) 矢印: 「男 → 女」の矢印と「王様 → 女王」の矢印は、 4096次元空間の中で『完全に同じ向きと長さ』で平行に並ぶ!

空間の特定の「向き」が、特定の「概念」を担う

4096次元という広大な空間の中では、特定の次元やベクトルの向きが、特定の概念を表現するようになります。

「王様」という概念は、大雑把に言えば「君主という権力」+「男性」という成分で構成されています。 そこから「男性」という方向の成分を引き算し、「女性」という方向の成分を足し算すれば、残るのは「君主という権力」+「女性」――すなわち**「女王」の座標に物理的に着地する**のは、幾何学的に極めて自然な必然なのです。

触って試す:ベクトル演算の体験

👑 ベクトル演算の体験(王 − 男 + 女 = ?)

単語を「王族度」と「女性度」の2次元に置いた単純化した空間です。「次へ」を押すと、王 − 男 + 女 の計算過程が矢印で描かれていきます。結果はどの単語の近くに着地するでしょうか?

王族度 → ↑ 女性度 原点 男 女 王 女王 りんご(無関係)

「次へ」を押して計算を始めましょう。

コサイン類似度:高次元空間の「定規」

高次元空間において、2つの単語がどれくらい似ているかを測定する絶対の定規が**「コサイン類似度(Cosine Similarity)」**です。

$$\cos(\theta) = \frac{\vec{A} \cdot \vec{B}}{\Vert{}\vec{A}\Vert{} \Vert{}\vec{B}\Vert{}}$$

角度 0°(同じ向き) cos(0°) = 1.0 ★ 完全に同義 角度 90°(直交) cos(90°) = 0.0 ★ 無関係 角度 180°(真逆) cos(180°) = -1.0 ★ 正反対の概念

ベクトルの「矢印の長さ」を1に正規化し、2つの矢印が**「どれくらい同じ方向を向いているか(角度 $\theta$)」**だけを測ります。 コサイン類似度が 1.0 に近ければ同義語、0.0 なら無関係、マイナスなら反対の性質を持つことを示します。


6. 完全実装:NumPyで作る「最小の埋め込み層とベクトル幾何学」

それでは、ここまでの理論をコードで完全に体感してみましょう。 外部の深層学習フレームワーク(PyTorchなど)を使わず、NumPyの基本的な行列操作だけで、埋め込み層の引き当て(Lookup)と、単語ベクトルの加減算アナロジーを再現します。

ファイル名を min_embedding.py として保存してください。

import numpy as np

class MiniEmbedding:
    """埋め込み層の最小実装(重み行列のLookup Table)"""
    def __init__(self, vocab_size: int, hidden_dim: int):
        self.vocab_size = vocab_size
        self.hidden_dim = hidden_dim
        # 再現性のための乱数シード
        np.random.seed(42)
        # 埋め込み重み行列: Shape [Vocab_Size, Hidden_Dim]
        # 正規分布で初期化
        self.weights = np.random.randn(vocab_size, hidden_dim)

    def forward(self, token_ids: list[int]) -> np.ndarray:
        """トークンIDの配列を行番号として、該当する行をスライスして取り出す"""
        # NumPyのアドバンスト・インデックス参照 (O(1)のLookup)
        return self.weights[token_ids]


def cosine_similarity(v1: np.ndarray, v2: np.ndarray) -> float:
    """2つのベクトル間のコサイン類似度(内積 / ノルムの積)を計算"""
    norm1 = np.linalg.norm(v1)
    norm2 = np.linalg.norm(v2)
    if norm1 == 0 or norm2 == 0:
        return 0.0
    return float(np.dot(v1, v2) / (norm1 * norm2))


# -------------------------------------------------------------
# 実験:意味のベクトル空間シミュレーション
# -------------------------------------------------------------
if __name__ == "__main__":
    print("=== フェーズ1: 埋め込み層のテンソルShape変化 ===")
    
    # 語彙数 10,000語、隠れ層次元 4次元 の小さな世界
    emb_layer = MiniEmbedding(vocab_size=10000, hidden_dim=4)
    
    # 入力トークンID: "The cat sat" を模した3つのトークン
    input_ids = [464, 3797, 3332]
    print(f"入力トークンID列 (Shape: [3])        : {input_ids}")
    
    # 埋め込み層を通過!
    embedded_vectors = emb_layer.forward(input_ids)
    print(f"出力埋め込みテンソル (Shape: [3, 4]):\n{embedded_vectors}\n")

    print("=== フェーズ2: 単語ベクトルの足し算・引き算(幾何学実験) ===")
    
    # 概念を分かりやすくシミュレートするため、手動で特徴ベクトルを設計
    # 次元の意味: [王族度, 女性度, 人間度]
    word_space = {
        "man":   np.array([0.0, 0.0, 1.0]),
        "woman": np.array([0.0, 1.0, 1.0]),
        "king":  np.array([1.0, 0.0, 1.0]),
        "queen": np.array([1.0, 1.0, 1.0]),
        "apple": np.array([0.0, 0.0, 0.0]) # 無関係な単語
    }

    # ベクトル演算: King - Man + Woman
    v_king = word_space["king"]
    v_man = word_space["man"]
    v_woman = word_space["woman"]

    target_vector = v_king - v_man + v_woman
    print(f"計算結果ベクトル (King - Man + Woman): {target_vector}")

    # 最も類似度が高い単語を探す
    print("\n【各単語とのコサイン類似度】")
    for word, vec in word_space.items():
        sim = cosine_similarity(target_vector, vec)
        print(f"  {word:<8}: 類似度 = {sim:.4f}")

実行結果

=== フェーズ1: 埋め込み層のテンソルShape変化 ===
入力トークンID列 (Shape: [3])        : [464, 3797, 3332]
出力埋め込みテンソル (Shape: [3, 4]):
[[ 0.49671415 -0.1382643   0.64768854  1.52302986]
 [-0.23413695  1.57921282  0.76743473 -0.46947439]
 [ 0.54256004 -0.46341769 -0.46572975  0.24196227]]

=== フェーズ2: 単語ベクトルの足し算・引き算(幾何学実験) ===
計算結果ベクトル (King - Man + Woman): [1. 1. 1.]

【各単語とのコサイン類似度】
  man     : 類似度 = 0.5774
  woman   : 類似度 = 0.8165
  king    : 類似度 = 0.8165
  queen   : 類似度 = 1.0000
  apple   : 類似度 = 0.0000

queen のコサイン類似度が 1.0000(完全一致) になりました! 高次元空間の中で特定の概念の方向(この例では第2成分の「女性度」)が正しく引き算・足し算された結果、目的の単語へ寸分の狂いもなくナビゲーションされたことが確認できます。


7. テンソルの視点:Shapeが覚醒する瞬間

ここで、本連載の絶対の羅針盤である**「テンソルの形状(Shape)」**を確認します。 トークナイザから渡された2階のテンソルが、埋め込み層をくぐり抜けた瞬間にどう変化したかを目に焼き付けてください。

入力テンソル X_tokens Shape: [Batch_Size, Seq_Len] 例:[2, 5](文数2・文長5の整数ID) 重み行列 W_emb Shape: [Vocab_Size, Hidden_Dim] 例:[50000, 4096] Embedding Lookup 出力テンソル X_emb(覚醒!) Shape: [Batch_Size, Seq_Len, Hidden_Dim] 例:[2, 5, 4096] 第4話:Self-Attention層へ! 各単語が4096個の武器を持って内積バトルを開始
【テンソルShapeの変化】
入力: [Batch_Size, Seq_Len]
       ↓ (Embedding Lookup)
出力: [Batch_Size, Seq_Len, Hidden_Dim]

この瞬間、ただの数字の平坦なリストだったテキストは、**「1単語あたり4096次元の情報を背負った立派な3階テンソル」**へと覚醒しました。


8. まとめ:埋め込みとは「言葉を物理法則の乗るレールに載せること」である

埋め込み(Embedding)の本質を総括すると、以下のようになります。

  1. 整数IDからの脱却: トークン番号の大小には意味がないため、そのままで四則演算を行うとモデルは破綻する。
  2. One-Hotの直交を回避: すべての単語が直交するスカスカな空間を捨て、数千次元の濃密な連続空間(Dense Vector)へ圧縮する。
  3. Lookup Tableの物理: 実態は高度な計算ではなく、巨大な重み行列から該当する行を引っこ抜く(スライスする)だけの高速な参照テーブルである。
  4. 意味の幾何学: 分布仮説に基づき大量の文脈を学習することで、空間内に「性別」「時制」などの意味の軸が自発的に結晶化し、ベクトルの加減算が可能になる。

言葉は、高次元空間の座標となった。 しかし、この段階の埋め込みベクトルには、まだ致命的な欠落が1つあります。

それは、「前後の文脈が一切考慮されていない」ということです。 この時点では、「銀行(Bank)」のベクトルも、「川の土手(River bank)」のベクトルも、同じ辞書の同じ行から引っこ抜かれた全く同一の数値です。文脈によって意味が変化していません。

この静止した単語ベクトルたちに、前後の文脈を読ませ、互いに視線を交わし合わせて「生きた文脈の意味」へと進化させる心臓部。 それこそが、次回解剖するTransformerの最大の革命、**「第4話:Self-Attention完全解剖(前編)――なぜ『内積』で関係性が分かるのか?」**です。


📝 3行で頭に入る『試験・実務の要点まとめ』


確認クイズ

📝 確認クイズ(3問)