[딥러닝] Transformer 정리
- Minwu Kim
- 2024년 9월 5일
- 6분 분량
참고자료:
하나. 문제의식
Transformer는 시퀀스 모델이다. 시퀀스라고 함은, 데이터 간의 선후관계, 내지는 위치 상의 관계가 있는 모든 데이터 종류를 의미한다. 대표적인 예시로는 자연어가 될 수 있을 것이고, 픽셀간의 상대적 위치에 따라 정보가 결정되는 이미지, 혹은 강화학습시 (s,a,r)로 이뤄지는 trajectory도 시퀀스가 될 수 있을 것이다. Transformer 구조를 처음으로 제시한 Attention is All You Need는 기계 번역을 예시로 들었으나, 그 후 Transformer는 비단 자연어 뿐만이 아닌 각종 컨텍스트에서 사용되었고, 또 좋은 성능을 보여줬다.

트랜스포머가 나오기 전 SOTA였던 시퀀스 모델은 seq2seq였다. 하지만 이 모델에는 치명적 단점이 두가지 있었으니, 바로 아래와 같다:
context vector에 모든 정보가 압축되어, 인코딩 된 vector representation에 정보 손실이 크다는 점.
sequential model인지라 학습 속도가 느리다는 점
첫번째 문제를 해결하기 위해 attention을 접목시킨 seq2seq이 개발되었다 (attention이 뭔지는 뒤에 가서 보다 자세히 설명을 하도록 하겠다). 즉, 모든 정보를 제한된 context vector에 압축할 것이 아니라, 매 time step마다의 출력값을 모두 가져와 디코더에 가져다 주는 방식이다.
하지만 여전히 두번째 문제, 즉 병렬처리가 불가하다는 점은 해결되지 않았다. 왜냐, seq2seq 모델은 RNN의 구조를 사용하기 때문이다. 그래서 Attention is All You Need 논문에선, RNN 같은 구조도 포기하고 어텐션만 사용하는 방법론을 제시한다. 그게 바로 transformer이다.
둘. 개괄적인 구조 파악.
Transformer도 전형적인 encoder-decoder 구조로 만들어져있다. 기계 번역을 생각했을 때 (불-영), 인코더에는 원문인 불어 문장을 벡터형태로 인코딩한 정보가 주입되며, 디코더에서는 target이 되는 영문 정보가 들어가게 된다. 추후 transformer에서의 다양한 활용 중, 인코더와 디코더에 각각 어떤 것이 input으로 주어지는지를 항상 살펴볼 필요가 있다.
참고로, transformer를 활용한다고 해서 무조건 인코더-디코더 구조를 띄는 것은 아니다. BERT의 경우 인코더만을 사용하고, GPT는 디코더만을 사용한다. 다만 이 모든 변주와 활용을 알기 위해선 이 기본적인 transformer 구조를 알아야 한다.
트랜스포머를 보면, 인코더와 디코더 모두 각각 6개의 레이어로 구성되어있다. 이 레이어 갯수는 하이퍼파라미터이니 큰 의미는 없다. 다만 주의할 점은, 나중에도 설명하겠지만, encoder-decoder attention(cross-attention)을 진행시, 인코더의 벡터를 디코더에 넘겨주어야 하는데, 모든 디코더 레이어에 마지막 인코더 레이어의 출력값을 주는 것을 볼 수 있다. 이게 상식적으로 당연한 것이, 최종 인코더 레이어에 도달을 한 인코딩이야 말로 토큰 간의 관계성에 대한 정보를 가장 잘 담아내고 있기 때문에, 소위 말해 가장 엑기스를 디코더에 넘겨줘야 하는 것이다.
위는 논문 원문의 참고도인데, 내부 디테일은 뒤에 가서 설명하도록 하겠다.
셋. Attention에 대해.
Attention을 설명하기에 앞서, 3가지 Preliminaries를 짚고 넘어가기로 한다.
특정 벡터가 linear transformation을 해도, 그 정보는 보존된다.
Weighted Sum: 가중치를 곱한 합은, 각 파편마다의 정보를 가중치만큼 보존한다.
Inner product: 비슷한 방향의 벡터의 내적은 값이 보다 양적이며, 반대방향을 음, 무관계 (orthogonal)하면 0으로 수렴한다.
그 다음 attention, 그 중에서도 인코더에 사용되는 self-attention을 알아보도록 한다.
해당 예시를 보면, 원문이 "Thinking Machines"이다. "Thinking"과 "Machines" 이 두가지 토큰으로 구성되어 있다.
첫째로, 각 단어에 대한 embedding vector를 구한다. 이 embedding은 pre-trained된 임베딩을 사용해도 되고, 랜덤한 값으로 초기화를 하고 파라미터를 조정해도 된다. 중요한 점은 이게 trainable parameter라는 점이다.
둘째로, Wq, Wk, Wv 매트릭스를 설정하고, 임베딩을 linear transformation를 해준다.
셋째로, 위와 같은 공식으로 어텐션 계산을 수행해준다. 여기서 주의해야할 디테일이 몇가지 있다:
하나, Query와 Key를 내적한다. 앞서 얘기했듯, 서로 연관성이 큰 단어들의 벡터의 내적은 양의 성질을 띄게 된다. 이런 단어들은 Corpus에서 계속해서 같이 나올 가능성이 많기에, 학습을 하며 자연스럽게 큰 양수의 값을 가질 것이다.
둘, d_k의 제곱근 만큼 나누어 normalize해준다. d_k가 클수록 내적 값이 커진다. 내적 값이 커지면 softmax를 진행할 때 극단적으로 크거나 작은 값으로 쏠릴 가능성이 크다 (exponential function이다). 그리고 softmax의 양 끝단의 기울기는 0에 가깝기에 gradient vanishing 문제가 생길 수가 있다.
셋, softmax를 진행해, value에 대한 weighted sum을 해준다.
넷, 실제 계산을 할 때는 각 query마다 순차적으로 계산을 진행하는 것이 아닌, matrix multiplication 형태로 병렬적으로 계산할 수 있다.
다섯, 최종 z의 shape은 처음 embedding의 그것과 동일해야한다. 그래야만 인코더 레이어를 하나 둘 쌓아올릴 수가 있다.
추가로, Attention is All You Need 논문에서는 padding mask를 추가하는데, softmax를 실행하기 전 <PAD>의 key를 아주 작은 음수로 대체한다.
주의: query에 대해서는 padding mask를 따로 추가하지 않는다. ChatGPT를 물어보니 아래와 같은 설명을 줬다:
The queries (Q) are not masked because:
Role of Queries: The query vectors represent the positions we are currently evaluating or "querying." Each position's query vector is used to calculate its attention distribution over all keys.
Independence of Queries: Queries should not be masked because every token, whether it's a real word or a padding token, needs to produce a query. The masking is about controlling where the query can look (i.e., which keys it can attend to), not about whether the query itself should exist.
넷. multi-head attention에 대해
멀티헤드 어탠션의 요지는 간단하다. (Q,K,V) triplet을 한 가지만 사용하는 것이 아닌, 여러개를 사용 후 concatenate하고 다시 원래 디멘션으로 LT를 해주면 되는 것이다. (Q,K,V) triplet를 여러가지 사용하는 이유는, 직관적으로 이해해보자면, 각 triplet마다 단어의 각기 다른 관계성을 잡아낸다고 생각해도 좋을 듯 하다.
위 그림을 보면, 각 attention head에서 구한 z를 concatenate하고, 그 후 Wo로 linear transformation 하여 차원을 축소한다. 주의할 것이, LT 이후의 최종 아웃풋은 초기 인풋과 같은 dimension을 공유한다. Input과 output의 차원이 일정해야만 여러 레이어를 쌓아올릴 수 있기 때문이다.
다섯. 인코더 구조 파헤치기
인코더의 구조는 위의 그림과도 같다. 해부하자면 아래와 같다:
각 토큰 마다 임베딩을 구한다.
Positional Encoding을 더해 위치 정보를 추가한다.
Residual connection + layer normalization을 통해 학습을 개선시킨다.
구해진 z의 열마다 각각의 FFNN이 주입한다. 이는 non-linearity를 추가하기 위함이다. 종전의 모든 계산은 선형적이었기에, 비선형적 복잡성을 캡쳐하기 어렵다.
FFNN을 통과한 결과값을 다시 residual connection + layer normalization을 통해 학습을 개선시킨다.
5.1. Positional Encoding
Seq2seq 같은 경우, 순차적인 모델이기에, 자연스럽게 위치 정보가 포함된다. 하지만 transformer의 경우 병렬적인 처리를 하기에, 위치에 대한 정보를 따로 주입해줘야 한다. 이를 수행하는 것이 바로 positional encoding이다.
Positional Encoding의 수식은 아래와 같다. 토큰의 위치마다 다른 삼각함수 값을 갖게 되며, 이를 기존의 embedding에 더해주는 식으로 위치정보를 주입하는 것이다.
보면 알겠지만, positional encoding은 첫번째 레이어 진입 전에만 적용되며, 나머지 stacked layer에서는 다시 추가되지 않는다. 위치 정보는 초반에 한 번만 넣어주면 충분하기 때문이다.
5.2. Residual Connection + Layer normalization
이 부분은 다소 사소한 디테일이지만, 그래도 짚고 넘어간다. 첫째로, residual connection은 기존 x값을 다시 주입해주는 식으로 정보 손실을 줄이는 것이라고 볼 수 있다. 둘째로, layer normalization에 대해 조금 짚고 넘어가자면, batch와 달리 하나의 백터에서 mean과 std를 구하고 정규화 하는 것으로 볼 수 있다.
5.3. FFNN
비선형성을 추가해주는 파트이다. 다만 주의할 한 가지는 포지션마다 독립적으로 FFNN을 수행한다는 것이다. 다만 헷갈리면 안 되는 것이, Feed-forwarding을 각 위치마다 독립적으로 수행한다고 해서 각기 다른 FFNN을 사용하는 것은 아니다. 그냥 같은 NN을 쓰지만 병렬처리한다, 그 뿐이다. 그냥 논문 상 병렬처리에 대한 부분을 강조하고 싶어 그렇게 한 것으로 보인다.
여섯. 디코더 구조 파헤치기.
디코더의 구조는 위와 같다 (오른쪽만 보자). 인코더와 유사한 점이 많은데, 가장 큰 차이는 아래와 같은 두가지가 있다:
Masked self-attention: look-ahead mask를 사용하여, 아직 주어지지 않은 뒷단의 정보를 참조하지 않도록 한다.
Encoder-decoder attention: 디코더에게 인코더의 정보를 넘겨준다.
6.1. Masked self-attention
Transformer가 기계를 번역하는 방식은 좌에서 우로 번역하는 방식이다. 아래와 같은 예시를 보자:
I like kimchi -> 나는 김치가 좋아.
그럼 번역을 하는 경우, "나는 -> 김치가 -> 좋아" 하는 방식으로 번역이 될 것이다. 그럼 "김치가"라는 부분을 번역할 때, 앞에 "나는"이라는 정보는 주어졌지만, "좋아"라는 정보는 주어지지 않았다. 고로, 직관적으로 생각했을 때, 잘 작동하는 번역 추론기를 만드려면 뒷 부분의 정답을 미리 참조하게 해서는 안 된다. 다른 말로 하자면, 추후 학습이 완료 된 후 추론을 할 때
이를 해결하기 위해 "look-ahead masking"을 더한다. 아래의 그림을 참고하면 좋다.
각 행이 query이고, 각 열이 key이다. 각 쿼리마다 자신 뒤의 키는 엑세스를 할 수 없다.
6.2. encoder-decoder attention
앞선 attention은 self-attention, 즉 자신들끼리의 상관관계를 참조한 것이었다. 하지만 Encoder-decoder는 target의 토큰 간의 관계가 아닌, encoder에서 구한 값 z와의 관계성을 캡쳐하는 과정이다.
위의 그림을 보면, decoder의 Q를 사용하고, encoder의 K와 V를 사용한다. 이를 통해 인코더의 정보를 디코더에 주입을 한다. 상당히 추상적이지만, 그냥 "정보를 주입한다"는 개념만 세워져있으면 된다.
여기서 K와 V는 마지막 레이어의 K,V를 쓰는 것이 아니라, z를 또 한 번 linearly transform한 것이라고 볼 수 있다.
다시 번역과정 이야기를 해보자. "I like kimchi" 번역 시, 저 영문 원문은 항상 존재하는 것이다. 고로 따로 마스킹을 해줄 필요가 없다.
7. 마지막 linear & softmax layer + loss function
디코더 역시 각 위치마다 z를 뱉어줄 것인데, 이제 이를 통해 다음 단어를 뽑아줘야 한다. 해당 과정은 아래와 같다.
일단 linear transformation을 통해 logit array를 구해준다. 여기서 array의 길이는 토큰갯수와 동일해야 하며, 각 element는 각 토큰에 해당한다. 그 다음 softmax를 통해 이를 확률분포로 변환시킨다.
Loss function은 cross entropy나 KL divergence이다.
8. 추론 과정
추론의 경우, 오로지 인코더에 인풋이 들어가며, 디코더에는 <SOS> 토큰만이 주어진다. 그리고 token-by-token의 형식으로 추론을 하는 것이다. <EOS> 토큰이 나오면 추론을 멈춘다.
추론 과정시 top p, 즉 확률 값이 가장 높은 몇가지만을 보고 샘플링을 할 수 있으며, temperature를 조정하여 확률 분포를 smoothing 시킬 수 있다.
9. 정리
문제 의식: Seq2seq는 context vector에 모든 정보를 욱여넣기에 정보 손실이 크며, sequential operation이기에 비효율적이다.
해결방안: Attention을 사용하여 정보 손실을 줄이고, 병렬처리를 통해 효율성을 높인다.
인코더에서는 self-attention을 사용해 단어 간의 관계성을 파악하고, positional encoding을 통해 위치 정보를 집어넣고, FFNN을 통해 비선형성을 파악한다.
디코더에서는 masked self-attention을 통해 단어 간의 관계성을 파악하고 (look-ahead masking을 통해 치팅 방지), cross attention을 통해 인코더로부터의 정보를 흡수한다.
마지막 linear + softmax 레이어를 통해 확률분포를 구한다, 그리고 cross entropy나 KL divergence를 손실함수로 정의하여 학습시킨다.
추론 할 때 디코더에는 <SOS> 토큰 만을 넣고, recursive한 방식으로 추론을 진행한다.
끄읕.
Comments