Positional Encoding 개념을 이해하고 code implementation을 보던 중 논문과 구현 방법이 달라 찾아보았다.
class PositionalEncoding(nn.Module):
def __init__(self, d_embed, max_len=256, device=torch.device("cpu")):
super(PositionalEncoding, self).__init__()
encoding = torch.zeros(max_len, d_embed)
encoding.requires_grad = False
position = torch.arange(0, max_len).float().unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_embed, 2) * -(math.log(10000.0) / d_embed))
encoding[:, 0::2] = torch.sin(position * div_term)
encoding[:, 1::2] = torch.cos(position * div_term)
self.encoding = encoding.unsqueeze(0).to(device)
기존의 수식에는 없던 로그 함수와 지수함수가 들어간다.. 이해가 안돼서 찾아봤는데 positional encoding 구현 설명, 이 링크에 설명이 되어 있었다. 조금 이해가 덜 돼서 chatgpt에 물어봤는데 60프로 정도 이해가 된 것 같다.
이유 1) computation의 안정성을 위해
-> 논문에 써있는 10000은 충분히 큰 숫자를 나타낸다. 이런 큰 숫자가 밑인 지수함수를 연산했을 때 floating point에 대한 정확도 이슈가 있다. 때문에 log를 씌워줌으로써 연산값을 축약한다.
이유 2) 계산 효율성
링크에 있는 설명에선 이런 log, 지수 연산이 아닌 논문 그대로의 방법을 했을 때는 nested for문이 들어가게 된다. 하지만 위의 div_term 변수에 저장된 값을 통해서 for문 없이 동일 연산을 할 수 있어 효율적이라고 한다. 왜 논문 그대로의 방식에선 for문 없이 사용할 수 없는지 모르겠는데 나중에 좀 더 찾아봐야겠다.
수식의 변환은 다음과 같이 이뤄진다.
지수 함수를 - 표현식으로 변경한 후 자연상수의 지수함수와 log를 동시에 취한다.
log를 씌웠을 때 지수 부분을 log 밖으로 빼면 pytorch implementation에서 원하는 수식으로 바뀌게 된다.
이제 이해가 됐으니 처음부터 line-by-line으로 주석을 달아본다.
token size인 max_len = 256, d_embed = 512로 가정하고 예시를 설명한다.
encoding = torch.zeros(max_len, d_embed)
encoding 변수에 shape이 (256,512)의 0으로 구성된 tensor를 저장한다. 나중에 이 값에 indexing을 통해 encoding 값을 할당받는다.
encoding.requires_grad = False
requires_grad 값이 false인 이유는 positional encoding은 고정된 값으로 back propagation의 대상이 아니기 때문이다.
position = torch.arange(0, max_len).float().unsqueeze(1)
(256,1) shape을 갖고 0~256으로 구성된 tensor를 생성한다. 지수함수에 곱해주기 위해 shape을 바꿔려고 unsqueeze method를 썼다.
div_term = torch.exp(torch.arange(0, d_embed, 2) * -(math.log(10000.0) / d_embed))
position 값에 곱해줄 logarithmic 표현이다.
encoding[:, 0::2] = torch.sin(position * div_term)
encoding[:, 1::2] = torch.cos(position * div_term)
0에서 시작해 interval을 2로 두는 방식으로 tensor에 대한 indexing을 하여 2i(짝수)번째 차원에 sin 함수의 결과 값을 할당한다. 두번째 라인 또한 홀수 차원에 대해 indexing하여 값을 할당하는 방식이 동일하다.
self.encoding = encoding.unsqueeze(0).to(device)
embedding vector는 3차원으로 구성되기 때문에(첫번째 차원은 단어 배치) unsqueeze로 앞쪽에 차원을 생성한다. 그럼 tensor의 shape이 [1,256, 512]이 된다.