BERTによる文章のネガポジ判定と根拠の可視化

#1.はじめに
　**「PyTorchによる発展的ディープラーニング」**を読んでいます。今回は、8章のBERTを勉強したので自分なりのまとめをアウトプットしたいと思います。

#2.BERTとは？
　[Transformer](https://qiita.com/jun40vn/items/91c3ad5e62494062647f) が発表された翌年の2018年に、とうとう自然言語処理分野でも人間を超える精度を持った**BERT**が発表されました。

　BERTは、あらゆる自然言語処理タスクにファインチューニングで対応でき、なんと11種類のタスクにおいて圧倒的なSoTAを達成したのです。

![スクリーンショット 2020-07-30 13.31.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/bfc49790-9291-fc4b-c433-6533daa02c78.png)

　これが論文に載っているBERTのモデル図です。単語の長さ方向に展開してあるので複雑に見えますが、簡単に言えば**TransformerのEncoder部分のみ**を取り出したものです。では、BERTとTransformerの違いは何処にあるのでしょうか。それは、**2種類のタスクによる事前学習**と目的のタスクに合わせた**ファインチューニング**の**2段階学習**を行うことです。

**1) 2種類のタスクによる事前学習**
![スクリーンショット 2020-07-31 17.54.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/aa45ef22-74e7-d9cb-ed0b-71e57c33df3b.png)

　センテンスの15％の単語にマスクをしてその単語を当てるタスク（**Masked Language Model**）と2つの文の文脈が繋がっているのかどうかを判定するタスク(**Next Sentence Prediction**)を**同時に学習**します。

　入力するセンテンスの先頭には**[CLS]**を入れ、2つの文には1つ目か2つ目を表す**埋め込み表現を加算**すると共に間に**[SEP]**を入れます。

　この2つのタスクを学習することによって、**単語を文脈に応じて特徴ベクトルに変換できる能力**や**文が意味的に繋がってるかどうか判定できる能力**（おおまかに文の意味を理解する能力)を獲得します。

　この地頭を鍛えるような事前学習にはかなり計算コストがかかり、TPUを4つ使っても4日間くらい掛かるらしいですが、誰かが1回やれば後はファインチューニングで様々なタスクが解けるネットワークに変身できるわけです。

**2) ファインチューニング**
![スクリーンショット 2020-07-31 17.54.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/dad7e7ad-fd7b-2f4e-048a-4fceeffddb10.png)

　**事前学習の重みを初期値**として、ラベルありデータでファインチューニングを行います。
事前学習によって地頭が相当鍛えられているので、**少ない文章データから性能の良いモデルが作成可能**です。論文では、様々なタスクのファインチューニングの計算コストは、TPU1つで1時間以内で終わったということです。

　以下は、BERTがSoTAを記録した11個のNLPタスクです。

|データセット |タイプ      |概要                               |
|------|-------|-----------------------------------------|
|MNLI |推論    |前提文と仮説文が含意/矛盾/中立のいずれか判定     |
|QQP  |類似判定 |前提文と仮説文が含意/矛盾/中立のいずれか判定     | 
|QNLI |推論    |文と質問のペアが渡され、文に答えが含まれるか否かを判定|
|SST-2|1文分類 |文のポジ/ネガの感情分析                        |
|CoLA |1文分類 |文が文法的に正しいか否かを判別                   |
|STS-B|類似判定 |2文が意味的にどれだけ類似しているかをスコア1~5で判別|
|MRPC |類似判定 |2文が意味的に同じか否かを判別                  |
|RTE  |推論    |2文が含意しているか否かを判定                   |
|SQuAD v1.1|推論  |質問文と答えを含む文章で、答えがどこにあるか予測  |
|SQuAD v2.0|推論　|v1.1に答えが存在しないという選択肢を加えたもの|
|SWAG      |推論　　　　|与えられた文に続く文を4択から選ぶ           |


**3) その他の違い**
・Transformerでは単語の位置情報をPositonal Encoderではsin,cosからなる値を与えていましたが、BERTでは学習させます。
・活性化関数の一部にReLUではなく、GELU(入力0のあたりの出力がカクっとせず、滑らかになっている)を使っています。

#3.今回実装するモデル

![スクリーンショット 2020-08-01 13.11.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/9b02891e-ec18-6e45-00bb-2269658f1b2e.png)


　今回は、BERTの**事前学習済みモデル**を使って、**文章のネガポジ判定をするタスクのファインチューンニング**を行います。BERTモデルにはモデルサイズの異なる2つのタイプがあり、今回使うのはBaseと呼ばれる小さい方のモデルです。

　BERTの出力は**識別用**と**トークンレベル用**の2つあり、今回は識別用に全結合層を接続しネガポジ判定を行います。使用するデータセットは、映画のレビュー(英文)の内容がポジティブなのかネガティブなのかをまとめた**IMDb**(Internet Movie Dataset)です。

　モデルを学習させることによって、ある映画のレビューを入力したら、そのレビューがポジティブなのかネガティブなのかを判定し、レビューの単語の相互Attentionから判定の根拠にした単語を明示させるようにします。

#4. モデルのコード

```python
from bert import get_config, BertModel, set_learned_params

# モデル設定のJOSNファイルをオブジェクト変数として読み込みます
config = get_config(file_path="./data/bert_config.json")

# BERTモデルを作成します
net_bert = BertModel(config)

# BERTモデルに学習済みパラメータセットします
net_bert = set_learned_params(
    net_bert, weights_path="./data/pytorch_model.bin")
```
　BERTモデルを作成し、事前学習済みの重みパラメータをセットします。

```python
class BertForIMDb(nn.Module):
    '''BERTモデルにIMDbのポジ・ネガを判定する部分をつなげたモデル'''

    def __init__(self, net_bert):
        super(BertForIMDb, self).__init__()

        # BERTモジュール
        self.bert = net_bert  # BERTモデル

        # headにポジネガ予測を追加
        # 入力はBERTの出力特徴量の次元、出力はポジ・ネガの2つ
        self.cls = nn.Linear(in_features=768, out_features=2)

        # 重み初期化処理
        nn.init.normal_(self.cls.weight, std=0.02)
        nn.init.normal_(self.cls.bias, 0)

    def forward(self, input_ids, token_type_ids=None, attention_mask=None, output_all_encoded_layers=False, attention_show_flg=False):
        '''
        input_ids： [batch_size, sequence_length]の文章の単語IDの羅列
        token_type_ids： [batch_size, sequence_length]の、各単語が1文目なのか、2文目なのかを示すid
        attention_mask：Transformerのマスクと同じ働きのマスキングです
        output_all_encoded_layers：最終出力に12段のTransformerの全部をリストで返すか、最後だけかを指定
        attention_show_flg：Self-Attentionの重みを返すかのフラグ
        '''

        # BERTの基本モデル部分の順伝搬
        # 順伝搬させる
        if attention_show_flg == True:
            '''attention_showのときは、attention_probsもリターンする'''
            encoded_layers, pooled_output, attention_probs = self.bert(
                input_ids, token_type_ids, attention_mask, output_all_encoded_layers, attention_show_flg)
        elif attention_show_flg == False:
            encoded_layers, pooled_output = self.bert(
                input_ids, token_type_ids, attention_mask, output_all_encoded_layers, attention_show_flg)

        # 入力文章の1単語目[CLS]の特徴量を使用して、ポジ・ネガを分類します
        vec_0 = encoded_layers[:, 0, :]
        vec_0 = vec_0.view(-1, 768)  # sizeを[batch_size, hidden_sizeに変換
        out = self.cls(vec_0)

        # attention_showのときは、attention_probs（1番最後の）もリターンする
        if attention_show_flg == True:
            return out, attention_probs
        elif attention_show_flg == False:
            return out
```
　BERTモデルにIMDbのネガ・ポジを判定するLinearをつなげたモデルです。重みパラメータの更新は、BertLayerの全部の層で行うとヘビーなので、BertLayerの最終層(12層目)と追加したLinearのみで行います。

#5.コード全体と実行
　コード全体は Google Colab で作成し Github に上げてありますので、自分でやってみたい方は、この [**「リンク」**](https://github.com/cedro3/BERT/blob/master/BERT_IMDb.ipynb) をクリックし表示されたシートの先頭にある「Colab on Web」ボタンをクリックすると動かせます。

　コードを実行するとたった2epochの学習ですが、テストデータの正解率は約90%でした。前回、[Transfomerでも同じタスクをやった](https://qiita.com/jun40vn/items/91c3ad5e62494062647f)のですが、その時の正解率が約85％でしたので、**＋5ポイントの改善**になっています。

　さて、判定根拠の明示ですが、
![スクリーンショット 2020-08-01 14.05.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/67dfde24-c7c7-208a-c178-a2d0407df6cd.png)
　こんな感じで、どの単語を判定根拠にしたかを明示します。

（参考）
・[つくりながら学ぶ! PyTorchによる発展ディープラーニング](https://github.com/YutaroOgawa/pytorch_advanced)
・[自然言語処理の王様「BERT」の論文を徹底解説](https://qiita.com/omiita/items/72998858efc19a368e50)