이번 포스팅에선 Positional Encoding과 Embedding에 대해 정리해보고자 한다.
Positional Encoding이 필요한 이유는 간단하다.
Transformer 이전 Seq2Seq 모델들을 확인해보자. Recurrent와 Convolution을 사용하는 이 모델들은 Input을 순차적으로 (Sequential) 처리하고, 이 처리하는 순서는 문장을 해석하는 데에 큰 영향을 주게 된다. 하지만 이런 Recurrent, Convolution을 버리고 Self-Attention 등 Attention만으로 문장을 해석하는 Transformer는 이런 문장 내 단어의 순서를 반영할 수가 없다. (Attention은 간단히 말하면 내적이다.) 이러한 점을 해결하기 위해 구글의 연구진들이 사용한 것이 Positional Encoding이다.
논문의 언급에 따르면 Positonal Encoding에는 여러 방법이 있다.
https://arxiv.org/pdf/1705.03122
(Convolutional Sequence to Sequence Learning)
특히 위 논문을 인용하면서, Positonal Encoding, 나아가 Positional Embedding에 방법이 어느 정도 연구가 끝나고 고착화되어있다는 식으로 언급한다.
이 부분이 <Convolutional Sequence to Sequence Learning>에서 Positoin Embedding에 대해 다룬 부분이다. 확인해보면 이 논문에선 단어의 절대 위치(absolute position)을 반영하여 임베딩 벡터와 더하는 식으로 Positional Embedding을 구현하였다.
이 방법이 얼마나 좋은지까진 잘 모르겠는데 <Attention is All You Need>를 확인해보면 이 방법을 쓰든 앞으로 소개할 방식을 쓰든 결과는 크게 차이는 없었다고 한다.
그래서 트랜스포머에서 사용하는 Positinal Encoding 방식은 바로 sinusoid, 굳이 한국어로 하자면 '정현파'이다. 다른 뜻은 아니고 그냥 우리가 싸인, 코싸인 함수에서 보는 그런 물결파다.
논문에 소개되어 있는 Positional Encoding 수식이다.
pos는 Input Sequence에서 단어의 위치를, i는 차원의 위치를, d_model은 모델의 차원을 의미한다. 이렇게 PE 벡터들이 쭉 생겨나면, 이를 Embedding layer에서 생성된 Input Matrix, 즉 Embedding 벡터들과 더해준다. 이 과정이 Positional Embedding이다. (인코딩과 임베딩은 엄연히 구분된다.) 이때 Embedding 벡터의 차원 수는 모델의 차원 수와 같으므로 합연산이 가능하다.
수식을 좀 더 자세히 파헤쳐보자. 차원의 위치를 인덱스라는 표현으로 바꿔보겠다. 모델의 차원(d_model = Embedding) 이 512라면, i는 0부터 255까지 존재할 수 있다. 그리고 input sequence length가 100이라면 pos 는 0부터 99까지 존재하게 된다. i = 0, pos = 0인 상황을 생각해보면, PE(0,0) 과 PE(0,1)은 각각 0, 1이 된다. (Sin 함수와 Cosine 함수 그래프 생각하면 됨)
이후 i = 0으로 고정시켜놓고, pos만 늘려간다고 생각해보자. 짝수 인덱스의 경우 PE(pos) = sin(pos) 와 항상 일치하게 되고, 홀수 인덱스의 경우 PE(pos) = cos(pos) 와 항상 일치한다. 즉 sin, cos 함수에 정수를 1씩 늘려가면서 넣게 되는 셈이다. 단, sin과 cos의 경우 주기가 2pi 이므로, 정수로 나누어 떨어지지가 않기 때문에 어느 position이건 값이 똑같은 경우가 발생하지 않는다.
다음으론 pos를 0으로 고정시켜보자. 이때는 i의 값에 관계없이 PE = sin(0), cos(0) 이 된다. 즉 pos = 0인 경우, 즉 첫 번째 입력 토큰에 대해선 값이 [0, 1, 0, 1 ... ] 인 것을 유추할 수 있다. 이 경우는 특이 케이스니까 우선 넘어가고 pos = 1 로 고정시켜보고 생각해보면, i를 증가시킬 때마다 sin 과 cos 함수의 주기가 급격하게 커진다. sin 과 cos 함수에서 주기는 변수 x 앞의 계수 a에 반비례하기 때문. 따라서 미세하게 i 에 따라서 값이 달라지게 될 것이다. i가 255에 가까워지면 주기가 20000pi,6만이 넘어가게 되는 셈이므로, 0부터 511까지의 값을 가지는 pos의 값의 변동에 따라 얼마 차이도 나지 않게 된다.
이를 근거로 한 PE의 시각화 자료다. 역시나 추측대로 depth, 즉 i가 커질수록 pos에 따라 별 값의 차이가 없는 모습이다.sin과 cos이 물결파 함수라 그런지 그림에서 파동이 보이기도 한다.
ChatGPT를 통해 Pytorch 를 사용하는 Positional Encoding의 예시를 하나 생성해보았다.
import torch
import torch.nn as nn
import math
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=0.1)
pe = torch.zeros(max_len, d_model)
# 토큰의 위치를 의미하는 position
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
#식이 다소 복잡하지만 나눠주는 연산을 곱셈으로 하다보니 log와 exp가 사용된 모습. 식은 논문의 식과 동일
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
#짝수 인덱스엔 Sin, 홀수 인덱스엔 Cos 함수 써주면서 최대한 토큰의 위치를 표현함
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0).transpose(0, 1)
self.register_buffer('pe', pe)
def forward(self, x):
x = x + self.pe[:x.size(0), :]
return self.dropout(x)
#임베딩과 합해지는 모습을 표현. 실제 트랜스포머 구현에선 이런 과정은 없고 인코더 레이어로 처리됨.
class EmbeddingWithPositionalEncoding(nn.Module):
def __init__(self, vocab_size, d_model, max_len=5000):
super(EmbeddingWithPositionalEncoding, self).__init__()
self.embedding = nn.Embedding(vocab_size, d_model)
self.pos_encoder = PositionalEncoding(d_model, max_len)
self.d_model = d_model
def forward(self, src):
src = self.embedding(src) * math.sqrt(self.d_model)
src = self.pos_encoder(src)
return src
Tensorflow로 구현한 예시는 아래 링크에서 가져왔다.
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)
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
def positional_encoding(self, position, d_model):
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)에는 사인 함수 적용
sines = tf.math.sin(angle_rads[:, 0::2])
# 배열의 홀수 인덱스(2i+1)에는 코사인 함수 적용
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], :]
슬슬 pytorch와 tensorflow의 다양한 함수들이 나오다 보니 헷갈리기 시작한다. 틈틈히 더 공부를 해야겠다.
정리
1. Positional Encoding을 위해 Sin, Cos 함수를 사용하여 단어 위치에 따라, 차원 깊이에 따라 값을 구하여 원래의 Embedding 벡터에 더해준다.
2. Attention Mechanism은 단어의 위치를 계산하지 않기 때문에 이 과정은 필수적이다.
'NLP' 카테고리의 다른 글
[NLP] 트랜스포머 구조 파악하기 (Attention is All You Need) (2) | 2024.06.11 |
---|---|
[NLP] Transformer의 Input은 어떻게 Embedding Vector로 변환되나? (0) | 2024.06.06 |
[NLP] 트랜스포머 사용 시 숫자 텍스트 데이터 전처리에 대해 (0) | 2024.04.18 |
[NLP Study] - LSTM (2) | 2024.03.22 |
[NLP Study] - RNN (0) | 2024.02.06 |