다음 의 점수를 실제 생성 문장으로 바꿉니다. generate.py 를 쓰고 ·· 를 하나씩 바꿔 결과를 비교해 보세요.
7
텍스트 생성
생성은 최근 을 에 넣고 예측한 하나를 붙이는 일을 합니다. 가 0이면 가장 높은 점수를 고릅니다. 양수면 점수를 조절한 뒤 무작위로 뽑으며, 높을수록 이 더 고르게 퍼집니다. 는 상위 후보만 남기고, 0이면 전부 씁니다.
는 무작위 선택을 하는 번호이며 정답을 보장하는 번호가 아닙니다. 이나 요청한 토큰 수에서 생성을 멈추고 새로 만든 글만 돌려줍니다. 입력은 때의 Q:/A: 형식을 맞춥니다.
생성 루프 — 한 토큰씩 붙여 나간다(자기회귀)
generate 는 이 순서를 정한 토큰 수만큼 되풀이합니다.
지금까지의 토큰에서 마지막 context(12)개만 잘라 에 넣습니다.
돌아온 점수 목록 가운데 마지막 자리의 점수 [-1] 만 씁니다. 그것이 다음 토큰의 점수입니다.
정한 방법(탐욕·온도·top_k)으로 토큰 하나를 고릅니다.
고른 토큰을 ids 뒤에 붙입니다. 다음 바퀴는 이 토큰까지 포함한 글을 봅니다.
고른 토큰이 줄바꿈이면 멈춥니다.
자기가 낸 결과를 다시 입력으로 쓰므로 (autoregressive) 생성이라고 합니다. 한 번 고른 토큰은 되돌리지 않으므로 앞에서 실수하면 뒤가 그 실수를 따라갑니다.
생성은 를 읽기만 합니다. 그런데 의 forward 는 학습 때와 같은 Value 계산을 하므로 쓰지 않을 까지 만듭니다. 는 torch.no_grad() 안에서 생성해 그래프를 만들지 않고, model.eval() 로 을 끕니다.
탐욕 선택 — temperature 0
temperature 를 0으로 주면 언제나 점수가 가장 높은 토큰을 고릅니다(, ). 같은 입력이면 언제나 같은 답이 나옵니다.
장점은 예측할 수 있다는 것입니다. 이 무엇을 가장 그럴듯하게 여기는지 그대로 보여 주므로 학습이 됐는지 확인할 때 좋습니다. 단점은 같은 구절을 되풀이하기 쉽다는 것입니다. "is warn is warn is warn" 처럼 한 번 고리에 들어가면 매번 같은 토큰이 가장 높아 빠져나오지 못합니다. 그래서 대화에서는 무작위를 조금 섞습니다.
온도 — 확률 분포를 뾰족하게 또는 평평하게
temperature(T)는 점수를 하기 전에 T 로 나눕니다. 확률은 exp(점수 ÷ T) 에 비례합니다.
T < 1: 점수 차이가 커져 가 뾰족해집니다. 높은 토큰이 더 자주 뽑힙니다.
T = 1: 모델이 낸 확률 그대로입니다.
T > 1: 점수 차이가 줄어 분포가 평평해집니다. 낮은 토큰도 자주 뽑혀 엉뚱해집니다.
아래 실험은 점수 warm 2.0, hot 1.0, blue 0.2, run −1.0 에 온도를 바꿉니다. warm 의 확률은 T 0.5 에서 0.858, 1.0 에서 0.632, 2.0 에서 0.447 로 바뀌고, run 은 0.002 에서 0.1 로 오릅니다.
generate.py 도 같은 계산을 합니다. 후보 점수에서 가장 큰 값을 빼고(수치 안정) T 로 나눠 exp 한 값을 무게로 random.choices 에 넘깁니다. 무게는 합이 1이 아니어도 되고, choices 가 비율대로 뽑습니다. 보통 0.7~0.9 를 많이 쓰고, 0.1~0.5 는 보수적이며, 1.0 이상은 엉뚱해지기 쉽습니다.
직접 확인하기 lab/temperature.py
import math
logits = {'warm': 2.0, 'hot': 1.0, 'blue': 0.2, 'run': -1.0}
for t in [0.5, 1.0, 2.0]:
weights = {w: math.exp(s / t) for w, s in logits.items()}
total = sum(weights.values())
print('T =', t, {w: round(v / total, 3) for w, v in weights.items()})
print('top_k=2 keeps', sorted(logits, key=logits.get, reverse=True)[:2])
cd ~/xslm
mkdir -p lab
micro lab/temperature.py
python3 -m lab.temperature
온도를 써도 아주 낮은 토큰들이 합쳐서 꽤 큰 확률을 가질 수 있습니다. 가 5만 개면 하나하나는 작아도 모두 더하면 무시할 수 없습니다. 그래서 뽑기 전에 후보를 줄입니다.
top-k: 점수 상위 k 개만 남기고 나머지는 버립니다. XSLM 의 top_k 8 은 어휘 40개 중 8개만 후보로 둡니다. 0이면 줄이지 않습니다. 실험의 top_k=2 는 warm·hot 만 남깁니다.
top-p(nucleus): 확률이 높은 순서로 더해 합이 p(예: 0.9)가 될 때까지만 남깁니다. 모델이 확신할 때는 후보가 몇 개, 헷갈릴 때는 많아져 상황에 맞게 바뀝니다. XSLM 에는 없지만 실제 서비스에서 많이 씁니다.
어휘가 수만 개인 모델은 top_k 40~60 을 흔히 씁니다. (온도 0.7, top_k 30)은 정확하게, (0.9, 50)은 균형 있게, (1.1, 100)은 창의적으로 쓰는 조합입니다. 어휘가 40개인 XSLM 에서는 8 정도가 비슷한 비율입니다.
seed — 무작위 선택을 재현한다
temperature 가 0보다 크면 random.choices 가 토큰을 무작위로 뽑습니다. generate 는 부를 때마다 random.Random(seed) 로 새 생성기를 만들므로, ··온도·top_k·seed 면 언제나 같은 글이 나옵니다.
아래 실험은 무게 5:3:2 로 a·b·c 를 12번 뽑습니다. seed 7 은 두 번 모두 같은 줄이 나오고 seed 8 은 다른 줄이 나옵니다. 무게가 큰 a 가 가장 많이 나오지만 순서는 seed 가 정합니다.
seed 는 결과를 재현하는 번호이지 좋은 답을 보장하는 번호가 아닙니다. 설정을 비교할 때는 seed 를 고정하고 하나만 바꾸고, 결과가 운인지 보려면 seed 를 여러 개 바꿔 봅니다.
직접 확인하기 lab/seed.py
import random
for seed in [7, 7, 8]:
rng = random.Random(seed)
print(seed, ''.join(rng.choices('abc', weights=[5, 3, 2])[0] for _ in range(12)))
cd ~/xslm
mkdir -p lab
micro lab/seed.py
python3 -m lab.seed
실행 결과
7 aababaabaaaa
7 aababaabaaaa
8 acabaacabaaa
멈추는 조건 — 줄바꿈과 최대 토큰 수
생성은 두 경우에 멈춥니다.
줄바꿈 토큰을 뽑았을 때: 에서 답 한 줄이 줄바꿈으로 끝났으므로, 모델이 "답이 끝났다"고 판단한 것입니다. 줄바꿈은 결과에 넣지 않습니다.
요청한 토큰 수(기본 24, 최대 64)를 채웠을 때: 모델이 끝을 내지 못해도 끝없이 쓰지 않게 막습니다.
결과에는 프롬프트를 빼고 새로 만든 토큰만 해서 돌려줍니다. 첫 토큰부터 줄바꿈이 뽑히면 빈 답이 됩니다.
실제 모델은 특수 토큰이나 정한 멈춤 표시(예: [/ANSWER])에서 멈추고 그 뒤는 잘라 냅니다. 토큰을 만들 때마다 화면에 바로 보내는 도 흔합니다.
프롬프트 형식 — 학습한 모양과 공백까지 똑같이
모델은 학습 자료에서 본 모양 뒤에 오는 글만 잘 잇습니다. 그래서 생성할 때 넣는 글(프롬프트)은 학습 형식과 똑같아야 합니다. XSLM 의 기본 프롬프트는 "Q: hi" 다음 줄에 "A: " 입니다.
공백 하나도 중요합니다. 학습 자료의 "A: " 는 한 토큰(30번)인데, 끝 공백을 빼고 "A:" 로 주면 "A"(5번)와 ":"(4번) 두 토큰이 됩니다. 이 두 토큰이 이렇게 나온 적은 학습 자료에 한 번도 없어서, 모델은 처음 보는 상황에서 답을 시작합니다. 데스크톱에서 0.1 로 3,000번 학습한 모델에 sun 을 물어보니, "A: " 로 끝내면 "te sun is warn is warn…" 처럼 학습 문장과 비슷한 글을, "A:" 로 끝내면 "ore" 같은 조각만 냈습니다.
형식이 길면 답의 첫머리까지 넣어 줍니다. 예를 들어 [ANSWER] 다음에 "요약:" 과 "-" 까지 넣으면 모델이 바로 요약 목록을 이어 씁니다.
답이 이상할 때 — 원인과 실험
작은 모델의 답이 이상한 원인을 흔한 순서로 봅니다.
학습 부족: 기본 20번은 확인용입니다. 이 3.7 근처면 모델은 아직 거의 아무것도 모릅니다.
자료 부족: 12쌍으로는 형식을 흉내 내는 데 그치고, 처음 보는 질문에는 답하지 못합니다.
형식 불일치: 프롬프트가 학습 형식과 다르거나 모르는 글자(�)가 섞였습니다.
문맥 부족: context 12토큰을 넘는 앞부분은 보지 못합니다.
설정: 반복되면 온도·top_k 를 조금 올리고, 엉뚱하면 내립니다.
무작위성을 높인다고 부족한 학습이 해결되지는 않습니다. 해 볼 만한 실험은 이렇습니다. 새 체크포인트로 학습률을 0.1 로 바꾸고 --steps 를 수백 번씩 나눠 학습하며, 같은 질문을 temperature 0 으로 물어 답이 어떻게 바뀌는지 기록합니다. 위에서 본 "te sun is warn…" 은 학습 문장 "the sun is warm." 을 흉내 내기 시작했지만 아직 외우지도 못한 단계입니다.
직접 타이핑하며 실습하기
generate.py를 작성하세요. 같은 체크포인트로 세 을 비교합니다. 먼저 temperature만 바꾸고, 다음에는 top_k와 seed를 바꾸어 보세요. 와 모델 크기는 체크포인트에 저장된 값을 읽습니다.
model.json 하나에서 설정과 토크나이저까지 읽으므로, settings.json 이나 tokenizer.json 이 나중에 바뀌어도 학습할 때와 같은 모델을 만듭니다. 같은 크기의 GPT 를 만든 뒤(처음 값은 곧 덮어씀) load 로 학습한 숫자를 넣습니다.
generate.py 입력 검사와 준비
def generate(model, tokenizer, prompt, tokens=24, temperature=0.8, top_k=8, seed=7): if not prompt or tokens < 1 or temperature < 0 or not math.isfinite(temperature) or top_k < 0: raise ValueError('invalid sampling parameters') ids, result, rng = tokenizer.encode(prompt), [], random.Random(seed)
빈 프롬프트, 1보다 작은 토큰 수, 음수·무한대· 온도, 음수 top_k 는 로 거절합니다. math.isfinite 는 inf·nan 이면 거짓입니다. 프롬프트를 번호로 바꾸고, 새로 만든 토큰을 모을 result 와 이번 생성만의 난수 생성기 rng 를 만듭니다.
generate.py 다음 토큰 점수 구하기
for _ in range(tokens): logits = model.forward(ids[-model.context:])[-1] ranked = sorted(range(len(logits)), key=lambda i: logits[i].data, reverse=True)
ids[-model.context:] 로 마지막 12토큰만 넣고, forward 결과의 마지막 줄 [-1] 만 씁니다. ranked 는 어휘 번호를 점수가 높은 순서로 정렬한 목록이고, ranked[0] 이 가장 높은 토큰입니다.
generate.py 토큰 하나 고르기
if temperature == 0: token = ranked[0] else: candidates = ranked[:top_k] if top_k else ranked peak = logits[candidates[0]].data weights = [math.exp((logits[i].data - peak) / temperature) for i in candidates] token = rng.choices(candidates, weights=weights)[0]
temperature 가 0이면 가장 높은 토큰을 고릅니다. 아니면 top_k 개(0이면 전부)를 후보로 남기고, 가장 큰 점수 peak 를 뺀 뒤 temperature 로 나눠 exp 한 값을 무게로 삼아 rng.choices 로 하나를 뽑습니다. choices 는 목록을 돌려주므로 [0] 으로 꺼냅니다.
generate.py 붙이고, 멈추고, 돌려주기
ids.append(token) if tokenizer.vocab[token] == '\n': break result.append(token) return tokenizer.decode(result)
고른 토큰을 ids 에 붙여 다음 바퀴의 입력에 넣습니다. 그 토큰이 줄바꿈이면 결과에 넣지 않고 멈추고, 아니면 result 에 모읍니다. 반복이 끝나면 result 만 decode 해서 돌려주므로 프롬프트는 결과에 들어가지 않습니다.
직접 실행하면 체크포인트를 읽고 --prompt·--tokens·--temperature·--top-k·--seed 옵션을 받습니다. argparse 는 --top-k 를 args.top_k 로 바꿔 줍니다(- 가 _ 로). 토큰 수가 1~max_tokens(64) 인지 로 확인한 뒤 생성한 글을 찍습니다. 에서 줄바꿈이 들어간 프롬프트를 주려면 --prompt "$( 'Q: cat\nA: ')" 처럼 printf 를 씁니다.
여기까지 이해했나요?
같은 체크포인트·질문·seed·설정이면 같은 글이 나와야 합니다. 가중치를 읽는 것과 학습하는 것의 차이를 설명해 보세요. 첫 선택이 줄바꿈이면 빈 답이 될 수 있습니다.
막혔을 때 확인하기
작은 모델을 잠깐 학습하면 이상한 글이나 빈 답이 나올 수 있습니다. 자료 형식·모르는 문자·학습 손실부터 확인하세요. 무작위성을 높인다고 부족한 학습이 해결되지는 않습니다.
자주 묻는 질문
같은 질문에 왜 매번 같은 답이 나오나요?
seed 가 같기 때문입니다. generate 는 요청마다 같은 seed 로 생성기를 새로 만듭니다. 다른 답을 보려면 --seed 를 바꾸거나 요청 에 "seed" 를 다른 정수로 넣으세요. temperature 가 0이면 seed 와 상관없이 언제나 같습니다.
온도를 높이면 더 똑똑해지나요?
아닙니다. 온도는 이미 계산된 확률에서 고르는 방법만 바꿉니다. 모델이 정답에 낮은 확률을 주고 있다면 온도를 어떻게 바꿔도 정답이 자주 나오지 않습니다. 똑똑함은 학습(자료·크기·횟수)이 정합니다.
빈 답이 나와요.
첫 토큰으로 줄바꿈이 뽑혀 바로 멈춘 경우입니다. 학습 자료에서 "A: " 다음에 바로 줄이 바뀌는 일은 없으므로, 학습이 부족하거나 프롬프트 형식이 다를 때 생깁니다. 프롬프트 끝이 "A: " 인지 보고, 학습을 더 하거나 seed·temperature 를 바꿔 보세요.