본문 바로가기
  • 공부한 것들과 여러가지를 기록해요
Python/NLP

트랜스포머(Transformer) - 인코더, 디코더

by 티권 2023. 7. 19.

전체적인 트랜스포머 구조

 

이 글을 통해 트랜스포머의 인코더와 디코더에서 각각 어떤 일이 일어나는지, 뭐가 사용되는지를 총 정리해보고자 한다

 

<인코더>

인코더의 구조

 

- num_layers : 트랜스포머의 하이퍼 파리미터. 지정한 개수의 인코더층을 쌓음.

 

트랜스포머의 인코더는 크게 2개의 sublayer로 나눠짐

1. 멀티헤드어텐션층

2. 포지션-와이즈 피드포워드 신경망 층

 

자세하게는 다음과 같이 나뉨!

1. 임베딩 벡터에 포지셔널 인코딩을 수행 -> 단어의 위치 정보 반영

2. 셀프 어텐션 층을 지남

- 멀티 헤드 셀프 어텐션

3. 잔차 연결과 층 정규화

4. 피드포워드 신경망 층을 지남

5. 잔차 연결과 층 정규화

- 포지션 와이즈 피드포워드 신경망, 일반적으로 알고있는 피드 포워드 신경망


인코더의 멀티 헤드 셀프 어텐션

어텐션 개념 복습

1. 주어진 '쿼리(Query)'에 대해서 모든 '키(Key)'와의 유사도를 각각 구히기

2. 유사도를 가중치로 하여 키와 맵핑되어있는 각각의 '값(Value)'에 반영

3. 유사도가 반영된 '값(Value)'을 모두 가중합하여 리턴

 

https://tgwon.tistory.com/57

 

어텐션(seq2seq, Dot-Product Attention)

https://tgwon.tistory.com/52 시퀀스-투-시퀀스(seq2seq) 시퀀스-투-시퀀스(Sequence-to-Sequence, seq2seq) - 입력된 시퀀스를 다른 시퀀스로 변환하는 작업을 수행하는 딥러닝 모델 - 주로 자연어 처리(NLP) 분야에

tgwon.tistory.com

 

위 글에서 말했듯이, Q,K,V를 어떻게 정의하느냐에 따라서 어텐션의 종류가 달라진다.

인코더-디코더 구조를 따르는  seq2seq 모델에서 어텐션이 활용될 때는 인코더-디코더 어텐션이 활용되고,

트랜스포머의 인코더에서는 셀프 어텐션이 활용된다.

 

각각의 어텐션이 어떤 Q,K,V를 사용하는지 정리하면 아래와 같다.

 

 

인코더-디코더 어텐션

Q : t 시점의 디코더 셀에서의 은닉 상태
K : 모든 시점의 인코더 셀의 은닉 상태들
V : 모든 시점의 인코더 셀의 은닉 상태들

 

(모든 시점으로 확장)

Q : 모든 시점의 디코더 셀에서의 은닉 상태들
K : 모든 시점의 인코더 셀의 은닉 상태들
V : 모든 시점의 인코더 셀의 은닉 상태들

 

 

 

셀프 어텐션

Q : 입력 문장의 모든 단어 벡터들
K : 입력 문장의 모든 단어 벡터들
V : 입력 문장의 모든 단어 벡터들


셀프어텐션의 컨셉

그 동물은 길을 건너지 않았다. 왜냐하면 그것은 너무 피곤하였기 때문이다.

-> 여기서 그것(it)은 길(street)일까, 동물(animal)일까?

우리는 쉽게 알 수 있지만, 컴퓨터는 그렇지 않다

 

셀프 어텐션의 컨셉은

입력 문장 내의 단어들끼리 유사도를 구해서

그것(it)이 동물(animal)과 연관되었을 확률이 높다는 것을 알 수 있다는 것!

 

위 그림을 예로 들면, 셀프어텐션 메커니즘이 it 이라는 단어를 인코딩 할 때

- 입력의 여러 단어들 중에서 animal 이라는 단어에 집중

- animal의 의미 중 일부를 it 이라는 단어를 인코딩할 때 이용

 

입력 문장 내의 다른 위치에 있는 단어들을 보고 거기서 힌트를 받아 현재 타겟 위치의 단어를 더 잘 인코딩할 수 있음

트랜스포머에게 셀프언텐션은 현재 처리 중인 단어에 다른 연관 있는 단어들의 맥락을 불어 넣어주는 method

 

인코더-디코더 어텐션 : 디코더셀의 은닉 상태와 인코더셀의 은닉 상태간 유사도 구함

셀프 어텐션 : 단어 간 유사도를 구함 -> Q,K,V가 입력 문장의 단어 벡터

그렇다면 Q,K,V 벡터는 어떻게 얻을 수 있을까??


Q, K, V 벡터 얻기

우선 각각의 단어 벡터들은 d(model)의 차원을 갖고 있는 상태이다.

-> 모델의 초기 입력 차원, 논문에서는 512

각 단어 벡터들로 부터 Q,K,V 벡터를 얻는 작업을 진행한다

 

여기서 얻게 되는 Q,K,V 벡터는 기존의 벡터보다 작은 차원을 갖게 됨

-> 멀티헤드어텐션을 위해!

 

인코더에서 진행되는 셀프 어텐션은 정확히는 멀티헤드셀프어텐션

-> 셀프어텐션 레이어에 멀티-헤드 어텐션이라는 메커니즘을 더해서 성능을 개선함!

 

헤드의 수 즉 num_heads라는 파라미터로 d(model)을 나눈 값이 Q,K,V 벡터의 차원

 

벡터의 차원을 num_heads를 통해 구한 차원으로 표현하기 위해서 학습 가능한 가중치 행렬을 곱해준다

Q,K,V 별 각 가중치 행렬을 모두 다름

이 과정을 그림으로 나타내면 아래와 같다

 

 

각 가중치 행렬은 d(model) x d(model)/num_heads 의 크기를 갖게됨

단어벡터 (1 x d(model))에 가중치행렬을 곱하여 Q,K,V벡터 (1 x d(model)/num_heads )를 얻게 됨

 

요약하자면, 하나의 단어 벡터에 대해서 학습이 가능한 서로 다른 가중치 행렬을 곱해줌으로써

서로 다른 Q,K,V 벡터를 얻게 됨

 

Q,K,V 벡터가 꼭 이렇게 더 작아야하는가?

-> 아님. 이건 그냥 이후 언급할 멀티헤드 어텐션의 계산 복잡도를 일정하게 만들기 위한 구조적 선택


스케일드 닷 프로덕트 어텐션

이후에는 이 Q,K,V 벡터를 사용해 이전 글에서 작성했던 어텐션 매커니즘대로 계산한다

 

각 Q벡터는 모든 K벡터에 대해서 어텐션 스코어를 구하고 

어텐션 분포를 구한 뒤에 이를 사용하여 모든 V벡터를 가중합하고

어텐션 값 또는 컨텍스트 벡터를 구한다

-> 이 과정을 모든 Q벡터에 대해서 반복

 

이 때 어텐션 함수로 내적만을 사용하지 않고 스케일링을 추가해서 계산하게 된다

내적한 값에 추가적으로 벡터(Q,K,V)의 차원에 루트를 씌운 값을 나눠준다

-> 그래서 스케일드 닷-프로덕트 어텐션 이라고 함

 

아래 예시는 I am a student 라는 입력에서

첫번째 단어인 I의 Q벡터에 대해서 스케일드 닷-프로덕트 어텐션을 수행한 사진이다

 

 

 

순서대로 요약해서 정리해보자면

1. 단어 벡터에 학습이 가능한 가중치 행렬을 곱해서 Q,K,V 벡터를 도출한다

-> 멀티-헤드 어텐션 수행을 위해 차원의 수가 줄어든 Q,K,V 벡터

2. 하나의 Q벡터에 대해서 K벡터와 내적후 스케일링을 하여 어텐션 스코어를 도출

-> 현재 위치의 단어를 인코딩할 떄 다른 단어들에 얼마나 집중해야하는지

3. 어텐션 스코어에 소프트맥스 함수를 적용하여 총합이 1인 어텐션 분포를 도출

4. 어텐션 분포와 V벡터를 가중합하여 어텐션 값 도출

-> 이후 이 값을 피드포워드 신경망으로 보내게됨

 

이 모든 과정은 실제 구현에서는 빠른 속도를 위해 행렬의 형태로 진행함!

아래 그림과 같이 Q,K,V 행렬을 도출하고

 

아래 그림과 같이 어텐션 스코어를 구함

행렬의 각 원소가 Q,K벡터의 내적 즉 어텐션 스코어 값이 됨

 

 

그리고 아래 그림과 같이

어텐션 스코어 값에 스케일링을 진행하고 그대로 소프트맥스 함수에 넣어서 어텐션 분포를 도출

그 후에 V행렬을 곱해줌으로써 가중합이 된 어텐션 값 행렬을 얻을 수 있음


멀티 헤드 어텐션(Multi-head Attention)

앞서 트랜스포머 인코더에서의 어텐션은 멀티 헤드 셀프 어텐션이라고 했었음

 

멀티 헤드 어텐션의 컨셉

-> 한 번의 어텐션 보다 여러번의 어텐션을 병렬로 사용하는 것이 좋다!

이를 위해서 앞서 d(model) 차원의 벡터를 num_heads로 나누었었음

 

어텐션을 병렬로 사용하면?

-> 여러가지 시각으로 정보를 수집할 수 있음!

-> 각 어텐션 헤드는 전부 다른 시각에서 보고 있음(어텐션 헤드마다 가중치 행렬이 다름)

 

그림으로 이해해보면

다음과 같이 num_heads 만큼 어텐션을 수행한다!

 

이 떄 각 헤드별 어텐션값 행렬을 아래와 같이 연결한다!

 d(model) / num_heads 차원을 가진 num_heads 개의 벡터들이 연결되었기 때문에 결국에는 d(model)차원의 벡터가 됨!

 

이렇게 헤드 별 어텐션값 행렬을 모두 연결한 행렬이 나왔다면,

아래 그림처럼 여기에 또 다른 가중치 행렬을 곱하고

그 결과가 최종적으로 멀티-헤드 어텐션의 결과물이 됨!

 

다음 그림을 통해 멀티헤드 어텐션의 효과를 이해해볼 수 있는데

 

첫번째 헤드에서는 it 이라는 단어를 인코딩할 때 animal 쪽에 집중을 하고 있다

하지만 두번째 헤드에서는 tired 쪽에 집중을 하고 있다

즉, 멀티헤드 어텐션을 수행함으로써 이렇게 다양한 시각으로 단어들을 바라보며 인코딩을 할 수 있다

 

 

지금까지의 과정을 요약하면 아래와 같다!

 

1. 단어 벡터에 학습이 가능한 가중치 행렬을 곱해서 Q,K,V 벡터를 도출

-> 멀티-헤드 어텐션 수행을 위해 차원의 수가 줄어든 Q,K,V 벡터

2. 하나의 Q벡터에 대해서 K벡터와 내적후 스케일링을 하여 어텐션 스코어를 도출

-> 현재 위치의 단어를 인코딩할 떄 다른 단어들에 얼마나 집중해야하는지

3. 어텐션 스코어에 소프트맥스 함수를 적용하여 총합이 1인 어텐션 분포를 도출

4. 어텐션 분포와 V벡터를 가중합하여 어텐션 값 도출

5. 이 과정을 행렬 단위로 각각의 헤드마다 진행하여 각 헤드 별 셀프어텐션 결과 행렬 도출

6. 각 헤드들의 셀프 어텐션 결과를 연결

7. 연결된 행렬에 가중치 행렬을 곱해줌

8. 멀티-헤드 어텐션 행렬 도출

-> 처음 인코더의 입력이었던 행렬과 동일한 크기가 됨

-> 이후 이 값을 피드포워드 신경망으로 보내게됨


포지션-와이즈 피드 포워드 신경망(Position-wise FFNN)

포지션 와이즈 FFNN은 인코더와 디코더에서 공통적으로 갖고 있는 서브층

포지션 와이즈 FFNN이 뭐냐?

'Position-Wise Feed-Forward Layer is a type of feedforward layer consisting of two dense layers that applies to the last dimension, which means the same dense layers are used for each position item in the sequence, so called position-wise'

논문에서는 다음과 같이 설명하는데, 

쉽게 말하면 그냥 피드포워드 신경망 유형 중 하나고, 두개의 밀집층이 사용되는 구조이다(F1,F3)

이렇게 동일한 두개의 밀집층이 사용되어서 포지션-와이즈 라고 한다고 한다

그냥 이러한 유형의 피드포워드 신경망이 있다 ! 정도 알고 가면 되겠다

 

멀티 헤드 어텐션의 결과는 여기를 지나게 되는데, 이를 수식과 그림으로 표현하면 다음과 같다

 

x : 멀티헤드어텐션의 결과

x에 가중치 행렬을 곱하고 편향을 더한 뒤

활성화함수를 지나고

다시 가중치 행렬을 곱하고 편향을 더한다

 

여기서 W1,W2,b1,b2는 하나으 인코더 층에서는 어떤 입력(문장, 단어)들이어도 동일하게 사용됨

하지만 다른 인코더층에서는 다른 값을 가짐.

 

아래 그림과 같이 트랜스포머의 인코더는

1. 멀티헤드어텐션층

2. 포지션 와이즈 FFNN

이라는 두개의 서브층이 있고,

이 두개의 서브층을 지난 결과가 인코더1의 출력이자, 인코더2의 입력이 됨!


잔차연결(Residual connection), 층정규화(Layer Normalization)

 

앞서 트랜스포머의 인코더는 크게 두개의 서브층이 있다고 했는데,

추가적으로 잔차연결과 정규화 과정이 존재한다!

 

그림에서 알 수 있듯이

각 서브층의 결과물에 이 두가지 과정이 적용되는 것을 알 수 있다

 

 

잔차연결이란?

 

 

이 그림처럼 잔차연결은 서브층의 입력과 출력을 더하는 것이다

-> 트랜스포머의 서브층은 입력과 출력이 동일한 차원을 갖기 때문에 이렇게 덧셈이 가능함!

첫번째 서브층인 멀티헤드 어텐션 층과 잔차연결을 함꼐 이해해보면

다음과 같다

 

 

왜 이러한 과정을 진행하는지는 아직 찾아보지는 않았지만,

모델의 학습을 돕는 기법이라고 한다

 

 

층 정규화란?

층 정규화는 두가지 과정으로 구성된다

첫번째는 정규화. 두번째는 감마와 베타 이다.

 

정규화는 말그대로 정규화이다. 평균과 분산을 통해 정규화를 한다

잔차 연결을 거친 결과를 입력 받아서 정규화하게 되는데

입력 벡터의 차원에 대해서 평균과 분산을 구한다

 

아래 그림을 보면 이해할 수 있다

 

텐서의 마지막 차원(d(model))의 벡터에 대해서

벡터 값을 정규화시킨다

 

단어마다 정규화가 진행되는 것이고,

예를 들어 512차원의 벡터라면 1차원의 값들에 대해서 정규화, 2차원의 값들에 대해서 정규화,,, 이렇게 세로 방향이 아니라

그림 처럼 가로 방향으로 정규화가 진행된다

수식으로 나타내면 다음과 같다

층 정규화를 수행한 단어 벡터

 

정규환된 i번째 단어 벡터의 k번째 차원의 값은 다음과 같다

 

여기서 입실론은 분모가 0이 되는 걸 방지하기 위한 값이다

 

여기서 앞서 언급한 감마와 베타가 도입되는데, 이들의 초기값은 1과 0이다.

이 값들은 학습이 가능한 파라미터이다.

 

층정규화의 모든 과정을 나타내면 다음 수식과 같다!


<디코더>

디코더의 구조

 

 

인코더의 값이 디코더로 어떻게 전해질까?

num_layers 만큼의 인코더 층만큼 순차적으로 모든 연산을 하고 

마지막 인코더층의 출력을 디코더에게 전달함!

이 전달된 값을 디코더의 모든 층에서 연산에 사용함!

 

 

디코더의 구성

- 디코더는 3개의 서브층으로 구성

1. 셀프어텐션

2. 인코더-디코더 어텐션

3. 포지션-와이즈 FFNN

 

 


룩-어헤드 마스크

디코더만 자세히 보면 다음과 같다

 

디코더도 인코더와 동일하게 임베딩 후 포지셔널 인코딩을 거친 입력값이 들어온다!

트랜스포머도 seq2seq와 마찬가지로 교사 강요를 사용하여 훈련됨

-> <sos> je suis etudiant 라는 문장 행렬을 한 번에 입력 받음

-> 디코더는 이 문장 행렬로부터 각 시점의 단어를 예측하도록 훈련됨 

 

자세하게 풀어적으면, 학습을 할 때

마지막 인코더층의 출력과 <sos>를 통해서 예측된 예측값이 다음 시점의 입력값으로 들어가지 않고,

정답값이(je) 다음 시점의 입력값으로 들어감

 

RNN 계열의 신경망

- 순차적으로 입력받으므로, 다음 단어 예측에 현재시점과 이전 시점의 입력 단어들만 참고 가능

 

트랜스포머

- 문장행렬을 한 번에 입력 받으므로, 다음 단어 예측에 미래 시점의 단어를 참고 할 수 있게 됨

-> 디코더에서는 현재 시점의 예측에서 현재 시점보다 미래에 있는 단어들을 참고하지 못하도록 해야함

-> 룩-어헤드 마스크(look-ahead-mask) 

 

예를 들어, 현재 시점의 단어로 부터(je) 다음 단어(suis)를 예측할 때

<sos> je 를 참고해서 suis를 예측해야하는데

<sos> je          etudiant -> 미래 시점의 단어들까지 참고하게 됨

 

미래 시점까지 참고하면 성능이 좋으니까 좋은 거 아닌가? 싶지만

생각을 해보니, 트랜스포머를 학습이 아니라 직접 활용을 할 때 이전에 예측된 값들을 바탕으로 이후의 값들을 예측해나가는 방식으로 출력이 나오니까미래 시점의 단어들까지 참고해서 예측을 하며 학습을 한다면, 트랜스포머의 원래 활용에 있어서는 미래 시점의 정보가 노이즈로 작용할 것 같다는 생각이 든다

 

이어서 룩-어헤드 마스크에 대해서 얘기해보자면

 

룩-어헤드 마스크는 디코더의 첫번째 서브층에서 이루어짐!

첫번째 서브층에서 인코더와 동일하게 멀티헤드어텐션을 수행하는데 

이 때 차이가 있다면

 

 

이러한 그림처럼 어텐션을 수행, 즉 단어들을 참고할 때 자기 자신보다 미래에 있는 단어들은 참고하지 못하게 마스킹을 함

이 외에는 인코더와 모두 동일하다

 

인코더-디코더 어텐션

디코더의 두번째 서브층은 인코더-디코더 어텐션이다

seq2seq에서 어텐션을 사용할 때랑 같은 개념이다

 

정리하자면 다음과 같다

 

인코더의 첫번째 서브층 : Query = Key = Value
디코더의 첫번째 서브층 : Query = Key = Value
디코더의 두번째 서브층 : Query : 디코더 행렬 / Key = Value : 인코더 행렬

 

 

디코더의 두번째 서브층에 다음과 같이 인코더로 부터 넘어오는 두개의 화살표가 있는데

이 두개의 화살표가 Key랑 Value이다

인코더-디코더 어텐션을 수행하고 나온 어텐션 값을 디코더 행렬과 연결한뒤 tanh를 통해 최종적으로 값을 출력함

 

이를 통해 디코더가 인코더의 입력 시퀀스에서 적절한 위치에 집중할 수 있도록 도와줌!

 

이후에는 인코더와 똑같이 포지션-와이즈 FFNN을 거쳐서 디코더층의 최종 출력값이 나옴

 

리마인드

1. 주어진 '쿼리(Query)'에 대해서 모든 '키(Key)'와의 유사도를 각각 구히기

2. 유사도를 가중치로 하여 키와 맵핑되어있는 각각의 '값(Value)'에 반영

3. 유사도가 반영된 '값(Value)'을 모두 가중합하여 리턴


디코더의 출력 - Linear Layer, Softmax Layer

디코더층의 출력 벡터를 하나의 단어 벡터로 바꿀 때 이 두 가지 레이어가 사용됨!

 

Linear Layer

- 완전연결층

- 디코더층의 출력 벡터를 그보다 더 큰 사이즈의 벡터인 logits 벡터로 투영

 

logits 벡터

- 학습시 사용된 단어가 10000개라면

- 크기가 10000인 벡터임

 

즉, logits 벡터의 각 셀은 그에 대응하는 각 단어에 대한 점수가 됨!

-> linear layer의 결과로서 나오는 출력에 대해서 해석 가능

 

그리고 softmax 함수를 통해 이 값들을 확률로 바꿔주고

최종적으로 가장 높은 확률 값을 가지는 단어가 해당 스텝의 최종 결과물로 출력됨!

 


참고하면 좋은 글

The Illustrated Transformer – NLP in Korean – Anything about NLP in Korean

 

The Illustrated Transformer

저번 글에서 다뤘던 attention seq2seq 모델에 이어, attention 을 활용한 또 다른 모델인 Transformer 모델에 대해 얘기해보려 합니다. 2017 NIPS에서 Google이 소개했던 Transformer는 NLP 학계에서 정말 큰 주목을

nlpinkorean.github.io