https://arxiv.org/abs/1706.03762
이번에 리뷰해볼 논문은 Transformer로 유명한 'Attention is All You Need'라는 논문입니다. 2017년에 발표된 이 논문은 NLP의 패러다임을 완전히 바꿔놓았죠. 뭐가 그렇게 대단하길래 다들 transformer, trasformer 하는지 알아봅시다.
1. 개요
시퀀스 데이터란 순서가 있는 연속되는 데이터를 말합니다. 예를 들면 일기예보나 문장 데이터 등이 여기에 속합니다. 이런 데이터들은 '순서'가 존재하기 때문에 순서 정보를 활용할 수 있는 모델 구조(RNN)가 많이 사용되었습니다.
그러나 RNN 구조는 한계점이 있습니다. 우선 시퀀스 데이터를 병렬적으로 처리할 수 없습니다. 이는 RNN이 이전 시퀀스의 정보를 활용해야 하는 특징 때문에 그렇습니다. 이로 인해 시퀀스가 길어질수록 연산량과 필요한 메모리가 커지는 문제가 발생합니다.
또 하나의 문제점은, RNN이 이전 시점의 시퀀스 정보를 활용하기 때문에 뒤로 갈수록 앞부분의 정보를 잊게 된다는 것입니다. 시퀀스가 길어지면 앞 부분의 미분 계산값이 0에 가까워지거나 값이 너무 커지는 문제가 발생할 수 있습니다.
이런 문제를 해결하기 위해 attention 메커니즘이 활용됩니다. Attention 메커니즘은 일련의 시퀀스 정보 중 어느 부분에 더 집중(attention)해야 하는지를 학습하기 위한 모델구조입니다. 그 중에서도 Self-attention은 하나의 시퀀스 내에 있는 서로 다른 위치의 단어끼리의 연관성을 계산하는 과정입니다.
이런 attention 메커니즘은 그동안 RNN이나 CNN과 같이 결합되어 사용되어 왔습니다. 그러나 'Attention is All You Need' 논문에서는 최초로 오로지 self-attention 메커니즘만을 활용한 모델 구조를 소개합니다. Transformer는 이를 통해 데이터를 병렬적으로 처리할 수 있게 될뿐만 아니라, 입력과 출력 간의 관계(global dependency)도 그려낼 수 있게 됩니다!
2. Attention이란?
Attention은 입력 시퀀스에 포함된 단어 간의 연관성을 계산하여 이를 반영한 벡터를 출력하는 함수입니다. 여기에는 Query, Key, Value 3가지 벡터가 사용됩니다.
- Query : 임베딩을 얻고자 하는 target 단어의 벡터.
- Key : target과의 연고나성을 비교하고자 하는 다른 단어들의 벡터.
- Value : 연관성에 따른 가중치 벡터.
정의만 보면 헷갈리니 직접 수식을 보면서 다시 생각해 봅시다. 논문에서 소개하고 있는 scaled dot-product attention의 식은 다음과 같습니다.
$$ Attention(Q,K,V)=softmax({QK^T \over \sqrt{d_k} })V $$
수식에 따르면 임베딩을 얻으려는 단어 Query에 나머지 단어들의 임베딩 Vector를 내적합니다. 이를 $\sqrt{d_k}$로 정규화한 뒤에, softmax 활성화 함수를 거친 뒤에, 가중치 Value를 적용하는 것입니다.
위 그림과 같이, '이순신은' 이라는 단어의 벡터를 계산하기 위해, 시퀀스의 각 단어들의 key 값들과 내적(dot-product)을 수행합니다. 그 뒤에 각 단어의 가중치 value 값을 곱해 최종적으로 output을 계산하게 됩니다. scaled dot-product attention은 그 사이에 $\sqrt{d_k}$로 정규화하는 과정이 추가되었을 뿐이구요.
만약 RNN이었다면 어땠을까요? RNN은 이전 시퀀스만을 참고하기 때문에 '조선'이라는 단어의 벡터를 추출하기 위해서 '이순신은' 이라는 단어만을 보게 됩니다. 그러나 위와 같이 attention 연산을 수행하면, '조선'이라는 단어의 벡터를 추출할 때 '이순신은', '중기의', '무신이다' 3개의 단어를 모두 살펴보게 됩니다. 여기에서 self-attention 구조의 장점이 드러납니다. Self-attention은 RNN과 다르게 모든 단어 시퀀스의 정보를 한번에 병렬적으로 활용할 수 있으며, 그렇기 때문에 문장이 길어짐에 따른 정보의 손실 문제가 발생하지 않습니다.
Attention 코드는 아래와 같이 꽤 단순하게 작성할 수 있습니다.
import torch
import numpy as np
import torch.nn as nn
class scaled_dot_product_attn(nn.Module):
def __init__(self, d_k): # d_k : dimension of key.
super(scaled_dot_product_attn, self).__init__()
self.d_k = d_k
def scaled_dot_product_attn(self, query, key, value):
x = torch.matmul(query, key.transpose())
x /= np.sqrt(self.d_k)
x = F.softmax(x)
x = torch.matmul(x, value)
return x
Multi-head attention
Attention을 먼저 본 김에 본 논문에서 사용한 multi-head attention까지 알아봅시다. Multi-head attention은 이름과 같이 head가 여러개입니다. 앞서 본 scaled dot-product attention을 여러 개의 head로 나눠 수행하는 것입니다. 본 논문에서는 8개의 head를 사용했다고 합니다.
Head를 왜 나누는 걸까요? 논문에서는 여러 개의 head로 나누어 시퀀스를 분석함으로써 시퀀스에 대해 모델이 보다 더 다양한 관점을 갖게 된다고 설명하고 있습니다. 실제로 실험을 해보면 multi-head를 적용했을 때와 적용하지 않았을 때의 성능이 적잖이 차이가 나는 것을 확인할 수 있습니다. 또, head를 나누는 과정에서 각 head의 차원 수가 줄어들기 때문에, scaled dot-product attention과 연산량도 크게 차이가 나지 않습니다.
class MultiHeadAttention(nn.Module):
def __init__(self, dim: int = 512, num_heads: int = 8) -> None:
super(MultiHeadAttention, self).__init__()
# 입력 벡터의 차원 수(dim)는 head 개수(num_head)로 나눠 떨어져야 합니다.
assert dim % num_heads == 0, "hidden_dim % num_heads should be zero."
self.d_head = int(dim / num_heads)
self.num_heads = num_heads
self.query_proj = Linear(dim, self.d_head * num_heads)
self.key_proj = Linear(dim, self.d_head * num_heads)
self.value_proj = Linear(dim, self.d_head * num_heads)
# scaled_dot_product_attn은 위의 scaled dot-product attention 클래스 코드입니다.
self.scaled_dot_attn = scaled_dot_product_attn(dim, scale=True)
def forward(self, query, key, value)
batch_size = value.size(0)
query = self.query_proj(query)
query = query.view(batch_size, -1, self.num_heads, self.d_head).transpose(1, 2)
key = self.key_proj(key)
key = key.view(batch_size, -1, self.num_heads, self.d_head).transpose(1, 2)
value = self.value_proj(value)
value = value.view(batch_size, -1, self.num_heads, self.d_head).transpose(1, 2)
context = self.scaled_dot_attn(query, key, value)
context = context.transpose(1, 2).reshape(batch_size, -1, self.num_heads * self.d_head)
return context
코드를 보면 길어 보이지만 사실 별거 없습니다. proj라는 이름의 Linear 레이어를 통해 query, key, value를 각각 head개수만큼 나눠준 뒤에, 그대로 scaled dot-product attention을 수행하고, 다시 head들을 합쳐주면 끝입니다.
Transformer 모델 구조
Transformer는 시퀀스를 입력 받아 시퀀스를 출력하는 인코더, 디코더 구조로 이루어져 있습니다. 인코더는 입력 시퀀스로부터 정보를 축약한 임베딩 벡터 z를 만들어내는 역할을 합니다. 디코더는 벡터 z를 이용해 출력 시퀀스를 만드는 역할을 하게 됩니다. 각 인코더와 디코더는 N개의 transformer 블록으로 이루어지게 됩니다. transformer 블록은 self-attention, point-wise fully-connected layer만으로 이루어집니다.
영어를 한글로 번역하는 문제를 예시로 들자면, 인코더에는 한글 문장이 들어가게 되고, 인코더는 한글 문장의 임베딩 벡터 z를 형성합니다. 디코더에는 영어 문장이 들어가, 한글 임베딩 z를 참고하여 영어 문장을 앞에서부터 차례대로 생성하게 됩니다.
인코더
인코더는 6개의 transformer 블록으로 구성되고, 각 블록은 2개의 sub-layer로 이루어져 있습니다.
1. Multi-head self-attention Layer : self-attention을 통해 입력 시퀀스 내의 단어간의 연관성을 계산합니다. Head의 개수는 8개를 사용했다고 합니다. 벡터의 차원 수는 512로, 각 head의 차원 수는 64가 됩니다.
2. Position-wise fully connected Layer : 앞의 attention layer의 결과를 다시 한 번 정제하는 역할을 하는 레이어라고 보면 됩니다. 2개의 linear layer와 하나의 ReLU로 구성되어 있으며, 아래와 같은 연산을 수행합니다. 첫번째 linear layer는 2048 차원의 output을 출력하고, 두번째 linear layer는 512 차원의 output을 출력합니다.
$$ max(0, w_1x+b_1)w_2+b_2 $$
2개의 sub-layer는 각각 residual connection과 layer normalization이 적용됩니다. Residual connection은 ResNet에서 나온 개념으로 레이어가 깊어짐에 따라 앞의 정보를 손실하는 문제를 해결하기 위해, 앞 레이어의 output을 뒤 레이어의 output에 더해주는 구조입니다. Layer normalization은 모델 파라미터가 지나치게 커지는 것을 방지하기 위해 값을 일정 범위 내로 제한해주는 정규화 역할을 합니다.
디코더
디코더 역시 6개의 transformer 블록으로 구성되어 있습니다. 다만, 디코더의 블록은 3개의 sub-layer로 분리되어 있습니다.
1. Multi-head self-attention Layer : 인코더와 동일.
2. Multi-head attention Layer : 이 두번째 sub-layer에서는 self-attention이 아니라, 인코더의 출력 벡터를 입력 받는 attention 레이어가 존재합니다. Query는 모델이 출력하려는 output의 임베딩 벡터, Key와 Value는 모델의 입력 input의 임베딩 벡터를 사용함으로써 모델이 input과 output 사이의 연관성을 계산하도록 합니다.
3. Position-wise fully connected Layer : 인코더와 동일.
디코더는 모델이 출력하고자 하는 output을 만드는 레이어입니다. 즉, 앞에서부터 한 단어(글자, 토큰)씩 하나하나 결과를 내놓습니다. 그렇기 때문에 만약 디코더에 모델 output 전체를 다 보여준다면 모델이 컨닝을 할 수도 있습니다. 그렇기 때문에 디코더에서는 masking을 통해 아직 모델이 보지 않아야 할 뒷 단어들은 가려주는 작업을 추가로 수행합니다.
Embeddings and Softmax
컴퓨터는 숫자만을 입력받을 수 있기 때문에 입력 시퀀스(문장)는 모두 사전에 정의된 숫자로 매핑합니다. 숫자들은 Embedding 레이어를 통해 학습 가능한 벡터 값으로 변환되게 됩니다. 여기서 Encoder와 Decoder의 input에 모두 Embedding 레이어가 존재하는데, 논문에서는 이 두 레이어의 파라미터는 서로 공유한다고 합니다. 각각 다른 언어를 처리해야 하는 Transformer의 구조에서 이상하게 느껴질 수도 있는 부분이지만, 파라미터를 공유하는 것은 연산량에 있어 효율적인 방법 중 하나이기 때문에 이런 식으로 사용하는 경우가 많다고 합니다.
모델의 output은 디코더가 다음 시퀀스를 예측하는데, softmax를 사용하여 어떤 단어의 확률이 가장 높은 지를 계산한다고 합니다.
Positional Encoding
지금까지 Transformer 구조를 보면 한 가지 의문이 생길 수 있습니다. 시퀀스 전체를 한번에 볼 수 있는 것까지는 좋은데, 어느 단어가 어느 위치에 존재하는가에 대한 고려는 전혀되고 있지 않다는 것이죠. 각 단어의 위치 정보를 모델에 반영시키기 위해 Positional Encoding이 사용됩니다. 본 논문에서는 Sinusoidal encoding 기법을 사용하였습니다. 이는 아래와 같이 계산됩니다.
\begin{align} PE_{(pos,2i)}=sin(pos/10000^{2i/d_{model}}) \\
PE_{(pos,2i+1)}=cos(pos/10000^{2i/d_{model}}) \end{align}
수식만 보고 너무 겁먹을 필요가 없습니다. 굳이 이런 식을 사용한 이유는 토큰위 위치를 단순한 0과 1사이의 값 이상의 표현력으로 표현하고 싶었던 것입니다. 위 식을 사용할 경우 각 위치마다 고유한 값을 가질 수 있고, 파라미터가 없으므로 모델 학습과 무관합니다. 또, 서로 다른 길이의 문장이여도 token 간 거리가 일정하고, 같은 위치는 항상 동일한 값을 출력합니다.
그러나 transformer가 오래된 논문인만큼 현재는 이런 방식보단 positional encoding으로 Embedding 레이어를 사용하는 방법이 더 많이 사용되고 있다고 합니다. 그러니 이런 방식으로 하기도 하는구나 하고 넘어가면 될 것 같습니다.
Self-attention의 효용성
Self-attention의 효용성을 입증하기 위해, 기존의 RNN, CNN과 3가지 요소를 중심으로 비교를 합니다.
첫번째는 레이어의 계산 복잡도(complexity)입니다. Self-Attention의 계산 복잡도는 $O(n^2d)$이고, RNN의 계산 복잡도는 $O(nd^2)$입니다. 즉, 시퀀스 길이(n)가 임베딩 벡터의 차원 수(d)보다 작을수록 self-attention의 계산 복잡도가 RNN보다 작아지게 됩니다.
두번째는 연산 횟수입니다.(Sequential Operations) 얼마나 병렬적으로 처리 가능한 지를 측정하는 지표입니다. RNN을 제외하고는 모두 $O(1)$로 RNN의 효율이 가장 떨어지는 것을 확인할 수 있습니다.
세번째는 모델이 역전파와 정방향 전파가 도달하는데 필요한 경로의 길이(Maximum Path Length)를 측정합니다. 이 경로의 길이가 길수록 앞선 개요에서 말했듯이 gradient vanishing/exploding 문제가 발생하기 쉽기 때문에 매우 중요한 문제입니다. RNN은 시퀀스를 순차적으로 전부 봐야 하기 때문에 $O(n)$으로 나타나는 것을 확인할 수 있습니다. 그러나 Self-Attention은 $O(1)$로 모든 시퀀스를 한번에 확인할 수 있다는 것을 알 수 있습니다.
결과
Transformer는 English-to-German and English-to-French newstest2014 test 셋에서 BLEU 점수로 SOTA를 달성하였습니다. Transformer의 기여점은 다음과 같습니다.
시퀀스 데이터 = RNN 모델 이라는 공식을 깨고 attention layer만을 사용한 새로운 구조를 제시하였습니다. 이를 통해 순차적으로만 수행되어야 했던 시퀀스 데이터 연산을 병렬적으로 수행할 수 있었고, 뒤로 갈수록 앞의 정보를 손실하는 long-term dependency 문제도 해결할 수 있었습니다.
또 이 논문을 통해 딥러닝의 패러다임이 바뀌게 되었다고 볼 수도 있겠습니다. 모두들 알고 있는 BERT나 GPT도 이 논문을 기반으로 만들어졌고, 이런 attention 기반의 모델은 NLP뿐만 아니라 비전 분야에서도 사용되고 있습니다. 그렇기 때문에 이 논문을 통해 attention이 어떤 레이어이고, 어떤 식으로 사용되는지를 확실히 이해하고 가면 좋을 것 같습니다!
Transformer 전체 코드가 궁금하다면 아래 제가 제작한 colab 노트북을 참고하면 좋을 것 같습니다.
https://colab.research.google.com/drive/1orHb-wiU9vxwotep9Xky5QCVJPzEf5OY?usp=sharing