Transformer 구현하고 이해하기(1)

‍한지영·2022년 3월 10일
2

Transformer

목록 보기
1/2
post-thumbnail
  • 무려 작년 5월 1일 발제했던 10달묵은 트랜스포머 정리.
  • 🤚 tf 코드입니다.
  • 킹키독스 : https://wikidocs.net/31379

1. 트랜스포머(개념)

1) 트랜스포머 등장 배경

  • RNN 계열 모델의 근본적 한계
    - 크기가 매우 큼
    - 병렬 처리가 불가능함
    - 문장이 길어지면 학습이 힘들어짐
    - 멀리 떨어진 두 정보가 하나의 context vector에 포함되기 위해서는 여러번의 행렬곱을 거쳐야 함
  • Transformer? Attention is all you need.
    Rnn을 전혀 사용하지 않는 혁신. 기존 seq2seq의 인코더 디코더 구조를 따르면서도 rnn을 전혀 사용하지 않고 어텐션만으로 인코더-디코더 구조를 설계. 성능도 RNN 기반보다 우수.
  • 그렇다면, 어텐션만으로 어떻게 인코더 디코더 구조를 설계했는가?

2) Positional Encoding

트랜스포머는 RNN을 사용하지 않기 때문에, 인풋값이 위치 정보를 담고 있지 못하다는 특성이 있다. 그래서 Positional Encoding을 통해 단어들이 문맥에서 '어느 위치'를 갖고 있는지를 나타내 준다. 어떻게?

인풋값인 임베딩 벡터에 포지셔널 인코딩 값을 더해주는 방법을 사용하면 순서 정보가 보존된다. 이는 임베딩 벡터에 포지셔널 인코딩 값을 더하면 같은 단어(token)라고 하더라도 문장 내의 위치에 따라서 트랜스포머의 입력으로 들어가는 임베딩 벡터의 값이 달라지기 때문이다.
따라서 결국, 트랜스포머의 입력은 Embedding vector + Positional Encoding, 즉 순서 정보가 고려된 임베딩 벡터라고 보면 된다.

3) Transformer Encoder/Decoder

Embedding vector+Positional Encoding값이 Transformer encoder에 입력이 되면, 포지셔널 인코딩 값이 더해진 벡터는 셀프어텐션과 피드포워드 신경망을 거치게 된다. 위 그림에서 볼 수 있듯, 트랜스포머 하나의 인코더는 두개의 서브레이어(1. Multi head self attention, 2.FFNN)로 이루어져 있다. 그리고 이 서브레이어 두개를 가지는 인코더를 하나의 층이라고 생각했을 때, 이 인코더(인코더 디코더)층의 개수(위 그림의 num_layers)는 트랜스포머 구현 시 주요한 하이퍼파라미터가 된다.
디코더도 역시 마찬가지이다. 하나의 인코더는 셀프어텐션과 FFNN이라는 서브레이어 두 개로 구성되고, 하나의 디코더는 셀프어텐션, 인코더 디코더 어텐션, FFNN 3가지 서브레이어로 구성된다. 참고로, 논문에서는 위 그림과 같이 인코더 디코더 층을 각각 6개씩 쌓았다.

그렇다면, 앞서 인코더 디코더를 설명하면서, 추가로 언급한 피드포워드 신경망(FFNN)은 차치하고라도, '어텐션' 혹은 '셀프 어텐션'을 총 3번 언급했는데, 언급한 트랜스포머 구조 내의 3개의 어텐션의 차이는 무엇일까? 이 역시 중요한 포인트이다.

4) 트랜스포머 내 attention의 종류

트랜스포머에는 총 3가지 종류의 attention이 사용된다.

  1. 인코더의 셀프 어텐션
  2. 디코더의 셀프 어텐션
  3. 인코더-디코더 어텐션

Attention에 대한 구체적인 설명은 지금은 생략하고, 이후 코드 구현에서 조금 설명해보도록 하겠다. 그래서 3개 attention의 차이가 무엇인가? 명칭에서도 알 수 있듯이, 1. 인코더의 셀프 어텐션과 2. 디코더의 셀프 어텐션은 모두 '셀프 어텐션'이고, 3. 인코더 디코더 어텐션은 아니다. 즉, 인코더 셀프 어텐션과 디코더 셀프 어텐션은 Query Key Value 값이 모두 인코더의 것이거나/디코더의 것인 특징이 있고, 반대로 인코더-디코더 어텐션은 Q값은 디코더에 존재하지만, K V 값은 인코더의 값이라는 차이가 있다. 위 그림의 트랜스포머 구조를 보면, 인코더 아웃풋이 디코더의 두번째 서브레이어인 Multi-head attention(Encoder-Decoder attention)에 인풋으로 들어가는 것을 확인할 수 있다.

이렇게 3. 인코더-디코더 어텐션의 특수성은 확인을 했다. 그렇다면 인코더의 셀프 어텐션과 디코더의 셀프 어텐션간의 차이는 무엇이 있을까? 유일한 차이는, 디코더 어텐션의 경우 마스크 매트릭스를 이용해서 예측 시 시점 상 뒤의 단어의 정보를 이용하지 않는다는 차이점이 있다. 이는 위 그림 왼편 빨간 상자의 encoder self-attention을 도식화한 그림과 masked decoder self-attention을 도식화한 그림의 정보 이용을 나타내는 "화살표"를 보면 쉽게 이해할 수 있다.

마지막으로, 1.2.3. 세 attention 모두 멀티 헤드 어텐션인데, 이(multi-head)는 어텐션을 병렬적으로 수행하는 방식을 나타내는 것이다.

그러면, 이제 Transformer를 직접 구현해보자.

2. 트랜스포머(코드)

1) Positional Encoding 함수 구현

A) 수식

transformer는 각 단어의 임베딩 벡터에 위치정보를 나타내는 positional encoding 값을 더해주는 방식으로 RNN 기반이 아님에도 위치 정보를 학습할 수 있다.
포지셔널 인코딩 값을 구하는 수식은 좌측 하단의 빨간 박스를 보면 된다. 여기서 사용되는 1. pos, 2. i, 3. dmodel 은 무엇인가?

  1. pos는 입력 문장에서의 임베딩 벡터의 위치를 나타낸다.
  2. dmodel은 임베딩 벡터의 차원을 의미한다. 이는 동시에 트랜스포머 모든 층의 출력 차원이다.
  3. i는 임베딩 벡터 내의 차원의 인덱스를 의미한다.

그래서 예시를 들어보면, pos는 문장 길이가 50일 때 50개의 값을 가질 수 있을 것이고, i는 임베딩 벡터의 차원(dmodel)이 128이라면 128개의 값을 가질 수 있을 것이다. 그래서 빨간 박스를 보면, 임베딩 벡터의 차원의 인덱스(i)가 짝수라면, 사인함수를 이용하고, 홀수라면 코사인함수를 이용해서 포지셔널 인코딩 값을 얻어내고 있다.

이렇게 얻어낸 포지셔널 인코딩 값 행렬과 문장 벡터 행렬을 덧셈해서 순서 정보가 보존된 transformer input이 완성된다.

+) 그런데, 왜 사인함수/코사인 함수인가?
포지셔널 인코딩이 어떤 조건을 만족해야 하는지에 답이 있다.

"어머님 는 별 하나 에 아름다운 말 한마디 씩 불러 봅니다."
"소학교 책상 을 같이 했 던 아이 들 의 이름 과 패 경 옥 이런 이국 소녀 들 의 이름 과 벌써 애기 어머니 된 계집애 들 의 이름 과 가난 한 이웃 사람 들 의 이름 과 비둘기 강아지 토끼 노새 노루 프란시스 쟘 라이너 마리아 릴케 이런 시인 의 이름 을 불러 봅니다"

일단, 하나 이상 단어의 위치가 같을 수는 없기 때문에, 각 위치마다 유일한 값을 출력해야 한다. 이에 추가로, 길이가 다른 문장의 단어 위치를 나타낼 때에도 단어의 상대적인 거리가 같으면 같은 차이를 보여야 한다.

그런데 "1, 2, 3, 4, ..." 처럼 단어 순서대로 고유한 positional encoding 값을 부여하면 긴 문장에서 맨 뒤에 위치한 토큰의 값이 매우 커지게 된다. 이렇게 포지셔널 인코딩 값이 매우 커지게 되면, 원래의 인풋 임베딩 값에 영향을 주게 된다.

그렇다면 범위를 정해놓고 등분하여 나타내면 되지 않을까? 해서 [0,1] 범위를 단어 개수만큼 등분해서 positional encoding 값을 나타내 보면, 문장 길이, 즉 문장을 구성하는 단어 개수에 따라 상대적인 거리 관계가 깨지게 된다. 즉, 위 짧은 문장에서 어머님, 그리고 긴 두번째 문장에서 소학교는 모두 바로 옆 단어이지만 첫번째 문장에서의 거리 차이는 0.091이고 두 번째 문장에서의 거리 차이는 0.018으로 5배나 차이가 나게 된다. 그래서 논문에서는 조건을 모두 만족시키는 사인 코사인 함수를 사용한 것이다.

B) 코드


포지셔널 인코딩 함수의 구현이다.

def get_angles부터 보자.
get angles 함수는 position, i, d_model의 파라미터를 가진다. A) 수식 파트에서 언급했듯이 position은 입력 문장에서의 임베딩 벡터의 위치, i는 임베딩 벡터 내의 차원의 인덱스, d_model은 임베딩 벡터의 차원을 의미한다.(사실상 트랜스포머의 모든 층의 출력 차원이다)

그래서 get_angles에서는 pos, i, d_model 값을 받아서 좌측 상단 Positional Encoding 값을 구하는 수식을 sin/cos 씌우기 전까지 계산해주고 있다.

  • tf.pow = 거듭제곱 값 계산
  • tf.cast = 자료형 형변환

그리고 positional_encoding def에서 get_angles를 호출해서 구해진 angle 중 짝수 인덱스에는 사인 함수, 홀수 인덱스에는 코사인 함수를 씌워 positional encoding 수식을 구현 완성하고 있다.

  • tf.newaxis = shape 간단히 바꿔주는 역할. (50,)->(50,1)

생각보다 포스트가 길어졌다. 다음 포스팅에서 transformer 3가지 멀티헤드 어텐션 설명/구현, 3가지 멀티헤드 어텐션을 이용해 최종적으로 인코더/디코더까지 쌓고 transformer 포스팅 마치도록 하겠다:)

profile
NLP 전공 잡식성 문헌정보 석사생

0개의 댓글