Transformer · 위치 인코딩
어텐션은 단어 순서를 모릅니다. 그래서 Transformer는 위치마다 고유한 사인·코사인 무늬를 임베딩에 더합니다. 그런데 왜 sin 하나가 아니라 sin과 cos를 짝으로 쓸까요? 답은 시계 바늘에 있습니다. 짝을 이룬 두 값은 원 위의 한 점이 되고, “k칸 뒤로 가기”는 그 점을 고정된 각도만큼 돌리는 일이 됩니다.
셀프 어텐션은 모든 단어 쌍의 내적을 구하고, 그 가중치로 값 벡터를 섞습니다. 이 계산 어디에도 “몇 번째 단어인가”는 들어가지 않습니다. 그래서 입력 순서를 섞으면 출력도 똑같이 섞여 나올 뿐, 각 단어가 받는 결과는 변하지 않습니다. dog bites man과 man bites dog가 단어마다 같은 표현을 받는다는 뜻입니다.
6차원 임베딩과 무작위 WQ·WK·WV로 실제 셀프 어텐션을 계산합니다. 오른쪽 숫자는 “원래 순서에서 같은 단어가 받았던 출력”과의 최대 차이입니다.
위치 인코딩을 켜면 같은 단어라도 몇 번째 자리에 있느냐에 따라 입력 벡터가 달라지고, 그 차이가 어텐션 출력에도 드러납니다.
「Attention Is All You Need」(Vaswani 외, 2017)는 위치 pos의 벡터를 이렇게 정합니다.
식을 주파수로 다시 쓰면 각 쌍은 그냥 \((\sin\omega_i\,pos,\ \cos\omega_i\,pos)\)입니다. 아래 히트맵은 행이 위치, 열이 차원입니다. 왼쪽 열들은 빠르게 깜빡이고 오른쪽으로 갈수록 천천히 변합니다.
파랑 = +1, 주황 = −1. 마우스를 올리면(또는 탭하면) 그 칸의 값이 나옵니다.
한 주파수만 놓고 보면 \(\sin x\)는 한 주기 안에서 같은 값을 두 번 냅니다(\(\sin x = \sin(\pi - x)\)). 값이 0.5라고 해도 올라가는 중인지 내려가는 중인지 알 수 없습니다. 짝이 되는 cos가 그 답을 줍니다. 두 지점에서 cos는 부호가 반대입니다.
주파수 ω 하나에 대해 sin(ω·pos)(파랑)과 cos(ω·pos)(주황)를 그렸습니다. 점선은 현재 위치의 sin 값이고, 빨간 점은 sin만 보면 현재 위치와 구분되지 않는 다른 위치입니다.
\(\sin^2\theta + \cos^2\theta = 1\)이므로 \((\sin\theta, \cos\theta)\)는 반지름 1인 원 위의 점입니다. 가로 좌표를 sin, 세로 좌표를 cos로 두면 \(\theta = 0\)일 때 점은 12시에 있고, θ가 커질수록 시계 방향으로 돕니다. 위치 인코딩의 한 쌍은 문자 그대로 시계 바늘입니다.
원 위의 점을 끌거나 슬라이더를 움직여 보세요. 점에서 내린 가로 길이가 sin θ(파랑), 세로 길이가 cos θ(주황)입니다. 두 변과 반지름(1)이 직각삼각형을 이루니 피타고라스 정리로 sin²θ + cos²θ = 1² = 1. 각도가 바뀌어도 반지름은 그대로 1이므로 점은 항상 원 위에 있습니다.
pos에서 pos+k로 가면 각도가 \(\omega_i k\)만큼 늘어납니다. 이 증가량에는 pos가 들어 있지 않습니다. 바늘이 지금 3시에 있든 8시에 있든 “30° 더 돌려라”라는 명령은 같습니다.
진한 점 = pos, 연한 점 = pos+k, 보라색 호 = 두 점 사이의 회전각 ωik. pos 슬라이더를 움직여 보세요. 바늘은 돌지만 호의 길이는 그대로입니다.
원 위의 점을 φ만큼 돌리는 연산은 2×2 회전 행렬 하나입니다. 덧셈 정리 \(\sin(a+b) = \sin a\cos b + \cos a\sin b\), \(\cos(a+b) = \cos a\cos b - \sin a\sin b\)를 행렬로 쓰면 다음과 같습니다.
Mk·PE(pos)를 계산해 PE(pos+k)와 비교합니다. pos를 바꿔도 Mk의 숫자는 하나도 안 바뀝니다.
sin만 있다면 어떨까요? \(\sin(\omega(pos+k))\)를 만들려면 \(\cos(\omega\,pos)\)가 필요한데, 그 값이 벡터에 없습니다. 아래 그림은 모든 pos에 대해 가로축 sin(ω·pos), 세로축 sin(ω(pos+k))를 찍은 것입니다. 점들이 직선이 아니라 타원을 그립니다. 같은 가로 값에 세로 값이 두 개 있으니 어떤 함수로도, 하물며 선형 함수로도 만들 수 없습니다.
파랑 점 = (sin ω·pos, sin ω(pos+k)). 주황 선 = 최소제곱으로 맞춘 최선의 직선.
쌍마다 \(\sin^2 + \cos^2 = 1\)이므로 위치 벡터 전체의 길이는 위치와 상관없이 항상 \(\sqrt{d_{model}/2}\)입니다. 두 위치 벡터의 내적은 쌍별로 \(\sin a\sin b + \cos a\cos b = \cos(a-b)\)가 되어
sin만 쓰면 사정이 다릅니다. pos=0이면 벡터 전체가 0이고, 위치마다 신호 세기가 들쭉날쭉하며, 내적도 기준 위치에 따라 모양이 바뀝니다.
고등학교에서 배운 이 공식이 위의 내용을 전부 설명합니다.
\(\cos(A-B) = \cos A\cos B + \sin A\sin B\)
sin과 cos가 둘 다 필요한 이유: 오른쪽 두 항은 따로 보면 바늘 위치에 따라 출렁입니다. 그런데 한쪽이 줄면 다른 쪽이 그만큼 늘어나는 시소처럼 움직여서, 더한 값은 늘 일정합니다. 한쪽만 쓰면(sin만 쓰면) 출렁이는 값이 그대로 남습니다. 아래 슬라이더로 확인해 보세요.
두 단어는 항상 1칸 떨어져 있습니다(ω = 0.5, k = 1).
위: 위치별 벡터 길이. 아래: 기준 위치 p0 세 개에서 잰 PE(p0)·PE(p0+k). sin·cos 쌍이면 세 곡선이 한 줄로 겹칩니다.
쌍이 \(d_{model}/2\)개이니 시계도 그만큼 있고, 각각 속도가 다릅니다. 주기는 \(2\pi/\omega_i = 2\pi\cdot 10000^{2i/d_{model}}\)로, 가장 빠른 시계의 \(2\pi \approx 6.3\)칸부터 가장 느린 시계의 거의 \(2\pi \times 10000\)칸까지 기하급수로 늘어납니다.
초침·분침·시침을 함께 봐야 정확한 시각을 알 수 있는 것과 같습니다. 여러 속도의 시계를 함께 보면 위치가 하나로 정해집니다.
| 쌍 i | ωi | 주기(칸) | 이웃 간 회전각 | 역할 |
|---|
dmodel = 512 기준, 256쌍 중 일부.
sin은 위치를 표시하고, 짝이 되는 cos는 그 위치를 회전 각도로 만듭니다. 덕분에 ① k칸 이동이 pos와 무관한 회전 행렬이 되고, ② 한 주기 안에서 위치가 모호하지 않으며, ③ 벡터 길이가 일정하고 내적은 거리에만 의존합니다.
사인·코사인 위치 인코딩은 위치 벡터를 임베딩에 더합니다. 그러면 어텐션 점수 \(q\cdot k\)를 계산할 때 단어 내용과 위치가 뒤섞여, “거리만 본다”는 성질이 그대로 남지 않습니다. 요즘 LLM 대부분이 쓰는 RoPE(Rotary Position Embedding, Su 외 2021)는 이 회전 아이디어를 한 걸음 더 밀고 갑니다. 쿼리와 키 벡터의 각 2차원 쌍을 위치에 비례하는 각도만큼 직접 돌립니다.
2차원 쿼리(파랑)와 키(주황)를 각자의 위치 m, n만큼 돌립니다. “둘 다 +5”를 누르면 두 벡터가 같이 돌고 내적은 변하지 않습니다.