[Daily morning study] LLM 텍스트 생성 디코딩 전략: Greedy, Beam Search, Sampling
#daily morning study
디코딩이란
LLM은 입력 컨텍스트를 바탕으로 다음 토큰의 확률 분포를 출력한다. 이 분포에서 실제 토큰을 선택하는 과정이 디코딩(decoding)이다. 동일한 모델이라도 디코딩 전략에 따라 결과물의 품질, 다양성, 일관성이 크게 달라진다.
| 모델의 마지막 레이어가 출력하는 원시 점수를 logit이라고 하고, softmax를 적용하면 확률 분포 $$P(w_i | \text{context})$$가 된다. |
Greedy Decoding
매 스텝마다 확률이 가장 높은 토큰 하나를 선택한다.
def greedy_decode(logits):
return logits.argmax(dim=-1)
- 장점: 빠르고 결정론적(deterministic)
- 단점: 지역 최적(locally optimal)만 선택하기 때문에 전체 시퀀스 관점에서는 최적이 아닐 수 있다. 반복 루프나 어색한 문장이 나오는 경우가 많다.
예시에서 문제가 드러난다.
“나는 밥을 먹었다. 나는 밥을 먹었다. 나는 밥을 먹었다.”
greedy는 반복 확률이 높은 토큰을 계속 선택하는 경향이 있다.
Beam Search
상위 k개의 후보(beam) 시퀀스를 동시에 추적하며, 각 스텝마다 k개의 확장을 만들어 전체 점수가 높은 k개를 유지한다.
beam_size = 3 이라면:
t=0: [토큰A, 토큰B, 토큰C]
t=1: [A+X, A+Y, A+Z, B+X, B+Y, B+Z, C+X, C+Y, C+Z] → 상위 3개 유지
...
최종적으로 가장 높은 누적 로그 확률을 가진 시퀀스를 선택한다. \[\text{score}(W) = \sum_{t=1}^{T} \log P(w_t | w_{<t})\]
- 장점: greedy보다 전체적으로 더 나은 시퀀스를 찾을 확률이 높다
- 단점: beam_size가 커질수록 계산 비용이 증가하고, 텍스트 생성 태스크에서는 오히려 뻔하고 짧은 출력을 선호하는 경향이 있다
Length Penalty: 짧은 시퀀스가 유리하게 평가되는 문제를 완화하기 위해 길이 페널티를 적용하기도 한다. \[\text{score\_norm} = \frac{\text{score}}{(5 + T)^\alpha / (5 + 1)^\alpha}\]
기계번역(MT), 요약(summarization) 같은 구조화된 출력에서 beam search가 여전히 자주 쓰인다.
Sampling
확률 분포에서 확률적으로 샘플링한다. 같은 입력이라도 매번 다른 토큰이 선택될 수 있다.
import torch
def sample_decode(logits):
probs = torch.softmax(logits, dim=-1)
return torch.multinomial(probs, num_samples=1)
- 장점: 다양하고 창의적인 텍스트 생성에 유리
- 단점: 확률이 낮은 이상한 토큰도 가끔 선택되어 문맥이 깨질 수 있다
Temperature Scaling
softmax 전에 logit을 temperature $T$로 나누어 분포를 조절한다. \[P(w_i) = \frac{\exp(z_i / T)}{\sum_j \exp(z_j / T)}\]
| Temperature | 효과 |
|---|---|
| $T \ll 1$ (예: 0.1) | 분포가 뾰족해져 greedy에 가까워짐 — 보수적이고 반복적 |
| $T = 1$ | 원래 모델 분포 그대로 |
| $T \gg 1$ (예: 2.0) | 분포가 평탄해져 랜덤에 가까워짐 — 창의적이지만 일관성 저하 |
실전에서는 0.7 ~ 1.0 범위를 가장 많이 사용한다.
Top-k Sampling
확률 상위 k개 토큰만 남기고 나머지를 0으로 마스킹한 뒤 재정규화해서 샘플링한다.
def top_k_sample(logits, k=50):
top_k_vals, top_k_idx = logits.topk(k)
probs = torch.zeros_like(logits).scatter_(0, top_k_idx, torch.softmax(top_k_vals, dim=-1))
return torch.multinomial(probs, num_samples=1)
- 확률이 너무 낮은 토큰이 선택될 위험을 제거
- 단점: k를 고정하면 분포가 좁은 상황(확실한 다음 토큰이 있는 경우)에서 k가 너무 클 수 있고, 분포가 넓은 상황에서는 너무 작을 수 있다
Top-p (Nucleus) Sampling
누적 확률이 $p$를 초과하는 시점까지의 최소 토큰 집합을 유지하고 샘플링한다.
p = 0.9 이라면:
확률: [0.4, 0.3, 0.2, 0.07, 0.03, ...]
누적: [0.4, 0.7, 0.9, ...] ← 3번째 토큰에서 0.9 초과
→ 상위 3개만 사용
top-k와 달리 분포의 형태에 따라 후보 크기가 동적으로 결정된다.
- 분포가 뾰족하면 → 후보 적음 (확실한 경우)
- 분포가 평탄하면 → 후보 많음 (다양한 선택 가능)
현재 대부분의 LLM API에서 기본으로 사용하는 전략이다. OpenAI, Anthropic API 모두 top_p 파라미터를 노출한다.
Min-p Sampling
Top-p의 변형으로, 가장 확률이 높은 토큰의 확률에 비례한 임계값을 기준으로 후보를 결정한다. \[\text{threshold} = p_{\max} \times \alpha \quad (\alpha \text{는 min\_p 파라미터})\]
- 최빈 토큰이 확실할 때는 집합을 작게, 불확실할 때는 크게 유지
- Top-p보다 품질 면에서 나은 경우가 많다는 연구 결과가 있다 (llama.cpp 커뮤니티에서 제안)
Repetition Penalty
이미 생성된 토큰의 logit을 패널티 인자로 나누어 반복을 억제한다.
for token_id in generated_ids:
if logits[token_id] > 0:
logits[token_id] /= repetition_penalty
else:
logits[token_id] *= repetition_penalty
repetition_penalty = 1.0이면 효과 없고, 1.1 ~ 1.3 범위가 일반적이다.
전략 선택 가이드
| 태스크 | 권장 전략 |
|---|---|
| 기계번역, 요약 | Beam Search (beam=4~8) |
| 코드 생성 | Greedy 또는 낮은 temperature (0.2~0.4) |
| 창작, 챗봇 | Top-p (0.9) + Temperature (0.7~1.0) |
| 다양한 후보 생성 | Top-p + 높은 temperature |
| 반복 방지가 중요한 경우 | Repetition Penalty 추가 |
정리
디코딩 전략은 모델 가중치를 건드리지 않고도 출력 품질을 크게 바꿀 수 있는 중요한 하이퍼파라미터다.
- Greedy: 빠르지만 단조롭고 반복적
- Beam Search: 구조화된 태스크에서 유용, 창의적 생성에는 부적합
- Temperature: 분포의 날카로움 제어
- Top-k: 하위 확률 토큰 차단, 고정 k가 단점
- Top-p (Nucleus): 분포 모양에 맞게 동적으로 후보 결정 — 현재 가장 널리 쓰임
실제로는 temperature + top_p + repetition_penalty를 함께 조합하는 경우가 대부분이다.