🕌

GPTを理解したい

に公開

GPTを理解したい記事です。

GPTはtransformerを基礎としたモデルです。transformerの解説記事はこちら。

https://zenn.dev/sunbluesome/articles/078ac9a9afca6a

元論文「Improving Language Understanding by Generative Pre-Training」をベースにまとめていきます。

凄いポイント

  • 様々なタスクをモデルの構造をほとんど変えずに行える。しかも12タスク中9つでSOTA。

GPTの構造

Transformer Decoder (T-D)

GPTは[2]で提案されたTransformer Decoder (T-D)を用いたモデルで、以下のような構造をしています(下図左側)。


[1] Figure 1 より引用

初期のTransformerもそうですが、当時の言語モデルはencoder-decoderネットワークが主流でした。例えば英語をドイツ語に変換する翻訳タスクでは、encoderに英語の単語列(入力)を、decoderに過去のドイツ語の単語列(過去の出力)を入力し、次単語予測を行います。

具体例

例えば、「Hello world」という英語の文を「Hallo Welt」というドイツ語に翻訳するタスクを考えます。

  1. Encoder:

    • まず、Encoderは入力として英語の文全体「Hello world」を受け取ります。
    • Encoderはこれを処理し、文全体の意味を表す情報(コンテキストベクトルなどと呼ばれる)を生成します。
  2. Decoder:

    • Decoderは、Encoderが生成したコンテキストベクトルを受け取ります。
    • 1単語目の生成: Decoderは、コンテキストベクトルと、翻訳開始を示す特別なトークン(例:<start>)を入力として、ドイツ語の最初の単語「Hallo」を予測・生成します。
    • 2単語目の生成: 次にDecoderは、コンテキストベクトルと、**直前に生成したドイツ語の単語「Hallo」**を入力として、次のドイツ語の単語「Welt」を予測・生成します。
    • 翻訳終了: 同様に、コンテキストベクトルと、**それまでに生成したドイツ語の単語列「Hallo Welt」**を入力として、文の終わりを示す特別なトークン(例:<end>)を生成し、翻訳が完了します。

つまり、「decoderに過去のドイツ語の単語列(過去の出力)を入力し」というのは、Decoderが次の単語を予測するために、それまでに自身が生成したドイツ語の単語列を使う、という意味です。ステップごとに、直前までの翻訳結果(過去の出力)を参照しながら、次の単語を生成していく流れになります。

GPTのようなDecoder-onlyモデルでは、このようなEncoderとDecoderの役割分担はなく、入力と出力を一つの系列として扱って学習する点が異なります。

Transformer Decorderはパラメータを半減することができる半面、decoder側の入力しか受け付けません。その為、入力列と過去の出力列を結合した系列を作り学習を行います。つまり、(m^1, \ldots, m^n) \mapsto (y^1, \ldots, y^n) のような系列変換写像があったとき、
これらを結合して (u^1, \ldots, u^{n+\eta+1}) = (m^1, \ldots, m^n, \delta, y^1, \ldots, y^\eta) のような1つの系列にして学習を行います。\delta は区切り文字トークンです。

具体例

英語からドイツ語への翻訳タスクを例にとると:

  • m = (m^1, ..., m^n) は入力である英語の単語列(例: Hello, world
  • y = (y^1, ..., y^η) は出力であるドイツ語の単語列(例: Hallo, Welt
  • δ は区切り文字トークン(例: <DELIMITER>

GPTのようなDecoder-onlyモデルで翻訳タスク(あるいは同様の系列変換タスク)を行う場合、学習データはこのように準備されます。

具体例:

英語「Hello world」をドイツ語「Hallo Welt」に翻訳する学習データの場合:

  1. 入力系列 m: (Hello, world)
  2. 出力系列 y: (Hallo, Welt)
  3. 区切り文字 δ: <DELIMITER>
  4. 結合系列 u: (Hello, world, <DELIMITER>, Hallo, Welt)

学習プロセス:

モデルは、この結合された系列 u を単一の入力として受け取ります。そして、次に来る単語を予測するように学習します。

  • Hello を見て world を予測する
  • Hello, world を見て <DELIMITER> を予測する
  • Hello, world, <DELIMITER> を見て Hallo を予測する
  • Hello, world, <DELIMITER>, Hallo を見て Welt を予測する
  • ... (文末を示すトークン <end> などを予測するまで続く)

Transformer DecoderのMasked Self-Attention機構により、各単語を予測する際には、それより前の単語しか参照できません。これにより、モデルは入力(英語)とそれまでに出力した部分(ドイツ語)の情報を使って、次に出力すべきドイツ語の単語を学習します。

推論(実際に翻訳を行う)時:

  1. モデルに (Hello, world, <DELIMITER>) を入力します。
  2. モデルは次の単語として Hallo を予測・出力します。
  3. 次に、モデルに (Hello, world, <DELIMITER>, Hallo) を入力します。
  4. モデルは次の単語として Welt を予測・出力します。
  5. これを繰り返し、文末を示すトークンが出力されたら翻訳を終了します。

このように、Encoder-Decoderモデルのように明示的に入力と出力を分けるのではなく、一つの長い系列として扱い、次単語予測の枠組みで系列変換タスクを実現するのが、この部分で説明されている方法です。

教師なし事前学習

トークンの集合が \mathcal{U} = \left\{u_1, \ldots, u_n\right\} で与えられるとき、パラメータ \Theta で定義されるニューラルネットワークの対数尤度 L_1 を最大化することを考えます。

\begin{align} L_1 (\mathcal{U}) = \sum_i \log P(u_i | u_{i-k}, \ldots, u_{i-1};\Theta) \end{align}

ここで、kはコンテキストのウィンドウサイズです。

GPTは先述したTransformer Decoderの構造を利用しており*、以下のように定義されます。

\begin{align} h_0 &= UW_e + W_p \\ h_i &= \text{transformer\_block}(h_{i-1}) \quad \forall i \in [1, n]\\ P(u) &= \text{softmax}(h_nW_e^T) \end{align}

ここで、\mathcal{U} = (u_{i-k}, \ldots, u_{i-1})はトークンのコンテキストベクトル、nは層の数、W_eはトークン埋め込み行列(隠れ状態へ埋め込むベクトル)、W_pはposition encodingです(position encodingについてはこちらを参照)。

*transformer blockはmulti-headed self-attentionが使われていることに注意してください。

教師ありファインチューニング

式 (1) を最大化するように学習した後、目標タスクの教師あり学習を行います。

目標タスクの学習データセット(X, y) \in \mathcal{C}を準備します。\left\{x^1, \ldots, x^m\right\} \in Xは入力トークン列、yは入力トークン列に対応するラベルです。このとき、次の尤度関数を最大化するようにファインチューニングを行います。

\begin{align} L_2 (\mathcal{C}) = \sum_{(x, y)} \log P (y|x^1, \ldots, x^m) \end{align}

GPTでいうところのファインチューニングでは、事前学習したモデルの最終層のみパラメータ更新します。つまり、入力トークン列を事前学習したモデルへ通し、transformer blockの最終出力h_l^mを得ます。次に、h_l^mを線形レイヤーへ入力し、y を予測する線形レイヤーのパラメータ W_y を学習しています。

\begin{align} P(y|x^1, \ldots, x^m) = \text{softmax}(h_l^m W_y) \end{align}

auxiliary objective

auxiliary objective(補助目的)を含めてファインチューニングを行うと、汎化性能と収束性が向上する事が分かったと筆者らは言っています。具体的には、新たな目的関数

\begin{align} L_3(\mathcal{C}) = L_2(\mathcal{C}) + \lambda * L_1(\mathcal{C}) \end{align}

を導入してファインチューニングを行います。ここで、\lambda は任意の定数です。

タスクに合わせた入力設計

分類タスクであれば、上記のようなファインチューニングを素直に行えば良いのですが、文章とそれに対する質問に対して回答を得るタスクや、文章間の類似度評価を行うためには入力を工夫する必要があります。GPTモデルは1系列での学習にしか対応しておらず、2系列の入力が必要な文の類似度評価や、3系列の入力が必要になる文章とそれに対する質問に対して回答を得るタスクはそのままでは学習できないためです。

[1]ではTextual entailement、Similarity、Question Answering and Commonsense Reasoningでの例が紹介されています。


再掲:[1] Figure 1 より引用

上図の右側でそれぞれのタスクにおける学習方法の模式図があるので、適宜確認すると以下の説明が分かりやすくなると思います。

Texutual entailement

日本語では、テキスト含意タスクと言います。前提トークン p と仮説トークン h が与えられたときに、ph を含意するかどうかを判定するタスクです。要は、p から h を推論することは可能かどうかを判定するタスクということですね。単純に ph をデリミタトークン($)で繋げはOKです。

Similarity

2つの文の類似度を評価します。デリミタトークンを文間に挟みこむのはTextual entailementと同様です。ただ、2つの文間に順序関係は無いので、順序の異なる2つの入力系列を用意し、それぞれの独立にモデルへ渡して隠れ状態を得ます。得られた2つの隠れ状態の要素ごとの和をとり、最後にlinear layerへ渡します。

Question Answering and Commonsense Reasoning

z、質問 q から、回答 \{a_k\} を予測するタスクです。これまでと同様に、デリミタトークン($)で全てを結合し、[z; q; $; a_k] とします。回答ごとに独立にモデルへ渡し、最後にsoftmaxを掛けることで回答に対する出力分布を得ます。

GPTの性能

最後に様々なタスクに対するGPTの性能を見て終わりましょう。

自然言語推論タスクの性能


[1] table 2 より引用

6つの自然言語推論タスクで、当時のSOTAモデルとの性能比較を行っています。ファインチューニング後のGPTモデルは6つ中5つでSOTAを達成しています。

Question Answering and Commonsense Reasoningの性能

これは質問に対する回答を得るタスクでした。全てのタスクでSOTAを達成しています。


[1] table 3 より引用

Semantic similarity and classificationの性能

6つ中4つでSOTAです。


[1] table 4 より引用

まとめ

  • GPTはTransformerのDecoder側を変形したモデル。
  • ほとんどモデル構造や目的関数を変えることなく様々なタスクでSOTAを達成

にわかには信じがたい威力のモデルですが、GPT-nと総称される後継モデルが人間とほぼ同じクオリティの文章を生成できるなるなど、このモデルの威力は十分実証されていますね。

参考文献

  1. Radford, A., Narasimhan, K., Salimans, T. & Sutskever, I. (2018). Improving Language Understanding by Generative Pre-Training.
  2. Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, L. & Polosukhin, I. (2017). Attention Is All You Need. arXiv. https://doi.org/10.48550/arxiv.1706.03762

Discussion