LabHub
배우기 러닝패스 코스

Transformers — Compute Attention By Hand

Put Position In by Rotating

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

회전 위치 임베딩(RoPE)을 표준 라이브러리만으로 직접 만든다. 짝수 차원을 두 개씩 쌍으로 묶어 쌍마다 다른 속도로 자리만큼 돌리고, 그렇게 돌린 질의와 키의 내적이 두 자리의 차이에만 의존한다는 것을 숫자로 확인한다. 더하는 방식(절대 위치 인코딩)을 나란히 만들어 같은 간격에서 값이 흔들리는 것을 견주고, 회전이 길이를 바꾸지 않는다는 것과 거리가 멀어질 때 어느 성분이 남는지를 잰다.

왜 중요한가

자리 벡터를 더하면 내적을 펼쳤을 때 q·P[n]P[m]·k 처럼 한쪽 자리만 든 항이 남는다. 그 항들은 차이로 묶이지 않으므로, 간격이 똑같아도 문장 앞쪽에서 잰 값과 뒤쪽에서 잰 값이 달라진다. 언어에서 중요한 것은 대개 몇 칸 앞의 낱말인가인데 점수에는 절대 자리가 섞여 들어와 있는 것이다. 회전은 그 문제를 연산 자체로 푼다. 두 벡터를 같은 방향으로 같이 돌리면 사이 각이 변하지 않으므로, 각각 제 자리만큼 돌린 뒤 내적하면 값이 차이에만 의존한다. 이것은 근사가 아니라 등식이고, 그래서 숫자로 확인할 수 있다. 이 실습은 모델을 부르지 않는다. 이 파드의 시스템 파이썬에는 numpy 가 없고 인터넷도 없다. math 만으로 만들고, 여기서 잰 숫자만 쓴다. 그래서 "어느 모델이 어떤 밑을 쓴다" 같은 말은 여기서 하지 않는다. 채점기는 여러분이 적어 둔 설명을 믿지 않는다. 여러분의 모듈을 실제로 불러 매번 다른 차원과 자리로 함수를 두드려 보고, 채점기가 따로 계산한 값과 허용 오차 안에서 대조한다. 입력은 실행마다 바뀌므로 값을 외워 넣을 수 없다.

단계

  1. /root/work/tf-rope/rope.pyDIM = 64·THETA_BASE = 10000.0·demo_vectors()·dot(a, b)·thetas(d, base=THETA_BASE) 를 만드세요. thetas 는 쌍마다 다른 회전 속도를 돌려줍니다.
  2. rotate_pair(x0, x1, angle) 를 더해 평면 위의 점 하나를 angle 라디안만큼 반시계로 돌리게 하세요.
  3. apply_rope(vec, pos, base=THETA_BASE) 를 더해 벡터를 쌍으로 묶고 각 쌍을 pos * theta_i 만큼 돌린 새 벡터를 돌려주게 하세요.
  4. rope_score(q, k, m, n, base=THETA_BASE) 를 만들어 m 번 자리의 질의와 n 번 자리의 키를 각각 돌린 뒤 내적하게 하세요.
  5. sin_pos(pos, d, base=THETA_BASE)add_score(q, k, m, n, base=THETA_BASE) 를 만들어 더하는 방식의 같은 점수를 재게 하세요.
  6. offset_scan(q, k, offset, starts) 를 만들어 같은 간격을 여러 시작 자리에서 재게 하세요. 돌려주는 값은 (시작자리, 회전 점수, 더하는 점수) 짝의 목록입니다.
  7. turns(d, delta, base=THETA_BASE)slow_pairs(d, delta, base=THETA_BASE) 를 만들어 간격마다 쌍이 몇 바퀴를 도는지, 아직 한 바퀴를 안 넘긴 쌍이 몇 개인지 재게 하세요.
  8. 잰 값을 /root/work/tf-rope/rope_report.json/root/work/tf-rope/rope_report.md 에 기록하세요.

참고

쌍마다 다른 회전 속도

/root/work/tf-rope/rope.pyDIM = 64·THETA_BASE = 10000.0·demo_vectors()·dot(a, b)·thetas(d, base=THETA_BASE) 를 만드세요. thetas 는 길이 d // 2 의 목록이고 i 번째 값은 base ** (-2 * i / d) 입니다. demo_vectors()q[j] = math.cos(0.7 * j + 0.3), k[j] = math.sin(0.4 * j + 1.1) 인 길이 DIM 의 두 목록을 돌려줍니다.

차원이 아니라 마다 하나입니다 — 64차원이면 32개입니다. i 가 0 일 때 지수가 0 이라 첫 값은 1.0 이고, 뒤로 갈수록 기하급수로 작아집니다. 나눗셈을 정수로 하면 지수가 전부 0 이 되어 값이 모두 1.0 이 되니 -2.0 * i / d 처럼 실수로 두세요. dotzip 으로 짝지어 곱한 뒤 더하면 한 줄입니다.

평면 위에서 한 번 돌린다

rotate_pair(x0, x1, angle) 를 더하세요. 점 (x0, x1) 을 angle 라디안만큼 반시계로 돌린 두 개짜리 튜플을 돌려줍니다. rotate_pair(1.0, 0.0, math.pi / 2)(0.0, 1.0) 입니다.

(x0*cos - x1*sin, x0*sin + x1*cos) 입니다. 두 부호 중 하나만 뒤집어도 시계 방향이 되어 뒤 단계의 값이 전부 달라집니다. 각이 0 이면 원래 점 그대로여야 하고, 어떤 각을 주어도 원점에서의 거리는 변하지 않아야 합니다 — cos 과 sin 의 제곱을 더하면 1 이기 때문입니다.

벡터를 자리만큼 돌린다

apply_rope(vec, pos, base=THETA_BASE) 를 더하세요. (vec[0], vec[1]), (vec[2], vec[3]) 처럼 이웃끼리 쌍을 묶고, i 번째 쌍을 pos * thetas(len(vec), base)[i] 만큼 돌린 새 목록을 돌려줍니다. 넘겨받은 목록은 그대로 두세요.

쌍마다 각이 다릅니다 — 0번 쌍의 각을 모든 쌍에 쓰면 그냥 통째로 돌린 것이라 자리 정보가 한 겹밖에 안 들어갑니다. pos 가 0 이면 모든 각이 0 이라 원래 벡터와 같아야 하고, 어떤 자리를 주어도 벡터의 노름은 변하지 않아야 합니다. base 를 받아 thetas 에 넘기는 것을 잊지 마세요 — 기본값만 쓰면 다른 밑으로 불렀을 때 조용히 틀립니다.

돌린 질의와 키의 점수

rope_score(q, k, m, n, base=THETA_BASE) 를 만드세요. m 번 자리의 질의와 n 번 자리의 키를 각각 돌린 뒤 내적한 실수를 돌려줍니다. √d 로 나누지 않습니다.

dot(apply_rope(q, m, base), apply_rope(k, n, base)) 한 줄입니다. 한쪽만 돌리면 절대 자리가 그대로 남아 뒤 단계의 성질이 무너집니다. mn 이 같으면 돌리지 않은 dot(q, k) 와 같은 값이 나와야 합니다 — 같은 방향으로 같이 돌린 것이니 사이 각이 그대로이기 때문입니다.

더하는 방식을 나란히 둔다

sin_pos(pos, d, base=THETA_BASE)add_score(q, k, m, n, base=THETA_BASE) 를 만드세요. sin_pos 는 길이 d 의 자리 벡터이고, j 번째 값은 angle = pos / (base ** ((2 * (j // 2)) / d)) 로 두었을 때 j 가 짝수면 math.sin(angle), 홀수면 math.cos(angle) 입니다. add_score 는 q 와 k 에 각각 자리 벡터를 더한 뒤 내적합니다.

이쪽은 값을 만들어 더하는 방식입니다. 한쪽에만 더하면 견주기가 성립하지 않습니다. 내적을 펼쳐 보면 q·k 말고도 q·P[n]P[m]·k 가 나오는데, 이 두 항에는 한쪽 자리만 들어 있어 차이로 묶이지 않습니다 — 다음 단계에서 그 결과를 숫자로 보게 됩니다.

같은 간격이면 같은 점수다

offset_scan(q, k, offset, starts) 를 만드세요. starts 의 자리 s 마다 질의를 s + offset 에, 키를 s 에 두고 두 방식으로 점수를 잽니다. 돌려주는 값은 (s, 회전 점수, 더하는 점수) 짝의 목록이고 순서는 starts 와 같습니다. 만든 뒤 offset_scan(q, k, 2, [3, 10, 100, 4000]) 을 직접 출력해 회전 쪽 네 값이 같고 더하는 쪽은 흔들리는 것을 눈으로 확인하세요.

앞에서 만든 rope_scoreadd_score 를 그대로 부르면 다섯 줄입니다. 질의와 키를 거꾸로 두면 간격의 부호가 뒤집혀 다른 값이 나옵니다. 회전 쪽 네 값은 소수점 열두 자리까지 같아야 정상입니다 — 완전히 같지 않고 아주 작은 차이가 남는데, 그것이 부동소수점 오차입니다. 더하는 쪽의 흩어짐과 자릿수를 견주어 보세요.

멀어지면 어느 쌍이 남는가

turns(d, delta, base=THETA_BASE)slow_pairs(d, delta, base=THETA_BASE) 를 만드세요. turns 는 길이 d // 2 의 목록이고 i 번째 값은 delta * theta_i / (2 * math.pi), 즉 그 쌍이 도는 바퀴 수입니다. slow_pairs 는 그 값이 1.0 보다 작은 쌍의 개수(정수)입니다.

한 바퀴는 2π 라디안입니다. 나누는 것을 잊으면 바퀴 수가 아니라 각도가 되어 개수가 전혀 달라집니다. 한 바퀴를 넘긴 쌍은 그 거리와 한 바퀴만큼 뺀 거리를 같은 각도로 적으므로 둘을 가르지 못합니다. 간격 1·16·256·4096 로 불러 보고 남는 쌍의 수가 어떻게 줄어드는지 보세요.

잰 것을 기록으로 남긴다

demo_vectors() 의 q·k 로 재어 /root/work/tf-rope/rope_report.jsondim·base·offset·starts·rope_scores·rope_spread·add_scores·add_spread·norm_before·norm_max_gap·deltas·slow_pairs·turns_first·turns_last 를 적고, /root/work/tf-rope/rope_report.md## 무엇을 쟀나 ## 같은 간격이면 같은 점수다 ## 더하는 방식은 왜 다른가 ## 멀어지면 어느 성분이 남는가 네 절로 쓰세요. 간격은 2, 시작 자리는 3·10·100·4000, 노름을 재는 자리는 0·1·7·100·4096, 바퀴를 세는 간격은 1·16·256·4096 입니다.

숫자는 손으로 적지 말고 여러분의 코드를 실제로 돌려 얻은 값으로 채우세요. rope_scores·add_scoresoffset_scan 이 돌려준 짝에서 뽑습니다. rope_spread·add_spread 는 각각 최댓값에서 최솟값을 뺀 값이고, norm_max_gap 은 다섯 자리에서 잰 차이의 절댓값 중 가장 큰 것입니다. turns_first 는 간격마다 0번 쌍의 바퀴 수, turns_last 는 마지막 쌍의 바퀴 수입니다. 보고서 본문에는 회전 쪽 첫 점수·더하는 쪽 흩어짐·q 의 노름을 숫자로 적으세요 — 채점기가 그 세 값이 글 안에 있는지 봅니다.