埋め込み(Embedding)――なぜ単語のID番号を、数千次元の「空間座標」に写すのか?
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(整数)の大小には、意味の近さが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つの致命的な物理的欠陥を抱えています。
① 直交の悲劇(意味の距離がゼロ)
高校数学で習う通り、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次元]
計算すらしていない:ただ配列の「行」を引っこ抜くだけ
埋め込み処理の正体は、数式で言えば「One-Hotベクトルと重み行列の掛け算」ですが、実装上はそんな無駄な掛け算は行いません。
**「入力されたトークンID(例: 3797)を行番号として、あらかじめメモリ上に確保された行列の 3797 行目を、定数時間 $O(1)$ でそのまま引っこ抜いて(スライスして)くるだけ」**です。
- 1単語目(ID: 464) $\to$ 行列の 464 行目(4096個の小数)を取り出す
- 2単語目(ID: 3797) $\to$ 行列の 3797 行目(4096個の小数)を取り出す
埋め込み層とは、賢いアルゴリズムではなく、**「10万個の単語それぞれに、4096個の浮動小数点が割り当てられた巨大な住所録」**に過ぎないのです。
4. なぜ「意味」が宿るのか?:分布仮説とバックプロパゲーションの魔法
「ただの住所録なら、最初はランダムな乱数が入っているだけではないか? なぜ学習が進むと、似た意味の単語同士が空間上で寄り添うようになるのか?」
ここが深層学習の真骨頂です。 言語学の世界に、J.R.ファース(J.R. Firth)が1957年に提唱した有名な金言があります。
「単語の意味は、その単語の『周辺の仲間(コンテキスト)』によって決まる(分布仮説:Distributional Hypothesis)」
人間が言葉の意味を覚えるプロセスを思い出してください。
「ある日、モフモフした [ ??? ] がニャーと鳴きながら喉をゴロゴロ鳴らした」
この文章を見れば、未知の言葉 [ ??? ] が入っていても、文脈から「猫のような小動物だな」と誰でも分かります。
LLMの事前学習では、第1話で解説した通り、何テラバイトものテキストを使って「次に来る1単語」を当てる過酷なゲームを繰り返します。
- 「猫」という単語も「犬」という単語も、「飼う」「エサ」「撫でる」「動物病院」という似通った周辺単語と一緒に登場します。
- 次の単語を正しく当てるためには、「猫」のベクトルも「犬」のベクトルも、Transformerの演算の中で同じような計算結果(テンソルの値)を出力できるような数値になっていなければなりません。
- 予測が外れるたびに、誤差逆伝播(バックプロパゲーション)の勾配が埋め込みテーブルの該当行へフィードバックされ、4096個の小数が少しずつ書き換えられます。
何千億回もの反復学習の果てに、「似た文脈で使われる単語同士の4096次元ベクトル」は、磁石が引き合うように高次元空間の同じ領域へと自然に集積していくのです。
5. 高次元空間の幾何学:「王様 − 男 + 女 = 女王」が成立する物理的理由
2013年、トマー・ミコロフ(Tomas Mikolov)らが発表したWord2vecによって、世界中の研究者を驚愕させた数式があります。
$$\vec{v}{\text{King}} - \vec{v}{\text{Man}} + \vec{v}{\text{Woman}} \approx \vec{v}{\text{Queen}}$$
「王様」のベクトルから「男」のベクトルを引き、「女」のベクトルを足すと、空間上で最も近い位置にあるベクトルが「女王」になる。 なぜ、こんなSFのような単語の足し算・引き算が成立するのでしょうか?
その秘密は、高次元空間の中に自発的に形成される**「平行な関係性ベクトル(意味の軸)」**にあります。
空間の特定の「向き」が、特定の「概念」を担う
4096次元という広大な空間の中では、特定の次元やベクトルの向きが、特定の概念を表現するようになります。
- 「男」から「女」へ向かうベクトル:【女性化ベクトル $\vec{v}_{\text{female}}$】
- 「日本」から「東京」へ向かうベクトル:【首都化ベクトル $\vec{v}_{\text{capital}}$】
- 「歩く(walk)」から「歩いた(walked)」へ向かうベクトル:【過去形化ベクトル $\vec{v}_{\text{past}}$】
「王様」という概念は、大雑把に言えば「君主という権力」+「男性」という成分で構成されています。 そこから「男性」という方向の成分を引き算し、「女性」という方向の成分を足し算すれば、残るのは「君主という権力」+「女性」――すなわち**「女王」の座標に物理的に着地する**のは、幾何学的に極めて自然な必然なのです。
触って試す:ベクトル演算の体験
👑 ベクトル演算の体験(王 − 男 + 女 = ?)
単語を「王族度」と「女性度」の2次元に置いた単純化した空間です。「次へ」を押すと、王 − 男 + 女 の計算過程が矢印で描かれていきます。結果はどの単語の近くに着地するでしょうか?
「次へ」を押して計算を始めましょう。
コサイン類似度:高次元空間の「定規」
高次元空間において、2つの単語がどれくらい似ているかを測定する絶対の定規が**「コサイン類似度(Cosine Similarity)」**です。
$$\cos(\theta) = \frac{\vec{A} \cdot \vec{B}}{\Vert{}\vec{A}\Vert{} \Vert{}\vec{B}\Vert{}}$$
ベクトルの「矢印の長さ」を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階のテンソルが、埋め込み層をくぐり抜けた瞬間にどう変化したかを目に焼き付けてください。
【テンソルShapeの変化】
入力: [Batch_Size, Seq_Len]
↓ (Embedding Lookup)
出力: [Batch_Size, Seq_Len, Hidden_Dim]
Batch_Size:同時に並列処理する文章の数(例: 2文)。Seq_Len:文章の長さ(トークン数、例: 5単語)。Hidden_Dim:新しく出現した第3の軸! 各単語が身にまとった「意味の座標(例: 4096次元)」。
この瞬間、ただの数字の平坦なリストだったテキストは、**「1単語あたり4096次元の情報を背負った立派な3階テンソル」**へと覚醒しました。
8. まとめ:埋め込みとは「言葉を物理法則の乗るレールに載せること」である
埋め込み(Embedding)の本質を総括すると、以下のようになります。
- 整数IDからの脱却: トークン番号の大小には意味がないため、そのままで四則演算を行うとモデルは破綻する。
- One-Hotの直交を回避: すべての単語が直交するスカスカな空間を捨て、数千次元の濃密な連続空間(Dense Vector)へ圧縮する。
- Lookup Tableの物理: 実態は高度な計算ではなく、巨大な重み行列から該当する行を引っこ抜く(スライスする)だけの高速な参照テーブルである。
- 意味の幾何学: 分布仮説に基づき大量の文脈を学習することで、空間内に「性別」「時制」などの意味の軸が自発的に結晶化し、ベクトルの加減算が可能になる。
言葉は、高次元空間の座標となった。 しかし、この段階の埋め込みベクトルには、まだ致命的な欠落が1つあります。
それは、「前後の文脈が一切考慮されていない」ということです。 この時点では、「銀行(Bank)」のベクトルも、「川の土手(River bank)」のベクトルも、同じ辞書の同じ行から引っこ抜かれた全く同一の数値です。文脈によって意味が変化していません。
この静止した単語ベクトルたちに、前後の文脈を読ませ、互いに視線を交わし合わせて「生きた文脈の意味」へと進化させる心臓部。 それこそが、次回解剖するTransformerの最大の革命、**「第4話:Self-Attention完全解剖(前編)――なぜ『内積』で関係性が分かるのか?」**です。
📝 3行で頭に入る『試験・実務の要点まとめ』
- One-Hotの限界と密ベクトル: One-Hot表現は全単語が直交(内積0)して類似度を表現できずメモリも浪費するため、4096次元等の密な連続空間(Embedding)へ圧縮して意味の近接性を表現する。
- Lookup Tableの物理構造:
nn.Embeddingの正体は複雑な関数ではなく[Vocab_Size, Hidden_Dim]の重み行列であり、トークンIDを行番号として $O(1)$ でスライス取得する参照テーブルである。 - Shapeの3次元化: 埋め込み層を通過することで、データは
[Batch, Seq_Len]の整数ID列から、各トークンが特徴量次元を持った[Batch, Seq_Len, Hidden_Dim]の3階テンソルへと覚醒する。