트랜스포머는 2017년에 구글브레인에서 작성한 Attention Is All You Need에서 언급된 self-attention 매커니즘을 기반으로하는 새로운 신경망 아키텍처라고 할 수 있다. 기계번역에 사용되며 Seq to seq 형태처럼 입력시퀀스를 받고, 디코더에서는 출력시퀀스를 반환한다. 특히, 강조되는 것은 오토인코더의 형식이 어텐션메커니즘(Attention mechanism)으로만 이용했다는 것이다.

Positional encoding

트랜스포머는 아래의 두 가지 문제에서 기술적 발전을 이뤘다고 할 수 있다.

1) RNN 계열을 순차적으로 연산한다는 점에 있어서 병렬처리에 어려움이 있고, 연상량이 많아 학습속도가 느림을 해결

2) RNN에서 시간차가 먼 경우의 정보의 활용이 떨어짐(timestamp가 멀어 문장의 앞단어와 문장의 뒷단어 활용 어려움)을 해결

 

Transformer은 단순히 AutoEncoder(오토인코더 형식)이기보다는 인코더층의 구성을 여러 인코더를 넣고, 디코더부분에 여러 디코더를 사용했다는 것이다. 본 연구에서는 인코더는 동일한 6개의 인코더를 쌓았고, 마찬가지로 디코더에서도 동일한 6개의 디코더를 쌓았다.

 


트랜스포머 구조

트랜스포머의 구조를 이해하려면 인코더, 디코더, 멀티해드 어텐션을 알아야한다.

인코더: 6개의 동일한 인코더(레이어)를 쌓았다. 또, 각 레이어(인코더)는 2개의 또 서브레이어로 구성되는데. 첫번째 서브레이어는 멀티해드어텐션(Multi-head attention)으로 구성하고, 두 번째 서브레이어에서는 포지션별 fully-connected layer을 구성했다. 각 서브 레이어에는 Resnet에 사용된것처럼, Residual connection(중간 정보를 전달하는) 연결을 각각 부착하고, 마지막에는 정규화(normalization)을 부착했다.

 

디코더: 디코더도 인코더와 동일하게 6개의 같은 디코더를 쌓았다. 인코더는 2개의 서브레이어를 활용했던 반면에, 디코더는 3개의 서브레이어를 활용했다. 인코더와 유사하지만, 인코더에서의 멀티해드어텐션(Multi-head attention)에 멀티해드어텐션(Multi-head attention)을 또 더한 형태이다. 디코더에서는 멀티해드어텐션(Multi-head attention)에 마스킹으 했는데, 이는 디코더가 적어도 i의 포지션의 단어를 예측할 때, i번째 결과 단어이전을 쓰기 위함이다 (i+1을 쓰지않는이유는 seq-to-seq에서 이미 반환된결과가 i인데, 없기 떄문에 이전의 출력단어만 쓰기 위함이다)

 

어탠션: 어텐션은 벡터형태인 쿼리, (키-벨류)의 쌍의 값을 결과에 매핑하는 기술이다. 이게 굉장히 모호한데, 이를 이해하려면 다시 "쿼리", (키-벨류)을 알아야한다. 아래의 그림을 보면 Q와 K을 이용해서 연산한다음(Scale, mask...) softmax을 적용한다. Context vector (=weight)으로 사용된다(softmax 의 의미로 그렇다).  Self-attention은 Q, K, V의 출처가 같다는 의미로 벡터가 같다는 의미는 아니다 [ref: https://wikidocs.net/31379] . Q, K, V을 적절히 바꾸면 RETAIN에 쓰이는 attention이 된다. 가령 Q은 입력값 Dense layer의 W, K은 LSTM에 통과시킨 (X), V은 X. 즉, 쿼리(Q)에 대해서 키(K)와의 유사도를 구하는 것(softmax을 취한값)이 attention weight 으로 쓸 수 있다. 이를 V와 곱하여 가중합을 반영한다. 핵심은 Q, K, V가 각각 다른 W_q, W_k, W_v와 X 곱해질 수 있는데, 출처가 X로 동일한 것을 의미한다 (해당 레퍼런스의 그림을 보면 단번에 이해할 수 있을 것이다 http://jalammar.github.io/illustrated-transformer/)

아래의 클레스에서 self.mha(x, x, x)가 self-attention을 의미한다.

class EncoderLayer(tf.keras.layers.Layer):

    def __init__(self, n_heads, d_model, dff, rate=0.1):
        super(Encoder, self).__init__()


        self.mha =  MultiHeadAttention(n_heads, d_model)
        self.ffn = point_wise_feed_forward_network(d_model ,dff)

        self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
        self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)

        self.dropout1 = tf.keras.layers.Dropout(rate)
        self.dropout2 = tf.keras.layers.Dropout(rate)


    def call(self, x, training, mask=None):
        '''
        Parameters
        ----------
        x
        training: bool
        mask: None
        Returns
        -------
        '''
        att_output, _ = self.mha(q=x, k=x, v=x, mask=mask)
        att_output = self.dropout1(att_output, training=training)
        out1 = self.layernorm1(x + att_output)

        ffn_output = self.ffn(out1)
        ffn_output = self.dropout2(ffn_output, training=training)
        output = self.layernorm2(x + att_output)

        return output

아래의 클레스에서 self.mha(x, x, x)가 self-attention을 의미한다.

반면, 디코더에서는 self.attetnion이 아니라 q, k는 같지만 예측하고자하는 v에 따른 align을 해주는 코드로 바뀐다.

class DecoderLayer(tf.keras.layers.Layer):

    def __init__(self, n_heads, d_model, dff, dropout_rate=0.1):
        super(Decoder, self).__init__()

        # block 1
        self.mha1 = MultiHeadAttention()
        self.norm1 = tf.keras.layers.Normalization(1e-6)
        self.dropout1 = tf.keras.layers.dropout(dropout_rate)

        # block 2
        self.mha2 = MultiHeadAttention()
        self.norm2 = tf.keras.layers.Normalization(1e-6)
        self.dropout2 = tf.keras.layers.dropout(dropout_rate)

        # block 3
        self.ffn = tf.keras.layers.Dense(dff)
        self.norm3 = tf.keras.layers.Normalization(1e-6)
        self.dropout3 = tf.keras.layers.dropout(dropout_rate)


    def call(self, x, encoder_output, training, look_ahead_mask, padding_mask=None):

        # block 1
        att_output, att_weight_block1 = self.mha1(x, x, x, look_ahead_mask)
        att_output = self.dropout1(att_output, training=training)
        out = self.norm1(x + att_output)


        # block 2
        att_output2, att_weight_block2 = self.mha2(encoder_output, encoder_output, out, padding_mask)
        att_output2 = self.dropout2(att_output2, training=training)
        out2 = self.norm2(att_output2 + out)

        # block 3
        ffn_out = self.ffn(out)
        ffn_out = self.dropout3(x, training=training)
        out = self.norm2(ffn_out + out2)

        return out, att_weight_block1, att_weight_block2

참고로 그러면 Self-attention말고 다른 것이 있느냐?라고 물어볼 수도 있는데, 대답은 있다는 것이다. Self-attention외에 global/local attention 이냐의 분류, soft 및 hard attention의 분류도 있다.

 

import tensorflow as tf

def dot_product_attention(Q, K, V, masking_op=False):
    '''
    Q: Query. dimension dk
    K: Key. dimension dk
    V: values of dimension dv.
    '''
    # QK^T
    QK_T = tf.linalg.matmul(Q, tf.transpose(K))
    
    # Scaling
    QK_T = d_k ** 0.5  
    
    # Softmax
    QK_soft = tf.nn.softmax(QK_T)  # As attention weight (=alightment weights)
    
    # dot-product
    output = tf.linalg.matmul(QK_soft, V)  # = context vector for output
    
    return output    

 

 

Multihead attention .


아래는 필자가 직접 그린 Multi-head attention의 matrix shape이다. Q, K, V가 한 오리진인 self-attetnion류이고, head split을 위해서 d_model을 (head by depth)으로 나눠주는 것이 특징이다.

 

Positional encoding(포지셔널 인코딩): Transformer는 RNN계열도아니며, Convolution 을사용하지 않기 때문에 sequence의 전후 맥락을 사용할 수 없다는 제한이 있다. 이러한 단점을 보완하기위한 것이 positional encoding이다. 포지셔널인코딩은 입력값인 input embedding에 positional eoncding값을 추가로 더(add)해주는 것을 의미한다. 본 논문에서도 Positional encoding 을 아래의 그림과 같이, 각 아래의 encoder, decoder에 전달하기전에 미리 더해준다.  "we add "positional encodings" to the input embeddings at the bottoms of the encoder and decoder stacks. The positional encodings have the same dimension dmodel as the embeddings, so that the two can be summed. There are many choices of positional encodings, learned and fixed"

 

 

이 때, 조건이 있는데, 포지셔널 인코딩의 차원은 d_model 같아야하며(=embedding 값의 차원과 같아야하며, 그래야 더할 수 있다). 이를 위해, 본 논문은 아래와 같이 2개의 함수를 이용해서 positional encoding을 생성한다.

Positional encoding function

서로다를 관찰빈도값에 대해서 sin 또는 cos 함수를 이용하는 것이다. 위의 함수식에서 pos은 각 포지션을 의미하고, i은 차원의 수를 의미한다. 즉, 각 차원의 포지셔널 인코딩을 주기함수에 대응하겠다는 것이다. 이상적인 positional encoding은 아래의 조건을 따라야한다. 특히, Sin, Cos과 주기함수를 사용하는 이유는 실제 embedding된 값에다가 [1, 2, 3.... K]까지의 점차 큰수가 더해지기 때문에 닫혀있는 함수를 사용한 것이다. 예를 들어 "I love you"에 [1, 2, 3]을 대입하고자할 경우에는 실제 값 너무 큰 수가 더해져 모델에 문제가 된다.  

또한, 위의 함수는 주기가 10000^{2i/d_model} * 2 pi인 함수이다. 모델이 지정한 차원의수는 d_model(=i)이기 때문에, 

(y=sin(bx+c)인 sin함수의 주기는 2pi / |b| 이다

 1. 시간에만 종속적이어야함 (즉, 함수가 시간과 관련된 변수만 있어야한다.

 2. 함수가 임베딩의 차원과 같아야한다.

 3. 매번 바뀌는 것이 아닌 Key 값처럼 정해져있어야한다(determnistic)

 4. Positional encoding은 함수에 따른 토큰위치의 값을 알수 있어야한다.

이 Positional encoding을 하고나면 아래와 같은 (TimeStamp, embedding size) 만큼의 positional encoding(PE)값이 반환된다.

Figure. Positional Encoding (PE) Value

 

 

Reference: http://vandergoten.ai/2018-09-18-attention-is-all-you-need/

반응형

+ Recent posts