본문 바로가기
머신러닝, 딥러닝/Transformer

트랜스포머 (Transformer)

by Deeppago 2022. 1. 25.

이전에 업로드했던 글에서 어텐션(Attention)에 대해 살펴보았다. 어텐션은 neural machine translation에서 정보 손실 문제를 개선하여 긴 입력 시퀀스에서도 모델의 성능을 개선하는데 도움이 된 개념이다. 이번 글에선 어텐션 메커니즘에 기반한 트랜스포머에 대해서 정리해보고자 한다. https://wikidocs.net/31379http://jalammar.github.io/illustrated-transformer/를 정독하고 나름대로 이해한 내용을 정리하였다. 

 

트랜스포머는 Attention is All You Need라는 논문을 통해 처음 발표되었다. 이 모델의 TensorFlow 구현은 Tensor2Tensor 패키지에서 확인할 수 있다. 트랜스포머는 어텐션을 학습하여 그를 통해 학습 속도를 크게 향상 시켰다. 기존의 seq2seq모델에서 순차적으로 문장의 단어를 입력하는 것과는 달리 문장 전체를 한 번에 입력함으로써 모델 학습 및 예측에 병렬 처리가 가능하다는 것이 매우 큰 장점이라고 할 수 있다.

 

구현 코드 : https://github.com/ShinWon-Chul/transformer

-목차-

1. 트랜스포머(Transformer)의 주요 하이퍼 파라미터

2. 트랜스포머(Transformer)의 구조

    2.1 인코더(Encoder)

    2.2 디코더(Decoder)

3. 포지셔널 인코딩(Positional Encoding)

4. 어텐션(Attention)

5. 인코더의 셀프 어텐션(Self Attention)

    5.1 스케일드 닷-프로덕트 어텐션(Scaled dot-product Attention)

    5.2 행렬 연산으로 일괄 처리하기

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

6. 피드 포워드 신경망(Feed Forward Neural Network)

7. 잔차 연결(Residual connection)과 층 정규화(Layer Normalization)

    7.1 잔차 연결(Residual connection)

    7.2 층 정규화(Layer Nomerilzation)

8. 디코더(Decoder)

    8.1 마스크드 셀프 어텐션(Masked Self Attention)

    8.2 인코더 디코더 어텐션(Encoder-Decoder Attention)

    8.3 Linear Layer와 Softmax Layer

9. 학습 과정

    9.1 Loss Function

마치며

 


1. 트랜스포머(Transformer)의 주요 하이퍼 파라미터

시작에 앞서 먼저 트랜스포머의 주요 하이퍼 파라미터가 어떤 것이 있는지 살펴보도록 하자.

 

  • \(d_{model}\) = 512
    • 트랜스포머의 인코더와 디코더에서의 정해진 입력과 출력의 크기를 의미한다. 임베딩 벡터의 차원 또한 \(d_{model}\)이며, 각 인코더와 디코더가 다음 층의 인코더와 디코더로 값을 보낼 때에도 이 차원을 유지한다. 논문에서는 512이다.
  • num_layers = 6
    • 트랜스포머에서 하나의 인코더와 디코더를 층으로 생각하였을 때, 트랜스포머 모델에서 인코더와 디코더가 총 몇 층으로 구성되었는지를 의미한다. 논문에서는 인코더와 디코더를 각각 총 6개 쌓았다.
  • num_heads = 8
    • 트랜스포머에서는 어텐션을 사용할 때, 한 번 하는 것보다 여러 개로 분할해서 병렬로 어텐션을 수행하고 결과값을 다시 하나로 합치는 방식을 택했다. 이때 이 병렬의 개수를 의미합니다.
  • \(d_{ff}\) = 2048
    • 트랜스포머의 인코더 블록과 디코더 블록 내부에는 피드 포워드 신경망이 존재하며 해당 신경망의 은닉층의 크기를 의미한다. 피드 포워드 신경망의 입력층과 출력층의 크기는 \(d_{model}\)다.

2. 트랜스포머(Transformer)의 구조

트랜스포머는 RNN을 사용하지 않지만 기존의 seq2seq처럼 인코더에서 입력 시퀀스를 입력받고, 디코더에서 출력 시퀀스를 출력하는 인코더-디코더 구조이다.

기존의 seq2seq 구조에서는 인코더와 디코더에서 각각 하나의 RNN이 t개의 시점(time step)을 가지는 구조였다면, 트랜스포머는 인코더와 디코더가 N개로 구성되어, N개의 인코더 블록과 디코더 블록이 위로 쌓여있는 구조이다.

위 그림에서 보여지듯 encoders, decoders라고 표현된 것도 그 때문이다. 트랜스포머를 제안한 논문에서는 인코더와 디코더의 개수를 각각 6개 사용하였다. 아래 그림은 encoders와 decoders 내부를 들여다보았을 때 구조이다.

 

인코더 들은 모두 정확히 똑같은 구조를 가지고 있다. 그러나 그들 간에 같은 weight를 공유하진 않는다. 트랜스포머는 위 그림처럼 인코더로부터 정보를 전달받아 디코더가 출력 결과를 만들어내는 구조이다. 이는 RNN은 사용되지 않았만 여전히 인코더-디코더 구조는 유지되고 있음을 보여준다.

 

2.1 인코더(Encoder)

인코더에 들어온 입력은 일단 셀프 어텐션 레이어를 지난다. 이 레이어는 인코더가 하나의 특정 단어를 인코딩하기 위해서 입력 내의 모든 단어들과의 관계를 살펴본다. 이 셀프 어텐션 레이어는 이후에 더 자세하게 살펴보도록 하자.

입력이 셀프 어텐션 레이어를 통과하여 나온 출력은 다시 피드 포워드 신경망으로 들어간다. 피드 포워드 신경망에서는 각 위치의 단어마다 독립적으로 적용돼 출력을 생성한다.

 

2.2 디코더(Decoder)

디코더 또한 인코더에 있는 두 레이어를 모두 가지고 있다. 그러나 그 두 레이어 사이에 seq2seq 모델의 어텐션과 비슷한 인코더-디코더 어텐션이 포함되어있다. 이는 디코더가 입력 문장 중에서 각 타임 스텝에서 가장 관련 있는 부분에 집중할 수 있도록 해준다.

 


3. 포지셔널 인코딩(Positional Encoding)

트랜스포머는 입력 문장에 대한 병렬 처리를 위한 모델이기 때문에 RNN과 다르게 모델이 입력 문장에서 단어들의 순서에 대해서 고려하고 있지 않다. 트랜스포머 모델은 각각의 입력 embedding에 “positional encoding”이라고 불리는 벡터를 더함으로써. 임력 문장의 단어들 간의 상대적인 위치 정보를 고려하도록 한다. 

만약 embedding의 사이즈가 4라고 가정한다면, 실제로 각 위치에 따른 positional encoding 은 아래와 같다.

 

 

포지셔널 인코딩 값들은 어떤 값이기에 위치 정보를 반영해줄 수 있는 것일까? 트랜스포머는 위치 정보를 가진 값을 만들기 위해서 아래의 두 개의 함수를 사용한다.

  • \(pos\) : 입력 문장에서의 임베딩 벡터의 위치
  • \(i\) : 임베딩 벡터 내의 차원의 인덱스

\[PE_{(pos, 2i)} = sin(pos/10000^{2i/d_{model}})\]

\[PE_{(pos, 2i+1)} = cos(pos/10000^{2i/d_{model}})\]

 

위의 식에 따르면 임베딩 벡터 내의 각 차원의 인덱스가 짝수인 경우에는 사인 함수의 값을 사용하고 홀수인 경우에는 코사인 함수의 값을 사용한다. 위의 수식에서 \((pos, 2i)\)일 때는 사인 함수를 사용하고, \((pos, 2i+1)\)일 때는 코사인 함수를 사용하고 있음을 주목하자. 포지셔널 인코딩에 대한 자세한 내용은 따로 다뤄 보도록 하겠다.

 

위 그림은 20개의 단어와 그의 크기 512인 embedding에 대한 positional encoding의 실제 예시이다. 반으로 나뉘어 있는 왼쪽 부분은(크기 256) sin 함수에 의해서 생성되었고, 나머지 오른쪽 반은 또 다른 함수인 cos 함수에 의해 생성된 포지셔널 인코딩 값이다. (그림의 x축이 잘못되어있는 것 같다. 왼쪽 절반은 0~512의 값 중 짝수 값 오른쪽 절반은 0~512의 값 중 홀수 값이 될 것이다.)

 

위와 같은 포지셔널 인코딩 방법을 사용하면 순서 정보가 보존되는데, 예를 들어 각 임베딩 벡터에 포지셔널 인코딩의 값을 더하면 같은 단어라고 하더라도 문장 내의 위치에 따라서 트랜스포머의 입력으로 들어가는 임베딩 벡터의 값이 달라질 것이다. 이에 따라 트랜스포머의 입력은 순서 정보가 고려된 임베딩 벡터가 된다.

 


4. 어텐션(Attention)

트랜스포머에서 사용되는 세 가지의 어텐션에 대해서 자세히 살펴보기 전에 먼저 간단히 정리해보자.

 

첫 번째 그림인 셀프 어텐션은 인코더에서 이루어지지만, 두 번째 그림인 셀프 어텐션과 세 번째 그림인 인코더-디코더 어텐션은 디코더에서 이루어진다. 셀프 어텐션은 본질적으로 Query, Key, Value가 동일한 경우를 말한다. 반면, 세 번째 그림 인코더-디코더 어텐션에서는 Query가 디코더의 벡터인 반면에 Key와 Value가 인코더의 벡터이므로 셀프 어텐션이라고 부르지 않는다.

  • 주의할 점은 여기서 Query, Key 등이 같다는 것은 벡터의 값이 같다는 것이 아니라 벡터의 출처가 같다는 의미이다.

정리하면 다음과 같다.

 

인코더의 셀프 어텐션 : Query = Key = Value
디코더의 마스크드 셀프 어텐션 : Query = Key = Value
디코더의 인코더-디코더 어텐션 : Query : 디코더 벡터 / Key = Value : 인코더 벡터

 


5. 인코더의 셀프 어텐션(Self Attention)

기존의 seq2seq 모델에서 어텐션은 디코더의 t시점(루옹 어텐션)이나 t-1시점(바다나우 어텐션)에서 은닉 상태(Query)와 모든 인코더의 은닉 상태(Key) 간의 유사도를 구하여 모든 인코더의 은닉 상태(Value)에 반영하고 이를 가중합 한 어텐션 값(Attention value, 또는 Context vector라고도 함)을 리턴한다. 그렇다면 Query, Key, Value가 모두 동일한 셀프 어텐션은 어떤 의미를 가지는 것일까? 아래 그림을 보자.

위의 예시 문장을 번역하면 '그 동물은 길을 건너지 않았다. 왜냐하면 그것은 너무 피곤하였기 때문이다.' 라는 의미가 된다. 그런데 여기서 그것(it)에 해당하는 것은 과연 길(street)일까? 동물(animal)일까? 사람은 피곤한 주체가 동물이라는 것을 아주 쉽게 알 수 있지만 기계는 그렇지 않다. 하지만 셀프 어텐션은 입력 문장 내의 단어들끼리 유사도를 구함으로써 그것(it)이 동물(animal)과 연관되었을 확률이 높다는 것을 찾아낸다.

입력 문장 내에 있는 단어들 간의 연관성을 얻는 작업을 셀프 어텐션이라고 볼 수 있을 것이다. 트랜스포머에서의 셀프 어텐션의 동작 메커니즘을 알아보자.

 

앞서 셀프 어텐션은 입력 문장의 단어 벡터들을 가지고 수행한다고 하였는데, 사실 셀프 어텐션은 인코더의 초기 입력인 \(d_{model}\)의 차원을 가지는 단어 벡터들을 사용하여 셀프 어텐션을 수행하는 것이 아니라 우선 각 단어 벡터들로부터 세 개의 학습 가능한 행렬들을 각각 곱함으로써 Q벡터, K벡터, V벡터를 얻는 작업을 거친다.

 

Q벡터, K벡터, V벡터는 초기 입력인 \(d_{model}\) = 512의 차원을 가졌던 각 단어 벡터들과는 다른 64차원을 가진다. 이는 멀티 해드 어텐션과 연결되는 개념이며, 트랜스포머 하이퍼 파라미터인 num_heads로 인해 결정되는데, 논문에서 제안되는 기본 값은 8이다. 트랜스포머는 \(d_{model}\)을 num_heads로 나눈 값인 64라는 값을 Q벡터, K벡터, V벡터의 차원으로 결정한다.

 

정리하자면 셀프 어텐션에서는 먼저 Q벡터, K벡터, V벡터를 서로 다른 세 개의 가중치 행렬과 곱함으로써 얻어내는데, 각 가중치 행렬은 \(d_{model}\)×(\(d_{model}\)/num_heads)의 크기를 가지며, 이 가중치 행렬은 훈련 과정에서 학습된다. 예를 들어 논문과 같이 dmodel=512이고 num_heads=8라면, 각 단어 벡터에 (512, 64)의 크기를 가지는 3개의 서로 다른 가중치 행렬을 곱하고 64의 크기를 가지는 Q, K, V 벡터를 얻어낸다.

 

5.1 스케일드 닷-프로덕트 어텐션(Scaled dot-product Attention)

셀프 어텐션의 두 번째 스텝은 어텐션 스코어를 계산하는 것이다. 이전에 어텐션에 대해 정리한 글에서 사용했던 내적만을 사용하는 어텐션 함수 \(score(q, k) = q\cdot k\)가 아니라 여기에 K벡터 사이즈의 제곱근을 나누어주는 \(score(q, k) = q\cdot k /\sqrt{d_k}\)를 사용한다. 아래 예시의 첫 번째 단어인 “Thinking”에 대해서 셀프 어텐션을 계산한다고 가정해보자. 

점수는 현재 단어의 query vector와 점수를 매기려 하는 다른 위치에 있는 단어의 key vector의 내적을 K벡터 차원의 제곱근으로 나누어 계산된다. 다시 말해, 위치 #1에 있는 단어에 대해서 셀프 어텐션을 계산한다 했을 때, 첫 번째 어텐션 스코어는 q1 k1의 내적에  \(sqrt{d_k}\) = 8을 나누어준 값일 것이다. 그리고 동일하게 두 번째 점수는 q1 k2의 내적에 (sqrt{d_k}\)를 나누어준 값일 것이다. 이러한 나눗셈은 벡터 값을 스케일링 함으로써 더 안정적인 gradient를 가지게 된다.

그리고 난 다음 이 값을 softmax 계산을 통과시켜 확률 분포인 어텐션 가중치를 구하고 V벡터와 가중합 하여 어텐션 값(컨텍스트 벡터)를 얻는다.  이는 단어 "Thinking"에 대한 컨텍스트 벡터이며, 이러한 과정을 모든 Q벡터에 대해 반복하며 각각에 대한 어텐션 값을 구한다.

 

5.2 행렬 연산으로 일괄 처리하기

사실 각 단어에 대한 Q, K, V 벡터를 구하고 스케일드 닷-프로덕트 어텐션을 수행하였던 위의 과정들은 벡터 연산이 아니라 아래와 같이 행렬 연산을 사용하면 일괄 계산이 가능하다. 

마지막으로, 앞서 설명했던 셀프 어텐션 계산 단계를 하나의 식으로 압축할 수 있다.

위의 그림은 행렬 연산을 통해 모든 값이 일괄 계산되는 과정을 보여준다. 해당 과정은 실제 트랜스포머 논문에 기재된 아래의 수식과 정확하게 일치한다. 

\[Attention(Q,K,V) = softmax(\frac{QK^t}{\sqrt{d_k}})V\]

 

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

이제 num_heads의 의미와 왜 \(d_{model}\)의 차원을 가진 단어 벡터를 가지고 어텐션을 하지 않고 차원을 축소시킨 벡터로 어텐션을 수행하였는지 이해해보자.

트랜스포머 연구진은 한 번의 어텐션을 하는 것보다 여러 번의 어텐션을 병렬로 사용하는 것이 더 효과적이라고 판단하였다. 그래서 \(d_{model}\)의 차원을 num_heads개로 나누어 \(d_{model}\)/num_heads의 차원을 가지는 Q, K, V에 대해서 num_heads개의 병렬 어텐션을 수행한다. 논문에서는 하이퍼 파라미터인 num_heads의 값을 8로 지정하였고, 8개의 병렬 어텐션이 이루어지게 된다. 다시 말해 위에서 설명한 어텐션이 8개로 병렬로 이루어지게 되는데, 이때 각각의 어텐션 값 행렬을 어텐션 헤드라고 부른다. 이때 가중치 행렬 \(W^{Q}\),\(W^{K}\),\(W^{V}\)의 값은 8개의 어텐션 헤드마다 전부 다르다. 

병렬 어텐션으로 얻을 수 있는 효과는 다른 시각으로 정보들을 수집하겠다는 것이다.

구체적으로 멀티 해드 어텐션이 가지는 이점은 아래와 같다. 

  • 앞서 사용한 예문 '그 동물은 길을 건너지 않았다. 왜냐하면 그것은 너무 피곤하였기 때문이다.'를 상기해보자. 단어 그것(it)이 쿼리였다고 했을 때 it에 대한 Q벡터로부터 다른 단어와의 연관도를 구하였을 때 첫 번째 어텐션 헤드는 '그것(it)'과 '동물(animal)'의 연관도를 높게 본다면, 두 번째 어텐션 헤드는 '그것(it)'과 '피곤하였기 때문이다(tired)'의 연관도를 높게 볼 수 있다. 각 어텐션 헤드는 전부 다른 시각에서 보고 있기 때문이다.

  • attention layer 가 여러 개의 “representation 공간”을 가지게 해준다. multi-headed attention을 이용함으로써 여러 개의 Query/Key/Value weight 행렬들을 가지게 된다. 이 각각의 Query/Key/Value set는 랜덤으로 초기화되어 학습된다. 학습이 된 후 각각의 세트는 입력 벡터들에 곱해져 벡터들을 각 목적에 맞게 투영시키게 되므로 이러한 세트가 여러 개 있다는 것은 각 벡터들을 각각 다른 representation 공간으로 나타낸 다는 것을 의미한다. 즉 앙상블을 이뤄 어텐션이 너무 한방향으로 치우치지 않도록 하는 역할을 한다.

위에 설명했던 대로 같은 셀프 어텐션 계산 과정을 8개의 다른 가중치 행렬들에 대해 8번 거치게 되면, 8개의 서로 다른 Z 행렬(Attention value)을 가지게 된다.

 

그러나 문제는 이 8개의 행렬을 바로 feed-forward layer으로 보낼 수 없다는 것이다. feed-forward layer 은 한 위치에 대해 오직 한 개의 행렬만을 input으로 받을 수 있다. 그러므로 일단 모두 이어 붙여서 하나의 행렬로 만들어버리고, 그다음 하나의 또 다른 가중치 행렬인 \(W^{O}\)를 곱하여 feed-forward layer의 입력을 만든다. 

 

위 그림은 어텐션 해드를 모두 연결한 행렬이 가중치 행렬 \(W^{O}\)과 곱해지는 과정을 보여준다. 이때 결과물인 멀티-해드 어텐션 행렬은 인코더의 입력이었던 문장 행렬 \((seq\underline{\;}len, d_{model})\)크기와 동일하다.

 


6. 피드 포워드 신경망(Feed Forward Neural Network)

지금은 인코더를 설명하고 있지만, FFNN은 인코더와 디코더에서 공통적으로 가지고 있는 레이어이다. FFNN는 쉽게 말하면 완전 연결 FFNN(Fully-connected FFNN)이라고 해석할 수 있다. 아래는 FFNN의 수식을 보여준다.

 

\[FFNN(x) = MAX(0,xW_{1}+b_1)W_2 + b_2\]

 

식을 그림으로 표현하면 아래와 같다.

여기서 \(x\)는 앞서 멀티 헤드 어텐션의 결과로 나온 \((seq\underline{\;}len, d_{model})\)의 크기를 가지는 행렬을 말한다. 가중치 행렬 \(W_{1}\)은 \((d_{model}, d_{ff})\)의 크기를 가지고, 가중치 행렬 \(W_{2}\)는 \((d_{ff}, d_{model})\)의 크기를 가진다. 논문에서 은닉층의 크기인 \(d_{ff}\)는 앞서 하이퍼 파라미터를 정의할 때 언급했듯이 2,048의 크기를 가진다.

여기서 매개변수 \(W_{1}\), \(b_{1}\), \(W_{2}\), \(b_{1}\)는 하나의 인코더 층 내에서는 다른 문장, 다른 단어들마다 정확하게 동일하게 사용된다. 하지만 인코더 층마다는 다른 값을 가진다.

 


7. 잔차 연결(Residual connection)과 층 정규화(Layer Normalization)

셀프 어텐션 레이어와 피드 포워드 신경망에 이어서 트랜스포머에서는 이러한 두 개의 서브층을 가진 인코더에 추가적으로 사용하는 기법이 있는데, 바로 Add & Norm이다. 더 정확히는 잔차 연결(residual connection)과 층 정규화(layer normalization)를 의미한다.

 

아래 그림은 앞서 다루어본 셀프 어텐션과 FFNN에 추가로 Add & Norm 레이어와 점선으로 입력의 흐름을 추가한 그림이다.

 

7.1 잔차 연결(Residual connection, skip connection)

잔차 연결은 모델의 깊이가 깊어질수록 입력에 정보가 손실될 수 있는 문제점을 해결하기 위해 특정 레이어의 아웃풋에 그 레이어의 인풋을 단순히 더해줌으로써 정보 손실의 문제점을 해결한 기법이다. 잔차 연결을 사용함으로써 얻을 수 있는 장점은 크게 두가지가 있다.

  • 상위층 레이어 일수록 입력 토큰이 가지는 실제 의미가 많이 소실되게 되는데 잔차 연결은 하위 레이어에 존재한는 토큰들의 실제 의미를 잘 전달하는 역할을 한다.
  • 레이어가 깊은 딥러닝 모델일 수록 gradient vanishing에 따른 학습 비효율이 일어나게 되는데 잔차 연결은 gradient를 상위에서 하위까지 잘 전달하여 모델의 학습 효율성이 커지는 효과가 있다.

출처 : https://arxiv.org/pdf/1512.03385.pdf

위 그림과 같이 실제로 ResNet의 실험을 통해 잔차 연결을 하지 않았을 때(Plain) 깊은 모델이 오히려 error율이 크다는 것을 알 수 있다. 잔차 연결을 함으로써 깊은 모델에서 더 error율을 낮게 만들어 줄 수 있다.

위 그림에서 왼쪽과 오른쪽은 각각 Plane Layer와 Redisual Connection을 사용하였을 때의 아키텍처를 보여준다.

Plane Layer의 경우 수식으로 표현하면 \(y = F(x)\)이 되는데 여기에서 아웃풋인 y는 x를 통해 새롭게 학습하는 정보이다. 즉, 기존에 학습한 정보를 보존하지 않고 변형시켜 새롭게 등장하는 정보가 된다.

 

Redisual Connection을 사용한 경우 수식은 \(y = F(x) + x\)이 되는데 여기에서 y는 x가 그대로 보존되므로, 기존에 학습한 정보를 보존하고, 거기에 추가적으로 학습하는 정보를 의미하게 된다. 즉, Output에 이전 레이어에서 학습했던 정보를 연결함으로써 해당 층에서는 추가적으로 학습해야 할 정보만을 학습하게 된다.

 

트랜스포머에서 잔차 연결을 예로 들자면 가령 이전에 통과한 레이어가 멀티 해드 어텐션이었다면 잔차 연결은 다음과 같다.

 

\[H(x) = Multi-head Attention(x) + x\]

 

7.2 층 정규화(Layer Nomerilzation)

잔차 연결을 거친 결과는 이어서 층 정규화 과정을 거치게 된다. 잔차 연결의 입력을 \(x\), 잔차 연결과 층 정규화 두 가지 연산을 모두 수행한 후의 결과 행렬을 \(LN\)이라고 하였을 때, 잔차 연결 후 층 정규화 연산을 수식으로 표현하자면 다음과 같다.

 

\[LN = LayerNorm(x + sublayer(x))\]

 

층 정규화는 텐서의 마지막 차원에 대해서 평균과 분산을 구하고, 이를 가지고 어떤 수식을 통해 값을 정규화하여 학습을 돕는다. 여기서 텐서의 마지막 차원이란 것은 트랜스포머에서는 \(d_{model}\) 차원을 의미한다. 아래 그림은 \(d_{model}\) 차원의 방향을 화살표로 표현하였다.

 

층 정규화를 위해서 우선, 화살표 방향으로 각각 평균 \(\mu\)과 분산 \(\sigma^2 \)을 구합니다. 각 화살표 방향의 벡터를 \(x_{i}\)라고 명명해보자.

 

층 정규화를 수행한 후에는 벡터 \(x_{i}\) \(ln_{i}\)라는 벡터로 정규화 된다.

 

\[ln_i = LayerNorm(x_i)\]

 

층 정규화의 수식을 알아보자. 여기서는 층 정규화를 두 가지 과정으로 나누어서 설명하도록 한다. 첫 번째는 평균과 분산을 통한 정규화, 두 번째는 감마와 베타를 도입하는 것이다. 먼저 평균과 분산을 통한 정규화는 수식으로 표현하면 아래와 같다. 이때 \(k\)는 \(x_{i}\)벡터의 각 차원을 의미하고 \(\epsilon \)(입실론)은 분모가 0이 되는 것을 방지하는 값이다.

 

\[\hat{x}_{i,k} = \frac{x_{i,k}-\mu_{i}}{\sqrt{\sigma_{i}^{2}+\epsilon}}\]

 

\(\gamma \)\(\beta\)를 도입한 층 정규화의 최종 수식은 다음과 같으며 \(\gamma \) \(\beta\)는 학습 가능한 파라미터이다.

 

\[ln_{i} = \gamma\hat{x}_{i}+\beta = LayerNorm(x_{i})\]

 

위와 같이 잔차 연결(Residual connection)과 층 정규화(Layer Normalization)은 디코더 내에 있는 sub-layer에도 똑같이 적용되어 있다. 만약 2개의 인코더와 디코더로 이루어진 단순한 형태의 트랜스포머를 생각해본다면 다음과 같은 모양일 것이다.

 

 


8. 디코더(Decoder)

 

디코더도 인코더와 동일하게 임베딩 층과 포지셔널 인코딩을 거친 후의 문장 행렬이 입력된다. 트랜스포머 또한 seq2seq와 마찬가지로 교사 강요(Teacher Forcing)를 사용하여 훈련되므로 학습 과정에서 디코더는 번역할 문장에 해당되는 <sos> I am a student의 문장 행렬을 한 번에 입력받는다. 그리고 디코더는 이 문장 행렬로부터 각 시점의 단어를 예측하도록 훈련된다.

여기서 문제가 있다. seq2seq의 디코더에 사용되는 RNN 계열의 신경망은 입력 단어를 매 시점마다 순차적으로 입력받으므로 다음 단어 예측에 현재 시점을 포함한 이전 시점에 입력된 단어들만 참고할 수 있다. 반면, 트랜스포머는 문장 행렬로 입력을 한 번에 받으므로 현재 시점의 단어를 예측하고자 할 때, 입력 문장 행렬로부터 미래 시점의 단어까지도 참고할 수 있는 현상이 발생한다.

가령, am을 예측해야 하는 시점이라고 해보자. RNN 계열의 seq2seq의 디코더라면 현재까지 디코더에 입력된 단어는 <sos>와 "I"뿐일 것이다. 반면, 트랜스포머는 이미 문장 행렬로 <sos> I am a student를 입력받았다.

이를 위해 트랜스포머의 디코더에서는 현재 시점의 예측에서 현재 시점보다 미래에 있는 단어들을 참고하지 못하도록 룩-어헤드 마스크(look-ahead mask)를 도입했다. 직역하면 '미리보기에 대한 마스크'이다.

 

8.1 마스크드 셀프 어텐션(Masked Self Attention)

디코더의 마스크드 셀프 어텐션 또한 인코더의 멀티 헤드 셀프 어텐션 층과 동일한 연산을 수행한다. 오직 다른 점은 어텐션 스코어 행렬에서 마스킹을 적용한다는 점만 다르다.

위에서 간단히 설명하였듯 “나는 학생이다.(I am a student)”라는 문장을 번역하는데 I 뒤에 올 단어를 예측할 차례라면 “am”, “a”, “student”에 대한 정보는 얻을 수 없도록 마스킹해야 한다. 아래는 인코더에서 수행하는 셀프 어텐션과 디코더에서 수행하는 마스크드 셀프 어텐션의 차이를 그림으로 나타낸 것이다.

 

 

그렇다면 어떻게 정보를 마스킹해줄 수 있을까? 아래 그림을 보면 디코더에서 "Thinking"이라는 단어를 예측할 때에는 그 뒤에 있는 "Machines"라는 단어의 Query와 Key의 내적 값인 \(q\cdot k\)의 값을 \(-\infty \)로 만들어 주는 것을 알 수 있다. 이렇게 되면 softmax함수를 통과하였을 때 생성되는 어텐션 가중치가 0이 되기 때문에 Machine에 해당하는 V벡터가 아무런 영향도 주지 않게 된다.

 

8.2 인코더 디코더 어텐션(Encoder-Decoder Attention)

디코더의 두 번째 서브층에 대해서 이해해보자. 디코더의 두 번째 서브층은 멀티 헤드 어텐션을 수행한다는 점에서는 이전의 어텐션들(인코더와 디코더의 첫 번째 서브층)과는 공통점이 있으나 이번에는 셀프 어텐션이 아니다.

셀프 어텐션은 Query, Key, Value가 같은 경우를 말하는데, 인코더-디코더 어텐션은 Query가 디코더인 행렬인 반면, Key와 Value는 인코더 행렬이기 때문이다. 다시 한번 각 서브층에서의 Q, K, V의 관계를 정리해보자.

 

인코더의 셀프 어텐션 레이어 : Query = Key = Value
디코더의 마스크드 셀프 어텐션 레이어 : Query = Key = Value
디코더의 인코더 디코더 어센션 레이어 : Query : 디코더 행렬 / Key = Value : 인코더 행렬

 

디코더의 두 번째 서브층을 확대해보면, 다음과 같이 인코더로부터 두 개의 화살표가 그려져 있다. 두 개의 화살표는 각각 인코더의 마지막 층인 FFNN에서 온 행렬을 Key와 Value로 사용함을 의미한다. 반면 Query는 디코더의 첫 번째 서브층인 마스크드 셀프 어텐션의 결과 행렬로부터 얻는다.

 

정리하자면 Masked Self-Attention층의 출력 벡터는 인코더 블록에서와 동일하게 Residual block과 Layer normalization을 거친 뒤에 인코더-디코더 어텐션(Encoder-Decoder Attention) 과정을 거치게 된다. 이 층에서는 인코더의 마지막 블록에서 출력된 Key, Value 행렬과 디코더의 마스크드 셀프 어텐션의 결과로 출력된 Qeury행렬 간의 셀프 어텐션 메커니즘을 한 번 더 수행한다.

그 외에 멀티 헤드 어텐션과 FFNN을 수행하는 과정은 다른 인코더 과정과 같다.

 

8.3 Linear Layer와 Softmax Layer

여러 개의 decoder를 거치고 난 후에는 소수로 이루어진 벡터 하나가 남는다. 이 벡터는 Linear layer와 Softmax layer를 통해 최종 결과물인 하나의 단어를 예측하도록 한다. 

먼저 Linear layer는 fully-connected 신경망으로 디코더가 마지막으로 출력한 벡터를 그보다 훨씬 더 큰 사이즈의 벡터인 logits 벡터로 투영시킨다.

training 데이터에서 총 10,000개의 영어 단어를 학습한다고 가정하자 이를 "output vocabulary"라고 부르는데, 이 경우엔 logis 벡터의 크기는 10,000이 될 것이다.

그다음으로 Softmax layer는 이 점수들을 확률로 변환해주는 역할은 한다. 이때 가장 높은 확률 값을 가지는 셀에 해당하는 단어가 해당 스텝의 최종 결과물로 출력된다.

 

 

아래 그림에서는 디코더에서 출력을 생성하는 과정에 대해서 보여준다.

 


9. 학습 과정

이제 트랜스포머의 전체 forward-pass 과정에 대해 알아보았으므로, 모델을 학습하는 방법에 대해 알아보자.
학습 과정 동안, 학습이 되지 않은 모델은 정확히 같은 forward pass 과정을 거친다. 그러나 label 된 학습 데이터 셋에 대해 학습시키는 과정에서는 모델의 결과를 실제 label 된 정답과 비교할 수 있다.
이 학습 과정을 시각화하기 위해, output vocabulary 가 6개의 단어만 ((“a”, “am”, “i”, “thanks”, “student”, and “<eos>” (‘end of sentence’의 줄임말))) 포함하고 있다고 가정하자.

 

 

output vocabulary를 정의한 후에는, vocabulary의 크기만 한 벡터를 이용하여 one-hot encoding을 통해 각 단어를 표현할 수 있다. 그러므로 아래 예제에서는, 단어 “am”을 다음과 같은 벡터로 나타낼 수 있다.

 

9.1 Loss Function

모델을 학습하는 상황에서 가장 첫 번째 단계라고 가정하자. 그리고 학습을 위해 “merci”라는 불어를 “thanks”로 번역하는 간단한 예시를 생각해보자.
이 말은 즉, 원하는 모델의 출력은 “thanks”라는 단어를 가리키는 확률이 가장 큰 확률 벡터란 것이다. 하지만 모델은 아직 학습이 되지 않았기 때문에, 모델의 출력이 그렇게 나올 확률은 매우 작다.

 

학습이 시작될 때 모델의 parameter들 즉 weight들은 랜덤으로 값을 부여하기 때문에, 아직 학습이 되지 않은 모델은 그저 각 셀(word)에 대해서 임의의 값을 출력한다. 이 출력된 임의의 값을 가지는 벡터와 데이터 내의 실제 출력 값을 비교하여, 그 차이와 backpropagation 알고리즘을 이용해 현재 모델의 weight들을 조절해 원하는 출력 값에 더 가까운 출력이 나오도록 만든다. 이러한 과정은 다중 분류 문제로 볼 수 있으므로 손실 함수를 Cross-entropy로 정의함으로써 weight들을 조절해 최적화할 수 있다.

 

조금 더 실질 적인 예제를 보도록 하자. 예를 들어 입력은 불어 문장 “je suis étudiant”이며 출력은 “i am a student”를 기대한다고 하자. 이때 모델이 출력할 확률 분포에 대해서 바라는 것은 다음과 같을 것이다.
아래 그림은 학습에서 목표로 하는 확률 분포를 나타낸 것이다.

 

 

  • 각 단어에 대한 확률 분포는 output vocabulary 크기를 가지는 벡터에 의해서 나타내 진다. (예제에서는 6이지만 실제 실험에서는 3,000 혹은 10,000과 같은 숫자일 것이다).
  • 디코더가 첫 번째로 출력하는 확률 분포는 “i”라는 단어와 연관이 있는 셀에 가장 높은 확률을 줘야 한다.
  • 두 번째로 출력하는 확률 분포는 “am”라는 단어와 연관이 있는 셀에 가장 높은 확률을 줘야 한다.
  • 이와 동일하게 마지막 ‘<end of sentence>‘를 나타내는 다섯 번째 출력까지 이 과정은 반복된다.(‘<eos>’ 또한 그에 해당하는 cell을 벡터에서 가진다.)

모델을 큰 사이즈의 데이터 셋에서 충분히 학습을 시키고 나면, 그 결과로 생성되는 확률 분포들은 다음과 같아질 것이다.

 

 

트랜스포머의 최종 출력은 Softmax 함수를 거치기 때문에 레이블 벡터들과는 달리, 모델의 출력 값은 비록 다른 단어들이 최종 출력이 될 가능성이 거의 없다 해도 모든 단어가 0보다는 조금씩 더 큰 확률을 가진다는 점이다.

모델은 한 타임 스텝 당 하나의 벡터를 출력하기 때문에 모델이 가장 높은 확률을 가지는 하나의 단어만 저장하고 나머지는 버린다고 생각하기 쉽다. 그러나 그것은 greedy decoding이라고 부르는 한 가지 방법일 뿐이며 다른 방법들도 존재한다.
예를 들어 가장 확률이 높은 두 개의 단어를 저장할 수 있다 (위의 예시에서는 ‘I’와 ‘student’). 그렇다면 모델을 두 번 돌리게 된다. 한 번은 첫 번째 출력이 ‘I’이라고 가정하고 다른 한 번은 ‘student’라고 가정하고 두 번째 출력을 생성해보는 것이다. 이렇게 나온 결과에서 첫 번째와 두 번째 출력 단어를 동시에 고려했을 때 더 낮은 에러를 보이는 결과의 첫 번째 단어가 실제 출력으로 선택된다. 이 과정을 두 번째, 세 번째, 그리고 마지막 타임 스텝까지 반복한다. 이렇게 출력을 결정하는 방법을 “beam search”라고 부르며, 고려하는 단어의 수를 beam size, 고려하는 미래 출력 개수를 top_beams라고 부른다. 예제에서는 두 개의 단어를 저장했으므로 beam size가 2이며, 첫 번째 출력을 위해 두 번째 스텝의 출력까지 고려했으므로 top_beams 또한 2인 beam search를 한 것이다. 이 beam size와 top_beams는 모두 학습 전에 미리 정하고 실험해볼 수 있는 hyperparameter이다.

 


마치며

seq2seq 모델에서 정보의 손실을 개선하기 위해 제안된 Attention에 기반한 모델인 트랜스포머에 대해 정리해 보았다.

이제 개념을 정리하였으니 직접 코드를 보고 구현하면서 혼동되는 개념에 대해 적립할 계획이다.

구현한 코드는 깃허브에 업로드할 생각이다.

댓글