제1부 기반: 언어모델과 트랜스포머
1.7 샘플링·디코딩
온도(temperature)라는 숫자 하나를 0.2에서 1.2로 높이면, 방금까지 건조하게 사실만 답하던 모델이 갑자기 산만한 이야기꾼이 된다. 가중치는 한 비트도 바뀌지 않았다. 프롬프트도 그대로다. 바뀐 것은 오직 모델이 출력한 확률분포에서 토큰을 어떻게 고르는가 하나뿐이다. 이것이 이 장의 주제인 디코딩(decoding)이고, 여기서 되풀이 마주칠 사실은 단순하다 — 언어모델은 결코 "문장"을 주지 않는다. 매 스텝 다음 토큰의 확률분포를 줄 뿐이다.
1.4에서 봤듯 트랜스포머의 마지막 선형층(언어모델 헤드)은 어휘 크기 $|V|$ 차원의 실수 벡터 하나, 즉 로짓(logit) 을 낸다. 어휘가 5만에서 30만이면 매 스텝 그만큼의 점수가 대량으로 산출된다. 이 점수 더미에서 실제 토큰 하나를 골라 시퀀스에 붙이고, 다시 순전파하고, 또 고르고 — 이 고르는 규칙 전체가 디코딩이다. 규칙은 모델 파라미터를 변경하지 않는다. 순수하게 추론 시점(inference-time)의 후처리다. 그런데도 같은 모델·같은 프롬프트에서 나오는 출력의 성격 전부 — 정확한가, 창의적인가, 반복하는가, 재현되는가 — 를 이 규칙이 정한다. 그래서 디코딩 파라미터는 프롬프트만큼이나 자주 조정하는 변수이고, 잘못 만지면 좋은 모델도 망가진 것처럼 보인다.
이 장은 로짓이 확률이 되는 수학(softmax와 온도)에서 출발해, 온도·top-k·top-p·min-p가 각각 분포를 어떻게 자르는지를 같은 숫자로 직접 계산해 비교하고, 빔서치가 왜 번역엔 좋고 챗엔 나쁜지, 반복을 억제하는 조정 변수들이 어디서 부호 함정에 빠지는지, 그리고 "seed를 고정했는데 왜 매번 다른가"라는 재현성의 불편한 진실까지 다룬다. 이 장의 확률 수치와 파라미터 권고값은 설명을 위한 개략값이자 견해다 — 모델·태스크마다 최적은 다르고, API 벤더의 파라미터 이름·범위·기본값은 수시로 바뀐다. 무게는 특정 숫자가 아니라 원리와 방향성에 있다.
1.7.1 모델이 주는 것은 문장이 아니라 분포다
로짓 $z \in \mathbb{R}^{|V|}$ 는 단순히 실수 점수라 음수도 나오고 합이 1도 아니다. 이것을 확률분포로 바꾸는 것이 softmax다.
$$p_i = \frac{\exp(z_i)}{\sum_{j=1}^{|V|} \exp(z_j)}$$
softmax의 성질 하나를 먼저 명시해 둘 가치가 있다. 지수를 쓰므로 확률을 결정하는 것은 로짓의 절대값이 아니라 로짓들 사이의 차이(gap) 다. 모든 로짓에 같은 상수 $c$ 를 더해도 $\exp(z_i+c) = \exp(z_i)\exp(c)$ 의 공통 인자 $\exp(c)$ 가 분자·분모에서 상쇄되어 $p_i$ 가 그대로다 — softmax는 평행이동 불변이다. 그래서 "로짓이 크다/작다"는 그 자체로는 의미가 없고, $z_i - z_j$ 가 얼마나 벌어졌는지가 전부다. 이 성질은 곧 볼 온도가 왜 "차이를 늘리고 줄이는" 조절기인지를 설명한다.
실제 구현은 softmax를 그대로 계산하지 않는다. $\exp$ 는 입력이 조금만 커도 오버플로하기 때문이다($\exp(1000)$ 은 부동소수 범위를 넘는다). 그래서 각 벡터의 최댓값을 먼저 빼고 계산한다 — $\text{softmax}(z)_i = \exp(z_i - \max_j z_j)/\sum_k \exp(z_k - \max_j z_j)$. 방금 본 평행이동 불변성 덕분에 결과는 똑같지만, 지수의 인자가 0 이하가 되어 오버플로가 사라진다. 사소해 보이는 이 트릭이 1.4의 온라인 softmax와 어텐션 커널의 수치 안정성을 뒷받침한다.
디코딩은 이 확률을 받아 토큰 하나를 고른다. 위치를 정확히 그리면, 자기회귀 생성 루프의 매 스텝 끝에 다음이 붙는다 — 모델 순전파로 마지막 위치 로짓을 얻고, 로짓을 후처리(패널티·bias·마스크)하고, 온도로 스케일해 softmax를 취하고, 후보를 절단해 재정규화하고, 샘플(또는 argmax)하고, 멈출지 판정하고, 고른 토큰을 시퀀스에 붙이고 KV 캐시를 갱신한다(5.1).
여기서 두 가지가 이 장 내내 걸린다. 첫째, 매 스텝은 독립적으로 하나를 고른다. 빔서치를 빼면 디코딩은 문장 전체의 확률을 전역 최적화하지 않는다 — "지금 이 자리 토큰 하나"만 본다. 좋은 다음 토큰들의 연쇄가 좋은 문장이 되기를 바랄 뿐, 그것을 보장하는 장치는 없다. 둘째, 순서가 있다. 대부분 구현은 로짓에 패널티·bias·마스크를 먼저 적용하고, 온도로 나누고, top-k·top-p/min-p로 자른 뒤, softmax·샘플 순으로 변형한다. 이 순서가 바뀌면 결과가 달라진다(1.7.5).
1.7.2 온도의 수학 — 분포를 데우고 식히기
온도 $T$ 는 로짓을 softmax에 넣기 전에 나눈다.
$$p_i(T) = \frac{\exp(z_i / T)}{\sum_{j} \exp(z_j / T)}$$
직관은 방금의 gap 이야기에서 곧장 나온다. $T<1$ 이면 로짓을 키워 차이를 벌리니 분포가 뾰족(sharp)해지고 상위 토큰이 지배한다 — 보수적·결정적. $T>1$ 이면 차이를 좁혀 분포가 평평(flat)해지니 하위 토큰도 살아남는다 — 다양·모험적. 극단에서 $T\to 0$ 이면 최댓값 하나가 확률 1로 수렴해 argmax, 즉 greedy와 같아진다(그래서 API의 temperature=0은 보통 실제로 0으로 나누지 않고 argmax로 구현한다). $T\to\infty$ 면 모든 토큰이 같은 확률인 균등분포로 수렴한다.
같은 로짓 $z = [2.0,\ 1.0,\ 0.5,\ 0.0]$ 네 토큰이 온도에 따라 어떻게 변하는지 softmax를 실제로 실행해 보자. $T=1$ 이면 $\exp(z) = [7.39, 2.72, 1.65, 1.00]$, 합 $12.76$ 이라 $p = [0.58, 0.21, 0.13, 0.08]$. $T=0.5$ 면 $z/T = [4,2,1,0]$, $\exp = [54.60, 7.39, 2.72, 1.00]$, 합 $65.70$ 이라 A가 $0.83$ 으로 치솟는다. $T=2$ 면 $z/T = [1, 0.5, 0.25, 0]$ 이라 네 토큰이 훨씬 고르게 나뉜다.
| 토큰(로짓) | $T=0.5$ | $T=1.0$ | $T=1.5$ | $T=2.0$ |
|---|---|---|---|---|
| A (2.0) | 0.83 | 0.58 | 0.47 | 0.41 |
| B (1.0) | 0.11 | 0.21 | 0.24 | 0.25 |
| C (0.5) | 0.04 | 0.13 | 0.17 | 0.19 |
| D (0.0) | 0.02 | 0.08 | 0.12 | 0.15 |
| 엔트로피 $H$ (nats) | 0.60 | 1.11 | 1.26 | 1.31 |
마지막 줄이 이 절의 핵심이다. 온도가 하는 일을 한 숫자로 요약하면 분포의 엔트로피 $H(p) = -\sum_i p_i \log p_i$ 다 — 분포가 얼마나 "퍼져 있나". $T=0.5$ 에서 0.60이던 것이 $T=2$ 에서 1.31로 오른다(네 토큰의 최대 엔트로피는 $\log 4 \approx 1.386$). 온도를 높이면 엔트로피가 오른다. 이것을 말이 아니라 수식으로 정확히 세울 수 있고, 그러면 왜 하필 이렇게 되는지가 보인다.
1.7.2.1 온도를 높이면 엔트로피가 오른다 — 유도
역온도(inverse temperature) $\beta = 1/T$ 로 바꿔 쓰면 온도 softmax는
$$p_i(\beta) = \frac{e^{\beta z_i}}{\sum_j e^{\beta z_j}}$$
가 되는데, 이것은 통계역학의 볼츠만·기브스 분포와 글자 그대로 같다 — $z_i$ 가 (음의) 에너지, $\beta$ 가 역온도다. 언어모델의 온도가 물리의 온도와 같은 이름을 쓰는 것은 우연이 아니라 같은 식이라서다. 로그 분배함수 $A(\beta) = \log \sum_j e^{\beta z_j}$ 를 두면 두 고전적 항등식이 나온다.
$$A'(\beta) = \sum_i p_i z_i = \mathbb{E}_p[z], \qquad A''(\beta) = \mathbb{E}_p[z^2] - \big(\mathbb{E}_p[z]\big)^2 = \operatorname{Var}_p(z) \ge 0$$
즉 로그 분배함수의 1차 미분은 현재 분포에서 로짓의 평균, 2차 미분은 로짓의 분산이다(분산이라 항상 0 이상). 이제 엔트로피를 $\beta$ 로 표현한다. $\log p_i = \beta z_i - A(\beta)$ 이므로
$$H(\beta) = -\sum_i p_i \log p_i = -\sum_i p_i(\beta z_i - A) = -\beta\,\mathbb{E}_p[z] + A(\beta) = A(\beta) - \beta A'(\beta)$$
이것을 $\beta$ 로 미분하면 $A'$ 항이 상쇄되어 명료한 결과가 남는다.
$$\frac{dH}{d\beta} = A'(\beta) - \big[A'(\beta) + \beta A''(\beta)\big] = -\beta\,\operatorname{Var}_p(z) \le 0$$
$\beta = 1/T > 0$ 이고 분산은 0 이상이니 엔트로피는 $\beta$ 에 대해 단조 감소한다. $T$ 로 되돌리면($\beta = 1/T$, $d\beta/dT = -1/T^2$)
$$\frac{dH}{dT} = \frac{dH}{d\beta}\cdot\frac{d\beta}{dT} = \big(-\beta\operatorname{Var}_p(z)\big)\!\left(-\frac{1}{T^2}\right) = \frac{\operatorname{Var}_p(z)}{T^{3}} \ge 0$$
온도를 높이면 엔트로피가 반드시 오른다는 것이 이것으로 증명됐고, 덤으로 얼마나 빨리 오르는지까지 나왔다 — 그 순간 살아 있는 로짓들의 분산에 비례한다. 모델이 확신할수록(로짓이 서로 멀리 벌어져 분산이 큼) 같은 온도 상승이 엔트로피를 더 많이 높인다. 반대로 이미 애매한 분포(로짓이 뭉쳐 분산이 작음)는 온도를 높여도 별로 안 퍼진다. 극한도 이 식과 맞는다 — $T\to 0$ 이면 모든 질량이 argmax에 몰려 $H\to 0$, $T\to\infty$ 면 균등분포라 $H\to \log|V|$ 로 최대다.
여기서 1.4의 $\sqrt{d_k}$ 와의 형제 관계가 분명해진다. 어텐션에서는 점수를 $\sqrt{d_k}$ 로 나눠 softmax에 들어가는 로짓을 $O(1)$ 로 유지한다 — 차원이 커져 로짓 전체가 커지면 softmax가 포화해 거의 원-핫이 되고 기울기가 죽기 때문이다. 여기 디코딩에서는 로짓을 $T$ 로 나눠 샘플링 softmax의 포화 정도를 일부러 조절한다. 둘 다 "로짓을 스케일로 나눠 softmax의 뾰족함을 제어한다"는 같은 연산이고, 의도만 반대다 — 하나는 학습 중 포화를 막으려는 방어이고, 다른 하나는 추론 중 포화를 원하는 만큼 데우거나 식히려는 조종이다. 온도를 "무작위성의 양"으로만 외우면 곧 볼 절단(top-k·top-p)과 혼동하는데, 정확히는 온도가 분포의 모양(뾰족/평평, 곧 엔트로피) 을 조절하고 절단은 후보 집합의 크기(어디까지 살릴까) 를 조절한다. 둘은 직교하며 보통 함께 쓴다.
1.7.3 탐욕과 순수 온도 샘플링, 그리고 각자의 한계
가장 단순한 규칙은 매 스텝 가장 확률 높은 토큰 하나를 뽑는 것이다.
$$x_t = \arg\max_{i}\ p(i \mid x_{<t})$$
무작위성이 없으니(난수를 안 쓴다) 재현성·안정성이 장점이고, 짧고 사실적인 출력 — 분류 라벨, 추출, 코드 한 줄 — 에 잘 맞는다. 단점은 둘이다. 하나는 다양성이 없다는 것 — 같은 입력엔 사실상 같은 출력이라 여러 후보가 필요하면 무력하다. 다른 하나는 더 미묘한 국소 최적의 함정이다. 매 스텝의 최선이 문장 전체의 최선은 아니다. "지금 확률 1등"만 계속 좇다 보면 뒤에서 막다른 골목에 몰려 어색하거나 반복적인 문장이 되기도 한다. 확률을 탐욕적으로 좇는 디코딩이 오히려 퇴행적 반복(degenerate repetition) — "the the the", 같은 절의 되풀이 — 에 빠지기 쉽다는 것은 잘 알려진 관찰이다(Holtzman 등이 2020년 "신경 텍스트 퇴행"을 다룬 연구에서 정리했다). 인간 텍스트는 매 단어가 최고확률이 아니라 적당히 놀랍기 때문인데, 이것은 뒤의 빔서치 논의에서 다시 만난다. greedy는 $T\to 0$ 또는 top_k=1의 특수 경우로 볼 수 있다.
greedy 대신 $p(T)$ 에서 뽑기(sampling)를 하면 다양성이 생기고, 온도가 그 세기를 조절한다. 그런데 절단 없는 순수 온도 샘플링에는 고약한 문제가 있다 — 긴 꼬리다. 어휘가 10만이면 확률 $10^{-5}$ 짜리 잘못된 토큰이 수만 개 있고, 개별로는 무시할 만해도 이들을 다 합치면 무시 못 할 질량이 된다. 온도를 조금만 높여도(1.7.2의 유도대로 엔트로피가 오르며) 이 꼬리 전체가 커져, 이따금 문맥과 전혀 안 맞는 토큰이 출현한다. 한 번 그런 토큰이 나오면 그 뒤 문장 전체가 그 실수 위에 쌓이니 피해가 국소적이지 않다. 그래서 실무에선 온도 단독으로 다양성을 주는 대신 온도와 절단을 함께 쓴다 — 꼬리를 먼저 잘라 버리고, 남은 그럴듯한 후보들 사이에서만 온도로 뽑는다. 다음 절이 그 자르는 법이다.
1.7.4 꼬리를 자르는 세 가지 — top-k · top-p · min-p
절단(truncation) 계열은 "샘플링 후보를 어디까지 살릴까"를 정한다. 살린 토큰만 남기고 그것들만으로 재정규화한 뒤 그 안에서 뽑는다. 세 방법은 무엇을 기준으로 자르나가 다르다. 아래에서 셋 다 같은 예시 분포에 적용해 살아남는 후보와 확률을 실제로 계산한다. 예시 분포는 온도 1에서 어떤 로짓이 softmax를 통과해 나온 결과라 치고, 내림차순으로 A 0.40, B 0.25, C 0.12, D 0.08, E 0.05, F 0.04, G 0.03, H 0.02, 그리고 나머지 꼬리가 합쳐 0.01이라 하자(전체 합 1.00).
1.7.4.1 top-k — 개수로 자르기
확률 상위 $k$ 개만 남기고 나머지는 버린다. $V^{(k)}$ 를 최상위 $k$ 토큰 집합이라 하면
$$p'_i = \begin{cases} \dfrac{p_i}{\sum_{j \in V^{(k)}} p_j} & i \in V^{(k)} \\[2mm] 0 & \text{그 외} \end{cases}$$
$k=3$ 이면 예시에서 $\{A, B, C\}$ 만 남고 재정규화 분모가 $0.40+0.25+0.12 = 0.77$ 이라, A의 최종 확률은 $0.40/0.77 \approx 0.519$ 로 오른다. 단순하고 빠르지만 약점이 분명하다 — $k$ 가 고정이다. 어떤 자리는 답이 뻔해 한 토큰에 90%가 몰리는데 $k=40$ 이면 잘못된 39개까지 후보에 넣고, 어떤 자리는 실제로 애매해 후보 100개가 자연스러운데 $k=40$ 이 억지로 자른다. 분포의 뾰족함에 적응하지 못한다.
1.7.4.2 top-p — 누적질량으로 자르기 (nucleus sampling)
고정 개수 대신 누적확률 $p$ 를 채우는 가장 작은 집합을 남긴다(Holtzman 등 2020). 확률 내림차순으로 담다가 누적합이 임계 $p$ 에 닿으면 멈춘다. 이 최소 집합 $V^{(p)}$ 를 핵(nucleus) 이라 부른다.
$$V^{(p)} = \arg\min_{V'} |V'| \quad \text{s.t.} \quad \sum_{i \in V'} p_i \geq p$$
예시에 top_p=0.9를 적용해 누적합을 따라가면 A 0.40 → B 0.65 → C 0.77 → D 0.85 → E 0.90 이다. E에서 0.9에 닿으니 핵은 $\{A,B,C,D,E\}$ 로 다섯 개, F 이하는 버린다. 재정규화하면 분모가 0.90이라 A는 $0.40/0.90 \approx 0.444$ 가 된다. top-p의 장점은 후보 개수가 분포에 따라 자동으로 늘고 준다는 것 — 뾰족하면(한 토큰이 이미 $p$ 이상) 후보 한둘, 평평하면 수십 개다. 그래서 top-k의 "고정 개수" 약점을 정확히 메운다. top_p=0.9 근처가 대화형 생성의 관행 기본값처럼 굳었지만, 모델·벤더마다 다르니 절대값으로 믿을 것은 아니다.
1.7.4.3 min-p — 1등 대비 상대 높이로 자르기
top-p도 약점이 있다. 온도가 높아 분포가 평평해지면 누적 $p$ 를 채우느라 질 낮은 꼬리 토큰까지 핵에 끌려 들어온다. min-p(Nguyen 등 2024의 제안)는 임계를 절대 누적질량이 아니라 최상위 토큰 확률에 상대적으로 설정한다. 최대 확률 $p_{\max} = \max_i p_i$ 에 대해
$$\text{토큰 } i \text{를 남긴다} \iff p_i \geq p_{\text{min}} \cdot p_{\max}$$
예시에서 $p_{\text{min}}=0.1$ 이면 임계는 $0.1 \times 0.40 = 0.04$ 다. $p_i \ge 0.04$ 인 토큰은 A, B, C, D, E, 그리고 F(정확히 0.04라 포함)까지 여섯 개이고 G(0.03)부터 탈락한다. 직관은 "1등의 10%보다는 그럴듯한 토큰만 남긴다"이다. 결정적인 성질은 이 임계가 분포에 따라 저절로 움직인다는 것 — 모델이 확신해 1등이 압도적이면(가령 A가 0.90) 임계가 $0.1\times0.90 = 0.09$ 로 높아져 대부분 탈락하고 거의 greedy처럼 좁아지고, 애매하면 임계가 낮아져 넓어진다. top-p가 "누적 질량"으로 자르는 반면 min-p는 "peak 대비 상대 높이"로 자르므로, 고온에서도 명백히 나쁜 꼬리를 더 잘 걸러낸다는 것이 원 제안의 주장이다 — 다만 이는 태스크·조건에 의존하고 벤치마크는 저자 설정이라, "항상 낫다"가 아니라 "고온에서 다양성과 일관성을 함께 원할 때의 대안" 정도로 두는 것이 정직하다.
1.7.4.4 같은 분포, 세 개의 컷
세 방법을 같은 예시 분포에 적용하면 살아남는 후보가 이렇게 갈린다.
| 토큰 | 확률 | top-k ($k{=}3$) | top-p ($p{=}0.9$) | min-p ($p_{\text{min}}{=}0.1$, 임계 0.04) |
|---|---|---|---|---|
| A | 0.40 | 남김 | 남김 | 남김 |
| B | 0.25 | 남김 | 남김 | 남김 |
| C | 0.12 | 남김 | 남김 | 남김 |
| D | 0.08 | 버림 | 남김 | 남김 |
| E | 0.05 | 버림 | 남김(누적 0.90) | 남김 |
| F | 0.04 | 버림 | 버림 | 남김(임계와 동률) |
| G | 0.03 | 버림 | 버림 | 버림 |
| H | 0.02 | 버림 | 버림 | 버림 |
| 꼬리 | 0.01 | 버림 | 버림 | 버림 |
| 후보 수 | 3 | 5 | 6 |
같은 분포인데 세 방법이 그은 경계가 다 다르다. 정렬한 막대 위에 세 개의 서로 다른 컷 라인이 그어지는 셈이다.
실무에서는 top-p 하나로 충분한 경우가 대부분이라는 것이 내 견해다. top-k는 top-p와 함께 상한(안전망)으로 추가하거나 단독으로 쓰고, min-p는 특히 높은 온도에서 창의성과 일관성을 동시에 원할 때 시도할 만하다. 이 밖에 typical sampling, tail-free sampling, $\epsilon$·$\eta$-sampling 같은 변형이 여럿 있으나 원리는 모두 "꼬리를 어떤 기준으로 자르나"의 변주다 — 개수, 누적질량, peak 대비 높이, 또는 정보량 기준으로 자를 뿐이다.
1.7.5 온도와 절단은 왜 함께 쓰나 — 순서와 상호작용
온도와 절단은 조절하는 대상이 직교하지만(모양 대 후보 크기), 파이프라인에서 순서에 민감하다. 대표 구현(HuggingFace 등)의 로짓 파이프라인은 대략 이렇게 흐른다.
로짓 z
→ (repetition / frequency / presence penalty, no-repeat n-gram, logit bias) # 로짓 가감·마스크
→ z / T # 온도
→ top-k 컷
→ top-p 컷 (또는 min-p 컷)
→ softmax → 재정규화
→ 샘플 (또는 greedy면 argmax)
온도가 절단보다 먼저 온다는 점이 실질적인 결과를 낳는다. 온도로 분포를 먼저 평평하게 만든 뒤 top-p를 적용하면 핵이 커진다. 숫자로 확인해 보자. 로짓 $z = [4, 3, 2, 1, 0, -1]$ 여섯 토큰을 두자. $T=1$ 이면 $\exp(z) = [54.60, 20.09, 7.39, 2.72, 1.00, 0.37]$, 합 $86.16$ 이라 $p = [0.634, 0.233, 0.086, 0.032, 0.012, 0.004]$ 이다. 여기 top_p=0.9를 적용하면 누적 A 0.634 → B 0.867 → C 0.953 이라 C에서 넘어, 핵은 $\{A,B,C\}$ 세 개다. 이제 온도만 $T=2$ 로 높이면 $z/2 = [2, 1.5, 1, 0.5, 0, -0.5]$, $\exp = [7.39, 4.48, 2.72, 1.65, 1.00, 0.61]$, 합 $17.84$ 라 $p = [0.414, 0.251, 0.152, 0.092, 0.056, 0.034]$ 로 훨씬 고르게 퍼진다. 같은 top_p=0.9의 누적이 이번엔 0.414 → 0.665 → 0.818 → 0.910 이라 D에서야 넘어, 핵이 $\{A,B,C,D\}$ 네 개로 늘어난다. 온도를 높였을 뿐인데 top-p가 더 넓은 후보를 통과시킨 것이다. min-p도 같은 방향으로 움직인다 — $T=1$ 에선 임계 $0.1\times0.634 = 0.063$ 이라 $\{A,B,C\}$ 셋만 통과하지만, $T=2$ 에선 임계가 $0.1\times0.414 = 0.041$ 로 낮아져 F 직전까지, 즉 $\{A,B,C,D,E\}$ 다섯이 통과한다.
그래서 "min-p는 온도 이전의 원 확률로 재야 한다"는 구현 논쟁이 있다 — 온도로 평평해진 뒤 재면 임계가 낮아져 의도보다 넓어진다는 것이다. 어느 순서가 옳은지는 구현에 따라 다르니, 쓰는 프레임워크가 절단을 온도 전에 재는지 후에 재는지는 직접 확인해야 한다. 몇 가지 상호작용을 더 명시해 두면, top-k와 top-p를 둘 다 걸면 교집합(둘 다 통과한 토큰)만 남는다 — top_k=50, top_p=0.9는 "누적 0.9 이내이면서 상위 50개 이내"라는 뜻이다. temperature=0은 대부분 API에서 절단 파라미터를 무시하고 argmax로 동작하니, 온도 0에 top-p를 같이 줘도 무의미하다. 그리고 극단값을 조심해야 한다 — 온도를 아주 높이면서(가령 2.0) 절단을 느슨하게(top-p 0.99) 두면 1.7.3에서 본 꼬리가 되살아나 깨진 출력이 나온다. 고온을 쓰려면 절단을 더 강하게 적용해 균형을 맞춘다. 요령 하나로 압축하면, 온도는 모양을 절단은 후보 크기를 정하되 함께 움직이며, 하나를 극단으로 밀면 다른 하나로 보정한다.
1.7.6 빔서치, 그리고 왜 열린 생성엔 안 맞나
1.7.6.1 빔서치와 길이 정규화
greedy는 매 스텝 1등만 좇아 국소 최적에 빠진다. 빔서치(beam search) 는 매 스텝 상위 $B$ 개 부분 시퀀스(빔)를 동시에 살려 두어, 전체 시퀀스의 로그확률이 더 높은 경로를 찾는다. $B=1$ 이면 greedy와 같다. 절차는 각 빔을 어휘 전체로 확장한 뒤 누적 로그확률 상위 $B$ 개만 남기고 반복하는 것이다.
여기서 미묘한 함정이 하나 있어 보정이 필요하다. 시퀀스 점수를 단순히 로그확률의 합 $\sum_t \log p(y_t\mid y_{<t})$ 으로 매기면, 각 항 $\log p \le 0$ 이라 토큰을 더할 때마다 점수가 무조건 감소한다. 그러면 빔서치가 짧은 시퀀스를 체계적으로 선호해, 번역에서 문장을 도중에 끊어 버리는 것으로 악명 높은 편향이 생긴다. 이를 막으려 길이로 나누는 길이 정규화(length normalization) 를 쓴다.
$$\text{score}(Y) = \frac{1}{|Y|^{\alpha}} \sum_{t=1}^{|Y|} \log p(y_t \mid y_{<t}), \qquad \alpha \in [0,1]$$
$\alpha=0$ 이면 정규화 없음(짧은 문장 편향 그대로), $\alpha=1$ 이면 토큰당 평균 로그확률(길이 완전 상쇄)이고, 실무에선 그 사이 $\alpha \approx 0.6\text{–}0.7$ 을 쓰는 것이 흔하다. $\alpha$ 자체가 하나의 조정 변수인 셈이다.
빔서치는 확률이 높은 시퀀스를 찾는 근사 탐색이다(전역 최적을 보장하진 않는다 — 실제 최적을 찾으려면 지수 개의 경로를 다 봐야 한다). 그래서 정답이 비교적 하나로 수렴하는 닫힌 과제 — 기계번역, 음성인식(ASR), 문법 교정, 짧고 결정적인 구조 출력 — 에서 greedy보다 나은 결과를 낸다. 이런 과제에선 "가장 그럴듯한 하나"가 곧 원하는 답이기 때문이다.
1.7.6.2 likelihood trap과 다양성 붕괴 — 챗엔 왜 나쁜가
대화·창작 같은 열린 생성에는 빔서치가 오히려 해롭고, 그 이유가 이 장 전체의 주제와 맞닿아 있다. 첫째는 likelihood trap이다. 확률을 최대화하면 인간이 선호하는 텍스트가 아니라 "평균적으로 안전한", 반복되고 밋밋한 텍스트가 나온다. 높은 확률이 곧 좋은 글이 아니라는 뜻인데, 1.7.3에서 언급한 관찰이 여기서 정확히 문제가 된다 — 인간 텍스트는 매 토큰이 최고확률이 아니라 적당히 놀랍다. 사람이 실제로 쓴 문장의 토큰별 확률을 재 보면 상당히 불균일한데, 확률을 극대화하려는 빔서치는 그 "적당한 놀라움"을 체계적으로 지워 버린다. 그래서 빔 수 $B$ 를 키우면 열린 생성에선 오히려 더 짧고 반복적인, 확률만 높은 문장으로 수렴하기도 한다 — "빔이 많을수록 좋다"는 여기선 틀린 통념이다.
둘째는 다양성 붕괴다. 빔들은 접두어(prefix)를 공유한 채 서로 비슷한 고확률 경로로 몰려, 결국 거의 같은 문장이 된다. 여러 후보를 뽑고 싶어 $B$ 를 키워도 실질적으로 하나를 얻는 셈이다. 이것을 억지로 흩는 diverse beam search 같은 변형이 있지만, 근본적으로 확률 최대화라는 목표 자체가 열린 생성과 어긋난다. 셋째는 비용과 서빙 궁합이다. 매 스텝 $B$ 배의 후보를 유지·정렬해야 해 지연·메모리가 커지고, 빔마다 KV 상태를 갈라 관리해야 해 연속 배칭·KV 캐시 관리(5.1·5.3)와 잘 안 맞는다. 넷째는 스트리밍 부적합이다. 빔서치는 끝까지 탐색해 봐야 최종 빔이 정해지므로, 토큰을 즉시 스트리밍하는 챗 UX(7.1)와 원리적으로 충돌한다 — 아직 어느 빔이 이길지 모르는데 무엇을 화면에 표시하겠는가.
정렬된 대화 모델은 SFT·RLHF를 거치며 이미 분포가 유용한 응답 쪽으로 형성돼 있어(3.1 이후), 확률을 애써 최대화하지 않아도 낮은 온도의 샘플링이나 top-p 샘플링만으로 충분하고 더 자연스럽다. 그래서 오늘날 챗 서비스는 대부분 빔서치 대신 온도와 top-p/min-p 샘플링을 쓴다. 빔서치는 폐기된 기법이 아니라 정답이 좁은 과제 전용 도구로 자리를 옮긴 것이다.
1.7.7 반복을 억제하는 조정 변수들
정렬 이전이든 이후든 모델은 같은 구절을 되풀이하는 성향이 있다(특히 저온·greedy·빔). 이를 로짓 단에서 억제하는 조정 변수가 여럿 있는데, 공통점은 이미 나온 토큰의 로짓을 낮춘다는 것이고 낮추는 방식이 다르다.
1.7.7.1 repetition penalty와 음수 로짓 부호 함정
CTRL 논문(Keskar 등 2019)의 방식으로, 이미 생성에 등장한 토큰의 로짓을 온도처럼 나눈다.
$$p_i = \frac{\exp\!\big(z_i / (T \cdot d(i))\big)}{\sum_j \exp\!\big(z_j / (T \cdot d(j))\big)}, \qquad d(i) = \begin{cases} \theta & i \in \text{이미 등장한 토큰} \\ 1 & \text{그 외} \end{cases}$$
$\theta > 1$(흔히 1.1–1.3)이면 등장한 토큰의 확률을 낮춘다 — 겉보기엔. 그런데 여기 실제 함정이 있다. 나눗셈으로 로짓을 줄인다는 발상은 로짓이 양수일 때만 성립한다. 로짓 $z = 3.0$ 을 $\theta = 1.2$ 로 나누면 $2.5$ 로 작아져 확률이 준다. 그런데 로짓 $z = -2.0$ 을 같은 $1.2$ 로 나누면 $-1.667$ 이 되어 0에 더 가까워지고, 따라서 확률이 오히려 커진다. 억제하려던 토큰을 도리어 높이는 것이다. 그래서 실무 구현(HuggingFace)은 부호를 나눠 처리한다 — 양수면 나누고, 음수면 곱해서 확실히 더 낮춘다($-2.0 \times 1.2 = -2.4$).
# 개념 코드 (개략)
for tok in set(generated_tokens):
if logits[tok] > 0:
logits[tok] /= penalty # penalty>1 → 감소
else:
logits[tok] *= penalty # 음수는 곱해서 더 낮춤
이 부호 갈림을 모르고 순진하게 나누기만 구현하면, 어휘 대부분의 로짓이 음수인 상황(흔하다)에서 반복 패널티가 정반대로 작동한다. repetition penalty는 곱셈적이라 태스크에 따라 세기를 가늠하기 어렵고, 너무 세면 정상적으로 필요한 반복 — 고유명사, 코드 토큰, 조사 — 까지 억제해 문장을 망친다.
1.7.7.2 frequency penalty · presence penalty
OpenAI 계열이 대중화한 가산(additive) 방식으로, 나눗셈이 아니라 로짓에서 직접 뺀다. $c_i$ 를 지금까지 생성에서 토큰 $i$ 가 나온 횟수라 하면
$$z'_i = z_i - \alpha_{\text{freq}} \cdot c_i - \alpha_{\text{pres}} \cdot \mathbb{1}[c_i > 0]$$
frequency penalty $\alpha_{\text{freq}}$ 는 많이 나올수록 횟수에 비례해 더 낮춰 같은 단어의 남발을 억제하고, presence penalty $\alpha_{\text{pres}}$ 는 한 번이라도 나왔으면 횟수와 무관하게 일정량만 낮춰 이미 쓴 화제를 새 주제로 전환시킨다. 숫자로 보면 차이가 분명하다. 어떤 토큰의 로짓이 $z = 5.0$ 이고 $\alpha_{\text{freq}} = 0.5$, $\alpha_{\text{pres}} = 0.8$ 이라 하자. 이 토큰이 네 번 나왔으면 $z' = 5.0 - 0.5\times 4 - 0.8\times 1 = 2.2$ 로 크게 낮아지고, 한 번만 나왔으면 $5.0 - 0.5 - 0.8 = 3.7$, 한 번도 안 나왔으면 $c_i = 0$ 이라 두 항 모두 사라져 $z' = 5.0$ 그대로다. frequency 항이 횟수에 비례해 벌어지는 것이 보인다 — 4회 등장 토큰은 1회 토큰보다 $0.5\times 3 = 1.5$ 만큼 더 억제된다. 값 범위는 대략 $-2.0$ 에서 $2.0$(양수면 억제, 음수면 오히려 반복 장려)이 흔하나 벤더·시점마다 다르다. repetition penalty(곱셈)와 frequency/presence(가산)를 둘 다 켜면 이중으로 걸리니, 하나만 골라 쓰는 편이 조율하기 쉽다.
1.7.7.3 no-repeat n-gram — 하드 금지
확률을 낮추는 대신 아예 금지하는 하드 제약도 있다. 크기 $n$ 의 n-gram이 이미 문맥에 나왔다면, 그 n-gram을 완성하게 될 다음 토큰의 로짓을 $-\infty$ 로 만들어 절대 선택되지 않게 한다(no_repeat_ngram_size=3이면 어떤 3-gram도 두 번 나올 수 없다). 강력하지만 부작용이 크다 — 정당하게 반복돼야 하는 구절(정형 표현, 코드의 반복 패턴, 표 형식)까지 막아 부자연스러워진다. 그래서 자유 생성엔 신중히, 반복이 치명적인 특정 상황에만 쓴다.
| 방식 | 연산 | 등장 횟수 반영 | 전형적 용도 |
|---|---|---|---|
| repetition penalty | 곱셈(로짓 나눗셈) | 아니오(등장 여부만) | 로컬 오픈모델에서 반복 방지 |
| frequency penalty | 가산(횟수×계수) | 예 | 같은 단어 남발 억제 |
| presence penalty | 가산(등장 시 1회) | 아니오 | 화제 다양화·새 주제 유도 |
| no-repeat n-gram | 하드 마스크($-\infty$) | — | 동일 n-gram 완전 금지 |
반복 억제는 근본적으로 증상 치료라는 점을 잊으면 안 된다. 극심한 반복은 대개 온도가 너무 낮거나(greedy 근처) 프롬프트·모델 자체의 신호다. 먼저 온도·top-p로 다양성을 주고, 그래도 남는 반복에만 패널티를 약하게 추가하는 순서가 안전하다. 패널티를 세게 적용해 문법·고유명사가 깨지는 쪽이 반복을 조금 남기는 쪽보다 나쁜 경우가 많다.
1.7.8 결정성과 다양성, 그리고 재현성의 한계
디코딩 설정은 결국 하나의 축 위에 있다 — 결정적·안전한 왼쪽 끝과 다양·창의·위험한 오른쪽 끝 사이 어딘가다.
왼쪽은 재현·사실·안전(같은 질문 같은 답), 오른쪽은 다양성·창의(브레인스토밍·다중 후보)다. "정답"은 없고 용도가 정한다(1.7.11). 그런데 왼쪽 끝, 즉 "같은 질문에 같은 답"을 실제로 달성하려 하면 놀랄 만큼 미끄럽다. 이 절의 나머지는 그 불편한 진실을 정직하게 다룬다.
온도 샘플링은 난수를 쓰므로 시드(seed)를 고정하면 같은 시드·같은 입력·같은 구현에서 같은 난수열이 재현되어, 이론상 같은 출력이 나온다. 문제는 "같은 구현"이라는 전제가 실무에서 거의 성립하지 않는다는 것이다. 근본 원인은 부동소수 덧셈이 결합법칙을 어긴다는 데 있다 — $(a+b)+c$ 와 $a+(b+c)$ 가 반올림 차이로 마지막 비트에서 다를 수 있다. GPU는 로짓 합과 softmax를 수천 스레드의 병렬 리덕션으로 계산하는데, 이 리덕션 순서가 실행 조건에 따라 달라지면 로짓이 미세하게 달라진다. 그리고 그 순서를 바꾸는 요인이 서빙에는 널려 있다. 배치 크기·시퀀스 길이·다른 요청과의 동시 배치가 바뀌면 커널 선택·타일링·리덕션 순서가 달라진다 — 즉 배치 불변성(batch invariance)이 기본으로 보장되지 않는다. 서빙에서 연속 배칭(5.3)으로 내 요청이 매번 다른 이웃과 묶이면, 같은 프롬프트인데도 로짓이 조금씩 달라질 수 있다는 뜻이다. 여기에 atomic 연산처럼 실행마다 순서가 달라지는 비결정 커널, 그리고 GPU 세대·드라이버·프레임워크 버전 차이까지 추가되면, 비트 단위로 같은 로짓을 두 번 얻는 것 자체가 사건이 된다.
이 미세한 차이가 왜 문제냐면, 두 최상위 토큰이 거의 동률인 자리에서 argmax를 뒤집을 수 있기 때문이다. 로짓이 마지막 비트에서 갈려 1등과 2등이 바뀌면, 그 뒤 문장 전체가 다른 길로 간다. 그래서 난수를 아예 안 쓰는 greedy(temperature 0)조차 배치·하드웨어에 따라 완전히 결정적이지 않다. 일부 API는 seed 파라미터와 재현을 돕는 지표(예: OpenAI의 system_fingerprint)를 제공하지만, 공식적으로도 이는 비트 단위 보장이 아니라 최선 노력(best-effort)임을 밝힌다 — 정확한 스펙은 각 벤더 공식 문서를 봐야 한다. 재현성이 정말 필요하면(회귀 테스트·감사) 실행 조건을 최대한 통제하되 — 배치 1, 고정 하드웨어, 라이브러리의 deterministic 모드(속도 손실을 감수), temperature 0 — 그래도 100%는 아니라는 점을 문서화하고, 확실한 재현이 필요한 값은 출력 텍스트 자체를 저장하는 것이 답이다. "seed를 넣었는데 왜 매번 다르지?"는 버그가 아니라 이 스택의 자연스러운 결과일 수 있다. 완전 결정성을 계약처럼 약속하지 마라.
1.7.9 logit bias로 특정 토큰을 밀고 당기기
특정 토큰의 확률을 인위로 조종하고 싶을 때는 그 토큰의 로짓에 고정 편향 $b_i$ 를 더한다.
$$z'_i = z_i + b_i$$
큰 음수(가령 $-100$)를 주면 그 토큰은 사실상 금지되어 절대 안 뽑히고 — 특정 단어·서식·심지어 EOS를 막을 때 쓴다 — 큰 양수를 주면 강제에 가깝게 높인다. 작은 값은 성향을 약간 조정한다. 주의할 것은 bias가 철저히 토큰 단위라는 점이다. 토큰화(1.2)를 모르면 잘못된 곳에 걸린다 — "New York"을 막으려면 그 문자열이 실제로 어떤 토큰들로 분할되는지 확인해야 하고, 한 단어가 여러 토큰이면 그중 일부만 걸려 금지가 무력화된다. 금지어나 간단한 서식 강제엔 유용하지만, 유효한 JSON 같은 복잡한 구조를 bias 몇 개로 강제하려는 것은 무리다 — 그것은 매 스텝 문법을 보고 허용 토큰만 남기는 제약 디코딩(6.9)의 몫이다.
1.7.10 언제 멈출까 — EOS · stop 시퀀스 · max tokens
생성은 언젠가 멈춰야 하고, 멈추는 방식이 셋 있다. 첫째는 EOS 토큰이다. 모델이 학습 중 문장·응답 끝에 배운 특수 토큰(예: </s>, 채팅 템플릿의 종료 토큰 — 1.2·3.2)을 뽑으면 자연 종료한다. 정렬된 챗 모델은 응답이 끝났다고 판단할 때 이 토큰을 낸다. 여기서 흔한 사고가, logit bias나 EOS 억제로 이 토큰을 막아 두면 모델이 멈추지 못해 max에 닿을 때까지 무한정 출력한다는 것이다. 둘째는 stop 시퀀스다. 지정한 문자열이 출력에 나오면 그 지점에서 자른다(보통 stop 문자열 자체는 결과에서 제외). 에이전트·툴 사용(6.5·6.6)에서 "\nObservation:" 같은 것으로 턴 경계를 끊는 데 필수다. 다만 stop 문자열이 여러 토큰에 걸쳐 있으면 토큰 경계와 어긋나 부분만 잘리거나 늦게 검출될 수 있어, 스트리밍(7.1)에서는 경계를 넘겨 추정하기 위한 버퍼링이 필요하다. 셋째는 max tokens(max_new_tokens/max_tokens)로, 생성 토큰 수의 상한이자 비용·지연의 안전판(7.5)이며 폭주 방지책이다. 도달하면 문장 중간이라도 끊기니, 응답이 부자연스럽게 잘렸다면 이 상한부터 의심하라. 많은 API가 완료 사유를 finish_reason(stop·length·content_filter 등)으로 알려주므로, 잘림을 프로그램적으로 구분하려면 이 값을 봐야 한다.
{
"temperature": 0.7,
"top_p": 0.95,
"max_tokens": 512,
"stop": ["\nUser:", "\n\n###"],
"frequency_penalty": 0.3,
"seed": 42,
"logit_bias": {"1234": -100}
}
예시 파라미터일 뿐, 실제 키 이름·허용 범위는 서빙 프레임워크·API마다 다르다(5.4·7.2).
1.7.11 용도가 파라미터를 정한다
파라미터에 절대적 정답은 없고, 태스크가 축의 어느 지점을 원하는지가 값을 정한다. 아래는 출발점으로 삼을 견해이고 — 모델·프롬프트에 따라 달라지니 최종값은 추측이 아니라 평가로 확정해야 한다 — 각 선택에 왜라는 근거가 붙는다.
| 용도 | temperature | top-p | 기타 | 왜 |
|---|---|---|---|---|
| 사실 QA · 추출 · 분류 | 0 (greedy) ~ 0.2 | 1.0 | — | 정확·재현이 최우선, 창의성 불필요 |
| 코드 생성 | 0 ~ 0.4 | 0.95 | stop으로 코드블록 경계 | 대체로 결정적, 약간의 대안 여지 |
| 요약 | 0.3 ~ 0.5 | 0.9 | 약한 presence penalty | 충실하되 너무 딱딱하지 않게 |
| 범용 챗 어시스턴트 | 0.7 | 0.9 ~ 0.95 | 약한 frequency penalty | 자연스러움과 안정성의 균형 |
| 브레인스토밍 · 창작 | 0.9 ~ 1.2 | 0.95 (또는 min-p) | frequency/presence로 반복 억제 | 다양성·발산이 목적 |
| 여러 후보 N개 생성 | 0.8 ~ 1.0 | 0.95 | 같은 프롬프트로 N회 샘플 | 후보 다양성 확보 후 재순위·투표 |
사실 QA에 낮은 온도를 쓰는 이유는 명확하다 — 이런 태스크는 정답이 하나에 가까워 다양성이 이득이 아니라 위험이고, 온도를 높일수록(1.7.2의 엔트로피 상승) 그럴듯하지만 틀린 꼬리 토큰이 섞일 여지가 커진다. 반대로 창작은 발산 자체가 목적이라 고온이 맞고, 대신 꼬리가 깨지지 않게 절단(top-p 또는 min-p)으로 균형을 맞춘다. 다만 여기서 널리 퍼진 오해 하나를 분명히 깨 둘 필요가 있다 — 낮은 온도가 환각을 없앤다는 믿음이다. 디코딩은 모델이 준 분포에서 고를 뿐, 분포에 없는 지식을 만들어 내지 못한다. 모델이 틀린 사실을 높은 확률로 확신하고 있으면, 온도를 0으로 내려도 그 확신에 찬 오류를 그대로 argmax로 뽑는다. 낮은 온도는 무작위성에서 오는 실수를 줄일 뿐이지, 지식의 결함을 고치지 못한다.
실무에서 지켜야 할 원칙 몇 가지. 한 번에 하나만 바꿔라 — 온도·top-p·패널티를 동시에 변경하면 무엇이 효과를 냈는지 특정하지 못한다. 온도부터 조정하라 — 대부분의 "성격"은 온도가 정하고, top-p는 꼬리 안전망으로 두고 온도로 조율하는 것이 원인 추적이 쉽다. 그리고 최종값은 오프라인 평가(4.2)와 A/B(8.2)로 고정하라 — 감으로 고른 온도가 배포 뒤 품질 회귀의 흔한 원인이다. 정확도가 특히 중요하면 단일 greedy보다 다중 후보 + 재순위(best-of-N) 가 나을 때가 많다 — 온도를 조금 높여 N개를 뽑고 검증기·투표(self-consistency)·보상모델로 고르면, 추론 시점 계산을 성능으로 바꾸는 셈이다(이 아이디어의 일반형은 3.7).
1.7.12 이 규칙 위에 추가되는 것 — 제약 디코딩·투기적 디코딩
이 장은 "확률에서 어떻게 고르나"였고, 두 확장이 뒤에서 이 규칙 위에 추가된다. 하나는 제약 디코딩(constrained decoding, 6.9) 이다. 출력이 유효 JSON, 정규식, 문맥자유문법 같은 형식을 반드시 지켜야 할 때, 매 스텝 문법상 허용되지 않는 토큰의 로짓을 $-\infty$ 로 마스킹해 문법에 맞는 토큰만 샘플링한다. 이 장에서 본 logit bias·마스킹을 "손으로 몇 개"가 아니라 "문법으로 자동" 거는 것이라, 구조화 출력·함수 호출(6.5)의 신뢰성을 이것으로 확보한다. 온도·절단·패널티 규칙은 그 마스크 위에 그대로 추가된다 — 즉 "무엇을 뽑을 수 있나"를 문법이 좁히고, "그중 무엇을 뽑나"는 여전히 이 장의 규칙이 정한다.
다른 하나는 투기적 디코딩(speculative decoding, 5.3) 이다. 작은 초안(draft) 모델이 여러 토큰을 미리 저렴하게 뽑고, 큰 타깃 모델이 한 번의 순전파로 검증해 맞으면 채택·틀리면 버린다. 핵심은 이 검증이 타깃 모델의 원래 샘플링 분포를 정확히 보존하도록 설계된다는 것(rejection sampling)이라 — 속도를 얻으면서도 출력 분포는 그대로다. 이 장의 "매 스텝 하나씩"을 "여러 스텝을 투기적으로 병렬화"로 바꾸는 서빙 최적화이며, 품질(무엇을 뽑나)이 아니라 처리량(얼마나 빨리)의 문제다. 두 확장을 한 줄로 대비하면, 제약 디코딩은 무엇을 뽑을 수 있는가를 형식으로 좁히고, 투기적 디코딩은 얼마나 빨리 뽑는가를 바꾸며, 온도·top-k·top-p·min-p·패널티라는 이 장의 규칙은 두 경우 모두 그 위에 그대로 남는다. 결국 디코딩은 가중치를 한 비트도 안 바꾸는 값싼 후처리인데도, 사실적인 조수와 산만한 이야기꾼을 가르고, 재현되는 시스템과 매번 다른 시스템을 가르며, 유효한 JSON을 출력하는 API와 깨진 응답을 출력하는 API를 가른다 — 프롬프트만큼이나 자주 만질 값어치가 있는 조정 변수인 이유다.