トークン化:バイト対符号化(BPE)の手順と性質
対象読者: プログラミングの初歩(文字列と配列の扱い)と、O 記法による計算量、数学的帰納法を学んだ人。言語モデルやニューラルネットワークの知識は仮定しません。
前提となる記事:計算量と O 記法:アルゴリズムの速さを入力サイズの関数で測る、証明の技術:数学的帰納法と背理法はなぜ正しいのか、不変量の原理
0. この記事の要点
Section titled “0. この記事の要点”文章を扱う AI は、文字をそのまま読むのではなく、あらかじめ文字列を「番号の列」に直してから読みます。この記事は、その番号の振り方のうち最も広く使われている方法を、小さな例と証明つきで調べます。読み終えると、「元の文字列に必ず戻るのはなぜか」「なぜ手順が必ず終わるのか」「どこまでが保証され、どこからが保証されないのか」を、自分で説明できるようになります。
- 言語モデルが受け取るのは文字ではなくトークン id の列です。文字列をその列に変える手続きのうち、広く使われているバイト対符号化(BPE)を、記号列の書き換え規則として定義します。
- BPE の学習は「いちばん多い隣接ペアを 1 つの新しいトークンにする」操作の反復です。 回のマージのあと語彙の大きさはちょうど になります(命題 3.3)。
- 符号化して復号すると必ず元の記号列に戻ります(定理 3.2)。バイト単位で始めれば、どんな文字列も符号化でき、未知語が生じません。
- マージを 1 つ適用するとトークン数は置き換えた回数だけ減ります。この量を変量に取ると、学習が停止することと、マージの回数が「総記号数 − 語の数」以下であることが出ます(命題 4.2)。
- BPE の符号化は貪欲な手続きで、語彙を固定したときの最短の分割を与えるとは限りません(命題 5.1)。また、バイト単位のトークンの境界は文字の境界と一致しないことがあります(命題 6.3)。
1. 動機:モデルに渡す「記号」を何にするか
Section titled “1. 動機:モデルに渡す「記号」を何にするか”言語モデルは、有限個の記号からなる列を入力に取り、次の記号の確率分布を出力する関数です。したがって最初に決めなければならないのは、文字列をどういう記号の列とみなすかです。素朴な選択肢は 2 つあります。
| 単位 | 語彙の大きさ | 列の長さ | 困る点 |
|---|---|---|---|
| 文字(またはバイト) | 小さい(バイトなら 256) | 長い | 列が長くなり、あとの章で見るとおり注意の計算量は列の長さの 2 乗で増える |
| 単語 | 大きい(数十万以上) | 短い | 学習時に無かった単語を表せない。語形変化や複合語のたびに語彙が増える |
BPE はこの中間を取る方法です。記号 1 個から始め、よく並んで現れる 2 つをまとめて新しい記号にする、という操作を決まった回数だけ繰り返します。よく現れる単語は 1 個のトークンになり、珍しい単語は短い断片の列になります。マージの回数 が、語彙の大きさと列の長さの釣り合いを決めるただ 1 つの調整量です。
もとは Gage がデータ圧縮の方法として発表したもので([1])、Sennrich・Haddow・Birch が機械翻訳の単語分割に転用しました([2])。Radford らは、文字ではなく UTF-8 のバイト列に対して BPE を行うことで、基本の語彙を 256 個に抑えたまま任意の文字列を扱えるようにしました([3])。この記事ではこのバイト単位の形を主に扱います。
この記事の目的は、手続きを紹介するだけでなく、「この手続きについて何が証明できるか」をはっきりさせることです。往復の一致や停止性のように必ず成り立つ性質と、最短性のように成り立たない性質を分けて示します。
2. 準備:記号列・語彙・復号
Section titled “2. 準備:記号列・語彙・復号”空でない有限集合 をアルファベット、その元を記号という。記号の有限列を記号列といい、その全体を 、長さ 1 以上のものの全体を と書く。記号列 をつないだ列を と書き(連結)、長さを と書く。空列を と書く。
有限集合 と写像 の組を語彙といい、 の元をトークン id(または単にトークン)、 をトークン の表す記号列という。 の元の有限列 をトークン列といい、
を の復号という( のときは )。、 で定まる語彙を基本語彙という。
復号は連結について を満たします。定義の右辺が、 の分の連結と の分の連結をつないだものだからです。この性質は以下で繰り返し使います。
バイトレベルの場合は で、 です。文字列は先に UTF-8 でバイト列に直しておきます(定義 2.4)。
語彙 のトークン と、 に属さない新しい id の組 をマージ規則という。規則 は語彙を 、 に広げる。
規則 の適用 を、列の長さについて再帰的に次で定める。
ここで は残りの列である。すなわち列を左から走査し、 の直後に があればその 2 個を 1 個に置き換えて 2 つ進み、そうでなければ 1 つ進む。
トークン列 について、ペア の出現回数を 、 が の上で行う置き換えの回数を と書く。 は の定義の場合分けに沿って、、1 つ目の場合 、2 つ目の場合 で定まる。
のとき、出現は重なりえます。列 では ですが、左から走査すると最初の 2 個が になり、残りは 1 個なので、結果は 、 です。 は「重なりを除いて左から数えた出現回数」にあたります。
アルファベット に全順序が与えられているとする。コーパスとは、 の元(語)の有限列 、 のことである。マージの回数の上限 が与えられたとき、BPE の学習は次の手続きである。
- (各語を基本語彙のトークン列とみなす)、、 と置く。
- ならば終了する。そうでなければ、 の各ペア について、コーパス全体での出現回数 を数える(語をまたぐペアは数えない)。
- すべてのペアで ならば終了する。
- が最大のペアを選ぶ。最大のものが複数あるときは、 の順序に関する辞書式順序(まず を比べ、等しければ を比べる)で最小のものを選ぶ。選んだペアを とする。
- 新しい id を作り、 とし、順序は を のどの元よりも大きいと定める。規則 をすべての語に適用して とする。 を 1 増やして 2 に戻る。
終了したときの を ()とする。出力はマージ規則の列 である。
一般に、マージ規則の列 (各 は、基本語彙に を加えた語彙 の上の規則)に対し、符号化 を
で定める。すなわち、記号列を基本語彙のトークン列とみなし、規則を学習した順に 1 つずつ適用する。
手順 4 の「同数のときの選び方」を定義に含めたのは、これが無いと学習の結果が実装によって変わるからです。全順序の取り方は何でもかまいませんが、決めておけば、同じコーパスと同じ からは必ず同じ規則の列が得られます。バイトレベルの実装では、バイトの値と id の整数としての大小を使えば手順 5 の順序と一致します。
定義 2.4(バイトレベル BPE・特殊トークン・圧縮率)↗
UTF-8 は、Unicode の符号位置(文字に割り当てた整数)を 1〜4 バイトの列に写す規格である。符号位置が U+0000〜U+007F のとき 1 バイト 0xxxxxxx、U+0080〜U+07FF のとき 2 バイト 110xxxxx 10xxxxxx、U+0800〜U+FFFF のとき 3 バイト 1110xxxx 10xxxxxx 10xxxxxx、U+10000〜U+10FFFF のとき 4 バイト 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx になる(x に符号位置の 2 進表示を上位から入れる。[4])。先頭が 10 のバイトを継続バイトという。
とし、文字列を UTF-8 のバイト列に直してから BPE の学習・符号化を行う方式をバイトレベル BPE という。
文の終わりや区切りを表すために、マージ規則とは別に語彙へ加える id を特殊トークンという。特殊トークンは符号化 の出力には現れず、システムが列に挿入する。
空でないバイト列 について、 を に対する圧縮率(1 トークンあたりのバイト数)という。
ひらがなや漢字の多くは U+0800〜U+FFFF にあるので 1 文字が 3 バイトです。たとえば「数学は積み重ねです」は 9 文字で 27 バイトになり、マージを 1 つも学習していないバイトレベル BPE ではトークン数も 27 です。
3. 往復の一致と語彙の大きさ
Section titled “3. 往復の一致と語彙の大きさ”最初に、マージの適用 1 回について成り立つことを補題にまとめます。以降の命題はすべてこの補題から出ます。
次の補題は、「置き換えても表す文字列は変わらない」「置き換えた回数だけ長さが減る」という、直感的には当たり前に見える 4 つの事実を確かめるものです。証明では、列の先頭の 1〜2 個を見て、残りの短い列に同じことを使う、という形の帰納法を 1 回だけ行い、その中で 4 つをまとめて示します。定義 2.2 の場合分け(先頭が のとき、そうでないとき)の 2 通りを順に見ればよく、新しい工夫はありません。
証明(補題 3.1)
列の長さ についての帰納法(長さが 未満のすべての列で成り立つと仮定して で示す形。定理 4.3[証明の技術])で、1〜4 を同時に示します。
のとき、、、 なので、1〜4 はすべて成り立ちます。
とし、 と書きます。
のとき。 、 です。 の長さは なので帰納法の仮定が使えます。
- 。2 つ目の等号で (定義 2.2)と仮定の 1 を使いました。
- 。
- での出現は、位置 1 の出現と、 の中の出現を含むので 。また です。
- とします。位置 2 から始まるペアは で、 なので ではありません。したがって での出現は位置 1 と の中の出現だけで、。
のとき。 、 です。 の長さは なので仮定が使えます。位置 1 は出現でないので です。
- 。
- 。
- 。 なら で、仮定より 。
- なら仮定より で、両辺はそれぞれ 、 に等しい。
を任意のマージ規則の列とする。符号化 は の全体で定義され、任意の記号列 について
が成り立つ。とくにバイトレベル BPE では、任意のバイト列(したがって UTF-8 で表せる任意の文字列)が符号化でき、語彙に無いために表せない入力(未知語)は存在しない。
証明(定理 3.2)
まず全域性を確かめます。 は長さについての再帰で定義されており、再帰のたびに引数の長さが 1 以上減るので、どの列に対しても有限回で値が定まります。 はそれを 回合成したものなので、すべての に対して定義されます。 の各記号は基本語彙のトークンなので、入力に「語彙に無い記号」はありえません。
次に等式を、適用した規則の個数 についての帰納法(定理 3.2[証明の技術])で示します。、 と置き、 を示します。
のとき、 は基本語彙のトークン列で、 なので です。
で成り立つとすると、補題 3.1 の 1 より です。
なので主張が従います。
この定理は「マージ規則の列がどのように作られたか」を使っていません。学習で得た列でも、手で書いた列でも成り立ちます。逆向きの は一般には成り立ちません。例 3.4 で見ます。
証明(命題 3.3)
についての帰納法です。。規則 は に属さない id を 1 つだけ加えるので(定義 2.2)、。したがって です。
例 3.4(異なる id が同じ記号列を表す例)
id の個数は ですが、表す記号列がすべて異なるとは限りません。 とし、手で書いた規則の列
を考えます。、 で、、 です。id 6 と 7 は同じ記号列を表します。したがって は単射ではなく、 です。
それでも符号化は 1 通りに決まります。 は規則を決まった順に決まった向きで適用する関数だからです。実際、 に を適用すると 、 は当てはまる箇所がなく、 で 、 は当てはまらず、 です。一方 なので、復号してから符号化し直すと元のトークン列に戻らないことがあります。
この規則の列は説明のために手で書いたもので、定義 2.3 の学習の出力として得られるとは主張しません。
4. 長さの単調性と学習の停止性
Section titled “4. 長さの単調性と学習の停止性”をマージ規則、 をトークン列とする。
- 。すなわちマージを 1 つ適用するとトークン数はちょうど (重なりを除いて左から数えた の出現回数)だけ減り、増えることはない。 なら減る数は出現回数 に等しい。
- 任意のマージ規則の列 と記号列 について 。 なら 。
証明(命題 4.1)
2 から、圧縮率 は必ず 1 以上です。
証明(命題 4.2)
回のマージのあとの総トークン数を とし、 と置きます。 が学習のループの変量(定義 5.1[契約による設計])であることを示します。
下に有界。 各語 は空でなく、(補題 3.1 の 1 を 回)なので です。よって 、 です。
真に減る。 回目のマージが行われるのは、手順 3 を通過したとき、すなわち選ばれたペアの出現回数が のときです。するとある語 で で、補題 3.1 の 3 より です。命題 4.1 の 1 より、その語のトークン数は 1 以上減り、ほかの語のトークン数は増えません。したがって 、 です。
非負の整数値を取り、ループを 1 回まわるごとに 1 以上減る量があるので、ループは有限回で終了します(定理 5.2[契約による設計] の 1)。回数については、 で、 回のマージのあと なので です。
上界 は、すべての語がトークン 1 個になった状態に対応します。そのときどの語にも隣接ペアが無いので、手順 3 で必ず終了します。
例 4.3(小さなコーパスで 5 回のマージを追う)
コーパスを、語 slow が 4 個、slower が 2 個、lowest が 3 個、newest が 3 個の計 語とします。総記号数は です。記号の順序は文字コードの順()とし、新しいトークンはそれらより大きいとします。
| 回 | 選ばれたペア | 出現回数 | 同数だったほかのペア | 新しいトークン | 総トークン数 | 各語の分割(slow / slower / lowest / newest) |
|---|---|---|---|---|---|---|
| 0 | 64 | s l o w / s l o w e r / l o w e s t / n e w e s t | ||||
| 1 | (l, o) | 9 | (o, w) | lo | 55 | s lo w / s lo w e r / lo w e s t / n e w e s t |
| 2 | (lo, w) | 9 | なし | low | 46 | s low / s low e r / low e s t / n e w e s t |
| 3 | (e, s) | 6 | (s, t)、(s, low) | es | 40 | s low / s low e r / low es t / n e w es t |
| 4 | (s, low) | 6 | (es, t) | slow | 34 | slow / slow e r / low es t / n e w es t |
| 5 | (es, t) | 6 | なし | est | 28 | slow / slow e r / low est / n e w est |
1 回目は (l, o) と (o, w) がともに 9 回(slow 4 + slower 2 + lowest 3)で並び、 なので (l, o) が選ばれます。3 回目は 3 つのペアが 6 回で並び、第 1 成分が最小の (e, s) が選ばれます。4 回目は (s, low) と (es, t) が並びますが、s は基本の記号で、あとから作った es より小さいので (s, low) です。
総トークン数は と、毎回ちょうど出現回数だけ減っています。5 回とも なので、命題 4.1 の 1 のとおりです。5 回のマージ後の圧縮率は バイト/トークン、語彙の大きさはバイトレベルなら です。
このまま上限なしで続けると 11 回のマージで全語が 1 トークンになって終了します(総トークン数 12)。命題 4.2 の上界 よりずっと少ないのは、1 回のマージで総トークン数が 2 以上減ることが多いからです。
学習した 5 つの規則で、コーパスに無い語 slowest を符号化すると、s l o w e s t → s lo w e s t → s low e s t → s low es t → slow es t → slow est で、2 トークンになります。
出現回数の表(ペアをキーとする辞書)の 1 回の更新・参照、トークン 1 個の比較と書き込みを、それぞれ 1 ステップと数える。コーパスの総記号数を 、行うマージの回数を とすると、定義 2.3 の手順をそのまま実行する実装のステップ数は である( のとき)。
証明(命題 4.4)
回目の反復のステップ数を見積もります。総トークン数は です(命題 4.1)。
- 手順 2(数える): 各語の隣接ペアを 1 つずつ表に加える。隣接ペアの総数は なので、 ステップ以下。
- 手順 3・4(選ぶ): 表に載っている相異なるペアは高々 個。最大値を探し、同数のものを辞書式に比べるのに、表の各項目につき定数ステップなので、ある定数 について ステップ以下。
- 手順 5(適用する): は各語を左から 1 回走査し、トークン 1 個につき定数回の比較と書き込みをするので、ある定数 について ステップ以下。
よって 1 回の反復は ステップ以下で、 回の反復の合計は 以下です。初期化(語をトークン列に写す)は ステップです。合計は です( 記法の定義は 定義 3.1[計算量と O 記法]、和と定数倍の扱いは 命題 3.4[計算量と O 記法] の 3 と 4)。
5. 貪欲な符号化は最短とは限らない
Section titled “5. 貪欲な符号化は最短とは限らない”なお、次の命題の後半は動的計画法の応用です。「先頭 記号の最短の分け方」を の小さい方から順に表にしていき、最後のトークン 1 個を選ぶ候補を全部試して最小を取る、という考え方です。
語彙 を固定すると、記号列 を「 のトークンの表す記号列の連結」に分ける方法は一般に複数あります。BPE の符号化はそのうちの 1 つを選びますが、それがトークン数最小とは限りません。
あるコーパスから 定義 2.3 の学習で得たマージ規則の列 と記号列 で、 を満たすトークン列 のうちに となるものが存在する例がある。
一方、語彙 と記号列 ()が与えられたとき、 を満たす最短の の長さは、 のトークンの表す記号列の最大の長さを として、「 の連続する一部分(第 〜 記号)が のどれかのトークンの表す記号列か」の判定を 回行えば求まる。
証明(命題 5.1)
反例。 (順序は )、コーパスを ab が 4 個、bcd が 2 個、bc が 1 個とし、 とします。
- 1 回目: 出現回数は (a, b) が 4、(b, c) が 3、(c, d) が 2。最大は (a, b) で、新しいトークン ab ができます。語 bcd と bc には a が無いので変わりません。
- 2 回目: (b, c) が 3、(c, d) が 2。最大は (b, c) で、bc ができます。語 bcd は (bc, d)、語 bc は (bc) になります。
- 3 回目: 残るペアは (bc, d) の 2 回だけで、bcd ができます。
したがって は 、、 で、語彙の表す記号列は a, b, c, d, ab, bc, bcd の 7 個です(ここではトークンを表す記号列で呼んでいます)。
を符号化します。 で (ab, c, d)。 は「b の直後に c」を探しますが、b はすでに ab の中に取り込まれていて、列には b というトークンがありません。 も bc が無いので当てはまりません。よって で 3 トークンです。ところが は同じ語彙のトークン列で 、長さは 2 です。
最短分割の計算。 について、 の先頭 記号を表す最短のトークン列の長さを と置きます(表せないときは )。 で、 では
が成り立ちます(該当する が無ければ )。理由は次のとおりです。先頭 記号を表すトークン列の最後のトークンの表す記号列の長さを とすると、 で、残りは先頭 記号を表すトークン列なので長さは 以上、よって全体は右辺以上です。逆に、右辺の最小を与える について、先頭 記号の最短のトークン列の末尾にそのトークンを足せば、長さが右辺に等しいトークン列ができます。
これは、部分問題が 個、 の依存先が で、依存の向きが添字の小さい方に限られる(閉路が無い)部分問題族です。各 の評価は高々 回の判定と比較なので、定理 3.2[動的計画法] より、全体は 回の判定で求まります。求める値は です。
貪欲さの原因は、先に学習した規則が後の規則の材料を消費してしまうことです。それでも BPE の符号化が使われるのは、学習時にコーパスに施したのと同じ分割を符号化時にも再現するためです。モデルは学習時に見た分割のしかたでトークンの意味を覚えるので、符号化時だけ別の(短い)分割にすると、学習時に見ていない並びを入力することになります。
6. トークンが運べる情報量と、文字の境界
Section titled “6. トークンが運べる情報量と、文字の境界”語彙の大きさを とする。
- 長さ のトークン列は全部で 個ある。
- 有限集合 から長さ のトークン列の全体への単射が存在するならば、、すなわち である。言い換えると、トークン 1 個が区別できる場合の数は 通り、ビットに直すと ビット以下である。
証明(命題 6.1)
1 は についての帰納法です。 のとき空列 1 個で 。長さ の列は、長さ の列( 個)の末尾に 通りのトークンのどれかを足したもので、足し方が違えば列も違うので、 個です。
2 は、単射の像が と同じ個数の元を持つ部分集合であることから 。両辺の を取ると( は単調増加) です。 のときが「トークン 1 個」の主張です。
例 6.2(圧縮率との関係)
Radford らの語彙の大きさは 50,257 です([3])。 なので、この語彙のトークン 1 個が区別できるのは 16 ビット弱の分です。
一方、圧縮率が仮に 4 バイト/トークンだとすると、トークン 1 個は平均して元のバイト列の 32 ビット分にあたります。4 バイトの列は 通りありますが、トークン 1 個は 50,257 通りしか区別できません。すべてのバイト列を 4 バイト/トークンで符号化することは 命題 6.1 より不可能で、圧縮率が 1 を大きく超えるのは、自然言語の文に現れるバイト列が全体のごく一部に偏っているからです。ランダムなバイト列ではマージがほとんど当てはまらず、圧縮率は 1 に近くなります。
例 4.3 の例では、語彙 261( ビット)で圧縮率が 2.29 でした。マージを増やすほど列は短くなりますが、語彙は 1 つずつ増えます。
| マージ回数 | 語彙の大きさ | コーパスの総トークン数 | 圧縮率 |
|---|---|---|---|
| 0 | 256 | 64 | 1.00 |
| 2 | 258 | 46 | 1.39 |
| 5 | 261 | 28 | 2.29 |
| 10 | 266 | 14 | 4.57 |
| 11 | 267 | 12 | 5.33 |
語彙を大きくする代償は、次章で見る埋め込み行列の大きさです。埋め込みの次元を とすると行列は で、語彙が 1 増えるごとにパラメータが 個増えます。列が短くなる利得(注意の計算量は列の長さの 2 乗に比例します)と、この行列の大きさ、そして珍しいトークンほど学習データに現れる回数が減ることとの釣り合いで、 を決めます。
命題 6.3(トークンの境界は文字の境界と一致するとは限らない)✓
バイトレベル BPE では、学習で得たマージ規則の列 と、UTF-8 として正しいバイト列 で、 の中に「表すバイト列が単独では UTF-8 として正しくない」トークンが現れる例がある。したがって、トークン列を文字列に戻すときは、トークンごとに UTF-8 として解釈するのではなく、 で連結したバイト列の全体を解釈しなければならない。
証明(命題 6.3)
「あ」の符号位置は U+3042、「い」は U+3044 で、どちらも U+0800〜U+FFFF にあるので 3 バイトです(定義 2.4)。16 進で「あ」は E3 81 82、「い」は E3 81 84 になります。
コーパスを、語「あい」(バイト列 E3 81 82 E3 81 84)2 個とし、 で学習します。語 1 個の中の隣接ペアは (E3, 81) が 2 回、(81, 82)、(82, E3)、(81, 84) が 1 回ずつなので、コーパス全体では (E3, 81) が 4 回で最大です。規則は で、トークン 256 の表すバイト列は E3 81 です。
を「あ」のバイト列 E3 81 82 とすると、 です。トークン 256 の E3 81 は、先頭バイト E3(2 進で 11100011、3 バイトの文字の先頭)のあとに継続バイトが 1 個しか無いので、UTF-8 として正しくありません。トークン 82(2 進で 10000010)は継続バイトだけで、先頭バイトが無いのでこれも正しくありません。
一方 定理 3.2 より で、連結した全体は「あ」の正しい UTF-8 です。
文字の途中でトークンが切れると、トークンを 1 個ずつ生成して画面に出す場面で問題になります。直前までのバイト列が文字の途中で終わっているあいだは表示を保留し、文字が完成してから出す必要があります。
注意 6.4(ほかの分割方式)
サブワードへの分割には BPE 以外の方式もあります。Kudo は、ユニグラム言語モデル(各サブワードに確率を割り当て、分割の確率をその積とする模型)に基づいて、1 つの文に対する複数の分割を確率つきで扱う方法を提案しました([5])。BPE の符号化が決定的に 1 つの分割を返すのに対し、こちらは分割の候補に確率が付く点が違います。WordPiece と呼ばれる方式も、マージするペアの選び方と符号化の手続きが BPE と異なります。
文字単位(バイト単位)は、この記事の枠組みでは の場合です。単語単位は、語彙を「学習データに現れた単語」に固定する方式で、語彙に無い単語を表せない点が 定理 3.2 と対照的です。どの方式が良いかは、言語やデータに依る経験的な問題です。
7. 最小実装と検査
Section titled “7. 最小実装と検査”バイトレベル BPE の学習・符号化・復号を、Python の標準ライブラリだけで書きます。apply_merge が 定義 2.2 の 、train が 定義 2.3 の学習、encode が 、decode が です。新しいトークンの id は 256 から順に振るので、整数の大小が手順 5 の順序になります。
# Python 3.12.14(標準ライブラリのみ)import randomfrom collections import Counter
def apply_merge(seq, pair, new_id): """列 seq を左から走査し、隣り合う pair を new_id に置き換える。""" out, i = [], 0 while i < len(seq): if i + 1 < len(seq) and (seq[i], seq[i + 1]) == pair: out.append(new_id); i += 2 else: out.append(seq[i]); i += 1 return out
def train(words, num_merges): """words: bytes のリスト(コーパス)。マージ規則の列を返す。""" seqs = [list(w) for w in words] merges = [] for step in range(num_merges): counts = Counter() for s in seqs: counts.update(zip(s, s[1:])) if not counts: break best = max(counts.values()) pair = min(p for p, c in counts.items() if c == best) # 同数なら id の辞書式順序で最小 new_id = 256 + step seqs = [apply_merge(s, pair, new_id) for s in seqs] merges.append(pair) return merges
def encode(data, merges): seq = list(data) for step, pair in enumerate(merges): seq = apply_merge(seq, pair, 256 + step) return seq
def token_bytes(merges): table = [bytes([b]) for b in range(256)] for a, b in merges: table.append(table[a] + table[b]) return table
def decode(ids, merges): table = token_bytes(merges) return b"".join(table[t] for t in ids)
if __name__ == "__main__": corpus = [b"slow"] * 4 + [b"slower"] * 2 + [b"lowest"] * 3 + [b"newest"] * 3 merges = train(corpus, 5) table = token_bytes(merges) for step, (a, b) in enumerate(merges): print(256 + step, table[a], "+", table[b], "->", table[256 + step]) print([table[t] for t in encode(b"slowest", merges)])
random.seed(0) ok = 0 for trial in range(2000): n = random.randrange(0, 40) alphabet = b"elnostw" if trial % 2 == 0 else bytes(range(256)) data = bytes(random.choice(alphabet) for _ in range(n)) ids = encode(data, merges) assert decode(ids, merges) == data and len(ids) <= len(data) ok += 1 print("roundtrip ok:", ok)実行結果です。マージの順は 例 4.3 の表と一致します。後半は、長さ 0〜39 のランダムなバイト列 2,000 本(半分はマージが当てはまりやすいように 7 文字だけから、半分は 256 種のバイト全体から選ぶ)で、往復の一致(定理 3.2)と長さが増えないこと(命題 4.1)を確かめています。
256 b'l' + b'o' -> b'lo'257 b'lo' + b'w' -> b'low'258 b'e' + b's' -> b'es'259 b's' + b'low' -> b'slow'260 b'es' + b't' -> b'est'[b'slow', b'est']roundtrip ok: 2000次に、命題 5.1 の反例を実行し、BPE の分割と動的計画法による最短分割を比べます。上のコードを bpe.py として保存してあるとします。
# Python 3.12.14(標準ライブラリのみ)。bpe.py は前のコードfrom bpe import train, encode, token_bytes
def shortest(data, vocab): """vocab(bytes の集合)のトークンへの分割のうち最短のものを動的計画法で求める。""" n, longest = len(data), max(len(t) for t in vocab) INF = float("inf") f, back = [0] + [INF] * n, [0] * (n + 1) for i in range(1, n + 1): for length in range(1, min(longest, i) + 1): if data[i - length:i] in vocab and f[i - length] + 1 < f[i]: f[i], back[i] = f[i - length] + 1, length parts, i = [], n while i > 0: parts.append(data[i - back[i]:i]); i -= back[i] return parts[::-1]
corpus = [b"ab"] * 4 + [b"bcd"] * 2 + [b"bc"]merges = train(corpus, 3)table = token_bytes(merges)print("merges:", [(table[a], table[b]) for a, b in merges])vocab = set(table[t] for t in list(b"abcd") + [256, 257, 258])print("vocab :", sorted(vocab))bpe = [table[t] for t in encode(b"abcd", merges)]opt = shortest(b"abcd", vocab)print("BPE :", bpe, len(bpe))print("DP :", opt, len(opt))merges: [(b'a', b'b'), (b'b', b'c'), (b'bc', b'd')]vocab : [b'a', b'ab', b'b', b'bc', b'bcd', b'c', b'd']BPE : [b'ab', b'c', b'd'] 3DP : [b'a', b'bcd'] 2最後に 命題 6.3 の例です。トークンを 1 個ずつ UTF-8 として解釈すると例外になり、連結してから解釈すると元の文字に戻ります。
# Python 3.12.14(標準ライブラリのみ)。bpe.py は前のコードfrom bpe import train, encode, decode, token_bytes
corpus = ["あい".encode("utf-8")] * 2merges = train(corpus, 1)table = token_bytes(merges)print("あ =", "あ".encode("utf-8").hex(" "), "/ い =", "い".encode("utf-8").hex(" "))print("merge:", table[merges[0][0]].hex(), "+", table[merges[0][1]].hex())ids = encode("あ".encode("utf-8"), merges)print("ids :", ids, [table[t].hex() for t in ids])for t in ids: try: print(t, "->", table[t].decode("utf-8")) except UnicodeDecodeError as e: print(t, "-> UnicodeDecodeError:", e.reason)print("whole:", decode(ids, merges).decode("utf-8"))あ = e3 81 82 / い = e3 81 84merge: e3 + 81ids : [256, 130] ['e381', '82']256 -> UnicodeDecodeError: unexpected end of data130 -> UnicodeDecodeError: invalid start bytewhole: あ8. 次章へ:語彙の大きさがモデルの大きさを決める
Section titled “8. 次章へ:語彙の大きさがモデルの大きさを決める”この章で作ったのは、文字列からトークン id の列 、 への写像と、その逆向きの復号です。以降の章のモデルが見るのはこの id の列だけで、文字やバイトは見ません。モデルにとって id は順序も大きさも意味を持たない名前なので、次章ではまず各 id を 次元のベクトルに置き換えます。その表が の埋め込み行列で、出力側でも「次のトークン」を 通りから選ぶために同じ大きさの行列が要ります。命題 3.3 の が、モデルの入口と出口の大きさをそのまま決めます。
(1) を列 に適用した結果と、、 の値を求めよ。(2) 例 4.3 の 5 つの規則で lower を符号化せよ。
解答
(1) 左から走査すると、位置 1・2 の (a, a) が c になり、次に位置 3・4 の (a, a) が c になるので、結果は (c, c) です。出現は位置 1, 2, 3 の 3 か所なので 、置き換えは 2 回なので です。長さは で、補題 3.1 の 2 と合います。
(2) l o w e r に (l, o) を適用して lo w e r、(lo, w) を適用して low e r。(e, s) は s が無いので変化なし、(s, low) は s が無いので変化なし、(es, t) も変化なしです。結果は (low, e, r) の 3 トークンです。
演習 9.2標準
演習 9.3標準
命題 4.2 の上界 で等号が成り立つコーパスの例を、任意の 、 について作れ。
解答
相異なる 個の記号 を並べた 1 語 をコーパスとし、上限なしで学習します。次を帰納法で示します: 回のマージのあと、語のトークン列に同じトークンは 2 回現れず、総トークン数は である。
では記号がすべて異なるので成り立ちます。 で成り立つとし、 とします。トークンがすべて異なるので、どの隣接ペアも出現回数はちょうど 1 で、手順 3 を通過します。選ばれたペア は で出現回数 1 なので、補題 3.1 の 2 と 4 より総トークン数は 1 だけ減って になります。新しいトークンは 1 か所にだけ現れ、ほかのトークンは元のままなので、同じトークンが 2 回現れることはありません。
したがって総トークン数が 1 になるまで、ちょうど 回マージが行われます。 なので等号が成り立ちます。
演習 9.4標準
記号列を連結してから符号化すると、別々に符号化して並べたものよりトークン数が増えることがある。 上の 3 つの規則の列で、 となる の例を作れ。
解答
、、 とし、、 とします。
: a b に は当てはまらず、 で (ab)、 は当てはまらないので 1 トークン。: c d に 、 は当てはまらず、 で (cd) の 1 トークン。合わせて 2 トークンです。
: a b c d に を適用すると (a, bc, d)。 は b が無いので当てはまらず、 も c が無いので当てはまりません。結果は 3 トークンで、 です。
境界をまたぐ (b, c) が先にまとめられ、両側の規則の材料が無くなったためです。命題 5.1 と同じ仕組みで、符号化が連結と両立しない( となりうる)ことも分かります。復号のほうは常に です。
- [1] P. Gage, “A New Algorithm for Data Compression”, The C Users Journal 12(2) (1994), 23–38 — バイト対符号化の原典。書誌は [2] の参考文献表による。
- [2] R. Sennrich, B. Haddow, A. Birch, “Neural Machine Translation of Rare Words with Subword Units”, ACL 2016. arXiv:1508.07909 — BPE を単語分割に使った論文。3.2 節に手続きと、語彙の大きさが「初期の語彙+マージ回数」になることの記述がある。
- [3] A. Radford, J. Wu, R. Child, D. Luan, D. Amodei, I. Sutskever, “Language Models are Unsupervised Multitask Learners”, OpenAI, 2019 — 2.2 節(UTF-8 のバイト列に対する BPE)と 2.3 節(語彙の大きさ 50,257)。
- [4] F. Yergeau, “UTF-8, a transformation format of ISO 10646”, RFC 3629 (STD 63), 2003. https://www.rfc-editor.org/rfc/rfc3629 — 3 節に符号位置の範囲とバイト列の対応表がある。
- [5] T. Kudo, “Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates”, ACL 2018. arXiv:1804.10959 — ユニグラム言語モデルによる分割。
執筆: Claude Code(Opus 5.5)、Claude Code(Sonnet 5.5)・監査 2 回(Claude Code)・最終 2026-10-12
この記事の誤りを報告する ・運営: 夢現技研合同会社 ・料金 ・利用条件 ・特定商取引法に基づく表記
© 2026 夢現技研合同会社 ・コード例は MIT ライセンスです。