Tech カテゴリのイメージ

埋め込みとは?トークンIDをLLMが計算できるベクトルへ変える

トークン化によって文章が [10, 13, 14] のようなID列になっても、IDの大小には意味がありません。14番が10番より「大きな概念」というわけではないからです。LLMは各IDを埋め込み表の行番号として使い、計算に使えるベクトルへ置き換えます。

埋め込みは、トークンごとに用意された複数の学習可能な数値です。語彙数を V、ベクトルの次元数を D とすれば、埋め込み表は V × D の行列になります。トークンIDが7なら7行目を取り出します。

one-hotベクトルとの違い

語彙が5個なら、ID 2を [0, 0, 1, 0, 0] と表す方法もあります。これはどの項目かを区別できますが、語彙数と同じ長さが必要で、どの二つのトークンも異なるという情報しか持ちません。

埋め込み表を使えば、ID 2を [0.8, 0.1, -0.4] のような短いベクトルへ変えられます。この数値は人が各軸へ「動物らしさ」「大きさ」と名前を付けて設定するものではありません。学習の損失を小さくする過程で、他の重みと一緒に調整されます。

埋め込み表から行を取り出してみる

次の例では、4語の埋め込みを2次元にしています。値は説明用に手で決めたもので、学習済みモデルから取得したものではありません。

import numpy as np
vocab = {"猫": 0, "犬": 1, "自動車": 2, "道路": 3}
embedding_table = np.array([
[0.90, 0.10], # 猫
[0.80, 0.20], # 犬
[0.10, 0.90], # 自動車
[0.20, 0.80], # 道路
])
token_ids = np.array([vocab["猫"], vocab["道路"]])
vectors = embedding_table[token_ids]
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
print("ID:", token_ids.tolist())
print("埋め込み:\n", vectors)
print(f"猫と犬: {cosine_similarity(embedding_table[0], embedding_table[1]):.3f}")
print(f"猫と自動車: {cosine_similarity(embedding_table[0], embedding_table[2]):.3f}")

出力:

ID: [0, 3]
埋め込み:
[[0.9 0.1]
[0.2 0.8]]
猫と犬: 0.991
猫と自動車: 0.220

配列 embedding_table[token_ids] が行の取り出しです。入力が2トークン、埋め込み次元が2なので、結果の形は 2 × 2 になります。PyTorchの nn.Embedding も、公式資料では固定された辞書サイズと埋め込み次元を持つ単純な検索表として説明されています。

後半ではベクトルの向きを比べるコサイン類似度を計算しました。ただし、この結果から「埋め込みとは似た言葉を近くへ置く手法だ」と言い切るのは正確ではありません。例の値をそう見えるよう手で置いたためです。実モデルの埋め込みがどんな関係を表すかは、学習目的とデータによって決まります。

同じトークンなら最初のベクトルも同じ

通常の検索表では、同じトークンIDは文中の場所が違っても同じ初期埋め込みになります。「銀行でお金を下ろす」と「川の土手」に同じ表記の語が現れた場合、検索直後のベクトルだけで文脈ごとの意味を確定するわけではありません。

Transformerの層を通ると、周囲のトークンとの関係を反映した表現へ変化します。埋め込み表から取り出したベクトルと、各層を通過した文脈依存のベクトルは区別して考える必要があります。

並び順は埋め込みだけでは分からない

「猫が犬を追う」と「犬が猫を追う」には同じ種類のトークンが含まれていても、意味は違います。トークン埋め込みを検索するだけでは、それぞれが何番目に置かれたかという情報を持てません。

原論文のTransformerは、入力埋め込みと同じ次元の位置エンコーディングを加算しています。つまりTransformerへ入る表現は、単に「どのトークンか」だけではなく「どの位置か」という情報も含みます。位置情報の作り方には複数の方式があり、すべての現代的なLLMが原論文と同じ方式を使うわけではありません。

埋め込みも学習される重み

埋め込み表は事前に完成した辞書ではありません。学習開始時の値から、誤差逆伝播で勾配を受け取って更新されます。ある学習例で使われたトークンの行には、その予測損失に基づく更新が伝わります。

ここまでで、文章は「文字列 → トークンID列 → ベクトル列」へ変わりました。次は位置情報を加える理由を確かめると、Attentionへ渡る入力の形が見えてきます。

講座の前後記事

参照資料