# AI 이론/DeepLearning

트랜스포머 정리(2)

alz 2022. 5. 9. 21:05

이전 글에 이어서 정리하는 글입니다.

앞의 글에서는 트랜스포머의 구조와 셀프 어텐션에 대해서 알아보았습니다.

 

멀티 헤드 어텐션(Multi-head Attention) 

앞서 배운 어텐션에서는 $d_{model}$의 차원을 가진 단어를 num_heads로 나눈 차원을 가지는 $Vector_{Q,K,V}$로 바꾸고 어텐션을 수행했는데, 왜 어텐션을 $d_{model}$로 하지않고, 굳이 num_heads로 나눈 크기만큼의 차원을 축소했을까요?

 

 

한번의 어텐션을 하는것 보다 여러번의 어텐션을 병렬로 사용하는 것이 더 효율적 이라고 합니다.어딘가를 감시할때 혼자 감시하는 것보다 여러명이 동시에 감시하는것이 더욱더 정확하게 하는것 처럼말이죠..

 

그래서 $d_{model}$의 차원을 num_heads로 나눠서 $d_{model}$/num_heads의 차원을 가지는 Q,K,V에 대해서 num_heads개의 병렬 어텐션이 이루어져 집니다.이때 num_heads개의 만큼 어텐션이 병렬로 이루어지는데, 각각의 어텐션 값 행렬을 어텐션 헤드라고 합니다.이때 가중치 행렬 $W^Q,W^K,W^V$의 값은 어텐션 헤드마다 전부 다릅니다.

 

 

 

병렬 어텐션을 모두 수행하면 어텐션 헤드를 연결합니다. 모두 연결된 어텐션 헤드 행렬크기는 (seq_len,$d_{model}$)이 되겠죠?

 

 

어텐션 헤드를 모두 연결한 행렬은 또 다른 가중치 행렬 $W^0$을 곱하게 되는데, 이렇게 나온 결과 행렬이 멀티-헤드 어텐션의 최종 결과물 입니다. 위의 그림은 가중치 $W^0$와의 곱 과정을 보여줍니다.

이때 결과물인 멀티-헤드 어텐션 행렬은 인코더의 입력이었던 문장 행렬인 (seq_len, $d_{model}$)과 동일

 

  • 멀티-헤드 어텐션 단계가 끝났을때, 인코더의 입력으로 들어왔던 행렬의 크기가 아직 유지
  • 첫번째 서브층인 멀티-헤드 어텐션과 두번째 서브층인 포지션 와이드 피드포워드 신경망을 지나면서 인코더의 입력으로 들어올때의 행렬의 크기는 계속 유지되어야함
  • 트랜스포머는 동일한 구조의 인코더를 쌓은 구조기 때문에, 인코더에서의 입력의 크기가 출력에서도 동일 크기로 유지되어야만 다음 인코더의 입력으로 사용 될 수 있음

 


 

패딩 마스크(Padding Mask) 

이전 글에서 스케일드 닷-프로덕트 어텐션 함수 내부에 mask 라는 값을 인자로 받고 , -1e9라는 아주 작은 음수값을 곱한후 어텐션 행렬에 더해주고 있습니다. 이 연산은 도대체 뭘까요?

 

def scaled_dot_product_attention(query, key, value, mask):
... 중략 ...
    logits += (mask * -1e9) # 어텐션 스코어 행렬인 logits에 mask*-1e9 값을 더해주고 있다.
... 중략 ...

바로, 입력문자에 <PAD> 토큰이 있을 경우 어텐션에서 제외하기위한 연산입니다.

 

 

이 연산을 살펴보면, <PAD>의 경우는 실질적인 의미를 가진 단어가 아닙니다. 그래서 트랜스포머는 Key의 경우 <PAD> 토큰이 존재한다면 이에 대해서는 유사도를 구하지 않도록 마스킹(Masking)을 합니다.

 

※여기서 마스킹이란 어텐션에서 제외하기 위해 값을 가린다 라는 의미 

어텐션 스코어행렬에서 행에 해당하는 문장은 Query, 열에 해당하는 문장은 Key

Key에 <PAD>가 있는경우 해다아 열 전체를 마스킹 합니다.

 

 

 

마스킹을 하는 방법은 어텐션 스코어 행렬의 마스킹 위치에 매우 작은 음수값을 넣는 것인데,

그렇게 되면 소프트맥스 함수를 거쳐서 0이 되어 <PAD> 토큰이 반영되지 않게 됩니다.

 

 

패딩 마스크를 구현하는 방법은 입력된 정수 시퀀스에서 패딩 토큰의 인덱스인지,아닌지를 판별하는 함수를 구현하면 됩니다.

def create_padding_mask(x):
	# 값이 0이라면, 1을 반환 0이 아니라면 0을반환
  mask = tf.cast(tf.math.equal(x, 0), tf.float32)
  # mask = [[0,0,0,1,1]] 
  # (batch_size, 1, 1, key의 문장 길이)
  # tf.newaxis 를 통해[1,1,1,5]로 reshape
  return mask[:, tf.newaxis, tf.newaxis, :]

 


 

다음으로는 인코더의 또다른 서브층인 포지션-와이즈 피드 포워드 신경망(Position-wise FFNN)에 대해서 알아보겠습니다.

 

Position-wise FFNN

 

현재까지 인코더에 대해서 설명하고 있지만 , FFNN은 인코더와 디코더에서 공통적으로 가지고 있는 서브층입니다.포지션 와이즈 FFNN은 완전 연결 FFNN(Fully-Connected FFNN)으로 해석할 수 있습니다.$$ FFNN(x) = MAX(0, x{W_{1}} + b_{1}){W_2} + b_2 $$

 

 

  • $x$ : 멀티 헤드 어텐션의 결과로 나온 (seq_len, $d_{model}$)의 크기를 가지는 행렬
  • $W_1$ : ($d_{model}, d_{ff}$)의 크기를 갖는 가중치
  • $W_2$ : ($d_{ff},d_{model}$)의 크기를 갖는 가중치 
  • $d_{ff}$는 논문에서 언급한듯이 2048의 크기를 갖음 

여기서 매개변수 $W_1,b_1,W_2,b_2$는 하나의 인코더 층 내에서는 다른 문장, 다른 단어들마다 정확하게 동일하게 사용

하지만, 인코더 층마다는 다른 값을 가집니다.

 

잠깐 정리해보자면,

  1. 인코더 #1의 입력이 임베딩 벡터와 포지셔닝 인코딩을 통해 입력으로 들어가서,
  2. 멀티 헤드 셀프 언텐션층을 통해 각 Q,K,V 벡터를 만들고 이를 하나로 연결(seq_len,$d_{model}) 
  3. 그 후 FFNN을 통과 하고, 인코더 #1의 출력이 다음층 인코더의 입력으로 들어감

으로 정리할 수 있을 거 같습니다.

 

 

잔차 연결(Residual Connetion)과 층 정규화(Layer Normalization)

앞서 인코더의 서브층 2개에 대해서 알아봤습니다. 트랜스포머 구조를 살펴보면 두개의 서브층을 가진 인코더에 추가적응로 사용하는 기법이 있었는데, Add & Norm 입니다.정확히는 잔차 연결(residual connection)과 층 정규화(layer Normalization) 입니다.

 

 각 서브층에서 실선의 화살표가 되어있는데, 이는 잔차연결과 층 정규화 이후에 이해해보도록 하겠습니다.

 

 

 

 

1) 잔차 연결

 

잔차 연결에 대해서 설명할때 자주 등장하는 그림입니다.

이 그림에서 $H(x)$ 에 대해서 잠시 알아보겠습니다.

위 그림은 입력 x와 x에 대한 어떤 함수 F(x)의 값을 더한 H(x) 구조로 볼수있는데요,

여기서! 어떤 함수 $F(x)$가 트랜스포머의 서브층에 해당됩니다(멀티 헤드 어텐션, FFNN 층 말합니다)

 

잔차연결이란 서브층의 입력과 출력을 더하는 것을 말합니다.

앞서 트랜스포머에서 서브층의 입력과 출력은 동일한 차원이므로, 덧셈연산이 가능합니다.

잔차연결은 컴퓨터 비전 분야에서 주로 사용되는 분야로 모델의 학습을 돕는데, 

 

식을 표현하면, $x+Sublayer(x)$ 로 나타낼 수 있습니다.

 

 

잔차연결(서브층이 멀티헤드 어텐션)

 

$ H(x) = x+Multi head\ Attention(x) $

 

 

2) 층 정규화

 

잔차연결을 거친 결과는 이어서 층 정규화를 거칩니다.

잔차연결의 입력을 x , 잔차연결과 층 정규화 두 가지 연산을 모두 수행한 후의 결과 행렬을 LN 이라하면,

$$ LN = LayerNorm(x+Sublayer(x)) $$ 

 

층 정규화는 텐서의 마지막 차원에 대해서 평균과 분산을 구하고, 이를 가지고 어떤 수식을 통해서 정규화하여 학습을 돕습니다. 여기서 텐서의 마지막 차원은 $d_{model}$ 차원을 의미합니다.

 

$d_{model}$ 차원의 방향을 화살표로 살펴보면,

(col 방향이므로)

마지막 차원방향

 

각 화살표 방향의 벡터를 $x_i$라고 명명해보겠습니다.

층 정규화를 위해 화살표 방향으로 평균,분산을 구한뒤,

 

 

층 정규화를 수행한 후 벡터 $x_i$는 $ln_i$라는 벡터로 정규화가 됩니다.

$$ln_{i} = LayerNorm(x_{i})$$

 

층 정규화 수식

 

층 정규화 방식에는 2가지가 존재합니다.

첫번째는 평균과 분산 방식,

두번째는 감마와 베타를 도입  하는 방식이 있습니다.

 

우선, 평균과 분산 방식으로 정규화를 살펴보면 $x_i$를 정규화 합니다. 

근데 $x_i$는 벡터고 , 평균과 분산은 스칼라 라는 값이기 때문에 벡터 $x_i$의 각 차원을 k 라했을때 $x_{i,k}$는 다음과 같이 정규화를 시행합니다.

 

$$\hat{x}_{i, k} = \frac{x_{i, k}-μ_{i}}{\sqrt{σ^{2}_{i}+\epsilon}}$$

 

 

두번째 방식 감마$γ$와 베타 방식$β$을 살펴보면, 이 둘의 초기값은 1과 0으로 시작합니다.

 

 

γ와 β를 이용한 층 정규화식은 다음과 같습니다.

 $$ln_{i} = γ\hat{x}_{i}+β = LayerNorm(x_{i})$$

 

 

인코더 구현하기

 

def encoder_Layer(dff, d_model, num_heads, dropout, name="encoder_layer"):
	inputs = tf.keras.Input(shape=(None,d_model),name="inputs")
    
    # Padding Mask
    padding_mask = tf.keras.Input(shape=(1,1,None),name="padding_mask)
    
    # Multi-head Attention (First SubLayer / Self Attention)
    attention = MultilHeadAttention(
    	d_model,num_heads,name="attention)({
        'query':inputs, 'key':inputs, 'value':inputs, 'mask':padding_mask})
    
    
    # Dropout, Residual Connection, Layer Normalization
    attention = tf.keras.layers.Dropout(rate=dropout)(attention)
    attention = tf.keras.layers.LayerNormalization(epsilon=1e-6)(inputs + attention) 
    
    # Position-wise FFNN 
    outputs = tf.keras.layers.Dense(units=dff, activation="relu")(attention)
    outputs = tf.keras.layers.Dense(units=d_model)(outputs)
    
    # Dropout + Residual Connection and Layer Normalization
    outputs = tf.keras.layers.Dropout(rate=dropout)(outputs)
    outputs=  tf.keras.layers.LayerNormalization(
    	epsilon=1e-6)(attention + outputs)
        
    return tf.keras.Model(
    	inputs=[inputs,padding_mask],outputs=outputs,name=name)

위의 코드의 경우 하나의 인코더 층을 구현한 것입니다. 트랜스포머의 경우 해당 층이 num_layers 개수만큼 인코더 층을 사용하므로 여러번 쌓아야합니다.

 

def encoder(vocab_size, num_layers, dff,
            d_model, num_heads, dropout,
            name="encoder"):
  inputs = tf.keras.Input(shape=(None,), name="inputs")

  # 인코더는 패딩 마스크 사용
  padding_mask = tf.keras.Input(shape=(1, 1, None), name="padding_mask")

  # 포지셔널 인코딩 + 드롭아웃
  embeddings = tf.keras.layers.Embedding(vocab_size, d_model)(inputs)
  embeddings *= tf.math.sqrt(tf.cast(d_model, tf.float32))
  embeddings = PositionalEncoding(vocab_size, d_model)(embeddings)
  outputs = tf.keras.layers.Dropout(rate=dropout)(embeddings)

  # 인코더를 num_layers개 쌓기
  for i in range(num_layers):
    outputs = encoder_layer(dff=dff, d_model=d_model, num_heads=num_heads,
        dropout=dropout, name="encoder_layer_{}".format(i),
    )([outputs, padding_mask])

  return tf.keras.Model(
      inputs=[inputs, padding_mask], outputs=outputs, name=name)

 

다음 글에서는 인코더에서 디코더로 출력을 전달하는 것과 디코더에 대해서 정리하겠습니다.

 

관련글

 
 

 

'# AI 이론 > DeepLearning' 카테고리의 다른 글

Variational AutoEncoder(VAE)  (0) 2022.05.12
트랜스포머 정리(3)  (0) 2022.05.10
트랜스포머 정리(1)  (0) 2022.05.09
어텐션 메커니즘  (0) 2022.05.09
RNN,LSTM,GRU에 대해서 알아보자  (0) 2022.05.03