見出し画像

言語AIの進化史⑨バイト対符号化(BPE)

前回はトークンをベクトル化する埋め込みについて解説しました。トークンの数値化によって、トークン間の関係を計算することが可能となりました。その代表的な例としてWord2Vecなどを紹介しました。

ただし、どのように文章のトークン化がなされるかについては、英語などを想定とした手法を前提としていました。

例えば、アルファベットだけを考慮した処理として以下があります。

  • 小文字化(Aからaへなど、大文字を全て小文字にする)

  • 単語レベルでのトークン化(スペースやカンマなどで区切る)

なお、英語ベースのトークン化の処理についてはこちらでも解説しています。

こういった手法は日本語など多言語にはそのまま適用することができません。

さらに、未知語の問題も生じます。

これはそもそも語彙(ボキャブラリ)に含まれていない単語などの言語要素に対応できないという問題です。例えば、各トークンに対してそのIDとなる整数値を設定する場合、将来的に処理したいテキストの中に未知の単語が含まれていると未知語(あるいは未登録語)として扱うしかありません。

かといって、どんどん登録するトークンを増やしていくわけにもいきません。単語レベルのトークン化では語彙サイズが非常に大きくなり過ぎてしまいます。また、未知語や新しい単語を常に追加し訓練し直すのは非常に時間と労力がかかります。

よって、これらの問題を乗り越える必要があります。つまり、多言語と未知語の問題を解決できるようなトークン化の仕組みが必要となります。

そこで今回紹介するバイト対符号化(Byte Pair Encoding、BPE)SentencePieceといった手法が登場します。


多言語の解決

これは割と簡単です。多言語に対応した文字コードを使えば良いからです。

アルファベットのみをサポートするASCII(American Standard Code for Information Interchange)は、英文でよく使われるアルファベット、数字、記号など7ビットを使って表現しています。つまり、0番から127番までの番号(数値)を文字や記号に対応させているわけです。

1バイトは8ビットなので、ASCIIは1バイト以下の文字コードになります。

一方、Unicode(ユニコード)は、世界中のほぼすべての文字や記号を一つの統一された体系で表現するための規格です。これには、アルファベット、数字、漢字、ひらがな、カタカナ、絵文字、特殊記号など、さまざまな言語や文化の文字が含まれています。

たとえば、英字の「A」はUnicodeでU+0041、漢字の「漢」はU+6F22が割り当てられています。また、このように各文字に割り当てられた一意の番号をコードポイント(Code Point)と呼びます。Unicodeコードポイントによって、各文字に対して整数値(インデックス)を対応させています。つまり、異なる文字に固有の番号が付けられています。Unicodeを使えば異なるシステムやプラットフォーム間で文字を正確にやり取りできます。

Unicodeは、世界中のほぼすべての文字を統一された方式で表現できるため、異なる言語や文化に対応することができます。これにより、システムやアプリケーションが国際的に利用される際に、文字化けや誤表示の問題が発生しにくくなります。

例えば、日本語、中国語、アラビア語、絵文字など、多様な文字セットを一つのコード体系で処理できるため、グローバルな市場に向けた製品やサービスの開発が容易になります。

このUnicodeで定義された文字を、コンピュータで扱えるバイト列に変換するエンコーディング方式の一つとしてUTF-8があります。

UTF-8は、可変長のエンコーディングを採用しており、1バイトから4バイトの範囲でエンコードが可能となっています。1バイトのみで表現できるASCIIコードはUTF-8に含まれており、互換性があります。そして、日本語の漢字などは2バイト以上で表現されます。

UTF-8は、世界中の文字を効率的に扱えるため、標準的に広く使用されています。

以上より、UTF-8を採用することで多言語サポートが可能となります。

ただし、これだけでは未知語や語彙がいつまでも増加してしまう問題は解決できません。

文字列として扱う?

大規模なコーパス(テキストデータの集合)を使用しても、すべての単語が語彙に含まれているわけではありません。

特に固有名詞や新しい単語、専門用語などは出現頻度が低いため、従来の単語レベルのトークン化の処理では未知語の問題が生じます。また、将来的に新しく追加される単語も生まれるため語彙をどんどん大きくするというアプローチは現実的ではありません。よって、未知語の問題は単語レベルのトークン化を行う限り解決することができません。

未知語をサポートするには、文字レベルでのトークン化を行う必要がありそうです。仮に、次のような単語がテキストで現れたとします。

gasdfg

また、この単語はトークンとして語彙に含まれていないので通常ならばUNK(Unknown、未知語)として扱うしかありません。

しかし、この単語を純粋な文字列として扱えば、次のようにトークン化されます。

(g, a, s, d, f, g)

これならどのような言葉でもトークン化することができます。この場合、この文字列は単純に文字の列(一つ一つの文字)として扱われます。

トランスフォーマーのような言語モデルでは、自己アテンションを使って「gasdfg」のようにそのままでは意味のない文字の列にも文脈を取り込んで処理することができます。

もちろん、このような処理は文脈によるもので、元々の文字列に直接意味があるわけではありませんが、トランスフォーマーはその文脈に基づいて最適な予測や処理を行うことができます。

もし、トークン自体が<UNK>(未知語)として扱われたらそれも難しくなるでしょう。

また、文字レベルのトークン化における語彙のサイズは、ASCIIのみで考えると、1バイトに収まる数値で表現できます。

ところが、ここで2つの問題が浮かび上がってきます。

まず、ASCIIではなく、Unicodeのコードポイントを扱う場合、すべてのUnicodeシンボルを表現する必要があるため、基本語彙が非常に大きくなります(13万以上)。

さらに、もっと深刻なのは文字レベルでのトークン化によって訓練された言語モデルは、単語レベルで訓練された言語モデルより性能が悪いことが実験結果としてわかっていることです。

この辺りのことはGPT-2の入力表現の記事でも詳しく扱いました。

よって、単語レベルと文字レベルの両方の良いとこどりができるような仕組みが必要となります。

サブワード分割

サブワード(Sub-word)を使ってトークン化を行うことを考えます。

例えば、"unhappiness" を "un", "happi", "ness" のように分割し、それぞれをトークンとして扱います。

多くの単語は、サブワードの組み合わせとして表現できるので、この手法では語彙サイズを抑えることができるメリットがあります。

さらに頻繁に使われるサブワードの組み合わせは、単語レベルでのトークンとして扱います。仮に"happiness"という単語がよく使われるならばサブワードとして分解したトークンを使うのではなく単語レベルのトークンとして処理するということです。

よって、頻繁に出現する単語は単語レベルのトークンになるので言語モデルの性能も良くなります。

また、ほとんど使われないような単語は、文字レベルのトークンとして扱われるため未知語に対応することが可能です。

なお、サブワードの手法を使うと、従来行われていたステミングレンマ化といった言語特有の文法に従った処理が軽減されます。例えば、名詞の複数形や動詞の活用変化などはサブワードとして処理することができます。

このような考えに基づいた手法としてByte Pair Encoding (BPE)SentencePieceがあります。

Byte Pair Encoding (BPE)

Byte Pair Encoding (BPE) は、もともとデータ圧縮の手法として1994年にPhillip Gageによって提案されました。

この手法は、頻出するバイトのペアを繰り返し結合し、新しいトークンを作成することで、データサイズを縮小するものです。

次のようなステップを踏みます。

  • 初期化

  • ペアの検出

  • ペアの結合

  • 繰り返し

具体例を上げます。

初期化では、コーパス内の各単語を文字単位に分割した形で表現します。

例えば、以下のように「hello」、「hill」、「world」の3つの単語しか登場しないテキストにBPEを適用しているとします。

hello world hill world hello world hill world hello world 
hello world hill world hello world hello world hello world

(この文章に特に意味はありません)

次に、「hello」、「hill」、「world」を文字ごとに分割します。

hello => ( h, e, l, l, o )

hill  => ( h, i, l, l )

world => ( w, o, r, l, d )

この状態では語彙は以下のように文字レベルのものだけです。

語彙 = [ d, e, h, i, l, o, r, w ]

さらに、各単語の頻度も数えます。例えば、「hello」が10回登場したという具合です。

頻度 = {
    ( h, e, l, l, o ): 7,

    ( h, i, l, l ): 3,

    ( w, o, r, l, d ): 10,
}

ここまでが初期化です。この初期状態ではテキストは全く圧縮されていません。

次にペアを検出します。

ペアとは隣同士に並んだトークンの組み合わせです。現状では全て文字レベルのトークンなので、各単語に対して次のようなペアが存在します。

(h, e), (e, l), (l, l), (l, o)

(h, i), (i, l), (l, l)

(w, o), (o, r), (r, l), (l, d)

このようにコーパス内のすべての文字ペアの出現頻度を数え、最も頻出するペアを特定して結合します。

ここでは、'l'と'l'のペアが最も頻出します。「hello」として10回出現し、「hill」として3回出現するので、'l'と'l'のペアはコーパス内で合計13回出現しています。

よって、'l'と'l'のペアを結合します。

頻度 = {
    ( h, e, ll, o ): 7,

    ( h, i, ll ): 3,

    ( w, o, r, l, d ): 10,
}

これで'll'が一つのトークンとして認識されます。

よって、文字レベルのトークンだけでコーパスを表現するよりも圧縮されます。

hello => ( h, e, ll, o )

hill  => ( h, i, ll )

world => ( w, o, r, l, d )

このような処理を繰り返します。指定された数の語彙サイズが達成された時点で終了となります。

コーパスは小さすぎるのでこれ以上の繰り返しを行っても圧縮効果はありません。

やるとしたら、再びペアを数えます。

(h,e), (e, ll), (ll, o)

(h, i), (i, ll)

(w, o), (o, r), (r, ll), (ll, d)

しかし、これ以上の結合は例としてもあまり意味がありません。

また最低限の頻度の指定があれば、語彙サイズが満たされなくとも結合しないで終了することも可能です。

通常は、上述の処理を繰り返し、指定された語彙サイズ(例えば10,000トークン)に達するまで新しいトークンを生成します。各ステップで、コーパス内の文字列がより長いサブワードに変換され、最終的には最適なサブワードの集合が得られます。また、サブワードにならないものは文字レベルにとどまります。

BPEによるテキストの圧縮の処理は、後に自然言語処理の分野でサブワードによるトークン化の手法として適用されるようになりました。未知語の処理が可能で、語彙サイズの管理も行えます。

GPT-2によるBPEの変更

バイト単位による実装

GPT-2はBPEを採用しましたが、いくつかの変更点があります。

まず、GPT-2では、BPEが従来の文字単位ではなくバイト単位で実装されています。つまり、Unicodeのコードポイントがある文字をUTF-8の4バイトで表現していたとしても、4つの異なるバイトとして別々に扱います。

これによって、「すべてのUnicodeシンボルを表現する必要がある」という問題を回避しています。1バイトでは256個の整数しか表現できないので語彙のサイズも制限できます。

また、UTF-8をバイト単位で扱うことは処理としても、言語による違い考える必要がなく、特殊文字なども一貫して同じように処理することができます。

GPT-2のトークンの語彙サイズは約50,000トークンに設定されていますが、非常に広範なテキストをカバーできています。バイトレベルでのBPEにより、少数のトークンで多くの言語表現をカバーできるため、語彙サイズを大きくしつつも効率的に処理が行えました。

文字カテゴリの区別

また、異なるカテゴリの文字(例えば、数字と漢字、アルファベットと句読点など)は通常、トークンとしては結合されません。

たとえば、「hello!」という文字列があった場合、「hello」と「!」を一つのトークンとして結合しません。アルファベットと句読点は異なる文字カテゴリに属しているため、「hello」と「!」は別々のトークンとして扱われます。

Unicode標準では、各文字に対して「一般カテゴリ」(General Category)というプロパティが定義されています。これにより、文字がどのカテゴリに属するかが示されます。

スペースの扱い

なお、GPT-2におけるBPEの実装では、スペースはトークン化の際に重要な役割を果たします。

スペースは文字カテゴリが異なっても結合を許します。

例えば、「New York」を、スペースを含んで一つのトークンとして扱うことが可能です。

以上より、GPT-2のBPEでは、入力テキストがすでに単語単位に分割されている必要がなく、入力テキストをそのまま処理し、スペースも含めたバイト単位でのトークン化が可能です。


SentencePiece

SentencePieceは、Googleが開発したオープンソースのトークナイザーです。BERTALBERTT5などの言語モデルでSentencePieceが採用されています。

SentencePieceでは、BPE か Unigram Language Model(ULM)を選択肢として選べるようになっています。

BPEを選んだ場合、GPT-2によるBPEと同じように頻度によってバイト単位のペアの結合によるサブワードの形成が繰り返し行われます。

ULMは、コーパスに含まれるサブワードの分布に基づいて最適なトークン化を行います。Unigramと聞いて、シャノンのn-gramを思い出した方は鋭いですね。$${n=1}$$のケースはユニグラム(Unigram)でした。

ここでは、各単語やサブワードの選択をその確率分布で行うという意味です。その際に前後の文脈を考慮しないのでユニグラムです。ただし、ここでのユニグラム自体は複数の文字列を含むサブワードです。そのサブワードの出現確率が前後のサブワードや文字に依存しないという意味で「ユニグラム」が使われています。

この方法は、BPEのようにペアを結合して新しいトークンを作るのではなく、最初からあらゆるサブワードの出現確率を計算し、与えられた語彙サイズに対して最も確率の高いトークンセットを選びます。

例えば、「hello」という文字列は、たくさんの分割方法があります。ULMでは、こうした分割の可能性を効率的に探索し、それぞれのサブワードがコーパス内でどれだけ頻繁に出現するかの確率を計算します。

(h, e, ll, o)

(he, l, l, o)

(h, ell, o)

(hello)

(hell, o)

(h, e, l, l, o)

(h, ello)

...

この中で、(hello)というサブワード(この場合は単語)の出現確率が十分に高く、指定された語彙サイズに含まれるならば採用されます。

ただし、ULMもバイト単位での処理を行うので、あらかじめ入力テキストを単語単位に分割しておく必要はありません。

BPEがペアの結合に基づいてトークンを生成するのに対し、ULMはあらかじめ分割の候補を考慮し、確率が高いセットを選ぶ点で異なります。

次回予告

これまで、シャノンのn-gramから始まるテキスト情報の数値化を追ってきました。

TF-IDFにより文章全体をベクトルとして表現し、埋め込みベクトルによってトークンレベルでのベクトル化が可能になりました。

さらに、今回紹介したBPEやSentencePieceによって、トークン化も進化し、多言語対応や未知語の処理ができるようになりました。

これにより、テキストは柔軟にトークン化され、埋め込みベクトルのシーケンスへと変換されます。

次回からは、言語モデルが行うシーケンス処理に焦点を移します。

お楽しみに!

いいなと思ったら応援しよう!