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

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

#2.Transformerとは？
　2017年自然言語処理の分野でエポックメイキングな論文**「Attention All You Need」**が発表されました。そこで提案されたモデルが **Transformer** で、翻訳タスクにおいて今まで主流であったRNNを一切使わずに、**Attention** のみでSoTAを達成しました。

　以後、自然言語処理分野では **BERT、XLNet、ALBERT** など、この Transformer をベースにしたモデルが席巻し、自然言語処理なら Transformer と言われるようになりました。

![スクリーンショット 2020-07-28 10.04.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/f790be3c-707a-5e02-61be-904ca04e6f02.png)


　これが、翻訳タスクを行うTransformerのモデル図です。例えば日英翻訳を考えると、左側の**Encoder**で日文の各単語のAttentionを学習し、その情報を参照しつつ右側の**Decoder**で英文の各単語のAttentionを学習するわけです。それでは、特徴を5つ説明します。

**1) Psitional Encoding**
　Transformer最大の狙いは、RNNのように単語を１つづつ処理するのではなく、センテンス毎に単語を全て**並列処理**することで、GPUを活用し処理速度の大幅アップを図ることです。そのために、**Positional Encoding**で各単語に単語順の情報を付加し、並列処理によって単語順の情報が失われることを防いでいます。

**2) Scaled dot-product Attention**
　これが Transformer の肝なので、少し丁寧に説明します。Attentionの計算には、**Query**(Attentionを計算したい単語ベクトル)、**Key**(関連度の計算に使う単語ベクトルの集まり)、**Value**(重み付け和計算に使うベクトルの集まり)の3つが登場します。

![スクリーンショット 2020-07-28 13.55.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/58d00581-90ac-46dc-190c-4200872b7546.png)
　センテンスが「吾輩」、「は」、「猫」、「で」、「ある」と5つの単語で構成されているときに、「吾輩」のAttentionをどう計算するかを説明します。

　関連度はベクトルの内積で計算できるので、「吾輩」ベクトル**Query**と5つの単語ベクトルの転置行列**$Key^T$**の内積を取ります。そして、${\sqrt{d_k}}$で割ってからSoftmaxを掛けることで、「吾輩」にどの単語がどの程度関連しているかを表す重み(**Attention Weight**)を求めます。

　${\sqrt{d_k}}$で割る理由は、内積計算で大き過ぎる値があるとSoftmaxを掛けたとき、それ以外の値が0になってしまう恐れがあるためです。

　次に、**Attention Weight** と5つの単語ベクトルの行列**Value**を**内積**することで、「吾輩」に**関連度の深い単語のベクトル成分が支配的な**Context Vectorが計算できます。これが「吾輩」のAttentionの計算です。

　さて、Transformer は並列計算が可能で、全てのQueryに対して一気に計算が出来るので、
![スクリーンショット 2020-07-28 18.41.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/496bea44-4253-03f6-2577-d01988a8e544.png)


　このように全てのQueryのAttention計算が一発で完了します。この計算が論文では下記の式で表されています。

```math
Attention(Q, K, V)=softmax(\frac{QK^T}{\sqrt{d_k}})V
```

**3) Multi-Head Attention** 

![スクリーンショット 2020-07-28 18.30.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/698e6c4b-f8a8-6d08-8fb5-e6f05a1ffcd9.png)

　Scaled dot-product Attentionへの入力 **Query, Key, Value**　は、前段の出力がそれぞれの全結合層を経由して入って来る構造になっています。つまり、前段の出力にそれぞれ重み$W_q,W_k,W_v$を掛けたものです。
　このとき、大きな Query, Key, Valueのセット(ヘッドと言います)を1つ持たせるよりも、小さな Query, Key, Valueのヘッドを複数個持たせ、それぞれのヘッドが潜在表現$W_q,W_k,W_v$を計算し最後に１つにした方がパフォーマンスが上がるというのが **Multi-Head Attention**です。

**4) Musked Multi-Head Attention**
![スクリーンショット 2020-07-29 09.37.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/94347c54-cb05-583d-18b0-7811b807ee80.png)

　Decoder側のAttentionも並列計算するわけですが、「I」のAtentionを計算するとき、「am」、「a」、「cat」を計算対象に入れると、予測すべき単語をカンニングすることになるので、Keyにある先の単語は見えなくするためにマスクを掛けます。この機能を加えた Multi-Head Attention を、**Musked Multi-Head Attention** と呼びます。

**5) Position-wise Feed-Forward Networks**
　これは、Attention層からの出力を2層の全結合層で特徴量を変換するユニットです。入力が(単語数,単語埋め込み次元数)、これに2つの全結合層の重みとの内積をとったものが出力(単語数,単語埋め込み次元数)となります。各単語毎に独立したニューラルネットワークがある様な形になるので、**Position-wise** という名前を付けています。


#3.今回実装するモデル
　今回は、Transformer翻訳モデルの左側のEncoderだけを使って、**センテンスの各単語のAttentionを学習することで分類タスクを解かせるモデル**を実装します。なお、分かりやすさを優先し、Multi-Head Attention ではなく、Single-Head Attention　です。
![スクリーンショット 2020-07-27 16.53.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/912ab460-1ed8-0912-c430-cd72f8c9db12.png)

　使用するデータセットは、映画のレビュー(英文)の内容がポジティブなのかネガティブなのかをまとめた**IMDb**(Internet Movie Dataset)です。

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

　それでは、入力から順番に実装して行きたいと思います。

#4.モデルのコード

```python
class Embedder(nn.Module):
    '''idで示されている単語をベクトルに変換します'''

    def __init__(self, text_embedding_vectors):
        super(Embedder, self).__init__()

        self.embeddings = nn.Embedding.from_pretrained(
            embeddings=text_embedding_vectors, freeze=True)
        # freeze=Trueによりバックプロパゲーションで更新されず変化しなくなります

    def forward(self, x):
        x_vec = self.embeddings(x)

        return x_vec
```
　Pytorchのnn.Embeddingユニットを使って、単語IDを埋め込みベクトルに変換する部分です。

```python
class PositionalEncoder(nn.Module):
    '''入力された単語の位置を示すベクトル情報を付加する'''

    def __init__(self, d_model=300, max_seq_len=256):
        super().__init__()

        self.d_model = d_model  # 単語ベクトルの次元数

        # 単語の順番（pos）と埋め込みベクトルの次元の位置（i）によって一意に定まる値の表をpeとして作成
        pe = torch.zeros(max_seq_len, d_model)

        # GPUが使える場合はGPUへ送る
        device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
        pe = pe.to(device)

        for pos in range(max_seq_len):
            for i in range(0, d_model, 2):
                pe[pos, i] = math.sin(pos / (10000 ** ((2 * i)/d_model)))
                pe[pos, i + 1] = math.cos(pos / (10000 ** ((2 * i)/d_model)))

        # 表peの先頭に、ミニバッチ次元となる次元を足す
        self.pe = pe.unsqueeze(0)

        # 勾配を計算しないようにする
        self.pe.requires_grad = False

    def forward(self, x):

        # 入力xとPositonal Encodingを足し算する
        # xがpeよりも小さいので、大きくする
        ret = math.sqrt(self.d_model)*x + self.pe
        return ret
```
　Positional Encoderの部分です。　

```python
class Attention(nn.Module):
    '''Transformerは本当はマルチヘッドAttentionですが、
    分かりやすさを優先しシングルAttentionで実装します'''

    def __init__(self, d_model=300):
        super().__init__()

        # SAGANでは1dConvを使用したが、今回は全結合層で特徴量を変換する
        self.q_linear = nn.Linear(d_model, d_model)
        self.v_linear = nn.Linear(d_model, d_model)
        self.k_linear = nn.Linear(d_model, d_model)

        # 出力時に使用する全結合層
        self.out = nn.Linear(d_model, d_model)

        # Attentionの大きさ調整の変数
        self.d_k = d_model

    def forward(self, q, k, v, mask):
        # 全結合層で特徴量を変換
        k = self.k_linear(k)
        q = self.q_linear(q)
        v = self.v_linear(v)

        # Attentionの値を計算する
        # 各値を足し算すると大きくなりすぎるので、root(d_k)で割って調整
        weights = torch.matmul(q, k.transpose(1, 2)) / math.sqrt(self.d_k)

        # ここでmaskを計算
        mask = mask.unsqueeze(1)
        weights = weights.masked_fill(mask == 0, -1e9)

        # softmaxで規格化をする
        normlized_weights = F.softmax(weights, dim=-1)

        # AttentionをValueとかけ算
        output = torch.matmul(normlized_weights, v)

        # 全結合層で特徴量を変換
        output = self.out(output)

        return output, normlized_weights
```
　Attentionの部分です。ここでの mask計算は、テキストデータが短く＜pad＞を入れた箇所は、softmaxを掛けたら0になって欲しいので、該当箇所をマイナス無限大(-1e9)に置き換えています。


```python
class FeedForward(nn.Module):
    def __init__(self, d_model, d_ff=1024, dropout=0.1):
        '''Attention層から出力を単純に全結合層2つで特徴量を変換するだけのユニットです'''
        super().__init__()

        self.linear_1 = nn.Linear(d_model, d_ff)
        self.dropout = nn.Dropout(dropout)
        self.linear_2 = nn.Linear(d_ff, d_model)

    def forward(self, x):
        x = self.linear_1(x)
        x = self.dropout(F.relu(x))
        x = self.linear_2(x)
        return x
```
　Feed Forward の部分です。単純な2層の全結合層です。

```python
class TransformerBlock(nn.Module):
    def __init__(self, d_model, dropout=0.1):
        super().__init__()

        # LayerNormalization層
        # https://pytorch.org/docs/stable/nn.html?highlight=layernorm
        self.norm_1 = nn.LayerNorm(d_model)
        self.norm_2 = nn.LayerNorm(d_model)

        # Attention層
        self.attn = Attention(d_model)

        # Attentionのあとの全結合層2つ
        self.ff = FeedForward(d_model)

        # Dropout
        self.dropout_1 = nn.Dropout(dropout)
        self.dropout_2 = nn.Dropout(dropout)

    def forward(self, x, mask):
        # 正規化とAttention
        x_normlized = self.norm_1(x)
        output, normlized_weights = self.attn(
            x_normlized, x_normlized, x_normlized, mask)

        x2 = x + self.dropout_1(output)

        # 正規化と全結合層
        x_normlized2 = self.norm_2(x2)
        output = x2 + self.dropout_2(self.ff(x_normlized2))

        return output, normlized_weights
```
　Attention と Feed Foward を組み合わせて、Transformer Block を作る部分です。両方とも、**Layer Normalization** と **Dropout**を掛けると共に、ResNetと同様な**残渣結合**を行っています。


```python
class ClassificationHead(nn.Module):
    '''Transformer_Blockの出力を使用し、最後にクラス分類させる'''

    def __init__(self, d_model=300, output_dim=2):
        super().__init__()

        # 全結合層
        self.linear = nn.Linear(d_model, output_dim)  # output_dimはポジ・ネガの2つ

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

    def forward(self, x):
        x0 = x[:, 0, :]  # 各ミニバッチの各文の先頭の単語の特徴量（300次元）を取り出す
        out = self.linear(x0)

        return out
```
　最後にネガポジ判定をする部分です。各文の先頭単語の特徴量を使用して分類し、その損失をバックプロパゲーションして学習することで、先頭単語の特徴量が自然と文章のネガ・ポジを判定する特徴量になります。

```python
class TransformerClassification(nn.Module):
    '''Transformerでクラス分類させる'''

    def __init__(self, text_embedding_vectors, d_model=300, max_seq_len=256, output_dim=2):
        super().__init__()

        # モデル構築
        self.net1 = Embedder(text_embedding_vectors)
        self.net2 = PositionalEncoder(d_model=d_model, max_seq_len=max_seq_len)
        self.net3_1 = TransformerBlock(d_model=d_model)
        self.net3_2 = TransformerBlock(d_model=d_model)
        self.net4 = ClassificationHead(output_dim=output_dim, d_model=d_model)

    def forward(self, x, mask):
        x1 = self.net1(x)  # 単語をベクトルに
        x2 = self.net2(x1)  # Positon情報を足し算
        x3_1, normlized_weights_1 = self.net3_1(
            x2, mask)  # Self-Attentionで特徴量を変換
        x3_2, normlized_weights_2 = self.net3_2(
            x3_1, mask)  # Self-Attentionで特徴量を変換
        x4 = self.net4(x3_2)  # 最終出力の0単語目を使用して、分類0-1のスカラーを出力
        return x4, normlized_weights_1, normlized_weights_2
```
　今まで定義したクラスを使って、最終的にモデル全体を組み上げる部分です。

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

　実行すると、

![スクリーンショット 2020-07-29 10.46.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/b94bf02e-c4b5-5e86-6c3d-1998439dfdb3.png)

　こんな形で、判定結果とその根拠を表示します。

#6.日本語データセットでもやってみる。
　色々Webを見ていると、**chABSA-dataset**という日本の上場企業の有価証券報告書から文章を取り出しネガポジ判定し判定根拠を表示する例があったので、同様に Google Colab でまとめてみました。自分でやってみたい方は、この [**「リンク」**](https://github.com/cedro3/Transformer/blob/master/transformer_ja_run.ipynb) をクリックし表示されたシートの先頭にある「Colab on Web」ボタンをクリックすると動かせます。


（参考）
・[つくりながら学ぶ! PyTorchによる発展ディープラーニング](https://github.com/YutaroOgawa/pytorch_advanced)
・[ディープラーニングでネガポジ分析アプリを作ってみた（python）【前編】](https://sinyblog.com/deaplearning/transformer_001/)
・[論文解説 Attention Is All You Need (Transformer)](http://deeplearning.hatenablog.com/entry/transformer)