Transformer[Attention Is All You Need]


0. Introduction


TransformerはGoogle Brainチームが2017年に発表した論文で、NLP分野だけでなく、コンピュータビジュアル分野でも大きな影響力を発揮している.Transforemrの主な機能は次のとおりです.
  • 注意力メカニズムのみに基づく.RNNとCNNを完全に排除したということだ.
  • 並列化処理を可能にする.
  • Positional Encodeを使用して重要な位置情報を保存します.
  • 当期「Transfomer摘要」では、以下の構成を行います.

    1.Transformerモデルの全体アーキテクチャ


    2. Self-Attentnion


    3. Encoder, Decoder


    1. Transformer Architecture


    Transformerは、次の図のようにエンコーダとデコーダで構成されています.エンコーダは2つのサブレイヤからなり、デコーダは3つのサブレイヤからなる.

    [出典]:https://arxiv.org/abs/1706.03762
    また、エンコーダとデコーダは、下図に示すように積み重ねられており、各サブレイヤは合計6回通過する.


    [ソース]:https://cpm0722.github.io/pytorch-implementation/transformer


    入力部では、TransformerがRNNを除外しているため、位置情報を含まないことにのみ注目し、Input Embeddingに位置符号化値を追加するすべての値がEncoderとDecoderに入ります.

    したがって、Transformerは位置符号化を用いて位置情報を含む.

    2. Self-Attention


    Transformerの最も重要な部分はこの部分です.一つ一つ詳しく説明します.
    位置コードへの入力は、次の図のように複数のヘッド注意層に入ります.Scaled Dot-Productの注目を集めます.

    [出典]:https://arxiv.org/abs/1706.03762
    まず、「Scaled Dot Product Attention」の前に隣接層を通過するので、Q、K、Vは同じレベルに投影されます.

    [ソース]:https://jalammar.github.io/illustrated-transformer/


    このときの各Q,K,Vの次元をdk=3 dk=3 dk=3と呼ぶ.ここでQ,K,Vが同じ階層である理由は,自己の意味を再指摘し,最後に同じ文内の他の単語(Token)との注意力scoreを求める必要があるため,Q,K,Vは同じ階層でしかない.最初は同じ文に同じTokenを投射するのでここで、Q、K、Vは、以下の意味を有する.
  • Query:Context、すなわち現在時刻のtoken
  • Key:Queryに反応し、注目の対象を求める
  • Value:注意したい目標token(Keyと同じtoken)を示す
  • Q KVの意味がわかった以上、注目してみてくださいね

    Attentionは上記のように図で説明します.
    Q,K,VともにLinear演算により同一次元に埋め込まれているが,このときの次元をdk=3 dk=3 dk=3 dk=3と呼ぶ.

    [出典]:https://cpm0722.github.io/pytorch-implementation/transformer
    上の式に従って、まずQにKを乗じる.

    [出典]:https://cpm0722.github.io/pytorch-implementation/transformer
    乗算とはQとKの関係を求めること,すなわちscoreに注意することを意味する.そこで,ここではKを拡張してQと他のKの関係を求める.

    [出典]:https://cpm0722.github.io/pytorch-implementation/transformer
    このように,「it」という語と残りの語との関係(注意スコア)は,以下のように解く.

    [出典]:https://cpm0722.github.io/pytorch-implementation/transformer

    [出典]:https://cpm0722.github.io/pytorch-implementation/transformer
    マトリクス乗算のサイズが大きくなると、オーバーフロー(Overflow)または小さな勾配(Slope)の問題が発生し、独自の次元のルート値に分割されます.そこで、「Dot-Product」という言葉にScaledが追加されました.これは、すべての値を標準化することを意味します.
    z=x−uσz =\frac{x-u}{\sigma}z=σx−u​
    標準化された式は次のとおりです.
    Q⋅Kdk=Q⋅Kσ\frac{Q\cdot K}{\sqrt{d_k}} =\frac{Q\cdot K}{\sigma}dk​​Q⋅K​=σQ⋅K​
    同じ数学的証明がある.もしそうであれば、文のすべてのToken(Queryではなく)をQueryとして作成してAttention Scoreを計算することもできます.

    [出典]:https://cpm0722.github.io/pytorch-implementation/transformer

    [出典]:https://cpm0722.github.io/pytorch-implementation/transformer
    上記の結果は,最終注意力値の階層がinputと同じであることを示した.

    [出典]:https://cpm0722.github.io/pytorch-implementation/transformer
    ここでもう少し拡張しましょう.論文ではMulti-Head Attentionと命名し,headの個数を8個に設定した.

    [出典]:https://arxiv.org/abs/1706.03762
    Transformerは、エンコーダレイヤごとにScaled Dot Attentionを1回実行するのではなく、h回実行した後に結果を統合する.一つの注意を反映するよりも、いろいろな注意を総合的に反映したほうが結果にいいからです.しかし,h次Scaled Dot Attentiondの実行は計算上あまりにも非効率である.マトリクスの使い方をよく知っているので、最初から入力の次元を8回に増やしました.すなわち、新しい次元をdmodel{model}dmodelと呼ぶと、dmodel=h87 dkd{model}=h*d kdmodel=h87 dkとなる.画像で以下のように表現します.

    [出典]:https://cpm0722.github.io/pytorch-implementation/transformer
    上記のScaled dot-productは、1つのdmodeld{model}dmodel単位で実行されます.最終的なMulti-Head ATTION層の入出力は以下の通りである.

    コードから次のように表示されます.
    
    def cal_attention(q, k, v, d_k, mask=None, dropout=None):
        
        scores = torch.matmul(q, k.transpose(-2, -1)) /  math.sqrt(d_k)
        
        if mask is not None:
            
            scores = scores + mask
        
        scores = F.softmax(scores, dim=-1)
        
        if dropout is not None:
            scores = dropout(scores)
            
        output = torch.matmul(scores, v)
    
        return output
    
    
    class MultiHeadAttention(nn.Module):
        def __init__(self, hid_dim, n_heads):
            super().__init__()
            
            assert hid_dim % n_heads == 0
            
            self.hid_dim = hid_dim # 768
            self.n_heads = n_heads # 12
            self.head_dim = hid_dim // n_heads ## 64
            
            self.query = nn.Linear(hid_dim, hid_dim)
            self.key = nn.Linear(hid_dim, hid_dim)
            self.value = nn.Linear(hid_dim, hid_dim)
    
            self.dropout = nn.Dropout(0.1)
                    
        def forward(self,hidden_states,attention_mask = None):  
    
    
            # 변수 설명 
            # hidden_states : BertEmbedding output [2, 9, 784] [Batch size, Max_sequence_length,model size ]
            # self.n_heads : multi-head attention에서의 head의 개수 여기서는 head수가 12 
            # self.head_dim : 하나의 임베딩된 벡터의 차원 여기서는 64이다.
            # self.hid_dim : model size를 의미 정확히 말하면 n_heads(헤드수 ) * head_dim(하나의 임베딩된 벡터의 차원) 
            # 의미 : Q, K, V 자체를 n(batch size) x head_dim가 아닌, n× hid_dim로 생성해내서 한 번의 Self-Attention 계산으로 n× hid_dim의 output을 만들어내게 된다.
            # 때문에 Q, K, V를 생성해내기 위한 dembed×head_dim의 weight matrix를 갖는 FC layer를 3∗h개 운용할 필요 없이  hid_dim× hid_dim의 weight matrix를 갖는 FC layer를 3개만 운용하면 된다.
    
    
            bs = hidden_states.size(0) # batch size를 얻는다.
    
    
            # Step 1
            #  Input vector로부터 Query, Key, Value vector 생성 
            # 그리고 shape를 모두 동일하게 [batch size,max_sequence_length,n_heads,head_dim]으로 통일한다.
            q = self.query(hidden_states).view(bs, -1, self.n_heads, self.head_dim)
            k = self.key(hidden_states).view(bs, -1, self.n_heads, self.head_dim)
            v = self.value(hidden_states).view(bs, -1, self.n_heads, self.head_dim)
            
            # attention score 계산을 위해 shape를 [batch size,n_heads,max_sequence_length,head_dim]로 변경시킨다.
            q = q.transpose(1,2) # q shape : torch.Size([2, 12, 9, 64])
            k = k.transpose(1,2) # k shape : torch.Size([2, 12, 9, 64])
            v = v.transpose(1,2) # v shape : torch.Size([2, 12, 9, 64])
    
            
            # Step 2. Query · Key (matrix product) 하여 계산
            # Step 3. Score를 Key vector 차원수의 제곱근으로 나눔
            # Step 4. Softmax 계산
            # Step 5. Value vector에 softmax score를 곱함
            # attention score를 계산한다.
            scores = cal_attention(q,k,v,self.head_dim, attention_mask, self.dropout)
    
            # Permute와 Reshape를 하여 최종적인 output size[batch size, max_sequence,hid_dim]으로 맞춘다.
            # concatenate heads and put through final linear layer
            concat = scores.transpose(1,2).contiguous().view(bs, -1, self.hid_dim)
            return concat

    3. Encoder, Decoder


    3-1. Encoder



    [出典]:https://arxiv.org/abs/1706.03762
    エンコーダは、マルチヘッダエージェントとプリアンブル転送(MLP)の2つのサブレイヤから構成される.そして各層の結果に対してLayer NormalizationとResidual接続を行う
    ここではLayer NormalizationとResidual Connectionについて詳しく説明しません.簡単に言えば,層正規化をNLPなどの分野で用いることで,特にTransformerなどの高速/並列計算の主な特性において,より優れた性能が得られる.「リアルコネクション」については、層の深さによる勾配の消失の問題を防止するために、かえって試験誤差が増大することが理解される.
    マルチヘッド注意事項を紹介したので,位置抽出転送層のみを簡単に紹介した.この層は2つのFC層を有する単純な層であり、マルチヘッド注意と同様に入力形状を保持することを特徴とする.
    実装コードは次のとおりです.
    class PositionwiseFeedforwardLayer(nn.Module):
        def __init__(self, hidden_dim, pf_dim, dropout_ratio):
            super().__init__()
    
            self.fc_1 = nn.Linear(hidden_dim, pf_dim)
            self.fc_2 = nn.Linear(pf_dim, hidden_dim)
    
            self.dropout = nn.Dropout(dropout_ratio)
    
        def forward(self, x):
    
            # x: [batch_size, seq_len, hidden_dim]
    
            x = self.dropout(torch.relu(self.fc_1(x)))
    
            # x: [batch_size, seq_len, pf_dim]
    
            x = self.fc_2(x)
    
            # x: [batch_size, seq_len, hidden_dim]
    
            return x
    [ソースソース]:https://colab.research.google.com/drive/1mt5G4MMneREGuQbaYYIfL_C1cvTTKIK1#scrollTo=0jn4VCWdXhK5

    3-2. Decoder


    デコーダは次のように構成されています.

    [ソース]:https://arxiv.org/abs/1706.03762


    主な特徴は以下の通りです.

  • 一番下のマルチヘッダエージェント層でmaskが使用されるため、現在の時点以降のtokenに対してmaskを加算して単語予測に使用しないようにします.

  • 2番目のmulti-head注意層が入力するQ値は最下位の結果値であり、K、Vはエンコーダ層の最終結果値である.
  • 説明1号は以下の通りです.
    次の図に示すように、現在の時点以降のtokenはmaskを使用して、次の値の予測を回避します.



    [ソース]:https://jalammar.github.io/illustrated-gpt2/


    マスクはsoftmax関数を適用する前に完了しなければならないことに注意してください.マスクの仕方を簡単に見ると、まず下三角行列でマスクが生成され、マスク値の0の部分のインデックスに負の値の無限大(実際にはコンピュータでは表現できず、負の値の小さい小数を取る)があり、そうでない部分は元のattention score値を維持します.
    Decoderを次のコードに実装します.
    class DecoderLayer(nn.Module):
        def __init__(self, hidden_dim, n_heads, pf_dim, dropout_ratio, device):
            super().__init__()
    
            self.self_attn_layer_norm = nn.LayerNorm(hidden_dim)
            self.enc_attn_layer_norm = nn.LayerNorm(hidden_dim)
            self.ff_layer_norm = nn.LayerNorm(hidden_dim)
            self.self_attention = MultiHeadAttentionLayer(hidden_dim, n_heads, dropout_ratio, device)
            self.encoder_attention = MultiHeadAttentionLayer(hidden_dim, n_heads, dropout_ratio, device)
            self.positionwise_feedforward = PositionwiseFeedforwardLayer(hidden_dim, pf_dim, dropout_ratio)
            self.dropout = nn.Dropout(dropout_ratio)
    
        # 인코더의 출력 값(enc_src)을 어텐션(attention)하는 구조
        def forward(self, trg, enc_src, trg_mask, src_mask):
    
            # trg: [batch_size, trg_len, hidden_dim]
            # enc_src: [batch_size, src_len, hidden_dim]
            # trg_mask: [batch_size, trg_len]
            # src_mask: [batch_size, src_len]
    
            # self attention
            # 자기 자신에 대하여 어텐션(attention)
            _trg, _ = self.self_attention(trg, trg, trg, trg_mask)
    
            # dropout, residual connection and layer norm
            trg = self.self_attn_layer_norm(trg + self.dropout(_trg))
    
            # trg: [batch_size, trg_len, hidden_dim]
    
            # encoder attention
            # 디코더의 쿼리(Query)를 이용해 인코더를 어텐션(attention)
            _trg, attention = self.encoder_attention(trg, enc_src, enc_src, src_mask)
    
            # dropout, residual connection and layer norm
            trg = self.enc_attn_layer_norm(trg + self.dropout(_trg))
    
            # trg: [batch_size, trg_len, hidden_dim]
    
            # positionwise feedforward
            _trg = self.positionwise_feedforward(trg)
    
            # dropout, residual and layer norm
            trg = self.ff_layer_norm(trg + self.dropout(_trg))
    
            # trg: [batch_size, trg_len, hidden_dim]
            # attention: [batch_size, n_heads, trg_len, src_len]
    
            return trg, attention
    [ソースソース]:https://colab.research.google.com/drive/1mt5G4MMneREGuQbaYYIfL_C1cvTTKIK1#scrollTo=0jn4VCWdXhK5
    class MaskedSelfAttention(nn.Module):
        def __init__(self, config):
            super().__init__()
            assert config.hidden_size % config.num_attention_heads == 0
            self.use_cache = config.use_cache # TRUE
    
            self.hidden_size = config.hidden_size # 768
            self.num_attention_heads = config.num_attention_heads # 12
            self.head_dim = self.hidden_size // config.num_attention_heads # 64
    
            self.attn = Conv1D(3 * self.hidden_size, self.hidden_size)
            self.proj = Conv1D(self.hidden_size, self.hidden_size)
    
            self.attenton_dropout = nn.Dropout(config.attn_pdrop)
            self.residual_dropout = nn.Dropout(config.resid_pdrop)
    
        def forward(self, hidden_states, attention_mask=None):        
            Q, K, V = self.attn(hidden_states).split(self.hidden_size, dim=2)
    
            batch_size = hidden_states.shape[0] 
            # [batch_size, n_heads, seq_len, head_dim]
            Q = Q.view(batch_size, -1, self.num_attention_heads, self.head_dim).permute(0, 2, 1, 3) 
            K = K.view(batch_size, -1, self.num_attention_heads, self.head_dim).permute(0, 2, 1, 3)
            V = V.view(batch_size, -1, self.num_attention_heads, self.head_dim).permute(0, 2, 1, 3)
    
            if self.use_cache is True:
                present = (K, V) # 현재 상태 저장
            else:
                present = None
                
            ### Masking 적용 ###
    
            # QK^T / sqrt(d_k) 계산 
            attention_score = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.head_dim)
    
            # mask 만들기 
            Max_seq_len =  hidden_states.size(-2)
            # mask shape :  [1,1,max_seq,max_seq]
            mask = torch.tril(torch.ones((Max_seq_len, Max_seq_len), dtype=torch.uint8)).view(1,1,Max_seq_len,Max_seq_len)
    
            # attention_score에서 mask 값이 0인 부분의 위치는 음수의 아주 작은 값(-1e4)으로 채운다 
            attention_score = torch.where(mask, attention_score,torch.tensor(-1e4))
    
            # softmax 적용
            attention_score = F.softmax(attention_score, dim=-1)
    
            # 최종 Scaled Dot-Product Attention
            # 1. softmax 적용 결과에 dropout을 수행.
            attention_score = self.attenton_dropout(attention_score)
    
            # 2. softmax  * V 수행(행렬 곱셈) - Matrix Multiplication 
            outputs = torch.matmul(attention_score, V)
    
            # outputs size[batch size, max_sequence,hid_dim]에 맞게 변경 
            outputs = outputs.transpose(1,2).contiguous().view(batch_size, -1, self.hidden_size)
    
            outputs = self.proj(outputs)
            
            outputs = self.residual_dropout(outputs)
            
            outputs = (outputs, present)
    
            return outputs

    Positional Encoding


    最後にPositional Encodeについて説明して最後にします.前述したようにTransformerは注意力のみに基づいているため,各単語の位置に関する情報はない.そこで、これらの位置情報を含むpositionテーブルを作成します.以下の資料で説明します.

    [出典]:https://github.com/ndb796/Deep-Learning-Paper-Review-and-Practice/blob/master/lecture_notes/Transformer.pdf
    上図に示すようにposはtokenの位置であり、iは各tokenの階層を表す.従って、上記のsin、cons関数式を代入することにより、右側のテーブルと同じ位置符号化(正確には入力と次元が同じマトリクス)を生成し、位置情報を加えることができる.Transformerの後,位置符号化そのもの(例えばsinとcos)を学習目標とし,Embedding layerを用いて処理した.

    実習コードリンク


    この練習コードはTransformerのエンコーダに関するマルチヘッドの注意事項である.colab環境で駆動してみることができます.
    https://colab.research.google.com/drive/1mAbJBCNpVjK4vQDbuWBTIs4gDq9RsYKa?usp=sharing

    参考資料


    https://jalammar.github.io/illustrated-gpt2/
    https://cpm0722.github.io/pytorch-implementation/transformer
    https://jalammar.github.io/illustrated-transformer/
    https://arxiv.org/abs/1706.03762