0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Token ID は数字なのに、なぜそのまま使わないのか?#10

0
Posted at

前回までの実験で、"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個の数 は、一体何を表しているのだろう?

次はここを調べてみたい。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?