트랜스포머 — 어텐션을 손으로 계산한다 · 임베딩·가중치 묶기·로짓 · 실습
임베딩 표에서 로짓까지
목표
토큰 번호가 벡터가 되고 그 벡터가 다시 어휘 전체의 점수가 되기까지를 표준 라이브러리만으로 만든다. 조회가 원-핫 벡터와 행렬의 곱과 같은 값을 낸다는 것을 두 방법으로 계산해 확인하고, 파라미터 수를 세고, 같은 표를 출력 쪽에서 한 번 더 쓰는 가중치 묶기를 만들고, 코사인과 내적이 갈리는 자리를 숫자로 본다. 마지막으로 임베딩에 √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.py 에 VOCAB = 512·DIM = 64·SEED = 20260917 과 make_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.py를 파이썬 모듈로 불러VOCAB·DIM·SEED·make_table·shape·param_count·lookup·one_hot·row_times_matrix·lookup_via_one_hot·one_hot_mults·logits·tied_params·untied_params·vocab_growth·dot·norm·cosine·stretch·nearest_by_cosine·nearest_by_dot·rms·scaled_lookup을 직접 씁니다. 스크립트로 실행하지 않으므로if __name__ == "__main__"은 없어도 됩니다. make_table(vocab, dim, seed)는random.Random(seed)하나를 만들어 0번 토큰의 0번 칸부터 줄 단위로 채웁니다. 칸마다random()을 한 번 뽑아0.5를 뺍니다. 그래야 채점기가 같은 표를 따로 만들어 값을 맞출 수 있습니다.shape(matrix)는(줄 수, 칸 수)를 돌려주고, 줄마다 칸 수가 다르면ValueError를 냅니다. 빈 표는(0, 0)입니다.lookup(table, ids)는 어휘 밖의 번호에IndexError를 냅니다. 음수도 어휘 밖입니다 — 파이썬의 음수 인덱스를 그냥 두면 뒤에서부터 세어 조용히 엉뚱한 줄을 줍니다.row_times_matrix(vec, matrix)는 길이 V 의 행벡터와 V×d 표에서 길이 d 의 벡터를 냅니다.out[c] = sum(vec[r] * matrix[r][c] for r in range(V))입니다.one_hot_mults(vocab, dim, count)는 곱셈 횟수입니다. 토큰 하나에 V 곱하기 d 번이고, 조회로 하면 0번입니다. 시간을 재지 말고 이 수를 세세요.logits(table, hidden)은 길이 V 의 목록입니다.out[t] = sum(table[t][c] * hidden[c] for c in range(d))이고, 은닉 벡터의 길이가 표의 폭과 다르면ValueError입니다. 표를 그대로 쓰는 것이 가중치 묶기입니다.vocab_growth(vocab, dim, factor)는vocab·bigger_vocab·dim·tied·bigger_tied·untied·bigger_untied·saved키를 가진 딕셔너리입니다.saved는 따로 둘 때에서 묶을 때를 뺀 값입니다.cosine(a, b)는 길이가 0 인 벡터가 오면0.0입니다.rms([])도0.0입니다.stretch(table, token_id, factor)는 그 줄만factor배로 늘린 새 표를 돌려줍니다. 넘겨받은 표를 제자리에서 고치지 마세요.nearest_by_cosine·nearest_by_dot은 점수가 큰 순으로k개의 번호를 돌려줍니다. 자기 자신은 빼고, 점수가 같으면 번호가 작은 쪽이 앞입니다.scaled_lookup(table, ids, dim)은lookup의 결과에math.sqrt(dim)을 곱한 것입니다.- 8단계는
VOCAB = 512·DIM = 64·SEED = 20260917로 표를 만들고 137번 토큰을 찔러 봅니다. 이웃은k = 5, 늘일 줄은 코사인 이웃의 다섯 번째(0부터 세어 네 번째 칸), 늘이는 배수는4.0입니다. 어휘를 키우는 배수는2입니다. - 이 파드에는 인터넷이 없습니다.
pip install은 되지 않고 시스템 파이썬에서import numpy도 되지 않습니다.math와random이면 충분합니다. - 공식 문서: [Attention Is All You Need](https://arxiv.org/abs/1706.03762) · [PyTorch — MultiheadAttention](https://docs.pytorch.org/docs/stable/generated/torch.nn.MultiheadAttention.html) · [NumPy — matmul](https://numpy.org/doc/stable/reference/generated/numpy.matmul.html) · [Python — math](https://docs.python.org/3/library/math.html)
- 흔한 실수: 표를 칸 단위로 채워 씨앗 순서가 어긋나기, 음수 번호를 그냥 통과시키기, 원-핫 곱에서 행과 열을 뒤집기, 로짓에 새 행렬을 만들어 쓰기, 따로 둘 때의 수를 두 배로 세지 않기, 코사인에서 길이로 나누지 않기,
stretch가 원본 표를 고치기, √d 대신 d 를 곱하기.
단계 8개
- 표의 모양과 파라미터 수
- 번호로 줄을 꺼낸다
- 조회는 원-핫 곱과 같은 값이다
- 같은 표로 점수를 낸다
- 묶으면 얼마나 줄어드는가
- 길이가 순서를 바꾼다
- √d 를 곱하면 크기만 바뀐다
- 재어 본 것을 기록으로 남긴다