前回までの実験で、"Hello" は次のように処理されていることを確認した。
"Hello"
↓
Tokenizer
↓
Token ID = 9707
↓
token_embd.weight
↓
GET_ROWS
↓
2048個の f32
ここで疑問が出てきた。
9707 はすでに数字なのに、なぜそのままモデルに使わないのだろう?
なぜわざわざ 9707 を使って、token_embd.weight から2048個もの数を取り出す必要があるのか。
今回はこれを実験してみる。
まず Token ID の隣を見てみる
"Hello" の Token ID は 9707 だった。
では、その前後の Token ID は何なのだろう。
for id in 9704 9705 9706 9707 9708 9709 9710
do
echo -n "$id : "
curl -s http://localhost:8080/detokenize \
-H "Content-Type: application/json" \
-d "{\"tokens\":[$id]}"
echo
done
結果:
9704 : {"content":" hook"}
9705 : {"content":" documentation"}
9706 : {"content":"Parameters"}
9707 : {"content":"Hello"}
9708 : {"content":"vt"}
9709 : {"content":" articles"}
9710 : {"content":" west"}
並べてみる。
9704 → " hook"
9705 → " documentation"
9706 → "Parameters"
9707 → "Hello"
9708 → "vt"
9709 → " articles"
9710 → " west"
Token ID 上では、
9706
9707
9708
は非常に近い。
しかし、
Parameters
Hello
vt
を見る限り、Token ID が近いからといって、Token の意味まで近いようには見えない。
少なくとも、
Token ID の数値的な近さから、Token の意味的な近さをそのまま読み取ることはできなさそうだ。
Token ID を直接渡してみる
今度は文字列 "Hello" ではなく、Token ID 9707 を直接渡してみた。
curl -N http://localhost:8080/completion \
-H "Content-Type: application/json" \
-d '{
"prompt": [9707],
"stream": true,
"n_predict": 1,
"temperature": 0
}'
結果には、
"tokens_evaluated":1
"prompt":"Hello"
と表示された。
つまり、
[9707]
↓
Token ID 9707
↓
"Hello"
として処理されている。
ただし、これは 9707.0 という普通の数値を、そのままモデル計算に使っているわけではない。
以前確認した llama-eval-callback では、
number of input tokens = 1
9707
common_debug_cb_eval:
embd = (f32)
GET_ROWS(
token_embd.weight{2048, 151936, 1, 1},
inp_tokens{1, 1, 1, 1}
)
= {2048, 1, 1, 1}
となっていた。
つまり実際には、
9707
↓
inp_tokens
↓
GET_ROWS
↓
2048個の f32
という処理が行われている。
2048個の値を全部取り出してみる
これまで llama-eval-callback では、
[-0.0021, 0.0273, -0.0070, ..., -0.0360, 0.0022, 0.0022]
のように途中が ... で省略されていた。
common/debug.cpp を確認すると、
common_debug_print_tensor(
data,
t->type,
t->ne,
t->nb,
3,
pimpl->abort_on_nan
);
となっていた。
さらに common_debug_print_tensor() を見る。
if (i0 == n && ne[0] > 2 * n) {
LOG(" ..., ");
i0 = ne[0] - n;
}
この 3 によって、
先頭3個
...
末尾3個
だけが表示されていた。
そこで embd については2048個すべて表示するように変更し、再実行した。
結果:
number of input tokens = 1
9707
embd = (f32)
GET_ROWS(
token_embd.weight{2048, 151936, 1, 1},
inp_tokens{1, 1, 1, 1}
)
= {2048, 1, 1, 1}
そして、2048個すべての値を取得できた。
[
-0.0021,
0.0273,
-0.0070,
-0.0168,
...
]
これで Token ごとの2048個の値を比較できる。
まず5個の Token を比較する
次の5個を使ってみる。
1879 → " world"
4616 → "cat"
9706 → "Parameters"
9707 → "Hello"
9708 → "vt"
それぞれが1 Token であることも確認した。
1879 → 1 token
4616 → 1 token
9706 → 1 token
9707 → 1 token
9708 → 1 token
各 Token の2048個の値を取得して比較する。
ユークリッド距離で比較する
まず使ったのが ユークリッド距離(Euclidean distance)。
これは、2つのベクトルを「点」と考えたときの直線距離。
例えば2次元なら、
$$
A=(1,2)
$$
$$
B=(4,6)
$$
なので、
横の差 = 3
縦の差 = 4
距離は、
$$
\sqrt{3^2+4^2}=5
$$
となる。
三平方の定理と同じ考え方。
今回の場合、
$$
\mathbf{a}=(a_1,a_2,\ldots,a_{2048})
$$
$$
\mathbf{b}=(b_1,b_2,\ldots,b_{2048})
$$
とすると、2つのベクトル間のユークリッド距離は、
$$
d(\mathbf{a},\mathbf{b})
=
\sqrt{
(a_1-b_1)^2+
(a_2-b_2)^2+
\cdots+
(a_{2048}-b_{2048})^2
}
$$
となる。
実験結果:
| Token ID A | Token ID B | ID の距離 | 2048次元のユークリッド距離 |
|---|---|---|---|
| 9706 | 9707 | 1 | 1.298084 |
| 9707 | 9708 | 1 | 1.348704 |
| 9707 | 4616 | 5091 | 1.277564 |
| 9707 | 1879 | 7828 | 1.126124 |
| 4616 | 1879 | 2737 | 1.157123 |
面白い結果になった。
例えば、
9707 ↔ 9708
は Token ID が 1 しか違わない。
しかし2048次元上の距離は、
1.348704
だった。
一方、
9707 ↔ 1879
は Token ID が 7828 も離れている。
しかし2048次元上では、
1.126124
と、むしろ近かった。
Token ID 上では近い
9707 ↔ 9708
↓
2048次元では 1.348704
Token ID 上では遠い
9707 ↔ 1879
↓
2048次元では 1.126124
少なくともこの5個を見る限り、
Token ID の数値的な距離と、2048次元ベクトル間の距離は単純には対応していないようだ。
100個の Token でも試してみる
5個だけでは偶然かもしれない。
そこで vocabulary から100個の Token ID をランダムに抽出した。
import random
random.seed(42)
ids = random.sample(range(1000, 151936), 100)
for x in sorted(ids):
print(x)
100個から2つずつ選ぶと、
4950 pair
になる。
今回は tokenizer を経由せず、指定した Token ID を直接 llama_decode() に渡せるよう llama-eval-callback を少し変更した。
TOKEN_ID
↓
tokens.push_back(token_id)
↓
llama_decode()
↓
inp_tokens
↓
GET_ROWS
↓
2048 f32
100個すべてについて2048個の値を取得した。
loaded tokens: 100
pairs: 4950
Token ID の距離とユークリッド距離
100 Token、4950 pair について、
Token ID の距離
vs
2048次元のユークリッド距離
を比較した。
もし Token ID が単なる index で、その数値差が2048次元上の距離を直接決めないのであれば、
Token ID が遠い
↓
2048次元でも必ず遠い
というような単純な関係は出ないはず。
そのため、両者に単純な線形関係がなければ、Pearson の相関係数は 0 に近くなるのではないかと考えた。
結果:
Pearson correlation: -0.260185
詳細:
ID distance:
min = 21
mean = 47457.41
max = 146979
Embedding distance:
min = 0.386296
mean = 1.212602
max = 1.503143
予想していた 0 付近ではなく、
-0.260185
という負の相関が出た。
なぜだろう。
2048次元ベクトルのノルムを調べてみる
ユークリッド距離は、ベクトルの「向き」だけではなく「長さ」にも影響される。
そこで、2048次元ベクトル自身の長さである ノルム(norm) も調べてみた。
ノルムは、ベクトル全体の長さを表す値。
例えば2次元ベクトル、
$$
\mathbf{x}=(3,4)
$$
なら、
$$
|\mathbf{x}|
=
\sqrt{3^2+4^2}
=
5
$$
となる。
2048次元なら、
$$
\mathbf{x}
=
(x_1,x_2,\ldots,x_{2048})
$$
に対して、
$$
|\mathbf{x}|
=
\sqrt{
x_1^2+x_2^2+\cdots+x_{2048}^2
}
$$
となる。
実験結果:
Embedding norm vs Token ID
------------------------------------------------------------
Pearson correlation: -0.414962
norm min = 0.245843
norm mean = 0.852170
norm max = 1.078022
Token ID とベクトルの長さの間にも、
-0.414962
という負の相関が出た。
つまり、先ほどのユークリッド距離の -0.260185 には、
ベクトル自身の長さの違い
も影響している可能性がある。
コサイン距離でも比較する
そこで今度は、ベクトルの長さよりも「向き」に注目する コサイン距離(Cosine distance) を使ってみる。
例えば、
A = [1, 1]
B = [10, 10]
は長さが違う。
しかし向きは同じ。
A ↗
B ↗
2つのベクトル $\mathbf{a}$ と $\mathbf{b}$ のコサイン類似度は、
$$
\cos(\theta)
=
\frac{
\mathbf{a}\cdot\mathbf{b}
}{
|\mathbf{a}||\mathbf{b}|
}
$$
で計算できる。
今回は、そこから、
$$
\text{Cosine distance}
=
1-\cos(\theta)
$$
として距離を求めた。
大まかには、
Cosine distance ≈ 0
→ 向きが近い
Cosine distance ≈ 1
→ 向きがかなり違う
Cosine distance ≈ 2
→ ほぼ反対向き
と考えられる。
整理すると、
ユークリッド距離
→ 2つのベクトル間の距離
ノルム
→ ベクトル自身の長さ
コサイン距離
→ ベクトル同士の向きの違い
を見るものになる。
Token ID の距離とコサイン距離
100 Token、4950 pair について、
Token ID distance
vs
Cosine distance
を比較した。
結果:
ID distance vs cosine distance
------------------------------------------------------------
Pearson correlation: 0.059552
Cosine distance min = 0.823016
Cosine distance mean = 0.990434
Cosine distance max = 1.100715
今度は、
0.059552
となった。
かなり 0 に近い。
今回の100 Token、4950 pair の範囲では、
Token ID の数値的な距離と、2048次元ベクトルの向きの近さには、明確な線形関係は見られなかった。
なぜ Token ID をそのまま使わないのか?
もし Token ID を普通の数値として使うなら、
1879 -------- 4616 -------- 9706 - 9707 - 9708
という1本の数直線上の関係が自動的にできる。
例えば、
9707 と 9708 の距離 = 1
9707 と 1879 の距離 = 7828
となる。
つまり Token ID の数字そのものを使うと、
ID が近い
→ 近い
ID が遠い
→ 遠い
という1次元上の距離関係まで、一緒に持ち込むことになる。
しかし実際のモデルでは、
9707
↓
index
↓
token_embd.weight
↓
2048個の f32
となっている。
そして今回、
Token ID distance
vs
Cosine distance
Pearson correlation
= 0.059552
だった。
少なくとも今回観察した範囲では、
Token ID の数値的な近さと、2048次元ベクトルの向きの近さには、明確な線形関係は見られなかった。
つまり Token ID は、
モデルが数値として解釈する「9707」
というより、
token_embd.weight から
対応するデータを取り出すための index
として使われていることが分かった。
今回分かったこと
今回の実験をまとめる。
"Hello"
↓
Token ID = 9707
↓
9707 という数値そのものを
モデル計算に使うわけではない
↓
index として使用
↓
token_embd.weight
↓
2048個の f32
さらに100個の Token をランダムに抽出し、4950 pair を比較した。
Token ID distance
vs
Euclidean distance
→ r = -0.260185
Token ID
vs
vector norm
→ r = -0.414962
Token ID distance
vs
Cosine distance
→ r = 0.059552
ユークリッド距離では負の相関が見えた。
一方で、Token ID とベクトル自身の長さであるノルムにも相関が見られた。
そこで、ベクトルの長さの影響を受けにくいコサイン距離で比較すると、
r = 0.059552
となった。
少なくとも今回の100 Token の範囲では、
Token ID の数値的な近さと、2048次元ベクトルの向きの近さには、明確な線形関係は見られなかった。
Token ID 9707 は、モデルが数値として解釈する「9707」というより、
token_embd.weight から
対応するデータを取り出すための index
として使われている。
では、その結果として取り出された 2048個の数 は、一体何を表しているのだろう?
次はここを調べてみたい。