😎

ChatGPT API で区切り線を表現する最適な文字列を見つけた件

2023/05/20に公開
3

TL;DR

ChatGPT API のメッセージに区切り線を入れる場合、何文字同じ文字を続ければ良いか実験・考察を行いました。

Translate this text into Japanese.

==================== (←これが区切り線)

I am a student.

結論として、ChatGPT API での区切り線は、-または=を4以上の2の冪乗(4, 8, 16, 32)文字数分だけ書くのがおすすめです。

つまり、以下の区切り線が最有力です。

----

--------

----------------

--------------------------------

====

========

================

================================

これらは1トークンとしてカウントされるため効果的です。
4, 8, 16, 32文字に大きな差はないようです。
また、5~16文字であれば、-または=を好きなだけ書いても1トークンですが、17文字以上は避けた方が無難です。

はじめに: ChatGPT API のメッセージに区切り線を入れたい

ChatGPT API では入力が文字列ですが、一つの文字列に複数のメッセージを入力したい場合があります。その場合、メッセージの区切りをどうするかが問題になります。

例えば、「特定の文字列を日本語に翻訳する」というプロンプトを作りたいとします。
大体以下のようなメッセージになるでしょう。

Translate this text into Japanese.

I am a student.

この場合、区切り線を入れることで、明確にメッセージを区切ることができ、大規模言語モデル側に意図が伝わりやすくなります。

Translate this text into Japanese.

====================

I am a student.

この区切り線を使い、あたかも複数の入力を与えられるかのように振る舞わせるテクニックはよく使われており、知ってる方や実際に使っている方も多いと思います。

疑問: 区切り線を書くとき、何文字書けばいいのか?

こんな感じで、ノリで毎回連打して入力してたのですが、ふと疑問に思いました。

この区切り線を書くときに何文字 - やら = を書けば良いのか?

仮説: 適切な区切り線は1トークンとしてカウントされる

ChatGPT API では、入力をトークン化してからモデルに入力しています。
このトークン化の際、適切な区切り線の文字数があるとすれば、それは1トークンとしてカウントされるのではないかと仮説を立てました。

検証の前に知っておくと良いこと: トークンとトークナイザーについて

GPT-3などの大規模言語モデルで文字列を入力するとき、文字列をそのままモデルに読み込ませるのではなく、文字列を トークン と呼ばれる数値で表現される単位に分割します。モデルを学習したり入出力するにあたり、数値表現を用いる必要があるからです。

文字列をトークンの配列に変換することをトークン化(tokenization) といい、この変換器のことをトークナイザー(tokenizer) といいます。

トークン化(文字列) = [トークン1, トークン2, ...., トークンN]

文字列をどのように分割しトークン化するかについては非常に任意性が高く、「この分割が良い」というのはなかなか難しいです。

一般に、文字列の長さとトークン数の関係は大きいこともあったり、小さいこともあります。

例えば、単純に1文字1トークンとみなすトークナイザーの場合、英語だと非効率です。なぜなら、26個のアルファベットは、独立して意味があるわけではなく、「単語」になって初めて意味が生まれてきます。ある文字を1トークンとするトークナイザーに比べ、ある一つの単語を1トークンとできるトークナイザーの方が、同じトークン数でより多くの情報量を表現できます(その代わり、各単語に整数を割り振るため、整数側が足りなくなる可能性はあります)。

例: ChatGPT APIで使われるトークナイザーでトークン化してみる

ChatGPT API(モデル: gpt-3.5-turbogpt-4)で使われている cl100k_base というトークナイザーによって英単語 the をエンコードすると、1820というトークン(整数)に変換されます。
同様に、 the book もエンコードすると、 [1820, 2363] というトークンに変換されます。

import tiktoken # tiktoken: OpenAIが公開するトークン化用ライブラリ
from tiktoken.core import Encoding

encoding: Encoding = tiktoken.get_encoding('cl100k_base')

print(encoding.encode('the')) # -> `[1820]`
print(encoding.encode('the book')) # -> `[1820, 2363]`

実は、 book (先頭にスペースを含む) が 2363 に変換されています。これは、 book という単語が使われる際、空白が前につくケースがほとんどであり、それに最適化されているからだと思われます。

アルファベット一文字を個別にトークン化してみると、それぞれのトークンが得られます。
アルファベット一文字は1トークンに変換されます。

試しに、無理やり一文字ずつトークン化すると...
t, h, e, , b, o, o, k それぞれに対するトークンが計算できます。アルファベットの o のトークンは 78 のようですね。`

print([encoding.encode(c)[0] for c in 'the book']) # -> `[83, 71, 68, 220, 65, 78, 78, 74]`

英単語である the は 1820 という1トークンで表現でき、 1文字1トークンのトークナイズ [83, 71, 68] に比べて3倍データを小さくできます。文字列の情報量を圧縮することができるため、学習精度向上にもつながります。

おまけ: 日本語の漢字一文字が複数トークンで表現されるなどしています。これは漢字の数が多すぎることが要因の一つです(この記事ではこの点について深く掘り下げませんが気になる人は試してみてください)。

参考: OpenAI 言語モデルごとのエンコーディング一覧

つまり

ある文字列を少ない数のトークンに変換できると、消費トークン数が減り、

  • 安い
  • 早い
  • 言語モデル側に圧縮された本質的なデータを送信できるため、負担が少なくなる(はず)

などなどメリットが多いです。

実験: 区切り線の文字数を変えてトークン数を計算する

さて、 仮説: 適切な区切り線は1トークンとしてカウントされる を検証するため、区切り線の文字数を変えて、トークン数がどのように変化するか実験してみましょう。

手法

  • -, = それぞれについてN個続けた文字列(N=1,2,...,32)に対して、encoding.encode を使ってトークナイズする。
  • 各結果のトークン数を計算する
雑な実装
import tiktoken
from tiktoken.core import Encoding
import pandas as pd

encoding: Encoding = tiktoken.get_encoding('cl100k_base')

df = pd.DataFrame({
    '-': [encoding.encode('-' * i) for i in range(1, 33)],
    '=': [encoding.encode('=' * i) for i in range(1, 33)],
},
    index=[i for i in range(1, 33)],
)

df['Token count of -'] = df['-'].apply(len)
df['Token count of ='] = df['='].apply(len)

結果

  • 1から16個 - または = を連続で繋げたものは全て1トークンで表すことができました
    • 例: ---------------- (- * 16) は1トークン([776])
  • この範囲内において、2の冪乗 2^M-, = どちらであっても全てトークン数1となる。
N トークン(-) トークン(=) トークン数(-) トークン数(=)
1 [12] [28] 1 1
2 [313] [419] 1 1
3 [4521] [8880] 1 1
4 [381] [608] 1 1
5 [15431] [47825] 1 1
6 [16974] [46447] 1 1
7 [21622] [54337] 1 1
8 [515] [895] 1 1
9 [23573] [44135] 1 1
10 [15700] [44356] 1 1
11 [18580] [36499] 1 1
12 [5272] [10615] 1 1
13 [20098] [36466] 1 1
14 [43191] [39703] 1 1
15 [39745] [46008] 1 1
16 [776] [1547] 1 1
17 [776, 12] [1547, 28] 2 2
18 [776, 313] [1547, 419] 2 2
19 [776, 4521] [1547, 8880] 2 2
20 [98804] [1547, 608] 1 2
21 [776, 15431] [1547, 47825] 2 2
22 [776, 16974] [1547, 46447] 2 2
23 [776, 21622] [1547, 54337] 2 2
24 [776, 515] [1547, 895] 2 2
25 [776, 23573] [1547, 44135] 2 2
26 [776, 15700] [1547, 44356] 2 2
27 [776, 18580] [1547, 36499] 2 2
28 [28914] [1547, 10615] 1 2
29 [776, 20098] [1547, 36466] 2 2
30 [86226] [1547, 39703] 1 2
31 [28914, 4521] [1547, 46008] 2 2
32 [1434] [3134] 1 1

考察

区切り線の文字数は、 -= で変えても、2の冪乗の文字数のときに一つのトークンに変換されます。

じゃあ、区切り線は2の冪乗文字数分繋げればよいのね、と思っていまいそうですが、それは間違いっぽいです。
これは、 -, = が少ない数のとき、例えば --, == の場合、このトークンは別の文脈で使われるものになってしまっており、単なる区切り線にはならない可能性があるからです。

GPT-4 に質問してみた

質問

これらのトークンの違いについて聞いてみました。
-, = のそれぞれについて、1, 2, 4, 8, 16, 32文字のトークンについて聞いてみました。

Explain the differences between the following tokens.

- "-"
- "--"
- "----"
- "--------"
- "----------------"
- "--------------------------------"
- "="
- "=="
- "===="
- "========"
- "================"
- "================================"

GPT-4 からの回答

これらの記号またはトークンの意味は、コンテキストに大きく依存します。それらはさまざまなプログラミング言語、シェルスクリプト、またはマークダウンのようなマークアップ言語で使用することができます。ただし、具体的なコンテキストが提供されていない場合、正確な説明を提供するのは難しいです。以下に一般的な説明を提供します:

  • -: 多くのプログラミング言語では、これは否定のための単項演算子、または減算のための二項演算子として使用されます。コマンドラインインターフェースでは、コマンドのオプションを指定するためによく使用されます。

  • --:プログラミングでは、これはよく値をデクリメントする演算子として使用されます。コマンドラインインターフェースでは、長い形式のオプションを表すためによく使用されます。

  • ------------------------------------------------------------:これらはプログラミング言語やコマンドラインインターフェースでは標準的な意味を持ちません。テキストで視覚的なセパレータとして使用されることがあります。

  • =:プログラミングでは、これはよく変数の値を設定するための代入演算子として使用されます。コマンドラインインターフェースでは、変数やオプションに値を割り当てるためによく使用されます。

  • ==:多くのプログラミング言語では、これは等しい比較演算子です。二つの値が等しいかどうかをチェックします。

  • ============================================================:上記の拡張ハイフンと同様に、これらはプログラミング言語やコマンドラインインターフェースでは標準的な意味を持ちません。テキストで視覚的なセパレータとして使用されることがあります。

マークダウンのようなマークアップ言語では、一つ以上の - または = 記号を使用して、異なるレベルの見出しを作成したり、水平線(--- または ***で)を作成することができます。

再度、これらは幅広い説明であり、具体的な使用法はコンテキストに大きく依存します。もっと具体的な回答を求めている場合は、より多くのコンテキストを提供してください。

回答について考察

GPT-4の回答から重要な観点が得られたと思います。
-, = の文字数が少なければ少ないほど、文字単体が含む意味合いが強く反映されてしまう、ということです。

つまり、区切り線に求める性質は、「区切りを表す」だけに限りたいので、プログラミングで使用されているような -, --, =, == などは、トークンに区切り線以外の情報が入ってしまうため、ノイズになる可能性があります。

実際どうかはわかりませんが、文字列A文字列Bの間に==と書くと、もしかしたら等価演算子と解釈されてしまうかもしれません。

4つ以上続けたものは、そのような色がついたトークンではなくなるため、純粋に「区切り線」として振る舞ってくれることが予想されます。

結論

ChatGPT API での区切り線は、-または=を4以上の2の冪乗(4, 8, 16, 32)文字数分だけ書くのがおすすめです。

つまり、以下の区切り線が最有力です。

----

--------

----------------

--------------------------------

====

========

================

================================

これらをトークナイズすると1トークンにエンコードされるためトークン数が節約できて効果的です。
4, 8, 16, 32文字に大きな差はないようです。

また、5~15文字の区切り線は、トークンこそ異なりますが、1トークンで表現できるので、使っても良さそうです。
ただし、17文字以上の区切り線の多くは、トークン数が2つになるため、トークン数が増えてしまうので、避けるのが無難でしょう。

まとめ

ここまで、読んでいただきありがとうございました。

文字列に対し、どのようにトークン分割が行われるかについて OpenAI Tokenizerでブラウザから簡単に試せます。

他に気になる点があれば、実際に試してください!(そして教えてください)

https://twitter.com/zawawahoge/status/1659909063524896770

ツイッターやってますので、フォロー・シェアしてもらえると励みになります。
よろしくお願いします!
https://twitter.com/zawawahoge

Discussion

YAMAMOTO YujiYAMAMOTO Yuji

公式ガイド https://help.openai.com/en/articles/6654000-best-practices-for-prompt-engineering-with-openai-api#h_21d4f4dc3d 曰く、###""" を使って区切れ、だそうです。

ざわきん/zawakinざわきん/zawakin

コメントありがとうございます!
このページは自分も目にしたことがあったのですが、区切り線に """### を使うべきかと言われると個人的に疑問に思うところがあります。
もちろん、おそらく普通に使う分にはこれらを使用しても差が出ないとは思うので、使う分には以下の点を考えた上で使用するべきかなと思ってます。

1. """ について

結論から言うと、 """ は複数行文字列の開始または終了を表すトークンと解釈されるので、メッセージを二つに分割するという今回の目的にはやや不向きと考えられます。

""" に関しては GPT が多く学習しているであろう Python の複数行文字列を記述する記法に使われます。


multiple_lines = """line1
line2
line3"""

公式ガイドでの記述も複数行を一つの文字列として解釈する場合に使用しているようです。
GPTにおいて、プログラミング言語で学習した """ のような特徴的な文字列のトークンは、自然言語においてもプログラミングの仕様と似た機能を果たす傾向があることは知られています。

文章の途中で、 """ が入力された場合、モデルは、おそらく、 複数行文字列が来る可能性が高い。また、もう一つ """ が来る可能性も高い と考えるはずです。

この挙動は、区切り線で実現したい目的からややずれてしまいます。

2. ### について

こちらも結論から言うと、 ### は区切り線としての使用を避けるべきだと思います。

### はマークダウンの見出し3に対応します。
まず、区切り文字に色をつけない方が純粋な区切り文字になると言う観点から、避けるべきだという話になります。

実は、 区切り線として使うトークンとマークダウン記法で用いる見出し3のトークンは同じもの になります。
試しに、 ###### (=### + 空白) のトークンを見てみましたが、結局後者も ### 単体のトークンと空白のトークンで分割していました。これは、マークダウンで使われる ### + 空白の記法を使う場合と、行に ### だけを書いたもののトークンは同じものが使われると言うことを意味します。

補足

もちろん、最適化すると言う観点であれば ---- などを使うべきだ、ということなので、 ###""" を使うのはアリだと思います。

参考(GPT-4にトークンを解釈してもらいました)

GPT-4 に `"""` と `###` のトークンについて解説をお願いした

これらの記号またはトークンの意味は、コンテキストや使用されるプログラミング言語やマークアップ言語によって異なります。

  1. ###
  • マークダウンでは、###はレベル3の見出しを示します。#記号の数は見出しのレベルを示し、#はレベル1の見出し、##はレベル2の見出し、といった具体的になります。
  • Pythonや他のプログラミング言語では、###は単にコメントです。同じ行の#の後に何も実行されません。この場合、余分な#記号は動作を変えませんが、コードの視覚的な整理や強調のために使用されることがあります。
  1. """
  • Pythonでは、"""(トリプルクォート)は複数行のコメントやdocstring用です。docstringは関数やクラスの目的を説明するために使用されるコメントの一種です。開始と終了のトリプルクォートの間のテキストがコメントまたはdocstringになります。
  • 他の言語では、このトークンは特定の意味を持たないかもしれません。

さらに詳細な説明をするためには、より多くのコンテキストが必要です。もしご提供いただけるなら、特定のプログラミング言語や、更に詳しいコンテキストがあるとより正確な回答ができます。

ざわきん/zawakinざわきん/zawakin

貼っていただいたリンクで、 few-shots の列挙をする際に ## という区切り線を使っていますね

Input parameters としていくつかの値を渡す場合の区切りに ## は感覚的にはアリかなという気がします。(ナンバリングを意識した区切りになる可能性がある)

文字列を純粋に分割する、というユースケースであれば ##### には色がついちゃってるかな、と思います(思うだけです)