6장. Qwen 모델 준비와 텍스트 생성
생성은 한 토큰씩 이어집니다
징검다리를 건너는 사람을 떠올려 봅니다. 한 번에 강을 건너뛰지 않고, 지금 선 돌에서 다음 돌 하나를 골라 디디고, 다시 거기서 다음 돌을 고릅니다. generate() 도 문장을 한 번에 만들지 않습니다. 다음 토큰 하나 고르기 → 입력 뒤에 붙이기를 되풀이합니다.
그림의 다섯 상자가 한 바퀴입니다. 지금까지의 토큰을 넣으면(1) 모델이 사전의 모든 후보에 점수를 매기고(2), Softmax 로 확률을 만들고(3), 하나를 고르고(4), 입력 뒤에 붙여 다시 1번으로 갑니다(5). max_new_tokens 만큼 돌거나 끝 토큰(<|im_end|>)이 나오면 멈춥니다. do_sample=False 이면 4번에서 늘 확률이 가장 큰 후보를 고릅니다. 이것이 Greedy 입니다.
한 바퀴의 각 단계
| 단계 | 하는 일 | 크기(Qwen 0.5B) | 징검다리로 치면 |
|---|---|---|---|
| 1. 입력 | 지금까지의 토큰 번호 | 처음 8개 → 한 바퀴마다 1개씩 늘어남 | 지금까지 디딘 돌 |
| 2. 점수 | 다음 토큰 후보마다 로짓 | 사전 크기만큼(15만 칸 남짓) | 앞에 보이는 돌마다 점수 |
| 3. 확률 | Softmax 로 합 1 | 같은 크기 | 점수를 비율로 |
| 4. 고르기 | Greedy 면 가장 큰 것, 샘플링이면 뽑기 | 토큰 1개 | 디딜 돌 하나 고르기 |
| 5. 붙이기 | 입력 뒤에 이어 붙인다 | 길이 + 1 | 돌을 디디고 다시 둘러보기 |
짧은 예시
# numpy = 숫자 배열 계산 도구
import numpy as np
# 장난감 모델: "마지막 토큰" 을 보고 다음 토큰 후보와 확률을 알려 주는 표(설명용 숫자)
next_probs = {
"서울": {"입니다": 0.6, "이다": 0.3, "시": 0.1},
"입니다": {".": 0.8, ",": 0.2},
".": {"<끝>": 0.7, "서울": 0.3},
}
tokens = ["대한민국의", "수도는", "서울"] # 1. 처음 입력
for step in range(5): # max_new_tokens = 5 와 같은 뜻
cands = next_probs[tokens[-1]] # 2~3. 마지막 토큰을 보고 후보와 확률을 받는다
names = list(cands)
probs = np.array(list(cands.values()))
pick = names[int(np.argmax(probs))] # 4. Greedy: 확률이 가장 큰 후보(argmax)
if pick == "<끝>": # 끝 토큰이 나오면 멈춘다
break
tokens.append(pick) # 5. 뒤에 붙이고 다시 돈다
print(step + 1, tokens)
# 결과: 1 [..., '서울', '입니다'] → 2 [..., '입니다', '.'] 에서 다음이 <끝> 이라 멈춘다이 코드가 뜻하는 것
징검다리를 건너는 사람이 매번 "점수가 가장 높은 돌"만 디디는 모습을 표 하나로 흉내 냈습니다. 실제 Qwen 은 표 대신 신경망이 15만 칸의 확률을 매번 새로 계산하지만, 돌고 붙이고 멈추는 뼈대는 같습니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
next_probs[tokens[-1]] | 마지막 토큰을 보고 후보 확률을 받는다 | 지금 선 돌에서 앞을 둘러보기 |
np.argmax(probs) | 가장 큰 확률의 자리 | Greedy 로 하나 고르기 |
tokens.append(pick) | 고른 토큰을 붙인다 | 돌을 디디기 |
if pick == "<끝>": break | 끝 토큰이면 멈춘다 | 건너편에 닿음 |
다섯 바퀴를 줬지만 두 바퀴 만에 끝 토큰이 나와 멈췄습니다. 실제 생성에서도 max_new_tokens 는 "최대"이므로 답이 일찍 끝날 수 있습니다.
주의할 점
- Greedy 는 같은 입력과 설정이면 늘 같은 답을 냅니다. 학습 전후를 견줄 때 쓰기 좋아서 실습 2의
chat함수가do_sample=False를 씁니다. - Greedy 의 단점은 같은 말을 되풀이하기 쉽다는 것입니다. 작은 모델일수록 심합니다. 그래서
repetition_penalty=1.2로 이미 나온 토큰을 불리하게 합니다. - 실제 모델은 한 바퀴마다 앞의 계산을 저장해 두고(KV 캐시) 새 토큰 부분만 계산합니다. 그래도 토큰 수만큼 바퀴를 돌아야 하므로 긴 답일수록 오래 걸립니다.