이번 포스트는 [딥러닝을 이용한 자연어 처리 입문 참조]의 자료를 발췌한 것으로 해당 링크를 통해 더 자세한 내용을 확인하실 수 있습니다.
GOOD EXPRESSION related to Transformer
트랜스포머(Transformer)는 2017년 구글이 발표한 논문인 “Attention is all you need”에서 나온 모델로 기존의 seq2seq의 구조인 인코더-디코더를 따르면서도, 논문의 이름처럼 어텐션(Attention)만으로 구현한 모델입니다.
트랜스포머 주요 파라미터
아래에서 정의하는 하이퍼파라미터는 트랜스포머를 제안한 논문에서 사용한 값으로 목적에 따라 값은 달라질 수 있습니다.
트랜스포머(Transformer)의 구조
트랜스포머는 RNN을 사용하지 않지만 기존의 seq2seq처럼 인코더에서 입력 시퀀스를 입력받고, 디코더에서 출력 시퀀스를 출력하는 인코더-디코더 구조를 유지하고 있습니다. 다만 다른 점은 인코더와 디코더라는 단위가 N개가 존재할 수 있다는 점입니다.
이전 seq2seq 구조에서는 인코더와 디코더에서 각각 하나의 RNN이 t개의 시점(time-step)을 가지는 구조였다면 이번에는 인코더와 디코더라는 단위가 N개로 구성되는 구조입니다. 트랜스포머를 제안한 논문에서는 인코더와 디코더의 개수를 각각 6개를 사용하였습니다.
1. 포지셔널 인코딩(Positional Encoding)
우선 트랜스포머의 입력에 대해서 알아보겠습니다. RNN이 자연어 처리에서 유용했던 이유는 단어의 위치에 따라 단어를 순차적으로 입력받아서 처리하는 RNN의 특성으로 인해 각 단어의 위치 정보(position information)를 가질 수 있다는 점에 있었습니다.
하지만 트랜스포머는 단어 입력을 순차적으로 받는 방식이 아니고 한 번에 문장이 가지고 있는 모든 단어를 받기 떄문에, 단어의 위치 정보를 통해 단어간의 sequential한 정보를 줄 필요가 있습니다. 따라서 트랜스포머는 각 단어의 임베딩 벡터에 위치 정보들을 더하여 모델의 입력으로 사용하는데, 이를 포지셔널 인코딩(positional encoding)이라고 합니다.
임베딩 벡터가 인코더의 입력으로 사용되기 전에 포지셔널 인코딩값이 더해지는 과정을 시각화하면 아래와 같습니다.
트랜스포머는 위치 정보를 가진 값을 만들기 위해서 아래의 두 개의 함수를 사용합니다.
좀 더 엄밀히 얘기하면, 위에서 본 임베딩 벡터와 포지셔널 인코딩의 덧셈은 사실 임베딩 벡터가 모여 만들어진 문장 벡터 행렬
과 포지셔널 인코딩 행렬
의 덧셈 연산을 통해 이루어진다는 점을 이해해야 합니다.
pos는 입력 문장에서의 임베딩 벡터의 위치를 나타내며, i는 임베딩 벡터 내의 차원의 인덱스를 의미합니다. 위의 식에 따르면 임베딩 벡터 내의 각 차원의 인덱스가 짝수인 경우에는 사인 함수의 값을 사용하고 홀수인 경우에는 코사인 함수의 값을 사용합니다. 위의 수식에서 (pos, 2i)일 때는 사인 함수를 사용하고, (pos, 2i+1)일 때는 코사인 함수를 사용하고 있음을 주목합시다.
위와 같은 포지셔널 인코딩 방법을 사용하면 순서 정보가 보존되는데, 예를 들어 각 임베딩 벡터에 포지셔널 인코딩값을 더하면 같은 단어라고 하더라도 문장 내의 위치에 따라서 트랜스포머의 입력으로 들어가는 임베딩 벡터의 값이 달라집니다. 결국 트랜스포머의 입력은 순서 정보가 고려된 임베딩 벡터라고 보면 되겠습니다. 이를 코드로 구현하면 아래와 같습니다.
2. 어텐션(Attention)
트랜스포머에는 총 3가지의 다른 어텐션이 사용됩니다.
셀프 어텐션은 본질적으로 Query, Key, Value가 동일한 경우를 말합니다.
반면, 세번째 그림 인코더-디코더 어텐션에서는 Query가 디코더의 벡터인 반면에 Key와 Value가 인코더의 벡터이므로 셀프 어텐션이라고 부르지 않습니다. 주의할 점은 여기서 Query, Key 등이 같다는 것은 벡터의 값이 같다는 것이 아니라 벡터의 출처가 같다는 의미
입니다.
인코더의 셀프 어텐션 : Query = Key = Value
디코더의 마스크드 셀프 어텐션 : Query = Key = Value
디코더의 인코더-디코더 어텐션 : Query : 디코더 벡터 / Key = Value : 인코더 벡터
위 그림은 트랜스포머의 아키텍처에서 세 가지 어텐션이 각각 어디에서 이루어지는지를 보여줍니다. 세 개의 어텐션에 추가적으로 ‘멀티 헤드’라는 이름이 붙어있습니다. 뒤에서 설명하겠지만, 이는 트랜스포머가 어텐션을 병렬적으로 수행하는 방법을 의미합니다.
3. 인코더(Encoder)
트랜스포머는 하이퍼파라미터인 num_layers 개수의 인코더 층
을 쌓습니다. 논문에서는 총 6개의 인코더 층을 사용하였습니다. 인코더를 하나의 층이라는 개념으로 생각한다면, 하나의 인코더 층은 크게 총 2개의 서브층(sublayer)
으로 나뉘어집니다. 바로 셀프 어텐션과 피드 포워드 신경망입니다. 위의 그림에서는 멀티 헤드 셀프 어텐션
과 포지션 와이즈 피드 포워드 신경망
이라고 적혀있지만, 멀티 헤드 셀프 어텐션은 셀프 어텐션을 병렬적으로 사용하였다는 의미고, 포지션 와이즈 피드 포워드 신경망은 우리가 알고있는 일반적인 피드 포워드 신경망입니다. 우선 셀프 어텐션에 대해서 알아봅시다.
1) 인코더의 셀프 어텐션
a) 셀프 어텐션의 의미와 이점
어텐션 중에서는 셀프 어텐션(self-attention)이라는 것이 있습니다. 단지 어텐션을 자기 자신에게 수행한다는 의미입니다. 이처럼 기존에는 디코더 셀의 은닉 상태가 Q이고 인코더 셀의 은닉 상태가 K라는 점에서 Q와 K가 서로 다른 값을 가지고 있었습니다. 그런데 셀프 어텐션에서는 Q, K, V가 전부 동일합니다. 트랜스포머의 셀프 어텐션에서의 Q, K, V는 아래와 같습니다.
Q : 입력 문장의 모든 단어 벡터들
K : 입력 문장의 모든 단어 벡터들
V : 입력 문장의 모든 단어 벡터들
주의할 점은 여기서 Query, Key 등이 같다는 것은 벡터의 값이 같다는 것이 아니라 벡터의 출처가 같다는 의미
입니다.
셀프 어텐션을 통해 얻을 수 있는 이점은 무엇일까요?
셀프 어텐션은 입력 문장 내의 단어들끼리 유사도를 구하므로서 그것(it)이 동물(animal)과 연관되었을 확률이 높다는 것을 찾아냅니다.
b) 셀프 어텐션의 동작 메커니즘: Q, K, V벡터 얻기
셀프 어텐션은 인코더의 초기 입력인 d_(model)의 차원을 가지는 단어 벡터들을 이용해 Q벡터, K벡터, V벡터를 얻는 작업을 거칩니다. 이때 이 Q벡터, K벡터, V벡터들은 d_(model)보다 더 작은 차원을 가지는데, 논문에서는 d_(model)=512의 차원을 가졌던 각 단어 벡터들을 64의 차원을 가지는 Q벡터, K벡터, V벡터로 변환하였습니다.
64라는 값은 트랜스포머의 또 다른 하이퍼파라미터인 num_heads로 인해 결정되는데, 트랜스포머는 d_(model)을 num_heads로 나눈 값을 각 Q벡터, K벡터, V벡터의 차원으로 결정합니다. 논문에서는 num_heads를 8로하였습니다. 이제 그림을 통해 이해해봅시다. 예를 들어 여기서 사용하고 있는 예문 중 student라는 단어 벡터를 Q, K, V의 벡터로 변환하는 과정을 보겠습니다.
기존의 벡터로부터 더 작은 벡터는 가중치 행렬을 곱하므로서 완성됩니다. 이 가중치 행렬은 훈련 과정에서 학습됩니다.
c) 스케일드 닷-프로덕트 어텐션(Scaled dot-product Attention)
각 Q 벡터는 모든 K 벡터에 대해서 어텐션 스코어를 구하고, 어텐션 스코어를 구한 뒤에 이를 사용하여 모든 V 벡터를 가중합하여 어텐션 값 또는 컨텍스트 벡터를 구하게 됩니다. 그리고 이를 모든 Q 벡터에 대해서 반복합니다.
여기서는 닷-프로덕트 어텐션(dot-product attention)에서 값을 스케일링하는 것을 추가하였다고 하여 스케일드 닷-프로덕트 어텐션(Scaled dot-product Attention)이라고 합니다. 이제 그림을 통해 이해해봅시다.
그런데 한 가지 의문이 남습니다. 굳이 이렇게 각 Q 벡터마다 일일히 따로 연산할 필요가 있을까요?
d) 행렬 연산으로 일괄 처리하기
케일드 닷-프로덕트 어텐션을 수행하였던 위의 과정들을 벡터 연산이 아니라 행렬 연산을 사용하면 일괄 계산이 가능합니다. 우선, 각 단어 벡터마다 일일히 가중치 행렬을 곱하는 것이 아니라 문장 행렬에
가중치 행렬을 곱하여 Q 행렬, K 행렬, V 행렬
을 구합니다. (Q, K, V가 벡터가 아니라 행렬이 되었다.)
이제 행렬 연산을 통해 어텐션 스코어는 어떻게 구할 수 있을까요?
어텐션 스코어 행렬을 구하였다면 남은 것은 어텐션 분포를 구하고, 이를 사용하여 모든 단어에 대한 어텐션 값을 구하는 일입니다. 이는 간단하게 어텐션 스코어 행렬에 소프트맥스 함수를 사용하고, V 행렬을 곱하는 것으로 해결됩니다. 이렇게 되면 각 단어의 어텐션 값을 모두 가지는 어텐션 값 행렬이 결과로 나옵니다.
조금 더 구체적인 예시를 보면 다음과 같습니다.
e) 멀티 헤드 어텐션(Multi-head Attention)
이제 num_heads의 의미와 왜 차원을 축소시킨 벡터로 어텐션을 수행하였는지 이해해 보겠습니다.
트랜스포머 연구진은 한 번의 어텐션을 하는 것보다 여러번의 어텐션을 병렬로 사용하는 것이 더 효과적이라고 판단하였습니다. 그래서 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)’의 연관도를 높게 볼 수 있습니다. 각 어텐션 헤드는 전부 다른 시각에서 보고있기 때문입니다.
병렬 어텐션을 모두 수행하였다면 모든 어텐션 헤드를 연결(concatenate)합니다. 모두 연결된 어텐션 헤드 행렬의 크기는 (seq_len, d_(model))이 됩니다. (결국엔 차원이 유지된다)
어텐션 헤드를 모두 연결한 행렬은 또 다른 가중치 행렬 W_O를 곱하게 되는데, 이렇게 나온 결과 행렬이 멀티-헤드 어텐션의 최종 결과물입니다. 때 결과물인 멀티-헤드 어텐션 행렬은 인코더의 입력이었던 문장 행렬의 (seq_len, d_(model))크기와 동일합니다. 트랜스포머는 다수의 인코더를 쌓기 때문에 행렬의 크기는 계속 유지되어야 합니다.
f) 패딩 마스크(Padding Mask)
아직 설명하지 않은 내용이 있습니다. 앞서 구현한 스케일드 닷 프로덕트 어텐션 함수 내부를 보면 mask라는 값을 인자로 받아서, 이 mask값에다가 -1e9라는 굉장히 큰 음수값을 곱한 후 어텐션 스코어 행렬에 더해주고 있습니다. 이 연산의 정체는 무엇일까요? 이는 입력 문장에 \<PAD> 토큰이 있을 경우 어텐션에서 사실상 제외하기 위한 연산입니다.
트랜스포머에서는 Key의 경우에 <PAD> 토큰이 존재한다면 이에 대해서는 유사도를 구하지 않도록 마스킹(Masking)을 해주기로 했습니다. 여기서 마스킹이란 어텐션에서 제외하기 위해 값을 가린다는 의미입니다. 어텐션 스코어 행렬에서 행에 해당하는 문장은 Query이고, 열에 해당하는 문장은 Key입니다. 그리고 Key에 <PAD>가 있는 경우에는 해당 열 전체를 마스킹을 해줍니다. 어텐션 스코어 행렬이 소프트맥스 함수를 지난 후에는 해당 위치의 값은 0에 굉장히 가까운 값이 되어 단어 간 유사도를 구하는 일에 <PAD> 토큰이 반영되지 않게 됩니다.
이제 두번째 서브층인 포지션-와이즈 피드 포워드 신경망에 대해서 알아보겠습니다.
2) 포지션-와이즈 피드 포워드 신경망(Position-wise FFNN)
지금은 인코더를 설명하고 있지만, 포지션 와이즈 FFNN은 인코더와 디코더에서 공통적으로 가지고 있는 서브층입니다. 포지션-와이즈 FFNN는 쉽게 말하면 완전 연결 FFNN(Fully-connected FFNN)이라고 해석할 수 있습니다. 앞서 인공 신경망은 결국 벡터와 행렬 연산으로 표현될 수 있음을 배웠습니다. 아래는 포지션 와이즈 FFNN의 수식을 보여줍니다.
여기서 x는 앞서 멀티 헤드 어텐션의 결과로 나온 (seq_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_2는 하나의 인코더 층 내에서는 다른 문장, 다른 단어들마다 정확하게 동일하게 사용
됩니다. 하지만 인코더 층마다는 다른 값
을 가집니다.
3) 잔차 연결(Residual connection)과 층 정규화(Layer Normalization)
이제 인코더에 대한 설명은 거의 다왔습니다! 트랜스포머에서는 인코더에서 추가적으로 사용하는 기법이 있는데, 바로 Add & Norm입니다. 더 정확히는 잔차 연결(residual connection)과 층 정규화(layer normalization)를 의미합니다.
위의 그림은 앞서 Position-wise FFNN를 설명할 때 사용한 앞선 그림에서 화살표와 Add & Norm(잔차 연결과 정규화 과정)을 추가한 그림입니다. 추가된 화살표들은 서브층 이전의 입력에서 시작되어 서브층의 출력 부분을 향하고 있는 것에 주목합시다. 추가된 화살표가 어떤 의미를 갖고 있는지는 잔차 연결과 층 정규화를 배우고 나면 이해할 수 있습니다.
a) 잔차 연결(Residual connection)
이를 식으로 표현하면 x + Sublayer(x)라고 할 수 있습니다.
가령, 서브층이 멀티 헤드 어텐션이었다면 잔차 연결 연산은 다음과 같습니다.
b) 층 정규화(Layer Normalization)
잔차 연결 후 층 정규화 연산을 수식으로 표현하자면 다음과 같습니다.
LN = LayerNorm(x + Sublayer(x))
이제 층 정규화를 하는 과정에 대해서 이해해봅시다. 층 정규화는 텐서의 마지막 차원(=d_(model))에 대해서 평균과 분산을 구하고, 이를 가지고 어떤 수식을 통해 값을 정규화하여 학습을 돕습니다. 여기서 텐서의 마지막 차원이란 것은 트랜스포머에서는 d_(model) 차원을 의미합니다. 아래 그림은 d_(model) 차원의 방향을 화살표로 표현하였습니다.
이제 감마와 베타라는 벡터를 준비합니다. 단, 이들의 초기값은 각각 1과 0입니다.
정규화의 최종 수식은 다음과 같으며, 감마와 베타는 학습 가능한 파라미터입니다.
4. 인코더에서 디코더(Decoder)로
지금까지 인코더에 대해서 정리해보았습니다. 이렇게 구현된 인코더는 총 num_layers만큼의 층 연산을 순차적으로 한 후에 마지막 층의 인코더의 출력을
디코더에게 전달
합니다. 인코더 연산이 끝났으므로 이제 디코더 연산이 시작되어 디코더 또한 총 num_layers만큼의 연산
을 하는데, 이때 매번 인코더가 보낸 출력을
각 디코더 층 연산에 사용
합니다. 이제 본격적으로 디코더에 대해서 이해해봅시다.
1) 디코더의 첫번째 서브층 : 셀프 어텐션
위 그림과 같이 디코더도 인코더와 동일하게 임베딩 층과 포지셔널 인코딩을 거친 후의 문장 행렬이 입력됩니다. 트랜스포머 또한
seq2seq와 마찬가지로 Teacher Forcing을 사용하여 훈련되므로
학습 과정에서 디코더는
번역할 문장에 해당되는 \<sos> je suis étudiant의 문장 행렬을 한 번에 입력
받습니다. 그리고 디코더는 이 문장 행렬로부터
각 시점의 단어를 예측하도록 훈련
됩니다.
여기서 문제가 있습니다. seq2seq의 디코더에 사용되는 RNN 계열의 신경망은 입력 단어를 매 시점마다 순차적으로 받으므로 다음 단어 예측에 현재 시점 이전 이전에 입력된 단어들만 참고할 수 있습니다. 반면, 트랜스포머는 문장 행렬로 입력을 한 번에 받으므로
현재 시점의 단어를 예측하고자 할 때
, 입력 문장 행렬로부터 미래 시점의 단어까지도 참고할 수 있는 현상
이 발생합니다. 가령, suis를 예측해야 하는 시점이라고 해봅시다. seq2seq의 디코더라면 현재까지 디코더에 입력된 단어는 <sos>와 je뿐일 것입니다. 반면, 트랜스포머는 이미 문장 행렬로 <sos> je suis étudiant를 입력받았습니다.
이를 위해 트랜스포머의 디코더에서는 현재 시점의 예측에서 현재 시점보다 미래에 있는 단어들을 참고하지 못하도록
룩-어헤드 마스크(look-ahead mask)를 도입
했습니다. 직역하면 ‘미리보기에 대한 마스크’라고 할 수 있습니다.
이제 자기 자신보다 미래에 있는 단어들은 참고하지 못하도록 다음과 같이 마스킹합니다.
트랜스포머에는 총 세 가지 어텐션이 존재하며, 각 어텐션 시 함수에 전달하는 마스킹은 다음과 같습니다.
인코더의 셀프 어텐션: 패딩 마스크
디코더의 셀프 어텐션: 룩-어헤드 마스크
디코더의 인코더-디코더 어텐션: 패딩 마스크
2) 디코더의 두번째 서브층 : 인코더-디코더 어텐션
디코더의 두번째 서브층에 대해서 이해해봅시다. 인코더-디코더 어텐션은 Query가 디코더에서 만들어진 행렬인 반면, Key와 Value는 인코더에서 온 행렬입니다.
디코더의 두번째 서브층을 확대해보면, 다음과 같이 인코더로부터 두 개의 화살표가 그려져 있습니다.
두 개의 화살표는 각각 Key와 Value를 의미하며, 이는 인코더의 마지막 층에서 온 행렬로부터 얻습니다. 반면, Query는 디코더의 첫번째 서브층의 결과 행렬로부터 얻는다는 점이 다릅니다. Query가 디코더 행렬, Key가 인코더 행렬일 때, 어텐션 스코어 행렬을 구하는 과정은 다음과 같습니다.
인코더와 마찬가지로 디코더도 num_layers개만큼 쌓아주면 트랜스포머 모델이 완성됩니다.
5. Inference
6. 참조
[DSBA연구실 김동화님의 Transformer & BERT 유튜브 강의]