본문 바로가기
Data Science

Attention is all you need 논문 리뷰+코드 실습

by Lora Baek 2023. 4. 26.
300x250

*동빈나님의 영상을 기반으로 공부한 내용을 문서로 정리한 것입니다. 내용에 대한 저작권은 원작자에게 있습니다.

https://www.youtube.com/watch?v=AA621UofTUA&t=3295s&ab_channel=%EB%8F%99%EB%B9%88%EB%82%98 

 

 

현대 딥러닝 기반 자연어 처리 기술의 핵심 아키텍쳐인 "Transformer"

Transformer 아키텍처의 논문 제목은 Attention is all you need.

 

2021년 기준, 최신 고성능 모델을은 transformer architecture를 활용하고 있다.

GPT : transformer의 디코더 아키텍처 활용

BERT : transformer의 인코더 아키텍처 활용.

 

가장 중요한 태스크는 "기계번역" 발전 과정을 보면..

1986 RNN

1997 LSTM

2014 Seq2Seq : 고정된 크기 context vector 사용. 소스 문장을 전부 고정된 크기의 한 벡터에 압축해야 해서 성능적 한계 존재했음.

2015 Attention : 이 때 이후 입력 시퀀스 전체에서 정보를 추출하는 방향으로 발전.

2017 Transformer : RNN 아예 빼고, attention만으로도 가능하다! 라는 논문.

이후 2018 GPT-1, 2019 BERT, 2020 GPT-3.

 

기존 seq2seq 모델들의 한계점

context vector v에 소스문장의 정보를 압축하면서, 병목(bottleneck) 현상이 발생해서 성능 하락의 원인이 된다.

인코더에서, 매번 단어가 입력될때마다 hidden state값을 계산해 새롭게 갱신한다.(이전까지의 정보를 담고 있는)

마지막 단어가 들어왔을 때의 hidden state 값은 소스 문장 전체를 대표하는 하나의 context vector로서 활용할 수 있다는 얘기.

이 context vector에는 입력문장의 문맥 정보를 담고 있다고 가정.

 

디코더에선,매번 출력단어가 들어올때마다, 이 context vector로부터 출발해서 마찬가지로 hiddenstate를 만들어서 매번 내보낸다.

반복적으로 이전까지 출력했던 단어에 대한 정보를 가지고 있는 hidden state와 같이 입력받아 새롭게 hidden state를 갱신한다.

매번 hidden state 값을 갱신하면서 eos가 나올 때까지 반복하게 된다.

 

이렇게 소스 문장을 대표하는 context vector로 만들기 위해 고정된 크기에 압축을 하려는건데, 전체 성능의 병목현상이 발생할 수 있음.

그래서, 이 context vector를 디코더에서 매번 넣어줌으로써 손실을 줄이고 성능을 향상시킬 수 있었다.

하지만 소스문장을 압축해야 하는 건 동일하므로 병목현상은 여전히 발생한다.

해결방안 : 

그렇다면 매번 소스 문장에서의 출력 전부를 입력으로 받으면? 최신 GPU는 많은 메모리와 빠른 병렬 처리를 지원하므로, 소스문장이 길어도 각 단어 출력값 전부를 특정 행렬에 기록해놨다가, 매번 반영할 수 있기 때문에 성능이 좋아질 것이다!

 

하나의 고정된 크기가 아니라, 소스 문장 출력값 전부를 매번 입력값으로 받아서 출력값으로 만들어보자!

이게 바로 Seq2Seq with Attention.

인코더의 모든 출력 outputs를 참고하도록 만들 수 있다.(Neural Machine Translation by Jointly Learning to Align and Translate, ICLR 2015)

소스문장 각각의 hidden state에 대해서 어느 단어에 집중해야하는지를 정해서, 각 비율에 맞게 더한 다음, 그 weighted sum 값을 매번 출력을 할 때 참고하겠다는 아이디어.

 

디코더 팥

i=현재 디코더가 처리중인 인덱스

j=각각의 인코더 출력 인덱스

에너지 Energy e_ij = 매번 디코더가 출력 단어 만들때마다, 모든 j를 고려하겠다. s: 디코더가 이전에 출력했던 단어를 만들때 사용했던 hidden state, h:인코더의 hidden state. 디코더 파트에서 이전에 출력했던 정보와, 인코더의 모든 출력값과 비교해서, 어떤 h값과 가장 연관도 높은지 계산함.

가중지 Weight(a_ij) = 이제 소프트맥스를 취해서 확률값을 구한다. 실제 비율적으로 어떤 값과 연관성 높은지 가중치를 만들고, 이 가중치를 h와 곱해서 각 가중치가 반영된 인코더 출력값을 더해서 반영하는 것.

매번 디코더 파트에서 hidden state값을 이용해서 만드는데, 이전에 사용했던 hidden state와 인코더 파트의 모든 hidden state를 비교해서 입력한다. 여기서 a_t,1 a_t,2 이 중에서 어떤 게 가장 연관도 높은지 계산해준다. 에너지 : 어떤 값과 가장 연관도가 높냐? 가중치 : 소프트맥스 확률값. 그 가중치를 각각의 소스문장의 hidden state와 곱해 더해준 값을 디코더에 입력으로 같이 넣어주겠다! 라는 것.

이런 attention은 시각화도 가능하다. Attention weight를 이용해서 각 출력이 어떤 입력 정보를 참고했는지 시각화해서 어떤 단어에 가장 많은 초점(가중치)을 뒀는지 확인할 수 있다.

딥러닝은 과정을 알기 힘든데, 어텐션 메커니즘은 딥러닝의 과정을 분석할 때 용이하게 사용할 수 있는 것이다.

 

트랜스포머 Transformer

현대 자연어 처리 네트워크에서 핵심이 되는 논문. 트랜스포머는 RNN,CNN을 전혀 필요로 하지 않는다.

그래서 각 단어의 순서에 대한 정보를 얻기 힘드니까 대신 positional Encoding을 사용해서 위치 정보를 준다.

 

또한 RNN방식은 아니지만, 인코더와 디코더로 구성되며, attention 과정을 여러 레이어에서 반복한다. 인코더도 여러번 중첩, 디코더도 여러번 중첩시킨다.

동작 원리

1. 입력 값 임베딩.

입력차원 자체는 특정 언어에서 존재할 수 있는 단어의 갯수와 같다.

차원이 많고 각 정보는 원핫인코딩으로 표현되므로, 일반적으로 임베딩 차원을 거쳐서 어떠한 실수값으로 표현되도록 한다.

I am a teacher 라는 문장이 들어오면, input embedding matrix로 만들어지고, 4개의 단어가 단어에 대한 정보를 포함하는 임베딩 값을 각각 구하게 된다. 이러한 임베딩 dim은 임의로 설정 가능한데, 논문에서는 512로 설정했다.

입력값을 임베딩 형태로 사용한다.

2. 인코더

이 때 seq2seq 같은 rnn 기반 아키텍쳐 사용하면, 각 단어는 rnn에 들어갈때 순서에 맞게 들어가므로 자동으로 각각의 hidden state 값은 순서 정보를 가지게 된다.

만약 rnn을 사용하지 않는다면, 어떤 단어가 앞에 오고 어떤 게 뒤에오는지 위치정보를 주는 임베딩을 할 필요가 있다.

그래서 트랜스포머에서는 input dim과 같은 차원을 가지는 positional encoding을 추가로 실제 attention에 같이 넣어준다.

positional encoding까지 임베딩이 끝난 이후에 Attention을 진행한다.

입력을 받아 각 단어에 대한 어텐션을 수행하게 된다.

*Self attention : 각 단어가 서로에게 어떤 연관도를 가지고 있는지 구하기 위해 사용한다. 

이 attention은 입력문장에 대한, 문맥에 대한 정보를 잘 학습하도록 만드는 것이다.

 

추가로, 성능 향상을 위해 잔여 학습(Residual learning)을 사용하는데, resnet 같은 네트워크에서 사용되고 있는 기법으로, 어떤 값을 레이어를 거쳐서 반복적으로 갱신하는 게 아니라, 특정 레이어를 건너뛰어서 복사된 값을 그대로 입력할 수 있도록 만든다.

이걸 residual connection이라고 부르며, 이렇게 해줌으로써 전체 네트워크는 기존 정보를 입력받으면서 추가로 잔여 부분만 받으니까 학습 난이도가 낮고, 초기 수렴속도가 높아지고, global optima를 찾기 쉽게 된다. 전반적으로 다양한 네트워크에 대해서 성능이 높아짐! 이렇게 받아서 normalization까지 수행해주면 된다. (ADD +Norm)

이 과정을 여러 레이어를 중첩해서 사용하면서 Attention, Normaliazation 과정을 반복한다.

유의점 : 각 레이어는 서로 다른 파라미터를 가진다. 이렇게 입력되는 값과 출력되는 값의 dimension은 동일하다.

인코더와 디코더

트랜스포머에서는 가장 마지막 인코더에서 나온 출력값이 디코더에 들어가게 된다.

디코더 파트에서는 매번 출력할 때마다 입력 소스 문장 중에서 어떤 단어에 가장 많은 초점을 두어야 하는지 알려주기 위함.

디코더 파트에서 가장 마지막에 나온 출력값이 번역 결과가 된다.

디코더에서도 출력 언어의 단어정보, positional encoding을 받아서 들어가게 된다.

디코더에서는 두 개의 attention을 사용한다.

첫 번째는 self-attention : 각 단어가 서로에게 가지는 가중치, 출력문장에 대한 전반적 표현 학습

두 번째는 인코더에 대한 정보 attention : 각 출력단어가 인코더 소스 문장의 정보를 받아, 소스문장의 어떤 단어와 연관도가 높은지를 알 수 있게 해 주는 것이다.

선생님이라고 단어를 번역한다면, 선생님이라는 단어는 i am a teacher에서 어떤 단어와 가장 연관되어 있는지를 보는 것.

디코더도 마찬가지로 입력 dimention, 출력 dimention이 똑같아야 한다. 각각의 디코더 레이어는 여러번 중첩해서 사용할 수 있다.

 

다시 말해,

트랜스포머에서 마지막 인코더 레이어의 출력이 모든 디코더 레이어에 입력된다.

레이어 갯수는 인코더, 디코더가 동일하도록 맞춰주는 경우가 많다. 만약 n_layers=4라면,

인코더 파트 마지막 4번째 레이어의 출력값이 디코더 파트의 두번째 어텐션에 입력된다.

 

트랜스포머에서도 encoder, decoder 구조를 따르지만 RNN을 사용하지 않도, 인코더와 디코더를 다수 사용한다는 점이 특징이다.

입력 단어 자체가 쭉 연결되어 한번에 입력되고, 한번에 인코더를 거칠 때마다 병렬적으로 출력값을 구해내므로 RNN보다 계산 복잡도가 더 낮다. 학습 수행 시, 입력값을 전부 넣으므로 rnn을 쓰지 않아도 된다.

모델에서 출력값을 내보낼 때는 eos가 나올 때까지 반복해서 만들어야 한다.

context vector가 필요없으므로 rnn이 필요없어진다.

 

Multi-head Attention

transformer의 각 attention의 이름이 multi-head attention이다.

Query, Key, Value

Query : 물어보는 주체. 어떤 단어가 다른 단어와 어떤 연관성을 가지는지 물어보는 주체. (I am a teacher 문장에서 셀프-어텐션 수행 시, I라는 단어가 다른 단어와 어떤 연관도를 가지는가? 라는 상황에서

Query == I

Key == I, am,a,techer

어떠한 단어가 다른 어떤 단어들에 대해 어떤 가중치를 가지는가?

이렇게 각 key에 대한 attention score를 구하고 나서, 실제로 value 값들과 곱해서 결과적으로 '가중치가 적용된' attention value 값을 구할 수 있게 된다.

행렬곱 수행-스케일링-필요하다면 마스크-softmax:확률값 산출->이 확률값과 value값을 곱해서 결과값 산출.

 

이렇게 입력값이 들어왔을때, value, key, query로 구분되는데, h개로 구분해서 h개의 서로 다른 attention concept을 학습하도록 해서, 더욱더 구분된 다양한 특징을 학습하도록 유도해준다.

 

입력으로 들어온 값이 세 개로 복제되어서, h개로 구분된 각각의 쿼리 쌍들을 만들어내게 되고, h=head의 갯수이므로 각각 서로 다른 head끼리 v,k,q 쌍을 받아서 attention을 수행해 결과를 내보낸다.

입력값과 출력값의 dim이 같아야 하므로, h개를 concat으로 붙인 다음,

Linear layer 거쳐서 output 값을 내보내게 된다.

 

다른 점?

사용되는 위치마다 Q,K,V를 어떻게 사용할지는 달라지지만 기본적인 multi-head attention은 같다.

 

수식

하나의 어텐션은 Q,K,V를 갖는다. 이 때, Q,K를 곱해서 각 키에 대한 Energy 값을 구하고, 이에 대한 확률값을 구하도록 해서 어떤 key에 대해 높은 가중치를 갖는지 구한다. 이 때 scaling factor d_k는 Key dimension. softmax 특성상 0 근처에서는 gradient가 많이 줄어드므로 기울기 소실 문제가 있어서, d_k를 넣어서 이를 방지해주는 것이다.이를 V와 곱해서 최종적으로 attention value 값을 구하게 된다.

 

이 때 입력으로 들어오는 각 값에 대해 서로 다른 h개의 레이어를 거치도록 해서 h개씩의 Q,K,V를 만든다.

실제로는 h갯수만큼 어텐션스코어의 그림이 나오게 된다.

결과적으로 각 head에 대한 출력값을 구해 쭉 붙인 다음 output matrix와 곱해서 multihead attention을 구해내게 된다.

나올 때는 입력으로 들어온 값과 동일한 어텐션을 가지게 된다.

 

transformer 동작 원리(단어)

각 head 마다 Q,K,V 값을 만든다.

만일 임베딩 차원이 512차원, 8head를 사용한다면? Q,K,V의 차원은 d_model/h=512/8, 즉 Query 64차원, Key 64차원, Value 64차원을 가지게 된다.

간단하게 임베딩 차원이 4차원, head가 2개인 상황을 가정해보자.

이럴 때 4차원의 데이터를 2차원으로 매핑해야 하므로 Q,K,V는 각각 4x2 matrix 가중치 매트릭스를 사용하게 된다.

love라는 4차원의 단어가 각각 2차원 Q,K,V로 바뀌게 되는 것이다.

이런 식으로 Q,K,V값을 구했다면 이제 Attention value를 구할 수 있다.

쿼리는 각 단어들과 행렬곱을 수행해서, 중간에 보이는 1개의 값인 attention energy값을 구해진다.

softmax 에 들어가는 값을 normalize해주기 위해 d_k로 나눠주고, 

이후에 softmax 취해서 각 키값에 대해 어떤 가중치를 가지는지 구하게 된다.

이 각 가중치에 value를 곱하고, 다 더해서 weighted sum을 구해서 attention value를 구할 수 있게 된다.

 

전체 문장이 입력되는 예시

행렬곱셈 연산을 이용해 한꺼번에 연산한 다음, 쿼리값들을 각 키값과 곱해줘서 attention energies를 만들어낼 수 있다.

각 단어가 서로에 대해 어떤 연관도를 가지는지 표현하는 것이니까 행과 열은 단어의 개수와 동일하게 된다.

여기에 softmax 취해서 각 키 값을 확률값으로 구해내고,

가중치값과 value값을 구해서 attention value matrix를 구할 수 있게 되며, 입력된 쿼리, 키, 밸류와 동일한 차원을 가진다.

마스크 행렬 mask matrix

특정 단어를 무시하기 위해 사용한다.

Attention Energies가 있을 때, Mask matrix의 마스크 값으로 음수 무한의 값을 넣어서 soft함수의 출력이 0%에 가까워지도록 해 특정 단어를 무시하는 식으로, 고려하지 않도록 처리되게 만들 수 있다.

 

 

 

 

동빈나님은 임베딩 4차원, head 2개인 상황을 예시로 들었는데 Attention energies를 구한 다음 Attention value로 이어지는 과정에서 행렬곱 연산이 잘 이해되지 않아, ChatGPT에게 좀 더 구체적으로 모델을 설명해달라고 요청했다.

 

!지금 행렬 형태로 되어있는데 head가 2개라는 점에 집중을 하고 attention 의 행렬곱 과정을 이해해야 잘 이해가 된다.

각 head에 대해서 계산을 다 하고 합치는 과정을 거친다는 점에 유의해야겠다.

 

각 head마다 쿼리, 키, 밸류 값을 각각 넣어서 attention을 수행하고, 이를 일자로 쭉 연결한다.

d_model = d_v x h였기 때문에, 결과적으로 이 concat한 행렬의 열의 갯수는 입력 demension과 동일하다. 차원이 동일하게 유지되는 것이다!

그렇기 때문에 맨 마지막 계산에서, d_model x d_model matrix를 곱해줌으로서 multihead attention의 값을 얻을 수 있다.

 

세 가지 종류의 Attention layer.

Transformer는 항상 Multi-head attention을 사용하는데, 사용되는 위치에 따라 종류가 달라진다.

1. Encoder Self-Attention : 각 단어가 서로에게 어떤 연관성을 가지는지 attention 통해 구하고, 전체 문장에 대한 representation learning.

2. Masked Decoder Self-Attention : 각 출력단어가 모든 출력단어를 전부 참고하는게 아니라, 앞쪽에 등장했던 단어들만 참조할 수 있도록 만든다. 뒤쪽에 나오는 단어가 무엇인지 참고할 수 있도록 만들어버리면 일종의 cheating이 되어서 정상적인 학습이 안 된다.

3. Encoder-Decoder Attention : 쿼리가 디코더에 있고, 각 키/밸류는 인코더에 있는 상황을 의미한다. 각 출력 단어들이 입력단어들 중에서 어떤 정보에 더 많은 가중치를 두는지 구해야 하는데, 디코더 쿼리 값이 인코더의 키와 밸류를 참조한다고 해서 이런 이름이 붙여졌다.

 

Self-Attention

인코더, 디코더 모두에서 사용된다.

매번 입력 문장에서 각 단어가 다른 어떤 단어와 연관성이 높은지 attention score를 계산할 수 있다.

실제 attention score를 시각적으로 출력하도록 만들 때 사용할 수 있다.

 

결과적으로 트랜스포머 아키텍쳐를 이해할 수 있게 되었다.

 

Input->Input embedding+positional encoding->첫 레이어->n번만큼 반복되는 인코더

디코더에서는 output embedding+positional encoding 수행->첫 레이어에서 Masked Multi-head Attention, Add&norm->

->인코더 마지막 레이어의 출력값이 디코더 레이어로 전달

->디코더 레이어도 n번만큼 중첩

->가장 마지막 출력값에 linear layer, softmax 취해서 각 출력 단어를 만들어낼 수 있게 된다!

 

*Positional Encoding

논문에서는 주기함수를 활용한 공식을 사용해서, 각 단어의 상대적인 위치 정보를 네트워크에게 입력.

파라미터로 들어와있는 값은 sin, cos 뿐만 아니라 다른 주기함수를 넣어도 되고,특정 주기성을 학습할 수 있도록 만들기만 한다면! 된다.

우리가 위치에 대한 임베딩 값을 따로 만들어서 넣어도 된다. 성능상의 차이는 sin, cos와 큰 차이는 없는데,

transformer 이후에는 따로 주기함수를 넣지 않고, 별도의 임베딩 레이어를 사용하기도 한다.

 

동빈나님의 코드 실습

BLEU 계산을 위해서 torchtext 버전 0.6.0으로 설치

토큰화는 spaCy, 영어와 독일어를 각각 설치해준다.

spacy.load로 언어를 불러올 때 입력값이 바뀌었다.

spacy_en = spacy.load('en_core_web_sm') # 영어 토큰화(tokenization)
spacy_de = spacy.load('de_core_news_sm') # 독일어 토큰화(tokenization)

 

아래 사이트에서 확인할 수 있다.

https://spacy.io/models

 

 

토큰화 수행한 결과를 리스트 형태로 반환하도록 정의한다.

Field 라이브러리로 전처리를 명시 : sos, eos 토큰 붙여주고 각 단어를 소문자로 바꿔주고, 시퀀스보다는 배치가 먼저 오게 함.

3만개 정도의 영어-독어 번역 데이터셋인 Multi30k로 수행.

학습 데이터 중 하나를 선택해서 출력해보자.

 

field 객체의 build_vocab을 이용해서 영어, 독어 단어 사전을 생성 : 각각의 초기 input dimension이 얼마인지 구하기 위함.

build_vocab 만든 후 각각의 len 출력해보면 몇 개의 단어가 있는지 알 수 있음.

vocab 객체에서 string to i (stoi) 함수를 호출해 각 단어가 어떤 인덱스에 해당하는지 확인 가능.

없는 단어면 0, 의미없는 값은 1, sos는 2, eos는 3, 존재하는 단어는 인덱스가 나오는 걸 확인할 수 있다.

0~3 단어는 실제로 존재하지는 않지만, 네트워크가 적절히 학습할 수 있도록 사용하는 토큰들.

 

하나의 배치에 포함된 문장들이 가지는 단어의 개수가 유사하도록 BucketIterator 사용해주자.

패딩토큰이 최대한 적게 들어가도록 해서, 입력으로 들어가는 차원을 줄이자.

 

첫 번째 배치를 확인해볼 수 있다.

현재 배치에 포함된 문장 128개 중, 가장 긴 문장의 길이는 35.

2~3 사이에 포함된 단어들이 실제 의미를 가지고 있고, 나머지는 1로 패딩되어있음을 알 수 있다.

 

MultiHead Attention 아키텍쳐 정의

Queries, Keys, Values.

hidden_dim : 하나의 단어에 대한 임베딩 차원

n_heads : h. head 갯수

dropout_ratio : 정규화테크닉. 드롭아웃 비율 설정

 

여기서 FC_layer가 나오는데 이게 뭘까?

Fully Connected layer. 완전연결층.

입력 뉴련과 출력 뉴런이 모두 완전히 연결되어 있는 층으로,

입력값에 대한 가중치 행렬 연산과 편향(bias)을 더해주는 작업을 수행한다.

import torch.nn as nn

FC_layer = nn.Linear(입력뉴런 수,출력뉴런 수)

여기서 nn.Linear()는 입력값과 출력값의 크기를 인자로 받아서

가중치 행렬과 편향을 초기화하는 작업을 수행한다.

FC_layer를 통과한 입력값은 다음 층으로 전달되거나 최종 출력값으로 사용된다.

 

 

해당 부분을 이해하고 나서 다시 동빈나님의 코드를 보러 왔다.

__init__ 파트 정의

이론 때는 Q,K,V들은 hidden dim에서 key의 차원으로 바뀐다고 배웠었는데,

실제로 구현할 때는 hidden dim->hidden dim으로 매핑하고,

결과 dimension을 각 head에서의 임베딩차원(head_dim)으로 설정했음을 볼 수 있다.

head_dim = hidden_dim // n_heads = 단어들의 임베딩 차원 // h(head 수)

scale 값도 각 쿼리, 키, 밸류 값(head_dim)에 루트를 씌워서 나중에 나눠서 소프트맥스에 넣도록 한다.

 

forward 파트 정의

query_len : 단어의 개수.

Q,K,V로 그대로 다 매핑을 해주고, 각 결과값을 h개로 나누어서 각 h마다 head_dim만큼의 차원을 가지도록 한다.

각각 h개의 Q,K,V들을 만든 것.

각 head마다 Q,K,V를 곱해주고, scale을 나눠준다.

필요시, 마스크 값을 사용한다면 0인 부분을 거의 0이 되도록 -1e10으로 채워줬다.

softmax취해서 attention score를 계산하고,

그 attention score 가중치 값과 V를 곱해서 attention value를 만들어준다.

이 결과를 일자로 늘어뜨려서 concat을 수행한 것과 같은 결과를 만들고, 

output linear 함수를 거쳐 결과를 뽑고, attention score 값도 return해서 추후 시각화하는 데 사용하자.

 

Position-wise Feedforward & Encoder Layer 파트 정의

입력과 출력의 차원이 같도록, 인코더 레이어를 정의한다. 이 레이어를 중첩해서 사용한다.

한 입력값(src)이 들어오면 Q,K,V로 복제해서 (src,src,src) 넣어주고, mask 벡터(src_mask)까지 하나 더 추가해준다.

 

전체 Encoder 아키텍처 정의

실제 EncoderLayer는 여러 개 쌓아서 사용한다.

원본 논문과 다르게 위치 임베딩을 학습하는 형태로 구현하셨는데, BERT와 같은 모던 transformer에서 사용되는 방식이다.

pad토큰은 무시할 수 있도록 마스크 값을 0으로 설정한다.

 

여기서 nn.ModuleList를 사용한다.

nn.Sequential과 마찬가지로 nn.Module의 list를 input으로 받아서 저장하는 역할을 한다.

forward() 메소드도 없고 모듈 간의 커넥션도 없지만,

nn.ModuleList 안에 모듈들을 넣어줌으로써 PyTorch에게 모듈의 존재들을 알려주어야 한다고 한다.

model을 wrapping해주는 것!

n_layer만큼 반복할 수 있도록 하기 위해서 필요한 과정이다.

 

처음에 입력이 들어오면, batch_size는 문장의 갯수, src_len은 가장 긴 문장의 단어 갯수

입력 임베딩 값에 위치 임베딩을 더한 것을 src로 사용하고, 반복적으로 거치면서 순전파 forward를 수행할 수 있도록 만든다.

 

Decoder 레이어 파트 정의

두 개의 Multi-head attention 레이어가 사용됨을 기억하자.

총 6개의 레이어가 사용되고 있다.

self-attention : Q,K,V 모두 trg(자기 자신), 마스크도 trg_mask

residual connection

encoder-decoder attention : Q는 trg /  K,V는 enc_src(인코더의 마지막 문장), 마스크도 src_mask

residual connection

feedforward layer

residual connection

마지막에 나온 출력 문장이 정보를 가지고 있다.

 

전체 Decoder 레이어 아키텍쳐에서는 이를 중첩해서 사용한다.

마찬가지로 sin, cos이 아니라 위치 임베딩을 학습하는 형태로 구현하고,

타겟 문장에서 각 단어는 다음 단어가 무엇인지 알 수 없도록 만들기 위해서 마스크를 사용한다.

 

결과적으로 transformer 아키텍쳐는

전체 인코더, 디코더 아키텍쳐 받은 후,

src, trg 각각 마스크를 각각 만들어준다.

인코더에 넣어서 출력값을 뽑고, 디코더를 커쳐서 output을 내보낸다.

transformer 객체를 만들어주고 parameter까지 모두 초기화한다.

 

model.train 파트에서 이 부분이 이해가 잘 안되어 좀 찾아보았다.

output_dim = output.shape[-1]

output = output.contiguous().view(-1, output_dim)

 

이 부분은 출력값 output의 shape을

(batch_size, trg_len-1, output_dim)에서

(batch_size*(trg_len-1),output_dim)으로 변경한다.

즉, 2차원 텐서로 만드는 과정이다.

이후 이 2차원 텐서를 이용해서 모델의 출력값과 타겟 문장을 비교해 손실을 계산하게 된다.

 

output_dim에 output의 마지막 차원 크기를 할당하고,

view()함수로 텐서의 shape을 변경한다.

contiguous()함수는 텐서의 메모리를 연속적으로 만들어주는 역할을 하는데, 이게 없으면 view 함수에서 오류가 발생한다.

 

BLEU4가 일반적으로 우리가 사용하는 BLEU 스코어!

 

 

논문 리뷰

오직 어텐션 기법만 사용해서 기계번역 태스크에서 좋은 성능 얻음.

Abstract

원래는 시퀀스간 변형이 이루어지는 모델은 rnn, cnn 기반이었다.

attention을 활용했을 때 더 결과가 좋았다는 이전의 연구 결과를 함께 언급하고 있다.

그런데 여기서 제안하는 Transformer 모델은, 오직 Attention만 활용함으로써 recurrence하게 처리할 필요없고, 행렬곱을 이용해 병렬적으로 처리할 수 있다는 장점을 가지고 있다.

WMT 2014 영어-불어 번역 task에서 SOTA 성능을 보였고 학습 효율이 더 높다는 것!

transformer는 구문분석 등 다양한 태스크에 대해서 일반화가 가능하다.

 

1. Introduction

RNN, LSTM, GRU와 같은 모델이 제안되었고 sequence 모델링을 위해 효과적으로 사용되고 있었다.

하지만 이 recurrent 모델은 한 번에 하나씩 넣는 것처럼 sequence에 포함되어있는 단어 정보를 먼저 정렬 후, 반복적으로 입력으로 넣어서 hidden state값을 갱신시키는 방식으로 동작한다. 토큰 갯수만큼 NN에 입력을 넣어야 하므로 병렬적 처리가 불가하다는 문제 존재.

 

번역에서는 문장의 길이만큼 입력을 수행해야 하므로 메모리, 속도 측면에서 비효율성을 야기할 수 있다.

Attention 은 매번 출력 단어를 만들 때마다 소스 문장의 어떤 정보가 중요한지 매번 가중치를 계산하도록 해서, 출력 단어를 효과적으로 생성할 수 있다. 하지만 RNN과 같이 사용되는 경우가 많았기 때문에 본 논문에서는 전적으로 attention 메커니즘만 활용하므로 '순차적으로' 넣지 않아도 된다.

12시간만에 상당히 좋은 결과를 얻었다.

 

2. Background

Self-attention : 자신이 문장 스스로에게 attention을 수행. 하나의 시퀀스에서 서로가 서로에게 가중치를 부여하도록 해서 한 시퀀스에 대한 representation을 효과적으로 계산하고 학습할 수 있게 된다.

결과적으로 GPT, BERT는 이 논문에서 제안된 아키텍쳐를 많이 따르고 있다.

 

3. Model Architecture

encoder-decoder 구조를 가지고 있다.

encoder에서 입력 시퀀스( x1~xn)를  임베딩 벡터 z(z1~zn)으로 바꿔주고,

이 z를 디코더는 출력 시퀀스(y1~yn)를 만드는 식으로 동작한다.

RNN 기반은 이전 단계에서 생성된 심볼을 이용해 다음 단계에서 동작하도록 한다.

 

Transformer도 인코더-디코더를 활용하지만,

시퀀스에 대한 정보를 '한번에' 입력으로 준다는 점이 특징이다.

3.1 Encoder, Decoder stacks

N=6번 인코더 레이어 중첩, residual connection 수행

입력값으로 identity mapping, d_model = 512로 구성.

디코더에서도 N=6번, 어텐션 수행, residual connection 수행

mask를 씌워서, 마스크가 붙은 형태로 학습.

 

3.2 Attention

쿼리 : 질문을 하는 주체

키 : 어텐션 수행하는 대상

3.2.1 Scaled Dot-Product Attention

각 쿼리가 키에 대해 질문하는 내용이 행렬곱으로 이뤄지고, scale 레이어 포함하고, mask 벡터 필요시 사용한 다음,

softmax 취해서 얼마나 중요한지에 대한 값을 확률 형태로 구하고, 

실제 value와 곱해서 결과값을 얻게 된다.

concat 후 linear layer 거쳐서 최종 결과값이 나오게 된다.

 

softmax : 값이 너무 크거나 하면 기울기 소실 문제가 있으므로 scale 팩터를 곱해줘서 학습이 잘 될 수 있도록 해 주는 것.

h만큼 임베딩 차원을 나누어서 쿼리, 키, 밸류를 나눠주고 Multi-head attention을 수행 후, 최종적으로 다시 이어붙여주자.

실제 쿼리, 키를 만들기 위한 행렬의 크기 : d_model 차원->d_k 차원으로 사용할 수도 있고,(논문)

그냥 d_model x d_model로 곱한 후 결과값 자체를 나누어서 사용할 수도 있다.(동빈나님 코드)

 

3.2.3 

인코더-디코더

출력 단어를 만들기 위해 소스 문장에 포함되어 있는 단어들 중에서 어떤 정보에 초점을 맞출지 결정하는 과정.

인코더 : self-attention layer

디코더 : self-attention : 마스크 적용

 

3.3 Position-wise feed-forwaard networks

hidden dim으로 고차원공간에 매핑되었다가 출력 레이어를 통해 feed-forward 수행되도록 한다.

 

3.4

seq2seq과 같이 특정 언어의 단어 갯수에 맞게 embedding을 거친다.

 

3.5 positional encoding

recurrent 쓰지 않기 때문에 위치 정보를 같이 넣어줘야 함.

논문에서는 sin, cos와 같은 주기함수를 사용했지만, 임베딩 레이어를 우리가 별도로 학습시킬 수도 있다는 점!

 

4. Why self-attention

왜 더 유리한가?

1) 각 레이어마다 계산복잡도가 줄어든다. (n(sequence 길이)은 d보다 낮을 확률이 높기 때문)

2) 리커런스 없앰으로써 병렬처리가 가능하다.

3) long-range dependency에서도 유리하다.

 

어텐션 메커니즘 자체가 우리의 nn을 설명 가능한 형태로 만들어준다는 장점도 있다.

단순히 각 헤드에 포함되어 있는 attention value's softmax값을 출력해보면 될 것이다.

 

5. Traning

사용한 데이터셋에 대한 설명, 하드웨어와 스케줄링.

베이스모델만으로도 sota 달성.

Adam optimizer 사용,

정규화를 위해 dropout도 사용했음.

label smoothing을 사용해서 정답값에 대해 확신을 갖지 않도록 만들어서 성능을 높이기도 했다.

댓글