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

임베딩 층과 포지셔널 인코딩을 거친 후의 문장 행렬이 입력.
num_layers만큼 인코더의 층을 쌓고, 논문에서는 총 6개의 인코더 층을 사용.
인코더를 하나의 층이라는 개념으로 생각한다면, 하나의 인코더 층은 크게 총 2개의 서브층(sublayer)으로 나뉘어짐.
트랜스포머는 RNN과 달리, 단어를 순차적으로 입력 받는 것이 아니라 병렬적으로 입력 받는다.
그러면, 어떻게 단어의 위치 정보(positional information)를 알려주는가?
각 단어의 임베딩 벡터에 위치 정보들을 더하여 모델의 입력으로 사용한다! -> positional encoding
아래는 embedding vector가 인코더 입력으로 사용되기 전 positional encoding 값과 더해지는 과정을 시각화한 것.
=> embedding vector + positional encoding = 트랜스포머 인코더의 입력!
이때, positional encoding 값은 사인함수와 코사인함수 로 구성된다!
따라서 위와 같은 positional encoding은 순서 정보를 보존하게한다.
트랜스포머에 존재하는 세가지 어텐션 기법.
위 그림은 트랜스포머 아키텍처에서 세가지 어텐션이 각각 어디에서 이루어지는지를 보여주는 것.
여기서 multi-head란 트랜스포머가 어텐션을 병렬적으로 수행하는 방법.
기존 어텐션 함수 동작 과정
1. 주어진 Query에 대한 모든 Key와의 유사도를 각각 구함.
2. 유사도를 가중치로 하여 키와 맵핑 되어있는 각각의 '값(Value)'에 반영.
3. '값(Value)'을 모두 가중합하여 리턴.
- Querys = 모든 시점의 디코더 셀에서의 은닉 상태들
- Keys, Values = 모든 시점의 인코더 셀에서의 은닉 상태들
그럼, self-attention은 어떻게 동작할까?
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) 크기를 가짐.
여기서 n = Q,K,V 벡터의 차원(논문에서는 64 = d model(512) / num_heads(8) )
위 그림은 "단어 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)가 됨.
앞서, 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은 입력 문장 내의 단어들끼리 유사도를 구할 수 있다.
"그 강아지는 사료를 먹었다. 왜냐하면 그것은 배가 고팠기 때문이다." 라는 문장에서 "그것"을 가리키는 것이 "강아지"인지, "사료"인지에 대하여, 셀프 어텐션은 입력 문장 내의 단어들끼리 유사도를 구함으로서 "그것"이 "강아지"와 연관되었을 확률이 높다는 것을 찾아낸다.
앞서 설명한 Scaled dot-product Attention 에서는 Padding Mask라는 기법이 사용된다.
입력 문장에 <PAD> 라는 토큰이 있는 경우 이는 어텐션에서 제외하기 위한 연산이다.
<PAD>는 실질적인 의미를 가진 단어가 아니므로, 트랜스포머에서는 Key에 <PAD>이 존재하는 경우, 이에 대해서는 유사도를 구하지 않도록 Masking(어텐션에서 제외하기 위해 값을 가림)을 한다. 아래 attention score 행렬에서 행은 Query, 열을 Key이므로, Key에 <PAD>가 있는 경우 열 전체를 마스킹한다.
마스킹은 어텐션 스코어 행렬의 마스킹 위치에 매우 작은 음수 값(-무한대에 가까운 수)을 넣어주는 것이다. 현재 마스킹 위치에 매우 작은 음수 값이 들어가 있으므로 attention score 행렬이 softmax를 지난 후에는 해당 위치의 값은 0이 되어 단어 간 유사도를 구하는 일에 <PAD> 토큰이 반영X.
방금 전까지는 인코더를 설명했다. 포지션 와이즈 FFNN은 인코더와 디코더에서 공통적으로 가지고 있는 서브층이다.
포지션 와이즈 FFNN(Feed-Forward Networks)은 완전 연결 (Fully-connected) FFNN으로 봐도 된다.



트랜스포머의 두 개의 서브층을 가진 인코더에 추가적으로 사용하는 기법이 있는데, 바로 잔차 연결(residual connection)과 층 정규화(layer normalization) -> Add & Norm 이다.
위 그림에서 추가된 화살표들은 서브층 이전의 입력에서 시작되어 서브층의 출력 부분을 향하고 있다.
만약 서브층이 multi-head attention이었다면 아래와 같이 잔차 연결 연산이 이루어진다.





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





https://colab.research.google.com/drive/18PI9muRNLCQVKNMOMvwJhm30ArMJclSp?usp=sharing
to be continued...
참고한 자료 목록