0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

なぜ LLM API は「文字数」ではなく「Token 数」で課金できるのか?#05

0
Posted at

前回までの実験で、文字列が Token に分割され、それぞれに Token ID が割り当てられていることを観察しました。

さらに、Token ID から対応する Token に戻せることも確認しました。

ここまで実験して、ひとつ気になることが出てきました。

LLM が回答を生成するときも、最初から「東京です。」という文字列を作っているのではなく、

Token ID を順番に生成し、最後に人間が読める文字へ戻しているのではないか?

という疑問です。

前回 /completion の出力を観察したとき、こんな値が出ていました。

tokens_predicted
tokens_evaluated

名前だけを見ると何となく意味を想像できます。

でも、名前から判断するのではなく、実際に値がどう変化するのかを観察してみます。

今回の疑問

今回確認したいのは、次のような流れです。

入力
 ↓
Token ID
 ↓
LLM
 ↓
Token ID ?
 ↓
文字

特に気になったのが、

tokens_predicted
tokens_evaluated

です。

これらが入力や生成に合わせて、どのように変化するのかを観察します。

なお、Token ID と Token の対応関係については、すでに前の実験で確認しているため、今回は実験しません。


実験1:生成する Token 数を変えてみる

まず、入力を固定します。

日本の首都は

temperature0 に固定し、n_predict だけを変えてみます。

n_predict = 1

curl -N http://localhost:8080/completion \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "日本の首都は",
    "stream": true,
    "n_predict": 1,
    "temperature": 0
  }'

結果:

data: {"index":0,"content":"東","tokens":[102356],"stop":false,"id_slot":-1,"tokens_predicted":1,"tokens_evaluated":3}

生成されたのは、

content:"東"
tokens:[102356]
tokens_predicted:1

でした。


n_predict = 2

次は n_predict を 2 にします。

curl -N http://localhost:8080/completion \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "日本の首都は",
    "stream": true,
    "n_predict": 2,
    "temperature": 0
  }'

結果:

data: {"index":0,"content":"東","tokens":[102356],"stop":false,"id_slot":-1,"tokens_predicted":1,"tokens_evaluated":3}

data: {"index":0,"content":"京","tokens":[46553],"stop":false,"id_slot":-1,"tokens_predicted":2,"tokens_evaluated":3}

今度は、

東 → tokens_predicted:1
京 → tokens_predicted:2

となりました。

tokens にも、それぞれ別の Token ID が出ています。

東 → 102356
京 → 46553

n_predict = 5

さらに増やしてみます。

curl -N http://localhost:8080/completion \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "日本の首都は",
    "stream": true,
    "n_predict": 5,
    "temperature": 0
  }'

結果:

data: {"index":0,"content":"東","tokens":[102356],"stop":false,"id_slot":-1,"tokens_predicted":1,"tokens_evaluated":3}

data: {"index":0,"content":"京","tokens":[46553],"stop":false,"id_slot":-1,"tokens_predicted":2,"tokens_evaluated":3}

data: {"index":0,"content":"です","tokens":[37541],"stop":false,"id_slot":-1,"tokens_predicted":3,"tokens_evaluated":3}

data: {"index":0,"content":"。","tokens":[1773],"stop":false,"id_slot":-1,"tokens_predicted":4,"tokens_evaluated":3}

data: {"index":0,"content":" ","tokens":[22441],"stop":false,"id_slot":-1,"tokens_predicted":5,"tokens_evaluated":3}

並べてみると分かりやすくなります。

順番 content Token ID tokens_predicted
1 102356 1
2 46553 2
3 です 37541 3
4 1773 4
5   22441 5

content がひとつ生成されるたびに、

tokens_predicted

が、

1 → 2 → 3 → 4 → 5

と増えていました。

同時に tokens には Token ID がひとつずつ現れています。

つまり今回の結果だけを見ると、

tokens_predicted は生成された Token の数に合わせて増えているように見えます。


実験2:今度は入力を変えてみる

生成側では tokens_predicted が変化しました。

一方、すべての結果で、

tokens_evaluated:3

は変わっていませんでした。

入力の、

日本の首都は

を固定していたからでしょうか?

今度は n_predict = 1 に固定して、入力だけを変えてみます。


「日本」

curl -s http://localhost:8080/completion \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "日本",
    "n_predict": 1,
    "temperature": 0
  }'

結果:

"content":"の"
"tokens_predicted":1
"tokens_evaluated":1

「日本の首都は」

curl -s http://localhost:8080/completion \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "日本の首都は",
    "n_predict": 1,
    "temperature": 0
  }'

結果:

"content":"東"
"tokens_predicted":1
"tokens_evaluated":3

「日本の首都はどこ?」

curl -s http://localhost:8080/completion \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "日本の首都はどこ?",
    "n_predict": 1,
    "temperature": 0
  }'

結果:

"content":"また"
"tokens_predicted":1
"tokens_evaluated":5

結果を並べます。

入力 tokens_evaluated tokens_predicted
日本 1 1
日本の首都は 3 1
日本の首都はどこ? 5 1

今回は出力を 1 Token に固定しているため、

tokens_predicted:1

は変わりません。

一方で入力を長くすると、

tokens_evaluated

1 → 3 → 5

と増えました。

少なくとも今回の結果では、tokens_evaluated は入力側、tokens_predicted は出力側の Token 数と関係しているように見えます。


もうひとつ気づいたこと

今回、stream:true のときには、

content:"東"
tokens:[102356]

のように content と Token ID が一緒に出ていました。

一方、stream:false で実行した結果では、

content:"東"
tokens:[]

となっています。

つまり、tokens というフィールドが常に生成した Token ID を同じ形で返しているわけではなさそうです。

これは LLM 自体というより、llama.cpp の API が結果をどのように返しているかにも関係していそうです。

今回はここには深入りしません。


今回分かったこと

今回の実験では、フィールド名から意味を推測するのではなく、実際に入力と出力を変えて値を観察しました。

その結果、

入力を増やす
    ↓
tokens_evaluated が増える

生成を増やす
    ↓
tokens_predicted が増える

という動きを確認できました。

さらにストリーミング生成では、

tokens_predicted:1
tokens:[102356]
content:"東"

        ↓

tokens_predicted:2
tokens:[46553]
content:"京"

のように、生成が進むたびに Token ID と人間が読める文字列が一緒に現れました。

前の実験では、

Token ID と Token の対応関係

もすでに観察しています。

ここまでの結果をつなげると、最初に考えていた、

入力文字
   ↓
Token ID
   ↓
LLM
   ↓
Token ID ?
   ↓
人間が読める文字

という流れが、少しずつ見えてきた気がします。

ただし、今回観察しているのは llama.cpp が API として外に見せている結果です。

LLM 内部で本当に Token ID がどのように予測されているのかまでは、まだ観察できていません。

これは別の疑問として残しておきます。


ところで、これって……

今回の実験では、

入力を増やす
    ↓
tokens_evaluated が増える

生成を増やす
    ↓
tokens_predicted が増える

という動きを観察できました。

ここで、普段 ChatGPT などの LLM API で見かける料金を思い出しました。

LLM API の料金は「文字数」ではなく、Input Token と Output Token を単位として計算されています。

もしかすると、商用の LLM にも llama.cpptokens_evaluatedtokens_predicted のように、

入力で何 Token 使ったか

出力で何 Token 生成したか

を数える仕組みが存在していて、その値を使って料金を計算しているのではないでしょうか?

今のところ、これは今回の実験結果から思いついただけの仮説です。

実際に ChatGPT などの LLM API でも同じ手段で Token 数を数えることができるのか、まだ確認していません。

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?