Introduction

트랜스포머는 구글이 발표한 Attention is all you need에서 제안된 모델이다. Self attention 매커니즘을 제안하여 입력데이터 간의 상호작용을 고려한 표현을 고려할 수 있게 해준다. 해당 모델은 차후 GPT와 BERT의 근간이 었고 2025년 현 시점까지 굉장히 큰 영향력을 미친 논문이다. 인공지능을 공부한다면 반드시 공부를 해야하는 모델이고, 처음 봤을때는 낯설게 느껴지는게 사실이지만 모델을 천천히 뜯어보면 트랜스포머의 철학과 원리에 대해 이해할 수 있다.

Background

트랜스포머의 논문에서는 번역하는 것을 주요 task로 설명하고 있다. 기존 모델의 경우 Seq2Seq + RNN(LSTM)을 통한 기계번역을 하는 것이 일반적이었다. 하지만 RNN 계열의 모델의 경우 순차적으로 데이터를 넣어야하므로 분산처리(병렬화)를 하는데 어려움이 있었고, long term dependency problem에서 자유롭지 못했다.

Transformer

그래서 나온게 바로 트랜스포머이다. 트랜스포머의 아키텍처는 다음과 같이 구성되어 있다. 간단하게 보면 Seq2Seq의 인코더 디코더 형태를 띄고 있으며 양옆에 Nx 라 적혀 있는 것을 통해 여러개의 인코더와 디코더가 쌓일수 있는 구조로 되어있다.

여기서 눈여겨봐야할 모듈들이 여럿있다. 이에 대해 설명하기 앞서 어떤 모듈에 대해 알아 볼 것인지 미리 정리해보자.

  1. Embedding
  2. Positional Encoding
  3. Multi-head Attention
  4. Add & Norm
  5. Feed Foward
  6. Mask Multi-head Attention

이렇게 크게보면 6개의 모듈에 대해서 순서대로 이해해보려고 한다.

1. Embedding

Input과 Output에서 공통적으로 적용하는 임베딩 단계이다. 해당 단계에서는 토큰화와 임베딩 단계로 크게 2가지 단계를 통해 이루어진다.

토큰화(Tokenization)

해당 단계에서는 문장을 토큰의 형태로 분할한다. BPE, WordPiece, SentencePiece와 같은 토크나이저를 통해 문장을 작은 단위로 분할한다.

예시 문장 : “I love NLP”

WordPiece 토크나이저를 통해 분할 시키면 아래와 같이 분할된다.

["I", "love", "NL", "##P", "."]

그러면 이제 분할된 토큰을 사전에 정의된 단어 사전(Vocabulary)에서 해당 토큰을 Vocabulary ID로 변환한다.

["I" → 101, "love" → 2057, "NL" → 4001, "##P" → 4569, "." → 102]

[101, 2057, 4001, 4569, 102]

임베딩

이제 토큰화된 단어 ID를 임베딩 행렬을 이용해 임베딩 벡터로 변환한다. 이때 임베딩 행렬은 Word2vec과는 적용방식이 다르다. Word2vec에서는 원래의 문장을 원핫인코딩을 진행한 후, 그 결과에 임베딩 행렬을 곱한다. 하지만 해당 방식은 효율적이지 못하고, 매번 임베딩 행렬을 곱하면서 발생하는 오버헤드가 있다.

그래서 트랜스포머의 임베딩을 할때는 임베딩 행렬의 행을 가져오는 방식을 이용한다. 여기서 임베딩 행렬은 V(Vocabulary size) x d_model(임베딩 차원) 이며, 각 어휘에 대한 임베딩 벡터가 행렬 형태로 구성되어 있어 그 토큰 ID에 해당하는 행을 가져와 임베딩벡터를 가져오는 방식으로 진행된다. 임베딩 행렬 같은경우 학습시 값이 계속 수정된다.

예시(d_model = 4라 가정)

101   →  [0.1, 0.3, 0.7, 0.5]  
2057  →  [0.2, 0.1, 0.8, 0.6]  
4001  →  [0.5, 0.2, 0.1, 0.9]  
4569  →  [0.7, 0.3, 0.6, 0.4]  
102   →  [0.4, 0.8, 0.2, 0.7]

따라서 결과적으로 해당 행렬값이 위 문장을 임베딩한 결과가 된다.

2. Positional Encoding

위와 같은 방식으로 문장을 임베딩을 할 경우엔 문장에서 단어의 순서에 대한 정보가 손실된다. 왜냐하면 토큰화 $\rightarrow$ 토큰화된 단어를 임베딩 벡터로 변환되었기 때문이다. 이때 트랜스포머는 Self-Attention을 사용해 병렬적으로 입력되기에 입력 시퀀스가 동시에 처리되어 토큰끼리의 순서 정보가 유실된다. 따라서 트랜스포머에서는 포지셔널 인코딩을 통해 단어 순서에 따른 정보를 넣어준다.

\[PE(pos, k) = \begin{cases}  \sin\left(\frac{pos}{10000^{\frac{k}{d_{\text{model}}}}}\right), & \text{if } k \text{ is even} \\[1ex] \cos\left(\frac{pos}{10000^{\frac{k-1}{d_{\text{model}}}}}\right), & \text{if } k \text{ is odd} \end{cases}\]
  • $pos$ : 시퀀스에서의 토큰 위치 (0, 1, 2, …)
  • $k$ : 임베딩 차원의 인덱스
  • $d_{\text{model}}$ : 전체 임베딩 차원 (예: 512, 768 등)
  • 10000 : 스케일링을 위한 고정 상수

직관적으로 이해가 바로 되지는 않는 수식이다. 이는 삼각함수를 이용한 포지셔널 인코딩으로 sinusoidal positional encoding이다. 해당 방법은 같은 포지션끼리 가까우면 내적이 크고, 멀면 내적이 작아져서 거리관계를 대략 유추해낼 수 있는 방법이기 때문에 사용한다. 자세하게 해당 내용에 대해 알고 싶다면 positional encoding,한글 정리다음 내용을 참고해보자.

해당 figure는 포지셔널 인코딩을 적용하는 예시이다. 다음과 같이 토큰 index k임베딩 차원 index i 2가지를 통해서 인코딩 값이 결정되는 것을 알 수 있다. 즉 이 방식을 통해 문장에서 해당 토큰의 순서 정보를 부여할 수 있다.

그리하여 위에서 구한 embedding 벡터와 positional encoding을 한 벡터 값을 합쳐지면, 포지셔널 인코딩 레이어까지의 작업이 완료된다.

Self Attention

Figure상으로 트랜스포머의 다음 레이어는 multi-head attention이지만 이를 이해하기 위해선 self-attention 개념부터 알아야한다. 트랜스포머의 핵심개념인 만큼 매우 중요하므로 차근차근 이해해보자.

\[\text{Attention}(Q, K, V) = \text{softmax} \left( \frac{QK^T}{\sqrt{d_k}} \right) V\]

위 figure와 수식은 같은 것을 의미한다. 둘중 이해하기 쉬운 것으로 이해하면 된다. 해당 연산은 self attention이라고도 불리지만 Scaled Dot-Product Attention라고도 불린다. 정말 직관적으로 잘 만든 수식이라 생각된다.

여기서 위에서 만들어진 임베딩 행렬을 Q, K, V로 나누어 연산을 하게 된다. 즉 하나의 input embadding을 3가지로 나누어서 사용한다. 여기서 역할은 다음과 같다.

  • Query (Q) : 현재 토큰이 무엇을 찾는지를 표현하고 있음
  • Key (K) : 토큰들이 가진 feature를 의미함
  • Value (V) : 최종적으로 전달되는 실제 정보가 담긴 벡터
\[Q = X W_Q, \quad K = X W_K, \quad V = X W_V\]

이때 학습이 가능한 가중치 행렬을 통해 입력된 임베딩된 벡터가 변환된다.

여기서 제일 먼저 하는 연산은 $QK^T$ 연산이다. 해당 연산은 Query와 Key의 내적으로 각 토큰간의 유사도를 계산한다. 내적값이므로 두값이 유사할수록 큰 값을 갖게 될 것이다. 이 값을 Attention score이라고 하며, scaled된 값을 이렇게 부르기도 한다.

두번째 연산은 $\sqrt{d_k}$ 로 위의 결과를 나눠주는 것이다. 이는 내적값의 scale를 조절해 다음 연산인 softmax값이 한쪽에 너무 치우치지 않도록 하는 것이다.

세번째 연산으로 위 결과에 Softmax를 적용한다. 스케일된 내적값에 softmax함수를 적용하면 두 단어간의 유사도를 확률 분포로 나타낼 수 있다. 해당 단계에서는 특정 단어가 다른 단어에 대한 집중정도, 즉 다른 단어에 얼마나 주의를 기울여야하는지를 나타낸다. 여기서 나오는 값을 Attention Weights라 한다.

마지막으로 하는 연산은 Attention Weights와 Value를 곱해주는 것이다. 이를 통해 해당 단어가 다른 단어에 얼마나 주의를 기울여야하는지 정보와 더불어, 실제 문맥정보를 종합한 벡터값을 얻을 수 있다. 세번째 연산의 결과가 m x m 행렬이므로 Square Matrix이기때문에 최종 행렬연산의 결과는 처음 임베딩 행렬의 크기와 같다.

위 이미지가 직관적으로 해당 과정을 표현한것 같아 가져왔다.

3. Multi-head Attention

이제 Self Attention을 이해했으니, Multi-head Attention에 대해 알아보자. 해당 연산은 Q, K, V를 h개의 헤드로 나누어 연산을 진행한다. 위에서 설명한 Q, K, V는 $d_\text{model}$의 차원을 갖고 있다. 이를 h로 나눠 작은 차원으로 쪼개는 것이다. 즉 여기서는 원본보다 더 작은 차원을 사용해 각 헤드에서 병렬적으로 self attention을 수행한다. 그리고 독립적으로 헤드에서 연산한 attention 값들을 그대로 연결하여 하나의 큰 벡터로 변환한다.

해당 연상을 통해 작은 차원으로 분할하여 연산 비용이 분산되는 효과가 있어서 연산 비용에서 이점을 볼 수 있다. 또한 단일 attention의 경우 결과가 평균되어버리거나 세부정보가 희석될 가능성이 있는데, 나눠서 처리하다보니 다른 관점에서 얻은 정보를 종합하는 것이므로 표현력이 보다 풍부해지는 효과가 있다. 즉 연산은 최적화하면서 더 다양한 다양한 정보를 학습할 수 있으므로 성능은 더 좋아지는 것이다.

4. Add & Norm

다음은 Add & Norm 레이어입니다. 굉장히 직관적으로 이름을 지었는데 좀 더 풀어서 설명하면 Residual Connection(Skip Connection)과 Layer Nomalization을 하는 단계입니다. Residual Connection의 경우 ResNet 포스팅을 참고해주시면 됩니다. 이전 레이어의 입력 $x$를 self attention 혹은 feedforward를 적용한 결과에 더해줍니다. 이를 통해 ResNet에서의 효과인 Gradient 흐름이 원할해지는 효과를 얻습니다.

다음은 Layer Nomalization을 진행한다. 이는 internal covariate shift를 일정하게 유지하고 vanishing gradinet problem을 완화하기 위해 사용한다. 이는 batch nomalization과 비슷하지만, 하나의 미니 배치내의 샘플이 아닌 각 개별 샘플에 정규화를 진행한다.

5. Feed Foward

\[\text{FFN}(x) = \max(0,\, xW_1 + b_1)W_2 + b_2\]

FFN(Feed-Forward Network)는 두개의 선형변환 사이에 non-linearity 즉 활성화 함수인 ReLU를 넣은 구조를 의미한다. 이때 선형변환은 Fully Connected Layer로 볼 수 있기 때문에 다시한번 요약해서 말하면 FC Layer $\rightarrow$ ReLU $\rightarrow$ FC Layer를 진행하는 것이다.

6. Mask Multi-head Attention

다음은 Mask Multi-head attention입니다. 사실 전에 있는 Multi-head Attention과 동일하게 작동하는데 Mask가 적용된다는 차이가 있습니다. 왜 그렇다면 디코더에서 Mask를 적용하는걸까요? 그 이유는 단순합니다. Self-attention은 자신을 포함해 미래의 값과는 Attention을 구하면 안되기 때문입니다.

예를 들어 ‘나는 너를 사랑해’라는 문장을 ‘I love you’라는 문장으로 바꿔야하는 상황일 때, 인코더에는 나는 너를 사랑해라는 문장이 그대로 넣어서 번역하기 때문에 전체 문장간의 관계를 학습해야합니다. 하지만 ‘I love you’의 경우는 Seq2Seq에서 그렇듯 <SOS>부터 시작해 ‘I love you’의 단어가 하나씩 그리고 <EOS>가 순차적으로 생성됩니다. 이때 현재 ‘<SOS> I’까지 생성된 상황에서 디코더는 ‘love you <EOS>‘라는 이제 생성해야 할 뒷부분을 봐서도, 그 간의 관계를 학습해서도 안됩니다. 그렇기 때문에 현재 생성된 시퀀스 뒷쪽 부분은 Mask로 가려 학습하지 못하게 하는거죠.

요약하자면 디코더에서 Self Attention은 미래의 단어의 정보를 참조해서는 안됩니다. 그래서 마스크를 통해서 현재 시점 이후의 토큰들은 Attention 연산을 하지 않도록 막아주는겁니다.

이를 시각화해서 보면 다음과 같습니다. 행쪽이 Q 열쪽이 K를 나타내며, 왼쪽위를 보면 현시점 이후의 단어들은 검정색으로 마스킹하여 하나의 단어만 시각화해서 볼 수 있도록 해줍니다.

정리

이제 아키텍처 그림을 다시한번 봅시다.

  • 임베딩 & 포지셔널 인코딩 : 해당 단계에서 문장을 토큰으로 변환하고 임베딩을 통해 의미 있는 벡터로, 그리고 포지셔널 인코딩을 통해 순서 정보를 보완해줍니다.
  • Multi-head Attention : 임베딩 벡터를 Q,K,V로 분리해 토큰간 상호작용을 계산합니다. 이때 여러개의 헤드로 나누어 병렬적으로 처리해 다양한 관점에서 정보를 포착해 다시 하나의 벡터로 결합합니다.
  • Add & Norm : Skip Connection과 Layer Nomalization을 진행해줍니다.
  • Feed Forward Network (FFN) : 두번의 FC와 ReLU를 통해 표현력을 더 올려줍니다.
  • Mask Multi-head Attention (디코더) : 디코더에서는 미래의 정보를 참조하지 못하도록 마스킹해 올바른 생성을 할 수 있도록 합니다.

또한 내용이 많지 않아 따로 다루진 않았던 부분에 대해서 간단히 언급하면

  • 디코더의 2번째 Multi-head Attention : 인코더에서 생성된 출력과 디코더의 출력을 상호작용하는 부분으로 인코더의 출력을 Key와 Value로 디코더의 출력을 Query로 하여 디코더가 입력 문장의 전체정보를 참조할 수 있도록 합니다.
  • 출력 부분 : 디코더의 마지막부분은 Linear + Softmax로 구성되어 마지막 Softmax가 Vocabulary의 확률을 알려줍니다. 여기서 가장 높은 값을 가진 단어가 출력되며, 이를 반복하며 문장이 생성되게 됩니다.

마지막으로 트랜스포머가 어떻게 작동하는지를 시각화한 움짤이다.

첫번째는 인코더 부분이다

마지막으로 디코더 부분이다.