埋め込み表からロジットまで
한국어 원문으로 표시합니다.
목표
토큰 번호가 벡터가 되고 그 벡터가 다시 어휘 전체의 점수가 되기까지를 표준 라이브러리만으로 만든다. 조회가 원-핫 벡터와 행렬의 곱과 같은 값을 낸다는 것을 두 방법으로 계산해 확인하고, 파라미터 수를 세고, 같은 표를 출력 쪽에서 한 번 더 쓰는 가중치 묶기를 만들고, 코사인과 내적이 갈리는 자리를 숫자로 본다. 마지막으로 임베딩에 √d 를 곱하면 크기만 바뀌고 방향은 그대로라는 것을 잰다.
왜 중요한가
모델을 이야기할 때 사람들이 가장 자주 틀리는 자리가 여기다. "임베딩은 조회고 출력은 행렬 곱" 이라고 나눠 생각하면 둘이 같은 표라는 것이 안 보이고, 파라미터 수가 왜 어긋나는지도 설명이 안 된다. 어휘를 키우자는 제안이 왜 메모리 회의로 끝나는지도 V 곱하기 d 를 세어 보기 전에는 감이 오지 않는다.
이 실습은 실제 모델을 부르지 않는다. 이 파드의 시스템 파이썬에는 numpy·torch·transformers 가 없다(numpy 는 /opt/onnx-lab/bin/python 안에만 있다). 그래서 실제 모델의 어휘 크기나 파라미터 수 같은 숫자는 여기서 쓰지 않는다 — 여러분이 만든 표에서 잰 값만 쓴다.
판정은 실수 비교가 많다. 채점기는 abs(a-b) <= atol + rtol*abs(b) 로 보되, 모양·파라미터 수·이웃 목록 같은 정수 판정도 함께 본다. 그래서 더하는 순서가 달라 마지막 자리가 흔들리는 것은 받아들이고, 실제로 틀린 구현은 거른다.
채점기는 여러분이 적어 둔 설명을 믿지 않는다. 여러분의 모듈을 실제로 불러 매번 다른 표와 다른 번호로 함수를 두드려 보고, 채점기가 따로 계산한 값과 대조한다.
단계
- /root/work/tf-embed/embed.py 에
VOCAB = 512·DIM = 64·SEED = 20260917과make_table(vocab, dim, seed)·shape(matrix)·param_count(vocab, dim)을 만드세요. 같은 씨앗이면 언제나 같은 표가 나와야 합니다. lookup(table, ids)를 더해 번호 목록을 벡터 목록으로 바꾸게 하세요. 같은 번호는 같은 벡터를 주고, 어휘 밖의 번호는IndexError입니다.one_hot(token_id, vocab)·row_times_matrix(vec, matrix)·lookup_via_one_hot(table, ids)·one_hot_mults(vocab, dim, count)를 더해 조회와 원-핫 곱이 같은 값을 내는지 보세요.logits(table, hidden)을 만들어 입력에 쓴 그 표로 어휘 전체의 점수를 내게 하세요. 새 행렬을 만들면 안 됩니다.tied_params(vocab, dim)·untied_params(vocab, dim)·vocab_growth(vocab, dim, factor)로 묶었을 때와 따로 둘 때의 수 개수를 세게 하세요.dot·norm·cosine·stretch·nearest_by_cosine·nearest_by_dot을 만들어 한 줄만 늘였을 때 두 목록이 어떻게 갈리는지 보세요.rms(vec)와scaled_lookup(table, ids, dim)을 만들어 √d 를 곱하면 크기가 √d 배가 되는 것을 재게 하세요.- 정한 상수로 전부 재어 /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 · PyTorch — MultiheadAttention · NumPy — matmul · Python — math
- 흔한 실수: 표를 칸 단위로 채워 씨앗 순서가 어긋나기, 음수 번호를 그냥 통과시키기, 원-핫 곱에서 행과 열을 뒤집기, 로짓에 새 행렬을 만들어 쓰기, 따로 둘 때의 수를 두 배로 세지 않기, 코사인에서 길이로 나누지 않기,
stretch가 원본 표를 고치기, √d 대신 d 를 곱하기.
표의 모양과 파라미터 수
/root/work/tf-embed/embed.py 에 VOCAB = 512·DIM = 64·SEED = 20260917 과 make_table(vocab, dim, seed)·shape(matrix)·param_count(vocab, dim) 을 만드세요. make_table 은 random.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_growth 는 vocab·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_lookup 은 lookup 의 결과에 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.json 에 vocab·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_mults 는 137번 토큰 하나를 조회할 때의 값이고 lookup_mults 는 0 입니다. max_abs_diff 는 lookup 과 lookup_via_one_hot 의 값 차이 중 가장 큰 절댓값입니다. repeat_same 은 같은 번호를 두 번 조회했을 때 두 벡터가 같은지입니다. logit_argmax 는 은닉 벡터에 137번의 임베딩을 그대로 넣었을 때 점수가 가장 큰 번호입니다. cos_unchanged_by_scale 은 √d 를 곱하기 전과 뒤의 코사인이 같은지입니다. rms_ratio 는 곱한 뒤의 크기를 곱하기 전의 크기로 나눈 값입니다.