트랜스포머·PEFT 2일 과정2장 · Transformer 입력 표현9 / 15← 이전목차다음 →MetaContents

2장. Transformer 입력 표현

사인·코사인 위치 인코딩

시계를 보면 초침, 분침, 시침이 서로 다른 빠르기로 돕니다. 초침만 보면 1분마다 같은 자리로 돌아와 몇 분인지 모르지만, 세 바늘을 함께 보면 하루 안의 모든 순간이 서로 다른 모양이 됩니다. 원 논문의 사인·코사인 위치 인코딩도 같습니다. 벡터의 앞쪽 칸은 빠르게 출렁이는 물결(초침), 뒤쪽 칸은 느리게 출렁이는 물결(시침)을 써서, 위치마다 고유한 무늬를 만듭니다.

식은 다음과 같습니다. pos 는 단어의 위치(0, 1, 2, ...), i 는 칸 번호의 짝(0, 1, 2, ...)입니다.

출력
PE(pos, 2i)   = sin( pos / 10000^(2i / d_model) )   짝수 칸은 사인
PE(pos, 2i+1) = cos( pos / 10000^(2i / d_model) )   홀수 칸은 코사인
위치 0~49 의 사인·코사인 위치 인코딩을 512차원으로 계산한 히트맵. 행은 위치, 열은 차원

그림은 위치 0~49 를 512차원 벡터로 계산한 표를 색으로 칠한 것입니다. 행이 토큰의 위치, 열이 벡터의 칸이고 색은 값의 크기와 부호입니다. 왼쪽 칸들은 위아래로 색이 빠르게 바뀌고(빠른 물결), 오른쪽으로 갈수록 거의 같은 색이 이어집니다(느린 물결). 그래서 어느 두 행도 똑같은 줄무늬가 되지 않습니다.

예시 토큰 임베딩 세 줄에 위치 0, 1, 2 의 인코딩을 더해 Transformer 입력을 만드는 세 단 히트맵

이 그림은 첫째~셋째 토큰의 임베딩(위)에 위치 0, 1, 2 의 벡터(가운데)를 더해 Transformer 의 입력(아래)을 만드는 모습입니다. 위쪽 임베딩 값은 덧셈을 보여 주려고 만든 예시이고, 세 그림은 같은 색 눈금을 씁니다.

식에 숫자 넣어 보기 (d_model = 4)

위치 pos칸 0: sin(pos/1)칸 1: cos(pos/1)칸 2: sin(pos/100)칸 3: cos(pos/100)
00.0001.0000.0001.000
10.8410.5400.0101.000
20.909-0.4160.0201.000
30.141-0.9900.0301.000

d_model 이 4일 때 i = 0 이면 10000^(0/4) = 1 이라 pos 를 그대로 넣고, i = 1 이면 10000^(2/4) = √10000 = 100 이라 pos 를 100으로 나눠 넣습니다. 위치 1의 칸 0은 sin(1) ≈ 0.841, 칸 2는 sin(0.01) ≈ 0.010 입니다. 칸 0~1 은 한 칸 옮길 때마다 크게 바뀌는 초침, 칸 2~3 은 거의 움직이지 않는 시침입니다.

짧은 예시

Python
import numpy as np

def positional_encoding(max_len, d_model):
    # pos: (max_len, 1) 세로 기둥, i: (1, d_model/2) 가로 줄. 둘을 나누면 (max_len, d_model/2) 표가 된다
    pos = np.arange(max_len)[:, None]
    i = np.arange(d_model // 2)[None, :]
    angle = pos / (10000 ** (2 * i / d_model))
    pe = np.zeros((max_len, d_model))
    pe[:, 0::2] = np.sin(angle)   # 짝수 칸(0, 2, 4, ...)은 사인
    pe[:, 1::2] = np.cos(angle)   # 홀수 칸(1, 3, 5, ...)은 코사인
    return pe

# 위의 표를 그대로 계산한다
print(positional_encoding(4, 4).round(3))
# 결과:
# [[ 0.     1.     0.     1.   ]
#  [ 0.841  0.54   0.01   1.   ]
#  [ 0.909 -0.416  0.02   1.   ]
#  [ 0.141 -0.99   0.03   1.   ]]

# 히트맵 그림과 같은 크기: 위치 50개 × 512칸
pe = positional_encoding(50, 512)
print(pe.shape, pe.min().round(3), pe.max().round(3))  # 결과: (50, 512) -1.0 1.0
# 50개 위치의 줄무늬가 모두 다른지 센다(np.unique 로 겹치지 않는 행의 개수)
print(len(np.unique(pe.round(6), axis=0)))              # 결과: 50

이 코드가 뜻하는 것

초침, 분침, 시침을 512개 겹친 시계를 만든 코드입니다. 표의 값은 모두 -1~1 사이라서 임베딩에 더해도 뜻 정보를 크게 해치지 않습니다. 마지막 줄의 50은 위치 50개가 모두 다른 무늬를 가졌다는 뜻입니다. NumPy 의 브로드캐스팅(기둥 ÷ 줄 = 표)으로 반복문 없이 표 전체를 한 번에 채웁니다.

줄하는 일쉽게 말하면
pos / (10000 ** (2*i/d_model))칸마다 다른 빠르기로 각도를 만든다바늘마다 다른 속도
pe[:, 0::2] = np.sin(angle)짝수 칸에 사인을 넣는다바늘의 가로 위치
pe[:, 1::2] = np.cos(angle)홀수 칸에 코사인을 넣는다바늘의 세로 위치
np.unique(..., axis=0)겹치지 않는 행을 센다모든 시각이 다른지 확인

주의할 점

  • 사인·코사인 위치 인코딩은 학습하지 않는 고정 함수입니다. BERT 는 위치마다 벡터를 학습하는 방식(최대 512 위치)을 썼고, Qwen 같은 최신 LLM 은 RoPE(Rotary Position Embedding, 회전 위치 임베딩)를 씁니다. Qwen 의 설정표에 있는 rope_theta 가 그 물결의 빠르기를 정하는 값입니다. 방식은 달라도 "위치마다 다른 정보를 준다"는 목적은 같습니다.
  • 위치 인코딩은 학습할 때 본 길이보다 훨씬 긴 문장에서 약해질 수 있습니다. 모델마다 정해진 컨텍스트 길이를 넘기지 않습니다.
← 이전 절2장 · 위치 정보가 필요한 이유다음 절 →2장 · 실습 지도