개요

16편까지 오면서 모델은 학습을 마쳤다. 이제 답을 만들어낼 차례다.

그런데 여기서 생각보다 큰 자유도가 하나 남아 있다.

학습이 끝난 모델은 다음 토큰의 확률 분포만 알려준다. 그중에 뭘 고를지는 아직 정해지지 않았다.

같은 모델, 같은 질문인데 답이 매번 다르게 나오는 이유가 여기에 있다. 이번 글은 그 고르는 방법에 대한 이야기다.

교육 자료는 대외비라 슬라이드 이미지나 예제 데이터는 싣지 않는다.


1. 자동회귀 생성 — 한 토큰씩 만든다

거대 언어 모델이 답을 만드는 방식은 순차적이다.

자동회귀 생성(Auto-regressive Generation) = 토큰을 하나 만들고, 그걸 다시 입력에 붙여서 다음 토큰을 만드는 방식

입력: [아까, 밥, 먹고]
        ↓ 모델
다음 토큰의 확률 분포:
   왔군   0.06
   왔어   0.5
   왔는데  0.2
   왔거든  0.1
   ...
        ↓ 하나 고른다
입력: [아까, 밥, 먹고, 왔어]
        ↓ 다시 모델
        ...

정리하면 이렇다.

  • 입력 $x = [x_1, \ldots, x_L]$ 이 주어진다
  • 모델은 다음 토큰에 대한 확률 분포 $\hat{p}(x)$ 를 내놓는다
  • 디코딩 알고리즘이 그 분포에서 $x_{L+1}$ 을 고른다

자동회귀 생성 루프

모델은 점수표만 주는 심판이다. 그 점수표를 보고 실제로 하나를 뽑는 사람이 디코딩 알고리즘이다. 둘은 완전히 다른 역할이다.

보충 — 모델이 실제로 뱉는 것은 확률이 아니다

한 단계 더 들어가면 모델의 마지막 층이 내놓는 값은 확률이 아니라 로짓(logit) 이다. 어휘 사전 크기만큼의 실수 벡터고, 음수도 있고 합이 1도 아니다.

$$ z = [z_1, z_2, \ldots, z_V] \xrightarrow{\text{softmax}} p = [p_1, p_2, \ldots, p_V] $$

softmax를 한 번 통과시켜야 비로소 합이 1인 확률 분포가 된다. 이 구분이 뒤에서 중요해진다. Temperature는 확률을 건드리는 게 아니라 softmax 이전의 로짓을 건드리기 때문이다.

즉 디코딩 알고리즘이 손대는 지점은 두 곳이다. softmax에 넣기 전(Temperature) 과 확률이 나온 다음(Top-K, Top-P).

또 하나. 그림의 고리는 매 토큰마다 모델을 한 번씩 다시 돌린다는 뜻이다. 그래서 생성 길이가 늘어나면 응답 시간도 그만큼 늘어난다. 앞부분 계산을 재활용하는 KV 캐시 같은 기법이 없으면 이 반복은 훨씬 더 비싸진다.

언제 멈추나

계속 만들다 보면 언제 끝내야 할지가 문제다. 두 가지 조건이 있다.

종료 조건 설명
EOS 토큰 생성 문장 끝을 뜻하는 특수 토큰이 나오면 종료
최대 토큰 수 도달 미리 정해둔 개수만큼 만들면 강제 종료

EOS(End Of Sentence) 는 토크나이저가 붙이는 특수 토큰이다. BERT의 [SEP] 같은 것이 그 역할을 한다.

text = "Tokenizing text is a core task of NLP."
        ↓ 토크나이저
['[CLS]', 'token', '##izing', 'text', 'is', 'a',
 'core', 'task', 'of', 'nl', '##p', '.', '[SEP]']
                                        ^^^^^^^
                                    문장 종료 표시

두 번째 조건이 실무에서 자주 걸린다. 답이 중간에 뚝 끊기는 현상은 대부분 최대 토큰 수에 먼저 도달한 것이다.


2. 디코딩 알고리즘 — 여섯 갈래

라이브러리에서 제공하는 방식만 봐도 여러 가지다. 이번 글에서는 여섯 가지를 본다.

디코딩 알고리즘 지도

# 이름 한 줄 요약
① Greedy Decoding 1등만 집는다
② Beam Search 후보 k개를 들고 간다
③ Sampling 확률대로 뽑는다
④ Sampling with Temperature 분포를 조작한 뒤 뽑는다
⑤ Top-K Sampling 상위 K개 안에서만 뽑는다
⑥ Top-P Sampling 누적 확률 P까지만 남기고 뽑는다

①과 ②는 11편에서 이미 다뤘으니 짧게 짚고 넘어간다.


3. ① Greedy Decoding

핵심 — 매 순간 가장 확률이 높은 토큰을 고른다.

   
장점 사용하기 쉽다. 항상 같은 답이 나온다
단점 직후만 고려하기 때문에 최종 결과가 최선이 아닐 수 있다

이 단점이 핵심이다. 지금 1등을 집었는데, 그 뒤에 이어질 수 있는 단어들이 다 별로일 수 있다.


핵심 — 확률이 높은 k개(beam size)의 후보를 동시에 들고 간다.

고르는 기준이 다르다. 한 시점의 확률이 아니라, 지금까지 만든 문장 전체가 나올 확률의 곱(누적 확률) 이다.

교재 예시가 직관적이다.

$$ P(\text{“The dog has”}) = 0.4 \times 0.9 = 0.36 $$

$$ P(\text{“The nice woman”}) = 0.5 \times 0.4 = 0.20 $$

Greedy vs Beam Search

Greedy는 첫 단계에서 0.5인 “nice”를 집는다. 그런데 그 뒤가 0.4밖에 안 된다. Beam Search는 둘 다 들고 가다가 끝에서 누적 확률이 높은 “The dog has” 를 고른다.

   
장점 최종적으로 좋은 응답을 생성할 확률이 높다
단점 계산 비용이 많이 늘어난다 (후보마다 추론을 수행)

보충 — 곱셈이 아니라 로그 덧셈으로 계산한다

확률을 계속 곱하면 값이 순식간에 0에 가까워진다. 토큰 100개면 0.5를 100번 곱하는 셈이라 부동소수점이 버티지 못한다. 그래서 실제 구현은 로그 확률의 합을 쓴다.

$$ \log P(y) = \sum_{t=1}^{T} \log p(y_t \mid y_{1:t-1}) $$

곱셈이 덧셈이 되니 언더플로우가 사라진다. 순서를 비교하는 데는 문제가 없다. 로그는 단조증가 함수라 큰 확률이 큰 로그 확률이다.

보충 — 짧은 답이 유리해지는 함정

로그 확률은 전부 음수다. 그래서 토큰이 하나 늘어날 때마다 점수가 무조건 깎인다. 그대로 두면 beam search는 짧은 답만 계속 고르게 된다.

그래서 길이로 나눠주는 보정을 넣는다.

$$ \text{score}(y) = \frac{1}{|y|^{\alpha}} \sum_{t} \log p(y_t \mid y_{1:t-1}) $$

$\alpha$ 는 보통 0.6~1.0을 쓴다. 라이브러리에서 length_penalty 라는 이름으로 노출되는 값이 이것이다.

Beam Search의 빔 크기 $k$ 를 1로 놓으면 Greedy와 정확히 같아진다. 둘은 다른 알고리즘이 아니라 같은 축의 양 끝이다.

한 가지 더. Beam Search는 누적 확률이 높은 문장을 고르는 알고리즘이다. 그런데 사람이 실제로 쓰는 글은 확률이 가장 높은 문장이 아니다. 이 어긋남이 뒤에 나올 반복률 표에서 그대로 드러난다.


5. ③ Sampling — 확률대로 뽑는다

여기서부터가 새로 다루는 내용이다.

핵심 — 모델이 제공한 확률을 기준으로 무작위로 고른다.

"What's your favorite color?"
        ↓
   green  50%
   red    30%
   the    0.2%
   a      0.1%
   ...

Greedy라면 항상 green이다. Sampling은 주사위를 던진다. 50% 확률로 green, 30% 확률로 red가 나온다.

   
장점 다양한 응답을 생성할 수 있다
단점 생성된 응답의 품질이 불안정해질 수 있다

단점의 이유가 중요하다. 모델은 사전에 있는 모든 단어에 대해 확률을 매겨놓는다. 0.001% 짜리 이상한 단어도 확률이 0은 아니다. 계속 뽑다 보면 언젠가 걸린다.

Sampling의 꼬리 문제

100면 주사위에 대부분 정상적인 답이 적혀 있고, 한두 칸에 엉뚱한 답이 적혀 있다고 하자. 한 번은 괜찮다. 그런데 문장 하나 만드는 데 주사위를 200번 던진다면 엉뚱한 칸이 안 걸릴 수가 없다.

보충 — 숫자로 보면 더 분명하다

꼬리에 걸릴 확률이 한 토큰당 $q$ 라면, $n$ 개를 뽑는 동안 한 번도 안 걸릴 확률은 $(1-q)^n$ 이다. 뒤집으면 한 번이라도 걸릴 확률은 이렇다.

$$ P(\text{최소 한 번}) = 1 - (1-q)^{n} $$

꼬리 확률 $q$ 200토큰 생성 시 걸릴 확률
0.1% $1 - 0.999^{200} \approx$ 18%
0.5% $1 - 0.995^{200} \approx$ 63%
1% $1 - 0.99^{200} \approx$ 87%

한 번에 0.1%면 무시할 만한 값처럼 보인다. 그런데 200번 반복하면 무시할 수 없는 값이 된다. 게다가 자동회귀 생성이라 한 번 이상한 토큰이 박히면 그게 다시 입력으로 들어간다. 뒤의 토큰들이 그 잘못된 문맥 위에서 생성되면서 문장 하나가 통째로 무너진다.

꼬리 자체가 문제가 아니라 꼬리 × 반복 횟수 × 되먹임이 문제다. Top-K와 Top-P는 바로 이 꼬리를 없애자는 발상이다.


6. ④ Sampling with Temperature — 분포를 주무른다

핵심 — 하이퍼파라미터 $T$ 로 확률 분포 자체를 임의로 조작한 뒤 뽑는다.

Temperature의 효과

방향은 두 가지다.

설정 분포가 결과
$T \gt 1$ 평평해진다 (Smooth) 더 다양한 응답
$T \lt 1$ 뾰족해진다 (Sharp) 확률 높은 응답에 집중

보충 — “조작”이 정확히 무엇인가

앞에서 모델이 내놓는 건 로짓이라고 했다. Temperature가 하는 일은 softmax에 넣기 전에 로짓을 $T$ 로 나누는 것, 그게 전부다.

$$ p_i = \frac{\exp(z_i / T)}{\sum_j \exp(z_j / T)} $$

$T = 1$ 이면 나누기가 무의미하니 원래 분포 그대로다. $T$ 를 1보다 작게 하면 로짓 간격이 벌어지고, 크게 하면 좁혀진다. 간격이 벌어질수록 softmax 결과는 뾰족해진다.

Temperature와 softmax

로짓 $z = [3, 2, 1, 0]$ 하나로 $T$ 만 바꿔보면 이렇다.

토큰 $T = 0.5$ $T = 1.0$ $T = 2.0$
$z = 3$ 0.865 0.644 0.455
$z = 2$ 0.117 0.237 0.276
$z = 1$ 0.016 0.087 0.167
$z = 0$ 0.002 0.032 0.102

순위는 절대 바뀌지 않는다. 바뀌는 건 1등과 나머지의 격차뿐이다. $T=0.5$ 에서 4등은 사실상 없는 것과 같고(0.2%), $T=2$ 에서는 10번에 한 번은 나온다.

여기서 두 극단이 자연스럽게 나온다.

극단 결과
$T \to 0$ 1등 확률이 1로 수렴 → Greedy와 완전히 동일
$T \to \infty$ 모든 후보가 $1/V$ → 완전한 무작위, 모델 지식이 사라짐

API에서 temperature=0 을 주면 “결정론적”이 되는 이유가 이것이다. 별도의 모드가 아니라 같은 수식의 극한이다.

$T \lt 1$ 일 때

분포가 뾰족해진다. 특정 후보 하나가 압도적으로 높은 확률을 갖고, 나머지는 아주 낮아진다.

모델이 항상 비슷한 답만 내놓게 된다. 안정적이지만 다양성이 떨어진다.

$T \gt 1$ 일 때

분포가 평평해진다. 모든 단어가 거의 비슷한 확률로 선택될 수 있게 된다.

다양성이 극대화되지만 품질이 불안정해진다. 창의적이나 헛소리가 섞일 수 있다.

극단적으로 $T$ 를 21까지 올리면 그래프가 거의 완전히 평평해진다. 10개 후보가 전부 0.1씩이다. 이 상태면 모델이 학습한 지식이 사실상 무의미해진다.

온도를 낮추면 모범생이 된다. 안전한 답만 반복한다. 온도를 올리면 자유로운 예술가가 된다. 참신하지만 가끔 이해할 수 없는 소리를 한다.

실무에서는 작업 성격에 맞춰 조절한다. 사실 확인이 중요한 작업은 낮게, 아이디어 발산이 필요한 작업은 높게 잡는다.


7. ⑤ Top-K Sampling — 후보를 줄인다

Sampling의 단점은 꼬리에 있는 이상한 단어까지 뽑힐 수 있다는 것이었다. 그럼 잘라내면 된다.

핵심 — 확률이 높은 K개의 토큰들 중에서만 확률에 따라 샘플링한다.

Top-K와 Top-P

P(w | "The") 의 상위 후보들

남긴다 (K=6): nice, dog, car, woman, guy, man
버린다: people, big, house, cat
   
장점 품질이 낮은 응답을 생성할 가능성을 줄일 수 있다
단점 확률 분포의 모양과 상관없이 고정된 K개의 후보군을 고려한다

단점을 구체적으로 보면 이렇다. 같은 K=6인데 문맥에 따라 상황이 완전히 다르다.

문맥 상위 6개의 확률 합
"The" 다음 0.68
"The car" 다음 0.99
  • "The" 다음에는 올 수 있는 말이 많다. 6개로 자르면 괜찮은 후보를 버리게 된다
  • "The car" 다음에는 사실상 drives, is, turns 정도다. 6개나 남기면 이상한 후보를 억지로 포함시킨다

문맥에 따라 선택지의 폭이 다른데 K를 고정해두면 어느 쪽에서든 어긋난다.

보충 — 자른 다음에는 반드시 다시 정규화한다

빠지기 쉬운 부분이다. 상위 K개만 남기면 남은 확률의 합이 1이 아니다. 위 예시라면 0.68밖에 안 된다. 그 상태로는 샘플링을 할 수 없으니 남은 것들끼리 다시 확률로 만든다.

$$ p’i = \frac{p_i}{\sum{j \in \text{top-}K} p_j} $$

"The" 다음 상위 6개의 합이 0.68이었으니, 각 확률을 0.68로 나눠주면 합이 1이 된다. 버려진 후보들이 갖고 있던 확률이 살아남은 후보들에게 비례 배분되는 셈이다.

그래서 Top-K는 “꼬리를 0으로 만든다”가 정확한 표현이다. 꼬리가 갖던 확률질량은 사라지지 않고 머리 쪽으로 옮겨간다. Top-P도 똑같다.


8. ⑥ Top-P Sampling (Nucleus Sampling)

핵심 — K를 고정하는 대신, 누적 확률(P) 을 기준으로 K를 자동으로 조절한다.

동작은 단순하다.

P = 0.9 로 설정한 경우

1. 확률이 높은 순서대로 더해나간다
2. 합이 처음으로 0.9를 넘는 지점에서 멈춘다
3. 거기까지의 후보만 남기고 그 안에서 샘플링한다

Top-P 누적 확률 컷오프

그림의 예시를 손으로 따라가면 이렇다. 확률을 내림차순으로 정렬해두고 위에서부터 더한다.

순위 확률 누적 판정
1 0.40 0.40 남김
2 0.22 0.62 남김
3 0.15 0.77 남김
4 0.09 0.86 남김
5 0.06 0.92 남김 — 여기서 0.9를 넘음
6 이하 0.03 … — 전부 버림

여기서도 자른 뒤 다시 정규화한다. $0.40 / 0.92 = 0.435$, $0.22 / 0.92 = 0.239$ 하는 식이다.

포인트는 P를 넘긴 그 토큰까지 포함한다는 것이다. 0.86에서 멈추면 P=0.9를 만족하지 못하기 때문에, 넘기는 순간의 토큰까지 넣고 자른다. 그래서 실제 누적합은 항상 P보다 조금 크다.

앞의 예시에 적용하면 이렇게 된다.

문맥 Top-K (K 고정) Top-P (P=0.9)
"The" 다음 6개, 합 0.68 9개, 합 0.94
"The car" 다음 6개, 합 0.99 3개, 합 0.97

선택지가 많은 문맥에서는 넓게, 적은 문맥에서는 좁게 알아서 조절한다. 후보 개수 K를 사람이 정하는 게 아니라 분포의 모양이 매 스텝마다 정해준다. 이름의 “핵(nucleus)”은 그렇게 남은 상위 덩어리를 가리킨다.

성능 비교에서도 좋은 결과를 냈다.

방법 반복률(Repetition %) 비고
Human (사람이 쓴 글) 0.28 기준선
Greedy 73.66 같은 말을 계속 반복
Beam (b=16) 28.94 여전히 높음
Pure Sampling 0.22 반복은 없으나 품질 불안정
Nucleus (p=0.95) 0.36 사람과 가장 비슷

Greedy의 반복률 73.66%가 눈에 띈다. 매번 1등만 집으면 같은 표현을 계속 되풀이하게 된다.


9. 한눈에 비교

알고리즘 장점 단점
Greedy Decoding 쉬운 사용법 최적해 보장 X
Beam Search 좋은 응답 생성 확률 ↑ 큰 계산 비용
Sampling 다양한 응답 생성 가능 품질 불안정
Sampling with Temperature 창의성/안정성 조절 가능 T↑ 품질 저하 / T↓ 다양성 부족
Top-K Sampling 잡음 단어 배제, 품질 향상 K값 고정 → 문맥 따라 불균형
Top-P Sampling (Nucleus) 확률 누적 기준, 품질·다양성 균형 P값 설정 필요 (경우에 따라 랜덤성 여전)

보충 — 이들은 택일이 아니라 파이프라인이다

여섯 갈래라고 나눠놨지만, 실제 구현에서 ④⑤⑥은 서로 배타적이지 않다. 한 줄로 이어 붙는다.

로짓 z
  ↓ ④ Temperature — z를 T로 나눈다
softmax
  ↓ 확률 분포 p
  ↓ ⑤ Top-K — 상위 K개만 남긴다
  ↓ ⑥ Top-P — 누적 P까지만 남긴다
  ↓ 남은 것들끼리 재정규화
주사위를 던진다

순서가 중요하다. Temperature는 softmax 앞, 잘라내기는 softmax 뒤다. 그래서 온도를 먼저 올려 분포를 평평하게 만들면, 그 다음 Top-P가 남기는 후보 개수도 함께 늘어난다. 두 손잡이는 독립이 아니다.

조합 실제로 일어나는 일
T 높임 + top_p 낮춤 평평해진 분포에서 상위만 다시 좁힘 — 어느 정도 상쇄된다
T 낮춤 + top_p 높임 이미 뾰족해서 상위 몇 개가 P를 채움 — top_p가 사실상 무의미해진다
top_k=1 1등만 남으니 Greedy와 동일
top_p=1.0, T=1.0 아무것도 안 자름 — Pure Sampling

그래서 OpenAI 문서도 temperature와 top_p 중 하나만 조절하라고 권한다. 둘 다 만지면 어느 쪽이 결과를 바꾼 건지 알 수 없게 된다.


10. 우리가 이미 만지고 있던 값들

이 값들은 이론 속에만 있는 게 아니다. OpenAI Playground 같은 곳의 모델 설정창에 그대로 노출된다.

Temperature   1.00
Max tokens    2048
Top P         1.00

같은 질문(“SSAFY에 대해 알려줘”)을 던져도 이 값을 바꾸면 답의 구성과 길이가 눈에 띄게 달라진다.

챗봇의 답이 마음에 안 들 때 질문만 바꿔볼 게 아니라, 이 손잡이들을 만질 수 있다는 것을 아는 것이 이번 글의 실질적인 소득이다.

보충 — 작업별 출발점

정답은 없지만 관례적으로 쓰는 출발값은 있다. 여기서 시작해서 결과를 보고 조절하는 것이 실무 흐름이다.

작업 Temperature Top-P 이유
코드 생성, 사실 추출 0 ~ 0.2 1.0 정답이 하나다. 흔들릴 이유가 없다
요약, 번역, 분류 0.2 ~ 0.5 1.0 원문에 매여야 한다
일반 대화 0.7 ~ 1.0 0.9 ~ 1.0 자연스러움과 정확성의 절충
브레인스토밍, 카피 1.0 ~ 1.2 0.95 뻔한 답이 나오면 실패다

한 가지 주의할 점. temperature=0 이어도 결과가 100% 똑같지 않을 수 있다. GPU 연산 순서나 배치 구성에 따라 부동소수점 결과가 미세하게 달라지고, 1등과 2등의 로짓이 거의 붙어 있으면 순위가 뒤집힐 수 있다. 결정론에 가깝다가 정확한 표현이다.

그리고 이 손잡이들은 품질 문제를 고치는 도구가 아니다. 모델이 모르는 사실은 온도를 낮춘다고 알게 되지 않는다. 다양성과 안정성 사이의 저울추일 뿐이다.

실제 코드에서 이 값들(temperature, top_p 등)을 어떻게 넘기는지는 이후 Python 시리즈 실습 편에서 다룰 예정이다. 개념은 여기, 실행은 그쪽이다.


정리

  • 자동회귀 생성 — 토큰을 하나 만들어 다시 입력에 붙이며 순차적으로 생성한다
  • 모델은 확률 분포만 준다. 고르는 일은 디코딩 알고리즘의 몫
  • 종료 조건은 EOS 토큰 또는 최대 토큰 수. 답이 뚝 끊기면 대개 후자
  • Greedy — 1등만 집는다. 쉽지만 최선이 아닐 수 있고 반복률이 매우 높다
  • Beam Search — 후보 k개를 들고 누적 확률로 고른다. 계산 비용이 크다
  • Sampling — 확률대로 뽑는다. 다양하지만 꼬리의 이상한 단어가 걸린다
  • Temperature — 분포를 평평하게(T>1) / 뾰족하게(T<1) 조작한다
    • 낮으면 모범생, 높으면 예술가. 작업 성격에 맞춰 조절
  • Top-K — 상위 K개만 남긴다. 단 문맥에 따라 선택지 폭이 다른데 K가 고정이다
  • Top-P (Nucleus) — 누적 확률로 자르니 K가 자동으로 조절된다. 균형이 가장 좋다
  • 자른 뒤에는 반드시 재정규화한다. 꼬리의 확률질량은 사라지는 게 아니라 머리로 옮겨간다
  • ④⑤⑥은 택일이 아니라 파이프라인이다. 순서는 로짓 → T로 나누기 → softmax → 자르기 → 재정규화 → 샘플링
  • $T \to 0$ 은 Greedy, top_k=1 도 Greedy. 극단은 결국 같은 곳에서 만난다
  • 이 값들은 Playground 설정창에 그대로 나와 있다. 만질 수 있다는 걸 아는 것이 중요하다

다음 글에서

디코딩은 모델이 준 확률을 어떻게 쓸 것인가의 문제였다. 그럼 애초에 모델에게 무엇을 넣을 것인가는?

다음 글에서는 프롬프트 엔지니어링 — 시스템 프롬프트, 예시 선택, Chain-of-Thought를 다룬다.

여기서 본 온도(temperature)는 생성할 때만 쓰는 값이 아니다. 26편에서 CLIP이 같은 수식을 학습 중 손실 계산에 쓰는 걸 보고, 이번엔 사람이 아니라 모델이 그 값을 정한다.

참고 자료

한줄 평

  • 재밌는 하이퍼 파라미터 조정여행