TT Lab
开始
学习 学习路径 课程

Transformer — 手算一遍注意力

同一个提示词,每次答案都不同

在 TT Lab 中继续学习

一句话总结

模型给出的不是答案,而是一组 logits,答案是在把 logits 转成概率再抽取一个的过程中确定的。温度、top-k、top-p 是在抽取之前修改这个概率分布的三个旋钮。

为什么需要它

第一次接上模型,会出现两件怪事。一件是同一个提示词输入两次,答案却不同;另一件是把答案生成得长一点,会一直重复同样的话直到结束。

两者都出在最后一步。Transformer 块所做的事,到给出一个与词表一样大的实数列表为止。这个列表叫 logits,它不是概率,而是没有归一化的分数。从中挑出实际要用的令牌(token),是模型之外的规则,规则是什么,同一个模型就会说出完全不同的话。

最简单的规则是选最大的 logit。叫 greedy。结果总是相同,便于测试,但生成得长了,容易陷入回到同一句话的循环。The Curious Case of Neural Text Degeneration 整理的正是这种现象——只沿着概率最高的路走,就会得到与人写的文章不相像、明显重复的文字。

于是要抽取。按概率的比例挑一个,重复就会减少。但这次问题出在尾部。词表有几万个时,概率是 0.00001 的令牌就有几万个,这整条尾部的总和不可忽视。哪怕只有一次从中被抽中,句子就会当场崩溃。三个旋钮全都是处理这种张力——重复与崩溃之间——的工具。

温度的要点在于除的位置

温度 T 是在输入 softmax 之前去除 logits 的值。不是先求出概率再去动它。

scaled = [value / T for value in logits]
top = max(scaled)                      # 빼는 것도 장식이 아니다
weights = [math.exp(value - top) for value in scaled]
probs = [w / sum(weights) for w in weights]

除法是怎么起作用的,用间隔来看就容易懂。假设第一名和第二名的 logit 之差是 1.1。T 为 0.25 时,这个差拉开到 4.4,取指数之后,第二名的份额缩小到第一名的 1% 左右。T 为 4 时,差缩小到 0.275,两者几乎一样。T 小则变尖,大则变平。T 为 1 就是原样使用 logits。

减去最大值的位置也不只是习惯。把 T 设成 0.01,logit 5.2 就变成 520,math.exp(520) 会直接溢出。意思是温度会制造出很大的输入。减去最大值之后,指数的自变量降到 0 以下,不会再有溢出的地方,而且是从同样的值里减去同一个数,概率不会改变。

有一点要钉死。不存在 T 为 0 的 softmax。除法做不了。“温度设为 0 就是 greedy”这句话,是把 T 趋向 0 时的极限简略说出来了,实际实现会在那个位置改用 greedy。负数更糟——排名会颠倒,最不可能的令牌成了第一名。

想用一个数字来看分散程度,就用熵。以 2 为底的熵,在概率集中于一处时接近 0,均匀分散到 n 个时是 log2(n)。提高温度,这个值会变大。

top-k 和 top-p 是截断尾部的两种方法

top-k 很简单。只保留概率最大的 k 个,其余丢掉,再把剩下的重新归一化。平局怎么打破,必须事先确定。本实验钉死为编号小的一方保留。

固定 k 的弱点是不看分布的形状。在很有把握的位置,只要第一名就够了,却还是保留 k 个;在岔路口,k 个又不够。

top-p,又名 nucleus sampling 不按个数,而是按质量来截断。把概率按降序排开并计算累积和,一直保留到累积和第一次达到 p 的那一项为止。这里有一个容易搞混的地方。如果把那一项去掉,剩下的总和就达不到 p。所以是“到越过的那一项为止”,而不是“到越过之前为止”。这样保留的个数会随分布自动变化——在尖的位置是一个,在平的位置是好几个。

两者一起用时,顺序会改变结果。先作用温度,分布本身就变了,所以即使 p 相同,保留的个数也不同。先做 top-k,幸存下来的会被重新归一化,概率变大,用变大的值计算累积和,top-p 就会更早停下。所以用什么顺序应用,是必须确定下来并写进文档的值。Hugging Face 的生成策略文档也是把这些旋钮当作一组配置来处理,而不是分开的。

同一个种子给出同一个序列

分布都改好之后,抽取一个。逆累积分布是标准的方法——抽一个大于等于 0、小于 1 的实数 u,按编号顺序累加概率,挑选第一次超过 u 的位置。概率恰好为 0 的位置不会增加累积和,所以绝对不会被抽中。意思是被截掉的令牌不会复活。

这里引出一个重要的性质。把提供 u 的随机数生成器的种子固定,同样的输入就会得到同样的序列。是随机的,却可以复现。要重现故障,必须有这个。

在现场相遇的样子

第一,无法复现“偶尔会出现奇怪答案”的反馈。因为没有留下种子。每个请求都把种子和温度、k、p 写进日志,就能把那一个请求原样重现。

第二,想提高温度获得多样性,却丢了准确率。在分类或抽取这类答案唯一的任务里,没有提高温度的理由。旋钮要按任务分别设定。

第三,同样的配置,换了库结果就不同。是应用顺序不同,或者平局处理不同,或者是否包含达到 p 的那一项不同。这三点在文档里都很少写明。

第四,缓存莫名其妙地命中。只把提示词当作键,而漏掉了温度、k、p、种子,配置不同的请求就会拿到同样的答案。

第五,评测分数抖动,抓不住回归。评测时关掉抽取、固定为 greedy 会更好。因为想测的是模型的变化,而不是随机数的变化。

实际工作中真正重要的事

下一项实验要做什么

一步步扩展 /root/work/tf-sample/sample.py。只使用标准库 math 和 random——这个 Pod 的系统 Python 里没有 numpy,只在 /opt/onnx-lab/bin/python 里才有。也不调用模型。自己写出一组 logits,之后的全部亲手来做。

从用温度相除之后的稳定 softmax 开始,做出熵、top-k、top-p、按规定顺序把四者连起来的函数、逆累积分布抽取、greedy 与温度扫描。最后用数字留下:用同一个种子抽两次,是否得到同样的序列;把温度调低的一侧是否与 greedy 相同。

评分器不会相信你写下的说明。它会真正导入你的模块,每次用不同的 logits 和不同的温度检验函数,并与它另外计算的概率向量对照。它不会判定随机抽取本身——只看固定了种子的编号序列和概率向量。这样,正确的实现就不会偶然失败,错误的实现也不会偶然通过。