LabHub
배우기 러닝패스 코스

Transformers — Compute Attention By Hand

Build Temperature, top-k and top-p by Hand

LabHub 에서 이어서 보기

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

목표

로짓 한 벌에서 다음 토큰 하나를 고르기까지, 분포가 거치는 자리를 표준 라이브러리만으로 전부 만든다. 온도로 나눈 뒤의 안정한 소프트맥스, 엔트로피, top-k, top-p, 넷을 정해진 순서로 잇는 함수, 역누적분포 추출, greedy 와 온도 훑기까지다. 마지막에 같은 시드로 두 번 뽑아 같은 수열이 나오는지와 온도를 낮춘 쪽이 greedy 와 같아지는지를 기록으로 남긴다.

왜 중요한가

트랜스포머 블록이 하는 일은 어휘 크기만 한 실수 목록 하나를 내놓는 것까지다. 실제로 쓸 토큰을 고르는 것은 모델 바깥의 규칙이고, 같은 모델이 매번 다르게 말하는 이유도 매번 같은 말을 되풀이하는 이유도 전부 거기 있다. 이 실습은 모델을 부르지 않는다. 이 파드에는 transformers 도 torch 도 없고 numpy 는 /opt/onnx-lab/bin/python 안에만 있어 시스템 파이썬에서는 import numpy 가 되지 않는다. 대신 로짓 한 벌을 직접 적어 두고 그 뒤를 손으로 만든다. 그래서 여기 나오는 숫자는 전부 여러분이 적은 로짓에서 잰 것이다. 어려운 것은 세부다. 온도를 나누는 자리가 소프트맥스 앞인지 뒤인지, 최댓값을 왜 빼는지, top-k 의 동점을 어떻게 깨는지, top-p 가 p 에 닿는 항목을 포함하는지, 온도와 top-p 와 top-k 를 어느 순서로 먹이는지. 하나만 달라도 같은 설정이 다른 시스템이 된다. 채점기는 여러분이 적어 둔 설명을 믿지 않는다. 여러분의 모듈을 실제로 불러 매번 다른 로짓과 다른 온도로 함수를 두드려 보고, 채점기가 따로 계산한 확률 벡터와 대조한다. 무작위 추출 자체는 판정하지 않는다 — 시드를 고정한 번호 수열과 확률 벡터만 본다.

단계

  1. /root/work/tf-sample/sample.pyVOCAB·LOGITSsoftmax_t(logits, temperature) 를 만드세요. 로짓을 온도로 나눈 뒤 최댓값을 빼고 소프트맥스합니다.
  2. entropy_bits(probs) 를 더해 분포가 퍼진 정도를 밑이 2 인 엔트로피로 재게 하세요. 확률이 0 인 자리는 0 으로 칩니다.
  3. top_k_filter(probs, k) 를 더해 확률이 큰 k 개만 남기고 나머지를 정확히 0.0 으로 두고 다시 정규화하게 하세요. 동점이면 번호가 작은 쪽이 남습니다.
  4. top_p_filter(probs, p) 를 더해 내림차순 누적합이 p 에 처음 닿는 그 항목까지 남기게 하세요. 그 항목을 빼면 합이 p 에 못 미칩니다.
  5. filtered_probs(logits, temperature, top_k=None, top_p=None) 를 만들어 온도 → 소프트맥스 → top-k → top-p 순서로 잇게 하세요. None 이면 그 단계를 건너뜁니다.
  6. sample_index(probs, u)sample_sequence(logits, temperature, top_k, top_p, n, seed) 를 만드세요. 역누적분포로 뽑고, random.Random(seed) 에서 random() 을 n 번 불러 같은 시드면 같은 수열이 나오게 합니다.
  7. greedy(logits)sweep(logits, temps, p) 를 만드세요. greedy 는 가장 큰 로짓의 번호(동점이면 작은 번호)이고, sweep 은 온도마다 (온도, 엔트로피, top-p 가 남기는 개수) 를 잽니다.
  8. 정해진 설정으로 재어 /root/work/tf-sample/sample_report.json/root/work/tf-sample/sample_report.md 에 결과를 기록하세요.

참고

온도로 나눈 뒤 소프트맥스

/root/work/tf-sample/sample.pyVOCAB(서로 다른 짧은 문자열 12개 이상)과 LOGITS(같은 길이, 값이 서로 다름, 1등과 2등의 차이 0.5 이상, 최댓값과 최솟값의 차이 3.0 이상)을 두고 softmax_t(logits, temperature) 를 만드세요. 로짓을 temperature나눈 뒤 최댓값을 빼고 지수를 취해 정규화합니다. temperature 가 0 이하이면 예외를 내세요.

나누는 자리가 요점입니다. 확률을 먼저 구해 놓고 거기에 온도를 먹이면 전혀 다른 결과가 나옵니다. 최댓값 빼기는 온도가 작을 때 살아납니다 — 0.01 로 나누면 로짓 5.2 가 520 이 되고 math.exp(520) 은 그대로 넘칩니다. 같은 값에서 같은 수를 빼는 것이라 확률은 달라지지 않습니다. 온도 0 은 나눗셈이 되지 않으므로 ValueError 를 내는 편이 낫습니다 — greedy 는 '온도 0' 이 아니라 별개의 규칙입니다.

퍼진 정도를 비트로 잰다

entropy_bits(probs) 를 더하세요. 밑이 2 인 엔트로피 -sum(p * log2(p)) 입니다. 확률이 0 인 자리는 0 으로 칩니다. 고르게 퍼진 n 개면 log2(n) 이 나옵니다.

math.log2(0) 은 예외를 냅니다. 그래서 0 인 자리는 건너뛰어야 하는데, 이것은 편법이 아니라 맞는 처리입니다 — p * log2(p)p 가 0 으로 갈 때의 극한이 0 이기 때문입니다. 온도를 올리며 이 값을 재 보면 커지는 것이 보입니다. 분포가 얼마나 퍼졌는지를 숫자 하나로 요약하는 자리입니다.

위에서 k 개만 남긴다

top_k_filter(probs, k) 를 더하세요. 확률이 큰 k 개만 남기고 나머지는 정확히 0.0 으로 두고 남은 것을 합이 1 이 되게 정규화합니다. 동점이면 번호가 작은 쪽이 남습니다. k 가 목록 길이 이상이면 아무것도 버리지 않고 정규화만 하고, k 가 1 보다 작으면 예외를 내세요.

번호를 확률 내림차순으로 정렬해 앞의 k 개를 집합으로 잡아 두면 나머지는 그 집합을 보는 것뿐입니다. 정렬 열쇠를 (-확률, 번호) 로 두면 동점 규칙까지 한 줄에 들어갑니다. 정규화를 잊으면 합이 1 이 안 되고, 그러면 뒤에서 뽑을 때 마지막 자리로 쏠립니다. 버린 자리를 아주 작은 값으로 두면 안 됩니다 — 낮은 확률로 되살아나기 때문입니다.

누적합이 p 에 닿는 그 항목까지

top_p_filter(probs, p) 를 더하세요. 확률을 내림차순(동점이면 번호가 작은 쪽이 앞)으로 늘어놓고 누적합을 재다가 누적합이 p 이상이 되는 첫 항목까지 포함하고 멈춥니다. 나머지는 정확히 0.0 이고 남은 것을 정규화합니다. p 가 0 이하이거나 1 보다 크면 예외를 내세요.

포함하느냐 빼느냐가 이 단계의 전부입니다. p 를 넘게 만든 그 항목을 빼 버리면 남은 것의 합이 p 에 못 미칩니다 — 그러면 'p 만큼의 질량을 남긴다' 는 말 자체가 성립하지 않습니다. 누적합이 p 에 닿는 순간 그 번호를 이미 넣어 둔 뒤에 멈추면 됩니다. p 가 아무리 작아도 적어도 하나는 남습니다.

네 가지를 정해진 순서로 잇는다

filtered_probs(logits, temperature, top_k=None, top_p=None) 를 만드세요. 온도 → 소프트맥스 → top-k → top-p 순서로 잇고, None 인 단계는 건너뜁니다.

순서가 결과를 바꿉니다. 온도를 먼저 먹이면 분포 자체가 달라지므로 같은 p 라도 남는 개수가 달라지고, top-k 를 먼저 하면 살아남은 것들이 다시 정규화되어 확률이 부풀기 때문에 top-p 가 더 일찍 멈춥니다. 함수는 네 줄이면 끝납니다 — 앞에서 만든 세 함수를 이 순서로 부르기만 하면 됩니다. top_ktop_pNone 인지 보는 조건을 잊지 마세요.

같은 시드는 같은 수열

sample_index(probs, u)sample_sequence(logits, temperature, top_k, top_p, n, seed) 를 만드세요. sample_index번호 순서대로 확률을 더해 가다 u 를 처음 넘는 자리를 돌려주고, sample_sequence 는 분포를 한 번만 만든 뒤 random.Random(seed)random() 을 n 번 불러 번호 n 개를 돌려줍니다.

u < 누적합 이 참이 되는 첫 자리입니다. 확률이 0.0 인 자리는 누적합을 늘리지 않으므로 절대 뽑히지 않습니다 — 잘라낸 토큰이 되살아나지 않는다는 뜻이고, 이것이 이 구조의 안전장치입니다. random.Random(seed) 를 함수 안에서 한 번만 만드세요. 뽑을 때마다 새 발생기를 만들면 같은 번호만 n 개 나옵니다. 분포도 반복문 밖에서 한 번만 만듭니다.

greedy 와 온도 훑기

greedy(logits)sweep(logits, temps, p) 를 만드세요. greedy 는 가장 큰 로짓의 번호이고 동점이면 작은 번호입니다. sweeptemps 의 온도마다 (온도, 엔트로피, top-p 가 남기는 개수) 세 쌍의 목록을 돌려줍니다.

greedy 는 확률을 구할 필요조차 없습니다 — 소프트맥스는 순서를 바꾸지 않기 때문입니다. 동점에서 max 가 어느 쪽을 주는지 헷갈리면 직접 훑으며 더 클 때만 갱신하세요. 그러면 작은 번호가 남습니다. sweep 의 개수는 top_p_filter 를 거친 뒤 0 이 아닌 자리의 수입니다. 온도를 올릴수록 엔트로피가 커지고 남는 개수도 늘어나는 것을 눈으로 보세요.

손잡이를 돌린 결과를 남긴다

고정된 설정으로 재세요. 시드 20260917, 뽑는 횟수 24, top_k5, top_p0.9 이고 더운 쪽은 온도 1.0, 찬 쪽은 온도 0.2 입니다. 결과를 /root/work/tf-sample/sample_report.jsonvocab_size·greedy_index·greedy_token·top_prob_t1·entropy_t1·sweep·nucleus·topk_mass·seed·draw_count·hot_draws·cold_draws·cold_is_greedy·hot_distinct·cold_distinct·repeat_matches 로, /root/work/tf-sample/sample_report.md## 무엇을 쟀나 ## 온도가 분포를 어떻게 바꾸나 ## top-k 와 top-p 가 남기는 것 ## 같은 시드는 같은 수열을 준다 네 절로 적으세요.

숫자는 손으로 적지 말고 여러분의 코드를 실제로 돌려 얻은 값으로 채우세요. sweep 은 온도 0.25·0.5·1.0·2.0·4.0 에 p 는 0.9 입니다. nucleus 는 온도 1.0 에서 p 를 0.5·0.8·0.9·0.95 로 바꿔 가며 잰 [p, 남은 개수] 이고, topk_mass 는 온도 1.0 의 원래 확률에서 상위 k 개가 차지하는 합을 k 1·3·5·10 에 대해 잰 [k, 합] 입니다 — 자른 뒤 정규화한 값이 아니라 자르기 전의 합입니다. cold_is_greedy 는 찬 쪽 수열이 전부 greedy(LOGITS) 와 같은지이고, repeat_matches 는 같은 시드로 한 번 더 뽑았을 때 같은 수열이 나오는지입니다. 여러분이 적은 로짓에 따라 cold_is_greedy 가 거짓일 수도 있습니다 — 그러면 그대로 적고 본문에서 왜 그런지 설명하세요.