LabHub
배우기 러닝패스 코스

Transformer — アテンションを手で計算する

埋め込み表からロジットまで

LabHub 에서 이어서 보기

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

목표

토큰 번호가 벡터가 되고 그 벡터가 다시 어휘 전체의 점수가 되기까지를 표준 라이브러리만으로 만든다. 조회가 원-핫 벡터와 행렬의 곱과 같은 값을 낸다는 것을 두 방법으로 계산해 확인하고, 파라미터 수를 세고, 같은 표를 출력 쪽에서 한 번 더 쓰는 가중치 묶기를 만들고, 코사인과 내적이 갈리는 자리를 숫자로 본다. 마지막으로 임베딩에 √d 를 곱하면 크기만 바뀌고 방향은 그대로라는 것을 잰다.

왜 중요한가

모델을 이야기할 때 사람들이 가장 자주 틀리는 자리가 여기다. "임베딩은 조회고 출력은 행렬 곱" 이라고 나눠 생각하면 둘이 같은 표라는 것이 안 보이고, 파라미터 수가 왜 어긋나는지도 설명이 안 된다. 어휘를 키우자는 제안이 왜 메모리 회의로 끝나는지도 V 곱하기 d 를 세어 보기 전에는 감이 오지 않는다. 이 실습은 실제 모델을 부르지 않는다. 이 파드의 시스템 파이썬에는 numpy·torch·transformers 가 없다(numpy 는 /opt/onnx-lab/bin/python 안에만 있다). 그래서 실제 모델의 어휘 크기나 파라미터 수 같은 숫자는 여기서 쓰지 않는다 — 여러분이 만든 표에서 잰 값만 쓴다. 판정은 실수 비교가 많다. 채점기는 abs(a-b) <= atol + rtol*abs(b) 로 보되, 모양·파라미터 수·이웃 목록 같은 정수 판정도 함께 본다. 그래서 더하는 순서가 달라 마지막 자리가 흔들리는 것은 받아들이고, 실제로 틀린 구현은 거른다. 채점기는 여러분이 적어 둔 설명을 믿지 않는다. 여러분의 모듈을 실제로 불러 매번 다른 표와 다른 번호로 함수를 두드려 보고, 채점기가 따로 계산한 값과 대조한다.

단계

  1. /root/work/tf-embed/embed.pyVOCAB = 512·DIM = 64·SEED = 20260917make_table(vocab, dim, seed)·shape(matrix)·param_count(vocab, dim) 을 만드세요. 같은 씨앗이면 언제나 같은 표가 나와야 합니다.
  2. lookup(table, ids) 를 더해 번호 목록을 벡터 목록으로 바꾸게 하세요. 같은 번호는 같은 벡터를 주고, 어휘 밖의 번호는 IndexError 입니다.
  3. one_hot(token_id, vocab)·row_times_matrix(vec, matrix)·lookup_via_one_hot(table, ids)·one_hot_mults(vocab, dim, count) 를 더해 조회와 원-핫 곱이 같은 값을 내는지 보세요.
  4. logits(table, hidden) 을 만들어 입력에 쓴 그 표로 어휘 전체의 점수를 내게 하세요. 새 행렬을 만들면 안 됩니다.
  5. tied_params(vocab, dim)·untied_params(vocab, dim)·vocab_growth(vocab, dim, factor) 로 묶었을 때와 따로 둘 때의 수 개수를 세게 하세요.
  6. dot·norm·cosine·stretch·nearest_by_cosine·nearest_by_dot 을 만들어 한 줄만 늘였을 때 두 목록이 어떻게 갈리는지 보세요.
  7. rms(vec)scaled_lookup(table, ids, dim) 을 만들어 √d 를 곱하면 크기가 √d 배가 되는 것을 재게 하세요.
  8. 정한 상수로 전부 재어 /root/work/tf-embed/embed_report.json/root/work/tf-embed/embed_report.md 에 기록하세요.

참고

표의 모양과 파라미터 수

/root/work/tf-embed/embed.pyVOCAB = 512·DIM = 64·SEED = 20260917make_table(vocab, dim, seed)·shape(matrix)·param_count(vocab, dim) 을 만드세요. make_tablerandom.Random(seed) 하나로 0번 토큰의 0번 칸부터 줄 단위로 채우고, 칸마다 random() 에서 0.5 를 뺍니다.

표는 리스트의 리스트입니다. rng = random.Random(seed) 를 한 번만 만들고 줄마다 dim 개씩 뽑으면 줄 단위 순서가 지켜집니다. 칸 단위로 돌면 같은 씨앗인데도 다른 표가 나옵니다. shape 는 줄마다 칸 수를 확인해 다르면 ValueError 를 내세요 — 모양이 어긋난 표는 뒤에서 조용히 이상한 값을 냅니다. param_count 는 곱이지 합이 아닙니다.

번호로 줄을 꺼낸다

lookup(table, ids) 를 더하세요. 번호 목록을 벡터 목록으로 바꿉니다. 같은 번호가 두 번 나오면 완전히 같은 벡터가 두 번 나와야 하고, 어휘 밖의 번호(음수 포함)는 IndexError 입니다.

줄을 꺼내는 것이 전부입니다. 다만 table[-1] 은 파이썬에서 뒤에서 첫 줄을 조용히 주므로, 0 <= token_id < len(table) 을 직접 확인해야 합니다. 꺼낸 줄은 list() 로 복사해 돌려주면 부르는 쪽이 원본을 건드릴 위험이 없습니다. 같은 번호가 같은 벡터라는 것은 버그가 아니라 성질입니다 — 임베딩에는 문맥이 없고, 문맥은 어텐션이 뒤에서 줍니다.

조회는 원-핫 곱과 같은 값이다

one_hot(token_id, vocab)·row_times_matrix(vec, matrix)·lookup_via_one_hot(table, ids)·one_hot_mults(vocab, dim, count) 를 더하세요. 원-핫 곱으로 계산한 값이 lookup 과 같아야 하고, one_hot_mults 는 토큰 하나에 V 곱하기 d 번인 곱셈 횟수를 돌려줍니다.

row_times_matrix 의 칸 하나는 sum(vec[r] * matrix[r][c] for r in range(V)) 입니다. 행과 열을 뒤집으면 길이부터 어긋나니 shape 로 확인하고 도세요. 원-핫은 한 자리만 1.0 이라 곱해 더하면 그 줄만 살아남습니다 — 값이 정확히 같아야 정상입니다. 시간을 재려 하지 말고 곱셈 횟수를 세세요. 조회는 0번입니다.

같은 표로 점수를 낸다

logits(table, hidden) 을 만드세요. 은닉 벡터 하나를 어휘 전체의 점수로 바꿉니다. 입력에 쓴 그 표를 그대로 써야 합니다(가중치 묶기). 은닉 벡터의 길이가 표의 폭과 다르면 ValueError 입니다.

줄마다 은닉 벡터와의 내적을 내면 길이 V 의 목록이 됩니다. 새 행렬을 만들어 쓰면 값이 전혀 달라집니다 — 묶는다는 것은 바로 그 표를 다시 쓴다는 뜻입니다. 확인하는 좋은 방법이 하나 있습니다. hidden 에 어떤 토큰의 임베딩을 그대로 넣어 보세요. 자기 자신과의 내적은 길이의 제곱이라 그 토큰의 점수가 가장 커집니다.

묶으면 얼마나 줄어드는가

tied_params(vocab, dim)·untied_params(vocab, dim)·vocab_growth(vocab, dim, factor) 를 만드세요. vocab_growthvocab·bigger_vocab·dim·tied·bigger_tied·untied·bigger_untied·saved 키를 가진 딕셔너리이고, saved 는 따로 둘 때에서 묶을 때를 뺀 값입니다.

묶으면 표가 한 벌, 따로 두면 같은 모양이 두 벌입니다. 어휘를 factor 배로 키울 때 폭은 건드리지 않는데도 두 값이 모두 그 배수만큼 커집니다 — 그것이 어휘 크기의 값입니다. 여기는 전부 정수 판정이니 실수로 계산해 반올림하지 마세요.

길이가 순서를 바꾼다

dot(a, b)·norm(vec)·cosine(a, b)·stretch(table, token_id, factor)·nearest_by_cosine(table, token_id, k)·nearest_by_dot(table, token_id, k) 를 만드세요. 이웃은 점수가 큰 순으로 k 개의 번호이고 자기 자신은 뺍니다. 점수가 같으면 번호가 작은 쪽이 앞입니다.

cosine 은 내적을 두 길이의 곱으로 나눈 것이고, 길이가 0 이면 방향이 없으므로 0.0 입니다. stretch새 표를 만들어야 합니다 — 원본을 제자리에서 고치면 뒤의 판정이 전부 어긋납니다. 정렬은 key=lambda item: (-점수, 번호) 한 줄로 동점 규칙까지 적을 수 있습니다. 한 줄을 늘여 놓고 두 목록을 나란히 보세요. 코사인 목록은 그대로인데 내적 목록에서는 늘인 줄이 앞으로 나옵니다.

√d 를 곱하면 크기만 바뀐다

rms(vec)scaled_lookup(table, ids, dim) 을 만드세요. rms 는 제곱평균제곱근이고 빈 벡터는 0.0 입니다. scaled_lookuplookup 의 결과에 math.sqrt(dim) 을 곱한 것입니다.

모든 칸에 같은 수를 곱하는 것이라 방향은 하나도 안 바뀝니다 — 코사인을 재 보면 곱하기 전과 같습니다. 바뀌는 것은 크기뿐이고 정확히 math.sqrt(dim) 배입니다. dim 을 그대로 곱하면 크기가 d 배가 되어 전혀 다른 값이 됩니다. rms 는 내적을 칸 수로 나눈 뒤 제곱근을 내면 됩니다.

재어 본 것을 기록으로 남긴다

VOCAB = 512·DIM = 64·SEED = 20260917 로 표를 만들고 137번 토큰을 찔러 전부 재세요. 이웃은 k = 5, 늘일 줄은 코사인 이웃의 다섯 번째, 배수는 4.0, 어휘를 키우는 배수는 2 입니다. /root/work/tf-embed/embed_report.jsonvocab·dim·seed·probe·table_shape·params·tied_params·untied_params·saved·bigger_vocab·bigger_tied·bigger_untied·lookup_mults·one_hot_mults·max_abs_diff·repeat_same·logit_len·logit_argmax·logit_argmax_is_self·cos_neighbors·dot_neighbors·neighbors_differ·stretch_target·stretch_factor·cos_after_stretch·dot_after_stretch·cos_unchanged_by_scale·rms_plain·rms_scaled·rms_ratio 를, /root/work/tf-embed/embed_report.md## 무엇을 쟀나 ## 조회와 원-핫 곱은 같은 연산이다 ## 가중치를 묶으면 무엇이 줄어드나 ## 내적과 코사인이 갈리는 자리 ## √d 를 곱하면 무엇이 달라지나 다섯 절로 쓰세요.

숫자는 손으로 적지 말고 여러분의 코드를 실제로 돌려 얻은 값으로 채우세요. one_hot_mults137번 토큰 하나를 조회할 때의 값이고 lookup_mults 는 0 입니다. max_abs_difflookuplookup_via_one_hot 의 값 차이 중 가장 큰 절댓값입니다. repeat_same 은 같은 번호를 두 번 조회했을 때 두 벡터가 같은지입니다. logit_argmax 는 은닉 벡터에 137번의 임베딩을 그대로 넣었을 때 점수가 가장 큰 번호입니다. cos_unchanged_by_scale 은 √d 를 곱하기 전과 뒤의 코사인이 같은지입니다. rms_ratio 는 곱한 뒤의 크기를 곱하기 전의 크기로 나눈 값입니다.