GPT-2を読む⑥入力表現
前回に引き続き、OpenAIが2019年に発表した論文「Language Models are Unsupervised Multitask Learners」(GPTのバージョン2)の「アプローチ」を読み進めます。
前回に読んだ部分では、タスクに依存しない方法でより品質の高い訓練データセットを集める方法が解説されていました。
ただ単にWebから莫大なテキストを収集するだけではなく、良質なデータを収集することがGPT-2の学習において重要であり、そのための工夫が語られていました。
今回は「アプローチ」の残りの部分を読みます。主に、モデルへのテキストの入力表現について理解します。
入力表現における課題
伝統的なトークン化の課題
セクション2.2「入力表現」は次のように始まります。
一般的な言語モデル(LM)は、任意の文字列(ストリング)の確率を計算し、生成することができるべきです。
A general language model (LM) should be able to compute the probability of (and also generate) any string.
ここでの「文字列」(ストリング)は、トークンのシーケンスと読み替えても同じ意味です。
これまで何度も登場した言語モデリングを行うことで言語モデルは言語の統計的な構造を習得します。よって、様々なシーケンスに対する確率を計算することができるようになります。
一般的な言語モデルであれば、そうあるべきです。
このため、訓練データセットにおける解説で強調されていたように、一般的な言語モデルの学習ではタスク固有の訓練データ選択は行われるべきではありません。
その一方で、伝統的なコーパスのトークン化ではいくつかの問題がありました。
しかし、現在の大規模な言語モデルには、前処理として小文字化、トークン化、未知語トークンの処理が含まれており、これによってモデリング可能な文字列の範囲が制限されています。
Current large scale LMs include pre-processing steps such as lowercasing, tokenization, and out-of-vocabulary tokens which restrict the space of model-able strings.
まず、以前のトークン化の問題点として言語に特有であることが挙げられます。
例えば、アルファベットだけを考慮した処理として代表的なものに以下があります。
小文字化(Aからaへなど、大文字を全て小文字にする)
単語レベルでのトークン化(スペースやカンマなどで区切る)
なお、英語ベースのトークン化処理についてはこちらでも解説しています。
未知語への対応の問題
さらに伝統的なトークン化では未知語を扱えません。これは、コーパスのトークン化が行われる時点で知られていない単語を扱えないという問題です。
例えば、トークン化された単語などに整数値(インデックス)を与えて語彙をを管理する場合にこの問題が生じます。それぞれのトークンにインデックスがあり、その値に対応する(マッピングされた)埋め込みベクトルを取り出すのですが、そもそもそのようなインデックスが存在しない場合は未知語となり扱い不能となってしまいます。
わかりやすい例としてこんなのはどうでしょうか。
私が見つけたのは手のひらに乗るほど小さいゼットンだった。「ゼットン」はウルトラマンに登場した宇宙恐竜ですが、語彙の中に含まれていなければトークン化がなされていないわけで未知語となります。
未知語をモデルが予測することも考えられますが、「ゼットン」の身長は本来60メートルぐらいなので「手のひらに乗る」といった文脈では予測するのは困難でしょう。
つまり、以前に主流だったトークン化手法では未知語が発生するため、対応する埋め込みベクトルが取得できず、モデルでの処理が困難になります。この制約により、モデリング可能な文字列の範囲が制限されてしまうという問題があります。
多言語への対応
言語特有のトークン化を避けるには多言語対応した文字列が必須となります。そこで、ちょっと話がそれますが、Unicode(ユニコード)について簡単に紹介します。
Unicodeは世界中のほぼすべての文字や記号を一つの統一された体系で表現するための規格です。これには、アルファベット、数字、漢字、ひらがな、カタカナ、絵文字、特殊記号など、さまざまな言語や文化の文字が含まれています。
たとえば、英字の「A」はUnicodeでU+0041、漢字の「漢」はU+6F22が割り当てられています。
また、このように各文字に割り当てられた一意の番号をコードポイント(Code Point)と呼びます。コードポイントによって、各文字に対して整数値(インデックス)を対応させています。つまり、異なる文字に固有の番号が付けられています。
以上より、Unicodeを使うことで異なるシステムやプラットフォーム間で文字を正確にやり取りできるようになっています。
1980年代後半から1990年代初頭にかけて、さまざまな文字エンコーディングが存在していたため、異なる言語やシステム間で文字の表示やデータ交換が困難でした。たとえば、日本語ではShift-JISやEUC-JPなどの方式が混在しました。
この問題を解決するために、1987年にApple社のJoe Becker、Xerox社のLee CollinsとMark Davisらが中心となってUnicodeプロジェクトが開始されました。その目的は、すべての言語の文字を統一されたコード体系で表現し、異なるシステム間での文字データの互換性を確保することでした。1991年にUnicode Consortium(ユニコードコンソーシアム)が設立され、Unicodeの規格が策定されました。
Unicodeで定義された文字をコンピュータで扱えるバイト列に変換するエンコーディング方式の一つとしてUTF-8があります。
UTF-8では、文字ごとに異なるビット数を使ってエンコードする可変長のエンコーディングを採用しています。よって、1バイトから4バイトの範囲でエンコードが可能となっています。
1バイト(Byte)は、8ビット(bit)から成ります。よって4バイトは32ビットです。
例えば、ASCII文字(英数字や記号)は1バイトで表現されるので、伝統的なASCII文字と互換性があります。それ以外の文字(日本語の漢字など)は2バイト以上で表現されます。
このようにして世界中の文字を効率的に扱えるため、現在ではUTF-8はほぼ標準として広く使用されています。
なお、Unicodeのエンコード方式には他にもUTF-16があります。
UTF-16は固定長に近いエンコーディング方式で、多くのUnicode文字を2バイトで表現しますが、一部の文字は4バイトになります。
特に東アジアの言語を中心に、多様な文字セットを効率的に扱う際に有利ですが、ASCII互換性が低く、メモリ容量(ストレージや通信)の観点からはUTF-8に劣ることが多いです。
UTF-16は、主にJavaやWindowsなどのシステム内部で使われています。
Byte Pair Encoding
UTF-8のシーケンス
では、単語レベルでのトークン化における未知語の問題の解決策を考えてみましょう。単語レベルでのトークン化ではなく、文字ごとにデータ処理するのはどうでしょうか。UTF-8の文字列を扱うならば多言語にも対応できます。
論文はこう続けます。
Unicode文字列をUTF-8バイトのシーケンスとして処理する方法(例:Gillickら、2015)は、この要件を上手く満たします。しかし、現在のバイトレベルの言語モデルは、大規模データセット(例:One Billion Word Benchmark)において、単語レベルの言語モデルと比べて性能が劣ります(Al-Rfouら、2018)。私たちも、WebTextで標準的なバイトレベルの言語モデルを訓練しようとした際に、同様の性能差を確認しました。
While processing Unicode strings as a sequence of UTF-8 bytes elegantly fulfills this requirement as exemplified in work such as Gillick et al. (2015), current byte-level LMs are not competitive with word-level LMs on large scale datasets such as the One Billion Word Benchmark (Al-Rfou et al., 2018). We observed a similar performance gap in our own attempts to train standard byte-level LMs on WebText.
つまり、Unicodeのシーケンスを使えば、言語特有のトークン化に陥ることなく未知語の問題を解決するための条件を満たしてはいます。
しかし、バイトレベルの言語モデル(文章をUTF-8のシーケンスとして学習する)と単語レベルの言語モデル(単語のトークン化を行い学習する)より性能が悪くなりました。これは、One Billion Word Benchmarkという大規模データセットで確認されています。
また、OpenAIでも彼らが収集したWebTextを使って同様の実験をし、バイトレベルの言語モデルの性能が比較的に悪いことが確認されました。
この問題を解決するために、バイト対符号化が登場します。
バイト対符号化
論文は、バイト対符号化(Byte Pair Encoding、BPE)についてこう語ります。
バイト対符号化(BPE)(Sennrichら、2015)は、文字レベルと単語レベルによる言語モデリングの両方の利点を兼ね備えた手法であり、よく出現するシンボル列には単語レベルの入力を使い、まれにしか登場しないシンボル列には文字レベルの入力を使います。名前に「バイト」が含まれていますが、BPEの実装はバイト列ではなくUnicodeコードポイントを使うことが多いです。
Byte Pair Encoding (BPE) (Sennrich et al., 2015) is a practical middle ground between character and word level language modeling which effectively interpolates between word level inputs for frequent symbol sequences and character level inputs for infrequent symbol sequences. Despite its name, reference BPE implementations often operate on Unicode code points and not byte sequences.
BPEの手法では、頻出する文字列のペアを繰り返し結合(マージ)して、新しい単語やトークンを作り出します。といっても抽象的すぎてわかりにくいので、単純な例を使って要点を解説します。
まず、あるコーパス(テキストを集めたデータセット)の中で4つの単語のみが存在するとします。以下に各単語の頻度を表示します。
頻度 = {
bell: 3,
big: 4,
small: 5,
tall: 2,
}この例のコーパスでは、'bell'という単語は3回出現しています。
さて、このコーパスに登場する4つ単語から文字レベルの語彙を抽出すると以下になります。
語彙 = [ a, b, e, g, i, l, m, s, t ]この状態での語彙は、文字レベルのトークンだけです。
では、文字レベルのトークンを使って各単語を表現します。
頻度 = {
(b, e, l, l): 3,
(b, i, g): 4,
(s, m, a, l, l): 5,
(t, a, l, l): 2,
}次に、隣接するトークンの中で頻出するものを結合します。
'l'と'l'が隣接したケースが全部で10回あります('bell'が3回、'small'が5回、'tall'が2回)。これが最も多く出現するので結合して'll'というトークンにします。
よって、語彙に'll'が追加されて、次のようになります。
語彙 = [ a, b, e, g, i, l, m, s, t, ll ]これを使って各単語を表現すると以下になります。
頻度 = {
(b, e, ll): 3,
(b, i, g): 4,
(s, m, a, ll): 5,
(t, a, ll): 2,
}次に、'a'と'll'が最も多く隣接して出現しています('small'が5回、'tall'が2回)。よって、これを結合して'all'とします。
語彙 = [ a, b, e, g, i, l, m, s, t, ll, all ]
頻度 = {
(b, e, ll): 3,
(b, i, g): 4,
(s, m, all): 5,
(t, all): 2,
}これで、'all'という単語は単語レベルのトークンとして扱われるようになりました。'll'という部分単語(サブワード)のトークンも登場しています。それ以外は文字レベルで扱われます。
以上からわかるように、各ステップでの結合処理で、ペアの頻度が変わってきます。よって、更新されたペアの頻度によって、次に結合するペアが選ばれます。より大きなコーパスでも同様のプロセスを繰り返すことでトークン化が行われます。
これはざっくりとした例ですが、BPEによって文字レベルから単語レベルまで頻度によってトークン化が行われる様子が見えます。
頻度が多いものは連結してまとめるので、文字レベルだけの処理より効率が良くテキストを扱うことができます。つまり、頻繁に使われる単語をより短く表現でき、まれな単語も細かく分割して扱えるようになります。
よって、未知語には文字レベルや部分単語で対処することができます。
以上はアルファベットのみのケースですが、他言語でも考え方は同じです。
例えば、日本語の場合、「こんにちは」という単語を文字レベルで分割すると「こ」「ん」「に」「ち」「は」となります。このようにして文字レベルから出発してBPEの処理を繰り返し適用していきます。
なお、バイト対符号化という名前に「バイト」(byte)と付いていますが、実際にはバイト列ではなく、Unicodeコードポイントを単位として実装されることが多いです。
ただし、GPT-2ではバイト単位でBPEを実装しています。この後に解説します。
UTF-8は、文字を1〜4バイトでエンコードする可変長エンコーディング方式です。よって、ある文字がUTF-8では2バイトで表現されることが(特に日本語などでは頻繁に)ありますが、Unicodeコードポイントでは1つの文字として扱われます。これにより、BPEはいわゆる文字をトークン化の出発点とするので、他言語の複雑な文字エンコーディングにも対応します。
語彙サイズの問題
なお、実際の実装ではいくつかのハイパーパラメータや設定が必要となります。
例えば、最終的な語彙サイズ(vocab_size)を指定します。語彙サイズを1000と指定すると、トークンの結合(マージ操作)が自動的に繰り返され、語彙サイズが1000に達するまで行われます。
マージ操作が多いほど、より大きなトークンが生成され、長い単語に対応することになります。一方で、操作回数が少ないと、トークンが細かく分割され、文字レベルに近いトークン化が行われます。
さらに、語彙サイズが大きいほど、言語モデルが予測するトークンの種類が増えるので、その分だけモデルのパラメータ数も増加し、必要なメモリや計算コストが高くなります。
しかし、Unicodeのコードポイントを全て含めるとなると語彙が膨大になってしまいます。
この点に関して論文は次にのように述べています。
これらの実装では、すべてのUnicode文字列をモデリングするためにUnicodeシンボルの全空間を含める必要があります。その結果、連結によるトークンが追加される以前でも、すでに基本語彙が13万を超えることになります。これは、BPEでよく使用される32,000から64,000トークンの語彙と比較すると、非常に大きなものです。対照的に、バイトレベルのBPEは256のサイズの基本語彙のみを必要とします。
These implementations would require including the full space of Unicode symbols in order to model all Unicode strings. This would result in a base vocabulary of over 130,000 before any multi-symbol tokens are added. This is prohibitively large compared to the 32,000 to 64,000 token vocabularies often used with BPE. In contrast, a byte-level version of BPE only requires a base vocabulary of size 256.
Unicodeコードポイントを扱う場合、すべてのUnicodeシンボルを表現する必要があるため、基本語彙が非常に大きくなります(13万以上)。
しかし、BPEの手法が使われる場合、32,000から64,000トークンぐらいの語彙サイズが通常です。
BPEをコードポイントではなく、バイトレベルで適用する場合、複数バイトの文字は分割されたバイトとして扱われるので、語彙サイズは256に収まります(1バイトで表現できる整数は256個のみ)。
文字カテゴリの区別
しかし…、と論文は続けます。
しかし、バイト列に直接BPEを適用すると、頻度が高いペアから結合するという単純なルールのために最適でない結合(マージ)が生じます。たとえば、"dog" という単語が、 "dog."、 "dog!"、 "dog?" のようにさまざまな形で出現する場合、BPEはこれらそれぞれを別のトークンとして含めることになります。これにより、限られた語彙とモデルの容量が最適に割り当てられない結果となります。
However, directly applying BPE to the byte sequence results in suboptimal merges due to BPE using a greedy frequency based heuristic for building the token vocabulary. We observed BPE including many versions of common words like dog since they occur in many variations such as dog. dog! dog? . This results in a sub-optimal allocation of limited vocabulary slots and model capacity.
「バイト列に直接BPEを適用する」と言っているのは、単純にバイト列から頻度の高いペアを取り出して結合していくことを指します。つまり、文字の種類などを考慮せずに結合を行うと問題が生じるというわけです。
問題の具体例として、同じ「dog」という単語でも、末尾の記号が異なるだけで、"dog."、 "dog!"、 "dog?"などの別々のトークンとして扱われるため、語彙が無駄に増えてしまいます。
ただし、前述のUnicodeコードポイントを使う例では、コーパスを単語に分けた状態からスタートしたのでそのような問題はありません。
よって、バイト単位での処理を行う場合でも、何らかの手段によって不要な連結が起こらないようにする必要があります。
論文はこう続けます。
これを避けるために、カテゴリが異なる文字の結合を防ぎます。ただし、例外としてスペースとの結合は許可することで、語彙トークンの断片化を最小限に抑えながら、圧縮効率を大幅に向上させることができます。
To avoid this, we prevent BPE from merging across character categories for any byte sequence. We add an exception for spaces which significantly improves the compression efficiency while adding only minimal fragmentation of words across multiple vocab tokens.
文字のカテゴリとは、以下のようなものです。
通常の文字:英字のアルファベットや、他の言語の文字。
数字:0〜9の数字や、漢数字など。
句読点:ピリオド(.)、カンマ(,)、感嘆符(!)、疑問符(?)
特殊文字:$、&、@ などのシンボル
例えば、「100点」という文字列が頻繁に登場したとしても、「100」と「点」を連結することはしないということです。
ただし、スペースに関しては連結を許します。
例えば、次のようなテキストはスペースを含めて分割されます。
I have a book.このテキストは、以下のように分割されます。
[ "I", " have", " a", " book", "." ]このように分割された各々のトークン内でバイト単位のBPEを実行します。
もう少し複雑なテキストを、HuggingfaceからのGPT-2のトークナイザーを使ってトークン化してみます。
from transformers import GPT2Tokenizer
# GPT-2トークナイザーのロード
tokenizer = GPT2Tokenizer.from_pretrained("gpt2")
# テキストの定義
text = "I have a book about AXGC99."
# トークン化
tokens = tokenizer.tokenize(text)
token_ids = tokenizer.convert_tokens_to_ids(tokens)
# 各トークンとその対応するID、逆トークン化されたテキストを表示
print(f"Token \t\t Token ID")
print(f"{'-'*30}")
for token, token_id in zip(tokens, token_ids):
print(f"{token:15} {token_id}")このコードでの入力テキストは以下になっています。
I have a book about AXGC99.この出力は次のようになります。
Token Token ID
------------------------------
I 40
Ġhave 423
Ġa 257
Ġbook 1492
Ġabout 546
ĠAX 43051
GC 15916
99 2079
. 13ここで、「Ġ」という記号はスペースを意味します。
また、「AXGC99」という適当な本のタイトルが「AX」、「GC」、「99」と分割されています。
このトークナイザーでは、「AX」や「GC」は連結されたトークンとして語彙に含まれているということです。トークナイザーを訓練したデータセットでそのようなパターンの頻度が高かったということでしょう。
なお、文字と数字が分割されているのも確認できます。
以上により、スペース以外は文字のカテゴリを超えて連結することなく、またUnicodeコードポイントではなくバイト単位でBPEを適用することで、少ない語彙数で多くのトークンを表現することが可能となりました。
OpenAIのGitHubで、GPT-2のBPEの実装を見ると単純な正規表現を使って特別な記号の判別をしているのがわかります。これは処理のスピードを考えると現実的なのかもしれません。ただし、多言語対応の面では不十分かもしれません。
GPT-2の入力表現
GPT-2の入力表現について、論文はこう結んでいます。
この入力表現により、経験的に知られている単語レベルの言語モデルの利点とバイト単位の手法による汎用性を組み合わせることができます。私たちの方法では、どのようなUnicode文字列にも確率を割り当てることができるため、テキストの前処理やトークン化や語彙サイズの大小にかかわらず、どんなデータセットでも言語モデルを評価できます。
This input representation allows us to combine the empirical benefits of word-level LMs with the generality of byte-level approaches. Since our approach can assign a probability to any Unicode string, this allows us to evaluate our LMs on any dataset regardless of pre-processing, tokenization, or vocab size.
これまでの経緯から分かるように、単語レベルのトークン化では経験的に良い性能が出ることがわかっていたが、多言語や未知語の問題を解決するために文字レベルのトークン化が必要だった。さらに、バイト単位の処理を行うことで語彙数を最小限に抑えることが可能になりました。
そのため論文は、この入力表現を使うことで、単語単位での言語モデルが持つ実用的なメリットと、バイト単位の手法による柔軟性を両立させる、と結論づけています。
Unicode文字列にも対応している上、事前にテキストを特定の形式に変換する必要もありません。例えば、テキストをすべて小文字にしたり、特定の単語に分割したりする前処理を必要としません。また、バイト単位の処理のおかげで語彙サイズが大きくなっても柔軟に対応できます。
どのようなデータセットでも、こうしてトークン化された入力表現のシーケンスに対して言語モデリングを行えば、どんな文字列に対しても確率を予測できるようになります。
モデルのアーキテクチャ
トランスフォーマーへの修正
セクション2.3「モデル」は次のように始まります。
私たちは、トランスフォーマー(Vaswaniら、2017)からのアーキテクチャを言語モデルに使用しています。モデルは主にOpenAI GPTモデル(Radfordら、2018)の詳細に従っていますが、いくつかの修正を加えています。
We use a Transformer (Vaswani et al., 2017) based architecture for our LMs. The model largely follows the details of the OpenAI GPT model (Radford et al., 2018) with a few modifications.
GPTシリーズは、トランスフォーマー(Vaswaniら、2017)からのデコーダをベースにしています。GPT-2は、GPT-1(Radfordら、2018)に幾つかの修正を加えたモデルです。両方ともIlya Sutskever(最近、OpenAIを去りました)が関わっています。
修正の概要について論文は以下のように説明しています。
レイヤー正規化
レイヤー正規化(Baら、2016)は、各サブブロックの入力に移動され、これは活性化の前への残差接続(スキップ結合)(Heら、2016)に似ています。また、最後の自己アテンション・ブロックの後に追加のレイヤー正規化が加えられました。
Layer normalization (Ba et al., 2016) was moved to the input of each sub-block, similar to a pre-activation residual network (He et al., 2016) and an additional layer normalization was added after the final self-attention block.
レイヤー正規化はバッチ正規化と似た目的を持ち、訓練中にデータの平均や標準偏差が大きく変わることを防ぎ訓練を安定させるためのものです。
オリジナルのトランスフォーマーやGPT-1ではレイヤー正規化は、自己アテンションや位置ごとのフィードフォワードの後に置かれていました。

オリジナルのトランスフォーマーの後の論文で、レイヤー正規化を自己アテンションや位置ごとのフィード・フォワードの前に行う手法の方がより訓練が安定することがわかりました。これを事前レイヤー正規化(Pre-LN)と呼びます。これがGPT-2では採用されています。
なお、残差接続は、勾配消失の問題を低減するための手法です。こちらで解説しています。
「最後の自己アテンション・ブロックの後に追加のレイヤー正規化が加えられました」とは、最後の自己アテンションの直後ではなく、最後のブロックの後にレイヤー正規化を付け加えたという意味です。さもないと、最後のブロックからの出力に正規化が施されない状態になってしまうからです。
残差接続
モデルの深さによる残差接続の積み重ねを考慮したパラメータ(重み)の初期化が行われています。初期化の際に重みを$${1/\sqrt{N}}$$でスケールします。ここで$${N}$$は残差層の数を示します。
A modified initialization which accounts for the accumulation on the residual path with model depth is used. We scale the weights of residual layers at initialization by a factor of $${1/\sqrt{N}}$$ where $${N}$$ is the number of residual layers.
深いニューラルネットワークでは、モデルが深くなるほど、各層での出力が積み重なって、残差接続を通して伝達される数値が大きくなりすぎることがあります。これを防ぐために、各層の重み(パラメータ)を初期化する際に工夫をしています。
ここで残差層(Residual Layers)と呼ばれているのは、残差接続が行われている層のことです。つまり、自己アテンションと位置ごとのフィードフォワードのことです。
例えば、自己アテンションへの入力と自己アテンションからの出力が結合されたものが、次のフィードフォワードへの入力となり、それがフィードフォワードからの出力へと残差接続されます。
モデルの深さ(ブロックの数)が増えるほど値が継続される回数が増えるので最終的な出力が大きくなり過ぎてしまうリスクがあります。
そこで、自己アテンションや位置ごとのフィードフォワードの重みを設定する際に、モデルの深さ(残差層の数)に応じて、初期値を小さくスケーリングします。
このスケーリングの方法として、重みを初期化する際に$${\frac{1}{\sqrt{N}}}$$ の値でスケールしています。$${N}$$は層の数なので、層の数が増えるほど重みの初期値は小さくなります。
文脈のサイズ
語彙は50,257に拡張され、文脈のサイズは512から1024トークンに増加され、バッチサイズも512に大きくされました。
The vocabulary is expanded to 50,257. We also increase the context size from 512 to 1024 tokens and a larger batchsize of 512 is used.
語彙のサイズが約5万トークンということなのでvocab_size=50,000でBPEを実行したのでしょう。
ここでの文脈は、入力文に与えられる条件となるシーケンスのことです。そのサイズが以前の512トークンから2倍の1024トークンへと増加されました。
GPT-1の事前学習のバッチサイズは64したが、GPT-2では512へと大きく変更されました。
なお、GPT-1のモデル構造はこちらで解説しています。
次回予告
次回も、この論文を読む方針に従って読み進みます。
これまで読んできた「アプローチ」が、どれだけ成功したのかを確認するために「実験」のセクションに目を通していきます。
言語モデルでは様々なタスクを実行するので、対象によっては「アプローチ」が生む効果が異なるでしょう。さて、結果からどんな結論が見えてくるでしょうか。
お楽しみに!
