본문 바로가기
AI/자연어처리

Transformer (33)

by 바다의 공간 2024. 12. 31.

▶ Transformer 트랜스포머

-2017년 구글브레인이 발표한 논문인 "ATTENTION is all you need"에서 나온 모델입니다.

https://proceedings.neurips.cc/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf

 

- 트랜스포머는 RNN을 사용하지않고 '인코더'와 '디코더'를 설계하였고 성능도 RNN보다 우수합니다.

처음에는 자연어 처리 분야에서만 사용되었으나 이후에는 컴퓨터비전분야까지 확정되고있고 지금은 다양한 분야에서 채택되고있음!

 

- 기존의 seq2seq의 구조인 인코더-디코더를 따르면서도 논문의 이름처럼 (attention)만으로 구현한 모델입니다.

- 이후 등장한 BERT , GPT, AlphaFold2 등이 Transformer 기반으로 만들어졌습니다.

 


▶ 기존의 seq2seq 모델의 한계..

r seq2seq 모델의 한계.. 기

기존의 seq2seqsms 인코더-디코더로의 구조로 구성되어있습니다.

- 인코더는 입력 시퀀스를 받아서 하나의 벡터 표현으로 압축을 하고

- 디코더는 이 벡터를 통해서 출력 시퀀스를 만들어냈습니다.

 

그래서 단점은

- 인코더가 입력 시퀀스를 하나의 벡터로 압축하는 과정에서 입력 시퀀스의 정보가 일부 손실된다는 단점(vanishin gradient)가 있었고 이를 보정하기 위해서 attention(어텐션)이 사용되었습니다.

 

어텐션을 rnn 보정을 위한 용도로 사용하는 것이 아니라 어텐션만으로 인코더와 디코더를 만들어보면 어떨까? 했던것이 트랜스포머의 아이디어입니다.


▶ Transformer의 주요 하이퍼 파라미터

주요 하이퍼 파라미터 수

수식이 있어서 이미지로 대체해서 첨부했습니다.


▶ Transformer 대략적인 구조

구조

트랜스포머는 RNN을 사용하진 않지만 기존의 Seq2Seq처럼 인코더-디코더 구조를 유지합니다.

- 인코더에서 입력 시퀀스를 입력

- 디코더에서 출력 시퀀스를 출력

 

이전 seq2seq구조에서는 인코더와 디코더에서 각각 하나의 rnn이 t개의 시점(타임스텝)을 가지는 구조였다면

트랜스포머에서는 인코더와 디코더라는 단위가 n가로 구성되는 구조(이를 레이러라고함)입니다.

논문에서는 인코더 디코더의 개수를 각각 6개 layer사용합니다.

 

 

인코더, 디코더가 각각 6개씩 존재하는 구조로 논문에서는 논했습니다.

이를 encoders, deoders라고 표현하겠습니다.

 

 

 

 

 

 

 

 

 

트랜스포머의 입력

- 트랜스포머는 인코더와 디코더는 단순히 각 단어의 임베딩 벡터들을 입력받는것이 아니라

임베딩벡터에서 '조정된 값'을 입력받습니다 

그 이유는 위치 정보가 필요하기때문이죠.

 


▶ 포지셔널 인코딩(Positional Encoding)

 

포지셔널 인코딩

RNN이 자연어 처리에서 유용했던 이유는 단어의 위치에 따라 단어를 순차적으로 입력받아서 처리하는 RNN 특성으로 인해 각 단어의 위치정보(Position Information)을 가질 수 있다는점입니다.

그러나...트랜스포머는 단어 입력을 순차적으로 받는 방식이 아니니까 단어의 위치 정보를 다른 방법으로 알려줘야겠죠?

그래서 각 단어의 '임베딩 벡터'에  '위치정보'들을 더해서 모델의 입력으로 사용하는데

이것을 포지셔널 인코딩이라고합니다.

인코딩값이 더해지는 과정을 시각화

 

임베딩벡터가 인코더의 입력으로 사용되기 전 포지셔널 인코딩값이 더해지는 과정을 시각화해 본 결과입니다.

포지셔널 인코딩 값들은 어떤 값이기에 위치정보를 반영해주냐면 사인 코사인 이반영된 PE(pos,2i) / PE(pos,2i+1)을 사용합니다.

 

사인과 코사인 함수의 그래프를 생각하면 좀 요동치는 값을 생각할 수 있는데

트랜스포머는 사인함수와 코사인 함수의 값을 임베딩 벡터에 더해주므로서 단어의 순서정보를 더해서 줍니다.

pos, i ,dmodel등의 생소한 변수들이 있습니다 이것을 이해하기 위해서는 위에서본 임베딩 벡터와 포지셔널 인코딩의 덧셈은 사실 인베딩 벡터가 모여서 만들어진 문장행렬과 토지셔널인코딩 행렬의 덧셈 연산을 통해 이루어진다는점이 있습니다.

 

pos는 입력문장에서의 임베딩 벡터의 위치

i는 임베딩 벡터 내의 차원의 인덱스

위의 식으로 보면 임베딩 벡터 내의 각 차원의 인덱스 i가 짝수인경우 사인, 홀수인 경우 코사인을 사용

  • (pos,2i) 일 때는 사인 함수를 사용!
  • (pos,2i+1)일 때는 코사인 함수를 사용!

위 처럼 볼 수 있습니다.

위의 식에서 dmodel은 트랜스포머의 모든 층의 출력 차원을 의미하는 트랜스포머의 하이퍼파라미터라고 했죠

앞으로 트랜스포머의 구조에서 dmodel값이 계속해서 등장합니다..

임베딩 벡터 또한 dmodel의 차원을 가지는데 위의 그림에서는 마치 4로 표현되었지만 실제노문에서는 512값을 가집니다.


▶ Positional Encoding 구현

케라스를 사용할거고 기본 임포트와  다시 돌릴때도 동일한 값이 나오게끔 고정해줄것입니다.

# 기본 import
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import os

import tensorflow as tf
from tensorflow import keras

tf.keras.utils.set_random_seed(42)
tf.config.experimental.enable_op_determinism()

 

케라스에서는 model을 상속받아 커스텀model정의가 가능하고 

layer도 상속받아 커스텀 layer을 정의할 수 있기에 클래스를 정의해보겠습니다.

class PositionalEncoding(tf.keras.layers.Layer):
  def __init__(self, position, d_model):
    super(PositionalEncoding, self).__init__()
    self.pos_encoding = self.positional_encoding(position, d_model)

  # sin, cos 에 넘겨줄 각도 (radian)
  def get_angles(self, position, i, d_model):
    angles = 1 / tf.pow(10000, (2 * (i // 2)) / tf.cast(d_model, tf.float32))
    return position * angles

  # position, d_model => PE 결괏값
  def positional_encoding(self, position, d_model):

    # position, d_model => angle
    angle_rads = self.get_angles(
        position=tf.range(position, dtype=tf.float32)[:, tf.newaxis],
        i=tf.range(d_model, dtype=tf.float32)[tf.newaxis, :],
        d_model=d_model)

    # 배열의 짝수 인덱스(2i) 에는 sin 함수 적용
    sines = tf.math.sin(angle_rads[:, 0::2])

    # 배열의 홀수 인덱스(2i+1) 에는 cos 함수 적용
    cosines = tf.math.cos(angle_rads[:, 1::2])

    angle_rads = np.zeros(angle_rads.shape)
    angle_rads[:, 0::2] = sines
    angle_rads[:, 1::2] = cosines

    pos_encoding = tf.constant(angle_rads)
    pos_encoding = pos_encoding[tf.newaxis, ...]

    print(pos_encoding.shape)
    return tf.cast(pos_encoding, tf.float32)

  def call(self, inputs):
    return inputs + self.pos_encoding[:, :tf.shape(inputs)[1], :]

 

동작확인을 하면 50*128 크기를 가지는 포지셔널 인코딩 행렬이 시각화되고 문장의 단어가 50개

각 단어의 표현은 128개 임베딩 벡터가 있습니다.

 

sample_pos_encoding = PositionalEncoding(50, 128)

 

을보면 (1,50,128)이 나오는것을 볼 수 있습니다. 또한 시각화까지 해보면

plt.pcolormesh(sample_pos_encoding.pos_encoding.numpy()[0], cmap='RdBu')
plt.xlabel('Depth')
plt.xlim((0, 128))
plt.ylabel('Position')
plt.colorbar()
plt.show()

 


속도가 많이 다르기때문에  GPU사용 권장합니다.

현재 코랩이 2.27을 사용하는데 2.12.0으로 다운해서 사용했다는점 참고해주시면서 읽어주세요.

!pip install tensorflow==2.12.0

▶ 어텐션(Attention)세가지 형태

세가지 형태

 

encoder self-attention는 '인코더'에서 이루어진다

masked decoder self-attention과 encoder decoder attention은 '디코더'에서 이루어진다

xxx self attention은 본질적으로 qurey key value가 동일한 출처인 경우를 말합니다.

- 여기서 동일하다는 뜻은 벡터값이아닌 출처가 같다는 의미입니다

 

그러나 세번째 그림인 encoder-decoder attention에서는 

-qurey가 '디코더의 벡터'인 반면에

-key, value가 '인코더의 벡터'이므로 셀프 어텐션이라고 부르지 않습니다.

정리하면

인코더의 셀프어텐션 : qurey = key = value
디코더의 마스크드 디코더 셀프 어텐션 : qurey = key = value
디코더의 인코더-디코더 어텐션 : qurey : 디코더벡터 / key=value : 인코더벡터

로 볼 수 있습니다.


▶ 트랜스포머 아키텍쳐

트랜스포머 아키텍쳐

* 입력받는 방식, 위치를 잘 보면 encoder, decoder에서 각각 받아오는것을 확인할 수 있음.

참고로 각각 1개층(num_layers)씩 표현한것입니다.

 

# multi-head : 어텐션이 병렬로 수행됨.

attention  = 장기기억을 할 수 있도록 도와주고 활용하는 장치


▶ encoder의 구조

트랜스포머는 하이퍼파라미터인 num_layers 개수의 인코더 층을 쌓습니다.

논문에서는 총 6개의 인코더 층을 사용했습니다.

 

인코더는 하나의 층이라는 개념으로 생각한다면 하나의 인코더 층은 크게 2개의 서브층(sublayer)으로 나뉘어집니다.(셀프어텐션+피드포워드 신경망)

 

왼쪽 그림에서 멀티 헤드 셀프 어텐션(multi-head self attention)과 포지션 와이즈 피드 포워드 신경망(FNN)이라고 적혀있는데 이건 '멀티헤드'셀프 어텐션은 셀프 어텐션을 '병렬적'으로 사용하였다는 의미고, 포지션 와이즈 피드 포워드 신경망은 우리가 알고있는 FNN입니다

 

 

 


▶인코더의 셀프 어텐션

여기서 복습을 하나 하고가자면 

 

1 ) 셀프 어텐션의 의미와 이점 : 

 

 

어텐션 함수는 주어진 '쿼리(Query)'에 대해서 모든 '키(Key)'와의 유사도를 각각 구합니다. 그리고 구해낸 이 유사도를 가중치로 하여 키와 맵핑되어있는 각각의 '값(Value)'에 반영해줍니다. 그리고 유사도가 반영된 '값(Value)'을 모두 가중합하여 리턴합니다.

 

 

 

여기까지는 앞서 배운 어텐션의 개념입니다.

그런데 어텐션 중에서는 셀프어텐션(self-attention)이라는것이 있습니다. 어텐션을 자기 자신에게 수행한다는말인데 앞에서 배운 seq2seq에서 어텐션을 사용할 경우의 qkv를 다시 생각해보자면

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

 

여기서 t시점(타임스텝)이라는것은 계속 변화하면서 반복적으로 쿼리를 수행하므로 결국 전체 시점에 대해 다음과 같이 일반화를 할 수 도있습니다. (아래표)

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

이처럼 기존에는 디코더 셀의 은닉상태가 q이고 인코더 셀의 은닉 상태가 k라는점에서 q와 k가 서로 다른 값을 가지고있습니다.

그런데 셀프어텐션은 qkv가 모두 동일합니다. 

트랜스포머의 셀프어텐션에서의 qkv 는 아래와 같습니다.

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

 

 

이걸 왜쓰냐면 예시를 보면서 말할 수 있습니다.

좌측은 셀프어텐션을 통해 얻을 수 있는 대표적인 효과입니다

 

사람은 금방알지만 기계는 모르겠죠?

self attention을 통해서 it이 가르키는것이 어떤것인지 알아낼 수 있습니다.

연관도를 알 수 있거죠. 무엇을 지칭하는지 어떤것을 참조하는지 알 수 있어요.

 

일반attention은 못했던것을 self-attention 은 찾아낼 수 있습니다.

 

 

 

 


2) Q, K, V 벡터 얻기

Q : QUREY
K : KEY
V: VALUE

 

앞서 셀프 어텐션은 입력 문장의 단어 벡터들을 가지고 수행한다고 했는데

사실 셀프 어텐션은 인코더의 초기 입력인 Dmodel의 차원을 가지는 단어 벡터들을 사용하여 셀프 어텐션을 수행하는것이아니라 우선 각 단어 벡터들로부터 q벡터 k벡터 v벡터를 얻는 작업을 거칩니다.

 

이때 q벡터 k벡터 v벡터들은

초기 입력인 디모델의 차원을 가지는 단어 벡터들보다 더 작은 차원을 가집니다.

논문에서는 512개의 차원을 가졌던 각 단어 벡터들을 (차원의 크기가)64을 가지는 q,k,v벡터로 변환했습니다.

 

 

 

64라는 값은 트랜스포머의 또 다른 하이퍼파라미터인 num_heads로 결정되는데

트랜스포머는 디모델을 넘헤드로 나눈 값을 q,k,v벡터의 차원으로 결정합니다.

논문에서는 넘헤드를 8로했으니 (512/8=64)가 됩니다.

 

예를들어서 여기서 사용하고 있는 예문중 student라는 단어 벡터를 

q,k,v 의 벡터로 변환하는 과정을 보겠습니다.

 

기존의 벡터로부터 더 작은 벡터는 '가중치 행렬'을 곱하며 완성됩니다.

곱해지는 각 ' 가중치 행렬'의 크기는 디모델 *(디모델/넘헤드스)입니다.

가중치행렬은 훈련과정에서 학습됩니다.

즉 논문과 같이 디모델이 512고 넘헤드스가 8이라면

각 벡터에 3개의 서로 다른 가중치 행렬을 곱하고 64의 크기를 가지는 QKV벡터를 얻어냅니다.

위 그림은 단어벡터중 STUDENT벡터로부터 QKV벡터를 얻어내는 모습을 보여줍니다.

모든 단어 벡터에 위와 같은 과정을 거치면 I, AM, STUDENT 이라는 각각의 Q K V 벡터를 얻습니다.

 

3) 스케일드 닷-프로덕트 어텐션(Scaled dot product attention)

qkv벡터를 얻었다면 지금부터는 기존에 배운 어텐션 메커니즘과 동일합니다.

각q벡터는 모든 k벡터에 대해서 어텐션 스코어를 구하고 어텐션 분포를 구한 뒤 

이를 사용하여 모든 v벡터를 가중합하여 ' 어텐션값' 또는 컨텍스트 벡터를 구하게됩니다.

그리고 이것을 모든 q백터에 대해서 반복합니다.

 

Attention Score 구하기

단어 시퀀스 : I, AM, Student

우선 단어 i에 대한 q벡터를 기준으로 설명을 남겨두겠습니다.

지금부터 설명하는 과정은 am,a,student에 대한 q벡터에서도 모두 동일한 과정을 거칩니다.

 

위의 그림은 단어 "I"에 대한 Q벡터가 모든 K벡터에 대해서 어텐션 스코어를 구하는 것을 보여줍니다.
※위의 128과 32는 그림 예시에서 임의로 가정한 수치로 신경쓰지 않아도 좋습니다.
위의 그림에서 어텐션 스코어는 각각 단어 "I"가 단어 "I", "am", "a", "student"와 얼마나 연관되어 있는지를 보여주는 수치입니다.
트랜스포머에서는 두 벡터의 내적값을 스케일링하는 값으로 K벡터의 차원을 나타내는  dk 에 루트를 씌운  dk−−√  사용하는 것을 택했습니다.
앞서 언급하였듯이 논문에서  dk  는  dmodel/num_heads  라는 식에 따라서 64의 값을 가지므로  dk−−√ 는 8의 값을 가집니다.

Attention Value 구하기

이제 어텐션 스코어에 소프트맥스 함수를 사용하여 어텐션 분포(Attention Distribution)을 구하고, 

각 V벡터와 가중합하여 어텐션 값(Attention Value)을 구합니다.


이를 단어 "I"에 대한 어텐션 값 또는 단어 "I"에 대한 컨텍스트 벡터(context vector)라고도 할 수 있습니다.
am에 대한 Q벡터, a에 대 Q벡터, student에 대한 Q벡터에 대해서도 모두 동일한 과정을 반복하여 각각에 대한 어텐션 값을 구합니다.

 

입력된크기와 출력된 크기는 같습니다. 지금은 설명하느라 각각 과정을 보여주지만 

사실 이 연산은 행렬연산으로 일괄처리가 됩니다.

 

4) 행렬연산으로 일괄처리하기

 

우선, 각 단어 벡터마다 하나하나 가중치 행렬을 곱하는것이 아니라

문장에 가중치행렬을 곱해서 Q K V의 행렬을 구합니다.

 

 

 

 

 

 

 

 

 

 

 

행렬 연산을 통해 어텐션 스코어는

어떻게 구할수있을까요? 

여기서 Q행렬을 K행렬을 전치한 행렬과

곱해준다고하면 

각각 단어의 Q벡터와 K백터의 내적이

각 행렬의 원소가 되는 행렬이 결과로 나옵니다.

 

즉 다시말해 위의 그림의 결과 행렬값에 루트 디케이를 나누어주면 각 행과 열이

어텐션 스코어 값을 가지는 행렬이 됩니다.

 

EX) i 행과 student열의 값은 i의 q벡터와 student의 k벡터의 어텐션 스코어값이라는거죠

 

위 행렬을 어텐션 스코어행렬이라고 합시다.

 

어텐션 스코어행렬을 구하였다면 남은것은 어텐션분포를 구하고

이를 사용하여 모든 단어에 대한 어텐션 값을 구하는 일입니다.

 

이는 간단하게 어텐션 스코어 행렬에 소프트맥스 함수를 사용하고 v행렬을 곱하는 것으로 해결됩니다.

이렇게 되면 각 단어의 어텐션 값을 모두 가지는 어텐션 값 행렬이 결과로 나옵니다

 

 

위의 그림은 행렬 연산을 통해 모든 값이 일괄 계산되는 과정을 식으로 보여줍니다.

 


다음에는 스케일드 닷-프로덕트 어텐션 구현해보도록 하겠습니다!