1本目! Language Models are Unsupervised Multitask Learners

ではでは、記念すべき1本目の論文はGPT-2についての論文です。

1. Model

Transformerに基づいたモデル(というか、かの有名なAttention Is All You Need)によって作られている。 まあ、そうだろうけど。 実装に関しては Open AI の Improviding Language Understanding by Generative Pre-Training がもとになっている。 Source Codeはここみたい。時間があれば、実装を見てみたい。

gpt-2において、前述のモデルからの変更点としては Layer normalization を追加したことで、sub-blockっていうところと、final self-attention blockってところの2か所に追加したらしい。両方とも何なのかわからん!多分、Transformerの論文を読めばわかると思うので次の次あたりにAttention is All You Need を読みたい。

話をもとに戻して、初期化というか、モデルの初期値に関しては residual layer において、N= residual layerの数として、 \frac{1}{\sqrt{N}}倍しているらしい。なぜだろう?

あと、vocab sizeが50,257で、最大の文章のtoken数が1024で、batch sizeが512って…。

ただの、バケモンやん"(-""-)"

しかも、GPT-3になるとこれが進化するっていう。

2.Framework

GPT-2におけるフレームワークは2つのステージからなる。

  1. 大容量の言語モデルを巨大なテキストデータを使って、教師なし学習により学習するステージ。

2.教師あり学習により、識別タスクを学習させて、Fine-Tuning を行うステージ。

疑問点としては、なんでタスクが異なるのにきれいに学習することができているのか?

というか、1の教師なし学習はどうやっているのかがわからん。

1.教師なし学習による事前学習

まずは、tokensを用意する。ただし、tokens = [token_1, token_2, ..., token_n-1]とする。

入力データは、input = [token_i-k, token_i-k+1, ... , token_i-1]として、token_iを予測するモデルを学習するみたい。

ではどうやってtoken_iを予測するのかってことが大事。ここではkはcontext windowというらしい。

ここで使われているモデルは multi-layer Transformer Decoderというもので、Generating Wikipedia by Summarizing Long Sequencesという、論文に使われているDecoder。

ああ、Decoderを学習するのか!Decoderの主な役割は次に来る単語の予測だし。なるほど。

でも、そうするとEncoderの入力とかはどうするんだろう?

教師あり学習だと、Training時にはDecoderのInputは、Traning dataからつくられたEncoderの入力と教師データの2つが入って、Prediction時にはDecoderのInputは、Test dataからつくられたEncoderの入力と tokenの入力がある。

つまり、Encoderの入力なしで、DecoderのPre-Trainingは行われることとなる。

まじでどうすんの?まあ、一旦このことはおいておいて、Decoderの Modelについて見ていこう!

とりあえず、数式だけ並べてみる。


h_0 = UW_e + W_p \\
h_l = transformer\_block(h_{l-1}) \\
P(u) = softmax(h_nW^T_e)

ここで、 W_e = embedding \; matrix, W_p = position \; embedding \; matrixとなっている。あとここで論文だと、 \forall i [1, n]となっているけど、たぶん、iじゃなくてnの間違い。

embedding matrixってword2vecのやつで、gpt-2とかだと、T5-TokenizerとかGPT2-Tokenizerとかのやつなんだろうけど、実際に説明してくださいと言われるとよくわかっていない。

position embedding なんて、もっとよくわかっていない( ゚Д゚)。

Transformerの論文でよく調べよう...。

2.教師なし学習による事前学習

input = [x_1, x_2, ..., x_m]として、yを出力したいとして…。


P(y|x^1, x^2, ..., x^m) = softmax(h_l^mW_y)

という式らしい。ここで分かんないのが [tex: h_lm\;W_y] の2つについて。

たぶん、前者についてはlじゃなくてnの間違い。後者の W_yがFine-Tuniningにおいて、学習するパラメーターらしい。

事前学習では転置したembedding の結果を用いていたから、そこの変更みたい。

rinnaの実装を見てみると、Softmaxの結果まで

model.train()
with amp.autocast():
      loss, ppl = forward_step(model, tokenizer, batch_data)

により、forward_stepでlossまで計算している。ではforward_stepの中身は?

def forward_step(model, tokenizer, batch_data):
    max_seq_len = max([len(seq) for seq in batch_data])

    # padding input sequences
    batch_data = [seq + [tokenizer.pad_token_id]*(max_seq_len-len(seq)) for seq in batch_data]

    # convert to tensors
    batch_tensor = torch.LongTensor(batch_data).to(model.device)

    # get inputs and outputs
    input_ids = batch_tensor[:, :-1].contiguous()
    output_ids = batch_tensor[:, 1:].contiguous()
    
    # forward
    gpt2_outputs = model(input_ids=input_ids, return_dict=True)
    loss = F.cross_entropy(
        gpt2_outputs["logits"].view(-1, len(tokenizer)),
        output_ids.view(-1),
        ignore_index=tokenizer.pad_token_id,
        reduction="mean"
    )
    with torch.no_grad():
        ppl = loss.exp()

    return loss, ppl

contiguous()はメモリの整理のための関数だから、一旦おいておいて、そのあと。 gpt_outputs["logits"]が出力データ。ここで、modelはGPT2LMHeadModelだから、実装の出力データを見てみる。

欲しいのはEmbeddingの転置行列ををかける前のデータ。

    def forward(
        self,
        input_ids=None,
        past_key_values=None,
        attention_mask=None,
        token_type_ids=None,
        position_ids=None,
        head_mask=None,
        inputs_embeds=None,
        encoder_hidden_states=None,
        encoder_attention_mask=None,
        labels=None,
        use_cache=None,
        output_attentions=None,
        output_hidden_states=None,
        return_dict=None,
    ):
        r"""
        labels (:obj:`torch.LongTensor` of shape :obj:`(batch_size, sequence_length)`, `optional`):
            Labels for language modeling. Note that the labels **are shifted** inside the model, i.e. you can set
            ``labels = input_ids`` Indices are selected in ``[-100, 0, ..., config.vocab_size]`` All labels set to
            ``-100`` are ignored (masked), the loss is only computed for labels in ``[0, ..., config.vocab_size]``
        """
        return_dict = return_dict if return_dict is not None else self.config.use_return_dict

        transformer_outputs = self.transformer(
            input_ids,
            past_key_values=past_key_values,
            attention_mask=attention_mask,
            token_type_ids=token_type_ids,
            position_ids=position_ids,
            head_mask=head_mask,
            inputs_embeds=inputs_embeds,
            encoder_hidden_states=encoder_hidden_states,
            encoder_attention_mask=encoder_attention_mask,
            use_cache=use_cache,
            output_attentions=output_attentions,
            output_hidden_states=output_hidden_states,
            return_dict=return_dict,
        )
        hidden_states = transformer_outputs[0]

        # Set device for model parallelism
        if self.model_parallel:
            torch.cuda.set_device(self.transformer.first_device)
            hidden_states = hidden_states.to(self.lm_head.weight.device)

        lm_logits = self.lm_head(hidden_states)

        loss = None
        if labels is not None:
            # Shift so that tokens < n predict n
            shift_logits = lm_logits[..., :-1, :].contiguous()
            shift_labels = labels[..., 1:].contiguous()
            # Flatten the tokens
            loss_fct = CrossEntropyLoss()
            loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))

        if not return_dict:
            output = (lm_logits,) + transformer_outputs[1:]
            return ((loss,) + output) if loss is not None else output

        return CausalLMOutputWithCrossAttentions(
            loss=loss,
            logits=lm_logits,
            past_key_values=transformer_outputs.past_key_values,
            hidden_states=transformer_outputs.hidden_states,
            attentions=transformer_outputs.attentions,
            cross_attentions=transformer_outputs.cross_attentions,
        )

ここでのtransformer_outputsはGPT2Modelの出力結果化なので見てみると。

        hidden_states = self.ln_f(hidden_states)

        hidden_states = hidden_states.view(*output_shape)
        # Add last hidden state
        if output_hidden_states:
            all_hidden_states = all_hidden_states + (hidden_states,)

        if not return_dict:
            return tuple(v for v in [hidden_states, presents, all_hidden_states, all_self_attentions] if v is not None)

        return BaseModelOutputWithPastAndCrossAttentions(
            last_hidden_state=hidden_states,
            past_key_values=presents,
            hidden_states=all_hidden_states,
            attentions=all_self_attentions,
            cross_attentions=all_cross_attentions,

実装を見た感じ、return_dict=Trueだから、hidden_statesを出力していることがわかる。けど、わかんないのがhidden_statesにsoftmaxをかけていないこと。

ここでF.cross_entorpyについて調べてみるとこうなる。

>>> import torch
>>> input = torch.randn(3, 5, requires_grad=True)
>>> input
tensor([[-1.0700, -1.2422,  1.8608,  0.1510, -0.6074],
        [-0.0301, -0.7651,  1.3767, -0.5843, -0.4207],
        [-0.4349, -0.6272,  0.4609, -0.5524,  1.1496]], requires_grad=True)
>>> target = torch.randint(5, (3,), dtype=torch.int64)
>>> target
tensor([3, 2, 1])
>>> import torch.nn.functional as f
>>> loss = f.cross_entropy(input, target)
>>> loss
tensor(1.6771, grad_fn=<NllLossBackward>)

つまり、softmaxはcross_entropy関数内で行われているみたい。

ってことは、論文にあるようにembeddingの転置行列をhidden_nにかけることは行われていないっぽい。

えー。

ちょっと、大元の実装を確認してみる。

        h = embed(X, we)
        for layer in range(n_layer):
            h = block(h, 'h%d'%layer, train=train, scale=True)

        lm_h = tf.reshape(h[:, :-1], [-1, n_embd])
        lm_logits = tf.matmul(lm_h, we, transpose_b=True)
        lm_losses = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=lm_logits, labels=tf.reshape(X[:, 1:, 0], [-1]))

ちゃんと、embeddingの転置行列をhiddenの出力にかけてる。

良く調べると、logitsはself.lm_head(hidden_states)により出力されている。

これは、

self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)

となっている。つまり、sizeがconfig.n_embdの入力を入れてから、config.vocab_sizeのsizeの出力をするっていうこと。 始めに入れるembdding_layerのサイズが入力がconfig.vocab_sizeで出力がconfig.n_embd,のサイズだから、ちょうどembeddingの転置行列をかけたのと同じ役割をサイズ的には果たしていることになる。

つまり、最初から W_yの計算をしていることになるのかな?

だから、Fine-tuniningにおいても最後のところで W_eを置き換えずに計算するとか?

なんとなく理解したようなしていないような?

3.Task-specific input transformations

さてさてさーて、これをどうFine-Tuningするのか?

論文を読んでみると、専用のArchitectureに導入するためにたくさんのCustomizationが必要だったみたい。

けれど、今回はそうじゃなくてTraversal-styleのアプローチをしたみたい。

この論文はReasoning about entailment with neural attentionっていう論文で次回読もう。

読んだ感じでは専用のtokenをいろいろなtokensの間に入れて、学習するみたいな感じがする。

でも、疲れたので今日はここまでノシ

追記: 2021/08/28

Transfomerの論文を読んで、Question-AnsweringのTaskについて怖くなったので、調べてみた。

なんか、GPT-2ではQuestion-AnsweringのTaskとTranlationのタスクの入力は同じだということで、調べてみると、english sentence= french sentence \n english sentence = french sentence \n ... \n english sentence = french sentence \n english sentence = の入力らしい。

疑問点が2つ。何個のenglish sentence = french sentence のペアを入力としていれりゃあいいんだ?ってことと。教師あり学習のLossの計算用として出力するデータはどこまで出せばいいのかってこと。

1つ目はenglish sentence= french sentenceの単体のペアだけをぶち込んでいけば、単純なんだけど、文脈がわからない。つまり、文脈がわかるところまでぶち込まなきゃいけないんだけど、どうしたらいいんだろう。 ついでに言えば、こうするとデータの絶対数が少なくなるのに、1つのsentenceの長さが大きくなるから、どうすんだろう?と思う。

2つ目については<\endofsentence>までの出力なのか?「.?」のどちらかまでの出力かわからない(´;ω;`)

いろいろ調べてみると、gpt-2を使って、dialogue generationしている論文(DialoGPT Large-Scale Generative Pre-training for Conversational Response Generation)ってやつがあるから読んでみよう!

https://github.com/microsoft/DialoGPT/blob/master/reddit_extractor/src/reddit.py Line:411, 412