[딥러닝] Transformer(트랜스포머) 이론 및 pytorch 코드

지수토리·2024년 1월 8일

딥러닝

목록 보기
2/2

논문: https://arxiv.org/abs/1706.03762

1. 등장 배경

👿 기존 seq2seq 모델의 문제점

기존의 seq2seq 모델은 인코더-디코더로 구성됨.

  • 인코더: 입력 시퀀스를 하나의 벡터 표현으로 압축.
  • 디코더: 백터 표현을 통해서 출력 시퀀스 만듦.

But, 입력 시퀀스를 하나의 벡터 표현으로 압축하는 과정에서 "입력 시퀀스의 정보가 일부 손실".
이를 보정하기 위해 어텐션 등장했고, 어텐션만으로 인코더와 디코더를 표현한 것이 Transformer 모델임!

2. Transformer 요약 설명

  • RNN이나 CNN을 전혀 필요로 하지 않다. -> 대신 Positional Encoding을 사용하여 순서에 대한 정보를 입력한다.
  • 인코더와 디코더로 구성 -> Attention 과정을 여러 레이어에서 반복한다.
  • self-attention이 적용되어, 각각의 단어가 서로에게 어떤 연관성을 가지고 있는지 알 수 있다.
  • 성능 향상을 위해 residual learning 사용. 모델 수렴 속도 up

3. Encoder & Decoder 상세 설명

🐥 Encoder

임베딩 층과 포지셔널 인코딩을 거친 후의 문장 행렬이 입력.

num_layers만큼 인코더의 층을 쌓고, 논문에서는 총 6개의 인코더 층을 사용.
인코더를 하나의 층이라는 개념으로 생각한다면, 하나의 인코더 층은 크게 총 2개의 서브층(sublayer)으로 나뉘어짐.

  • 셀프 어텐션(Self-Attention)
  • 피드 포워드 신경망 (Position-wise FFNN)

📌 Positional Encoding

트랜스포머는 RNN과 달리, 단어를 순차적으로 입력 받는 것이 아니라 병렬적으로 입력 받는다.
그러면, 어떻게 단어의 위치 정보(positional information)를 알려주는가?

각 단어의 임베딩 벡터에 위치 정보들을 더하여 모델의 입력으로 사용한다! -> positional encoding

아래는 embedding vector가 인코더 입력으로 사용되기 전 positional encoding 값과 더해지는 과정을 시각화한 것.

=> embedding vector + positional encoding = 트랜스포머 인코더의 입력!

이때, positional encoding 값은 사인함수와 코사인함수 로 구성된다!

  • pos: 입력 문장에서 embedding vector의 위치
  • i: embedding vector내의 차원의 인덱스
    i가 짝수인 경우는 sin함수를 i가 홀수인 경우는 cos함수를 사용!
  • d model: 트랜스포머 모든 층의 출력 차원 -> 눈문에서는 512개

따라서 위와 같은 positional encoding은 순서 정보를 보존하게한다.

📌 Transformer의 Attention

트랜스포머에 존재하는 세가지 어텐션 기법.

  1. Encoder Self-Attention: Encoder, Query = Key = Value
  2. Masked Decoder Self-Attention: Decoder, Query = Key = Value
  3. Encoder-Decoder Attention: Decoder, Query = 디코더 벡터 / Key = Value = 인코더 벡터
  • 참고로 벡터 값이 같다는 것이 아니라 벡터의 출처가 같다는 것.

위 그림은 트랜스포머 아키텍처에서 세가지 어텐션이 각각 어디에서 이루어지는지를 보여주는 것.
여기서 multi-head란 트랜스포머가 어텐션을 병렬적으로 수행하는 방법.

  • Multi-head Self-Attention => 1. Encoder Self-Attention
  • Masked Multi-head Self-Attention => 2. Masked Decoder Self-Attention
  • Multi-head Attention => 3. Encoder-Decoder Attention

📌 Self-Attention

기존 어텐션 함수 동작 과정
1. 주어진 Query에 대한 모든 Key와의 유사도를 각각 구함.
2. 유사도를 가중치로 하여 키와 맵핑 되어있는 각각의 '값(Value)'에 반영.
3. '값(Value)'을 모두 가중합하여 리턴.

  • Querys = 모든 시점의 디코더 셀에서의 은닉 상태들
  • Keys, Values = 모든 시점의 인코더 셀에서의 은닉 상태들

그럼, self-attention은 어떻게 동작할까?

1) Q, K, V 벡터

d model 차원을 가지는 입력 문장의 단어 벡터를 바로 사용하는 것이 아니라, 우선 각 단어 벡터들로부터 Q 벡터, K 벡터, V 벡터를 얻는다.

이때, Q,K,V 벡터의 차원(논문에서는 64 = d model(512) / num_heads(8) ) < d model의 차원 (논문에서는 512)

각 가중치 행렬은 d model x (d model / num_heads) 크기를 가짐.

2) Scaled dot-product Attention

  1. Attention Score 구하기
    여기서 n = Q,K,V 벡터의 차원(논문에서는 64 = d model(512) / num_heads(8) )
    그래서 루트 n = 8
  2. Attention Distribution 구하기
    attention score 모음 값에 softmax를 적용하여, 어텐션 가중치의 모음 값을 구함.
  3. Attention Value 구하기
    각 인코더의 은닉상태와 어텐션 가중치 값들을 곱하고, 모두 더함. => 가중합(weighted sum) => context vector라고도 함.

위 그림은 "단어 I"에 대한 어텐션이 적용된 것이고, "am"에 대한 Q벡터, "a"에 대 Q벡터, "student"에 대한 Q벡터에 대해서도 모두 동일한 과정을 반복하여 각각에 대한 어텐션 값을 구한다. 그런데 굳이 이렇게 각 Q벡터마다 일일히 따로 연산을 하는가?

따라서, 위의 벡터 연산들을 아래처럼 행렬 연산으로 일괄 처리할 수 있다.

문장 행렬에 가중치 행렬을 곱하여, Q행렬, K행렬, V행렬을 구한다. Q행렬을 K행렬을 전치한 행렬 곱하면, 각각의 단어의 Q벡터와 K벡터의 내적 행렬이 됨. 위 결과 행렬의 값에 전체적으로 루트 n를 나누어주면, 이는 각 행과 열이 어텐션 스코어 값을 가지는 행렬이 됨. -> 여기서 softmax 적용하여 attention distribution 구한 후, V를 곱하여 Attention Value를 구함. 식으로 표현하면 아래와 같다.

입력 문장의 길이를 seq_len(단어 갯수)이라 할 때, 문장 행렬의 크기는 (seq_len, d model)이다. 여기에 3개의 가중치 행렬을 곱해서 Q, K, V 행렬을 만들어야 합니다.

우선 행렬의 크기를 정의하기 위해 행렬의 각 행에 해당되는 Q벡터와 K벡터의 차원을 dk라고 하고, V벡터의 차원을
dv라고 하자. 그렇다면 Q행렬과 K행렬의 크기는 (seq_len, dk), V행렬의 크기는 (seq_len, dv)임.
Q의 가중치 행렬과 K의 가중치 행렬 크기는 (d model, dk)이고, V의 가중치 행렬 (d model, dv)이다.
단, 논문에서 dk, dv는 d model / num_heads .

Attention Value 크기는 (seq_len, dv)가 됨.

3) Multi-head Attention

앞서, d model의 차원을 num_heads로 나누어 ```d model / num_heads```차원을 가지는 Q,K,V에 대해 num_heads개의 병렬 어텐션을 수행하는 것을 볼 수 있었다. 이때, **num_heads(논문에서는 8)개의 병렬 어텐션**이 이루어지게 되는데, 이때 각각의 어텐션 값 행렬을 **Attention Head**라고 부른다. 이때, 가중치 Q, 가중치 K, 가중치 V는 8개의 어텐션 헤드마다 전부 다르다.

이렇게 여러번의 어텐션을 병렬로 사용하는 이유는 각 어텐션 헤드는 전부 다른 시각에서 보고 있기 때문에, 서로 다른 시각으로 정보들을 수집할 수 있어, 다른 단어와의 연관도를 구할 수 있기 때문이다.

병렬 어텐션을 모두 수행한 후에는 concatenate한다. 모두 연결된 어텐션 헤드 행렬 크기는 (seq_len, d model)이 된다.

어텐션 헤드를 모두 concatenate한 행렬에 또 다른 가중치 행렬을 곱하면, Multi-head Attention 최종 결과물이 된다. 이 크기는 인코더 입력이였던 문장 행렬 (seq_len, d model) 크기와 동일하다. 즉, 인코더의 입력으로 들어왔던 행렬의 크기가 아직 유지되고 있다!

이처럼, self-attention은 입력 문장 내의 단어들끼리 유사도를 구할 수 있다.
"그 강아지는 사료를 먹었다. 왜냐하면 그것은 배가 고팠기 때문이다." 라는 문장에서 "그것"을 가리키는 것이 "강아지"인지, "사료"인지에 대하여, 셀프 어텐션은 입력 문장 내의 단어들끼리 유사도를 구함으로서 "그것"이 "강아지"와 연관되었을 확률이 높다는 것을 찾아낸다.

4) Padding Mask

앞서 설명한 Scaled dot-product Attention 에서는 Padding Mask라는 기법이 사용된다.
입력 문장에 <PAD> 라는 토큰이 있는 경우 이는 어텐션에서 제외하기 위한 연산이다.

<PAD>는 실질적인 의미를 가진 단어가 아니므로, 트랜스포머에서는 Key에 <PAD>이 존재하는 경우, 이에 대해서는 유사도를 구하지 않도록 Masking(어텐션에서 제외하기 위해 값을 가림)을 한다. 아래 attention score 행렬에서 행은 Query, 열을 Key이므로, Key에 <PAD>가 있는 경우 열 전체를 마스킹한다.

마스킹은 어텐션 스코어 행렬의 마스킹 위치에 매우 작은 음수 값(-무한대에 가까운 수)을 넣어주는 것이다. 현재 마스킹 위치에 매우 작은 음수 값이 들어가 있으므로 attention score 행렬이 softmax를 지난 후에는 해당 위치의 값은 0이 되어 단어 간 유사도를 구하는 일에 <PAD> 토큰이 반영X.

📌 Position-wise FFNN

방금 전까지는 인코더를 설명했다. 포지션 와이즈 FFNN은 인코더와 디코더에서 공통적으로 가지고 있는 서브층이다.
포지션 와이즈 FFNN(Feed-Forward Networks)은 완전 연결 (Fully-connected) FFNN으로 봐도 된다.

  • 수식
  • 그림
  • 여기서 x = multi head attention 결과(seq_len, d model)이다. 가중치 행렬 W1 = (d model, d ff)크기를, 가중치 행렬 W2 = (d ff, d model)크기를 가진다. 논문에서 d ff = 은닉층 크기 = 2,048개
  • W1, b1, W2, b2는 하나의 인코더 내에서는 다른 단어들마다 정확하게 동일하게 사용되며, 인코더 층마다는 다른 값을 가짐.
  • 아래의 그림에서 인코더 입력 벡터들이 멀티 헤드 어텐션 층이라는 인코더 내 첫번째 서브 층을 지나 FFNN을 통과하는 것을 볼 수 있음. 두번째 서브층을 지난 인코더의 최종 출력은 여전히 인코더의 입력의 크기였던 (seq_len, d model)크기가 보존되는 것을 확인.

📌 Residual Connection & Layer Normalization

트랜스포머의 두 개의 서브층을 가진 인코더에 추가적으로 사용하는 기법이 있는데, 바로 잔차 연결(residual connection)과 층 정규화(layer normalization) -> Add & Norm 이다.

위 그림에서 추가된 화살표들은 서브층 이전의 입력에서 시작되어 서브층의 출력 부분을 향하고 있다.

1) Residual connection

  • F(x): 서브층
  • 잔차 연결: 서브층의 입력 + 서브층의 출력

만약 서브층이 multi-head attention이었다면 아래와 같이 잔차 연결 연산이 이루어진다.

2) Layer Normalization

  • 잔차 연결 후에는 층 정규화 연산이 이루어진다.
  • 층 정규화: 텐서의 마지막 차원(d model)에 대해서 평균과 분산을 구하여 정규화하는 것.
  • 1) 평균과 분산을 통한 정규화
    여기서 입실론은 분모가 0이 되는 것 방지.
  • 2) 감마와 베타를 통한 정규화
    감마와 베타의 초기값은 각각 1과 0임.

🐥 Decoder

디코더도 인코더와 동일하게 임베딩 층과 포지셔널 인코딩을 거친 후의 문장 행렬이 입력으로 들어옴.

📌 첫번째 층 (self-attention & look-ahead mask)

  • 트랜스포머는 입력을 문장 행렬로 한 번에 받으므로, 현재 시점의 단어를 예측할 때 미래 시점의 단어까지도 참고할 수 있는 문제가 있음.
  • 룩-어헤드 마스크(look-ahead mask)를 통해 현재 시점의 예측에서 미래에 있는 단어들을 참고하지 못하도록 할 수 있음.
  • 인코더에서의 multi-head attention 층과 동일한 연산을 수행하며, attention score 행렬에서 마스킹을 추가로 적용한다는 점만 다르다!
  • 마스킹 된 후의 score 행렬

📌 두번째 층 (Encoder-Decoder Attention)

  • multi-head attention을 수행한다는 점에서는 이전의 attention(인코더와 디코더의 첫번째 서브층)과는 공통점이 있으나 self-attention이 아니다!
  • self-attention과 달리 Key와 Value는 인코더 행렬이고, Query는 디코더 행렬이다.
  • 이후 수행 과정은 다른 어텐션들과 같음.

4. 전체 구조 및 과정

  • Transformer 전체 아키텍쳐이다. 왼쪽은 인코더, 오른쪽은 디코더이다.
  • 인코더와 디코더에는 모두 Positional Encoding이 적용된다. sequence 모델을 사용하지 않아도 위치정보를 담을 수 있다.
  • 하나의 인코더 안에는 두 개의 sub-layer가 있다. ( multi-head self attention, position-wise fully connected feed-forward)
    • self attention을 통해 입력 간의 관계를 독립적으로 계산하기에 각 위치에 대한 연산을 병렬로 수행할 수 있다. 입력 문장의 단어들 간 유사성 정보를 알 수 있다.
    • multi-head 를 통해 더 다양한 상황을 반영하여 다양한 관점에서의 정보를 수집할 수 있다.
    • position-wise fully connected feed-forward를 통해 비선형 활성화 함수(일반적으로 ReLU)를 사용하여 입력 특징 간의 비선형 관계를 학습, 입력의 종단 간의 관계를 보존할 수 있다. (시계열에서 중요한 요소)
  • 추가적으로 인코더에서는 Add&Norm, 즉, residual connection과 layer normalization을 활용한다.
  • 디코더는 3개의 sub layer로 구성되어 있으며 FFN, multi-head, masked multi head로 구성되어 있다. 그리고 Add&Norm은 동일하게 적용됩니다. 인코더와 다른 것은 encoder-decoder attention과 masked multi-head attention이다.
    • encoder-decoder attention을 통해 인코더와 디코더를 연결하고, 인코더 입력과 디코더 입력 간의 유사성, 어텐션 결과를 만든다.
    • masked multi-head attention을 통해 미래 시점 단어들을 보지 못하도록 한다.
  • 동일한 인코더 레이어와 디코더 레이어가 N번 반복되어 stack된다. (논문에서는 stack되는 layer 수를 6개로 제안)
  • 연산의 흐름을 보면 문장이 들어오고 처음 인코더 레이어부터 연산하여 마지막 인코더 레이어의 출력을 각 디코더로 보낸다. 하나의 레이어에 병렬로 문장이 들어간다.

5. 코드 구현

Keras 코드

https://colab.research.google.com/drive/18PI9muRNLCQVKNMOMvwJhm30ArMJclSp?usp=sharing

Pytorch 코드

to be continued...


참고한 자료 목록

0개의 댓글