最可能的,不是最好的
第 1 章说过,一段话是从候选表里反复「挑」出来的。怎么挑,前面一直没细说。最自然的想法是:每次都挑概率最高的那个——模型最有把握的,总该是最好的吧?这一章用 GPT-2 证明这个想法错得多彻底,然后讲清楚聊天产品里那几个旋钮——温度、top-p——到底在拧什么。
让 GPT-2 续写 The meaning of life is,写 200 个 token,每一步都挑候选表里的第一名(这叫贪心解码)。
把这 200 个 token 切成连续的 4 个一组(第 1–4 个、第 2–5 个……共 197 组),问:其中有多大比例是「整段文字里独一无二」的?(人写的文章,这个比例接近 100%。)
答案在本章末尾。
每次都挑第一名
直接看它写了什么:
The meaning of life is⟪ not the same as the meaning of death.⟫ The meaning of life is not the same as the meaning of death. The meaning of life is not the same as the meaning of death. The meaning of life is not the same as the meaning of death. ……(一直到第 200 个 token)
它写出了一句不坏的话,然后从第 17 个 token 开始,把它原样重复到了最后。200 个 token 里,不重复的 4 词片段只有 8%。换个开头试试:
My favorite programming language is⟪ Python. It's a great programming language for beginners, but it's also a great programming language for advanced programmers.⟫ Python is a great programming language for beginners, but it's also a great programming language for advanced programmers. Python is a great programming language for beginners, ……
同样的结局,不重复的只有 14.7%。这不是 GPT-2 太小的问题——更大的模型用贪心解码写长文,也会以各种方式掉进这个坑。
为什么会复读:越写越有把握
拿 GPT-2 自己去读它刚写的这段话,量每个 token 的意外:
| 文字 | 平均意外(比特/token) | 不重复的 4 词片段 |
|---|---|---|
| 贪心解码(全部 200 个) | 0.18 | 8% |
| 贪心解码(最后 100 个) | 0.008 | — |
| 人写的文章(第 4 章那篇) | 4.53 | — |
| 按原始概率随机抽(温度 1) | 5.82 | 100% |
复读到后面,每个 token 的意外只剩 0.008 比特——模型对自己写的东西有 99% 以上的把握。原因是一个自我强化的循环:一句话出现过一次,第 12 章的归纳头就会把「再来一遍」的概率推得很高;再来一遍之后,「再来一遍」的证据又多了一份。贪心解码挑的永远是最安全的那条路,而最安全的路就是重复自己。
对照人写的文字:每个 token 平均 4.53 比特。人写东西不是在挑最可能的词——人写的句子里不断有「意外」,那恰恰是信息所在(第 4 章)。2019 年的一篇论文(Holtzman 等人,《The Curious Case of Neural Text Degeneration》)专门量过这件事,结论正是这一章的标题:人类的文字,并不是概率最高的那种文字。一段好的文字,意外应该落在某个「正常」的水平上——太低是复读机,太高是胡言乱语。
温度:把表压尖,或者摊平
反过来,如果完全按表里的概率去抽签(「温度 1」),GPT-2 写出来的东西平均意外是 5.82 比特,比人写的还高——它会时不时抽到一个概率很低的怪词,然后顺着这个怪词越写越偏。
中间的办法是调温度。做法很简单:在 softmax 之前,把每个候选的原始分数除以一个数 T,再算概率。T 小于 1,分数差距被放大,表变尖;T 大于 1,差距被缩小,表变平。看 Once upon a 后面那张表在不同温度下的样子:
| 温度 T | ' time' 的概率 | 熵 | 凑够 90% 要几个候选 |
|---|---|---|---|
| 0.3 | 91.54% | 0.46 比特 | 1 |
| 0.7 | 50.82% | 3.57 比特 | 33 |
| 1.0(原样) | 17.66% | 8.51 比特 | 1956 |
| 1.5 | 2.48% | 12.62 比特 | 11187 |
| 2.0 | 0.62% | 13.94 比特 | 19623 |
温度 0.7 时,「time」占了一半;温度 2 时,它只剩 0.62%,要凑够 90% 的概率得从将近两万个候选里去凑——那基本就是乱码了。温度趋近 0,就是贪心解码;温度越高,越随机。用温度 0.7 续写同样的开头,不重复的 4 词片段回到了 61%(「meaning of life」)和 88%(「programming language」),平均意外 2.1 到 2.5 比特——介于复读机和人之间。
top-p:只在靠谱的候选里抽
温度的问题是它一刀切:同一个 T,在「Eiffel 后面」这种只有一个合理答案的地方和「Once upon a 后面」这种有很多合理答案的地方,效果完全不同。top-p(也叫 nucleus 采样)换了一个思路:每一步只保留累计概率刚好够 p 的那几个候选(比如 p = 0.9),把它们重新归一化,再在里面抽;剩下的长尾一律砍掉。
关键是「那几个」到底是几个,随上下文变化剧烈。拿第 4 章那篇文章的 1024 个位置量一下,凑够 90% 需要的候选数:
- 最少 1 个:14.2% 的位置,第一名一个就超过 90%(比如一个词的后半截)。
- 中位数 126 个。
- 最多 12102 个:52.1% 的位置需要超过 100 个候选——比如一句话的开头,什么都可能。
top-p 让「该确定的地方确定,该开放的地方开放」。用 top-p 0.9 续写那两个开头,不重复的 4 词片段都是 100%,平均意外 4.47 和 3.95 比特——离人写的 4.53 最近。这几个解码旋钮,调的其实是一件事:让生成文字的「意外」落在像人的那个区间里。
transformers 的 generate 把这些挑法都做成了参数(top_k=0 是为了关掉它默认的「只在前 50 名里抽」):
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
tok = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2").eval()
x = tok("The meaning of life is", return_tensors="pt").input_ids
def distinct4(ids): # 200 个 token 里,不重复的 4-gram 占多少
g = [tuple(ids[i:i + 4]) for i in range(len(ids) - 3)]
return len(set(g)) / len(g)
torch.manual_seed(1)
for name, kw in [("贪心", dict(do_sample=False)),
("T=0.7", dict(do_sample=True, temperature=0.7, top_k=0)),
("T=1.0", dict(do_sample=True, temperature=1.0, top_k=0)),
("top-p 0.9", dict(do_sample=True, top_p=0.9, top_k=0))]:
out = model.generate(x, max_new_tokens=200, pad_token_id=50256, **kw)[0, x.shape[1]:].tolist()
print(f"{name:9} 不重复的 4-gram {distinct4(out):4.0%} {tok.decode(out[:14])!r}")
本机输出(贪心那一行每次都一样;随机抽的几行换个种子就会变,正文里那几个数是另一次抽样的结果,但「贪心远低于其余三种」这一点不会变):
贪心 不重复的 4-gram 8% ' not the same as the meaning of death.\n\nThe meaning of' T=0.7 不重复的 4-gram 94% ' a matter of interpretation, and the language of life is the way of' T=1.0 不重复的 4-gram 100% ' a collection of commanded values, as proposed by the Cyclops. It' top-p 0.9 不重复的 4-gram 99% " no longer theirs and won't take place in accordance with life-giving"
pip install torch transformers && python 14.py
在线跑:Google Colab。也可以去任何一个提供温度参数的聊天 API 上,把 temperature 调到 0 和 1.5 各问一遍同一个开放式问题。
- API 里的
temperature和top_p就是这一章的两个旋钮。写代码、抽取信息、做分类,用低温度(要的是「最可能的那个」,而且短,没机会复读);写文案、头脑风暴,温度调高一点。 - 不少接口还有
frequency_penalty、repetition_penalty这类参数:给已经出现过的 token 扣分。它们存在的理由,就是这一章开头那段复读。 - 温度设成 0,也不保证每次输出完全一样。线上服务会把很多人的请求拼成一批算,批次不同,浮点运算的顺序就不同,最后一位的舍入差异偶尔会让第一名换人——一旦换人,后面整段就分岔了。(浮点运算的顺序为什么会改变结果,《抹零》讲了一整本。)
「让模型每一步都选它最有把握的那个词,写出来的就是最好的文字。」
每一步都选最可能的,GPT-2 写到第 17 个 token 就开始复读,200 个 token 里不重复的片段只有 8%;它对自己写的东西越来越有把握,最后每个 token 只有 0.008 比特的意外。而人写的文字是 4.53 比特。
好文字的特征不是「每个词都最可能」,而是意外落在一个正常的区间里。解码策略(温度、top-p、重复惩罚)调的就是这个区间。在有唯一正确答案的短任务上,挑最可能的没问题;在长的、开放的文字上,它是一条通往复读的路。
正确答案是 D:只有 8%(197 组里约 16 组)。第一次出现重复的 4 词片段,是从第 17 个 token 开始的那一组。
同样的开头,温度 1 随机抽是 100%,top-p 0.9 也是 100%。
《意外》里的熵在这里有一个具体的用处:一段「正常」的文字,每个符号的平均意外应该在熵附近——贪心解码的输出远低于它,所以一眼就假。《抹零》解释了为什么温度为 0 时,线上服务仍可能给你两个不同的答案。
这一章的一句话
最可能的词连起来,不是最好的文字:贪心解码会越写越有把握,最后原地复读。好文字的意外落在一个正常的区间里,温度和 top-p 这些旋钮调的就是这个区间——让该确定的地方确定,该开放的地方开放。
下一章算账:写这些字要花多少计算。一个反直觉的事实是,「读」和「写」的价钱完全不同——同一台 GPT-2,在本机一次读入 512 个 token,每秒能处理 5947 个;一个一个往外写,每秒只有 98 个,慢 60 倍。这就是为什么 API 的输出 token 总比输入 token 贵。