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

Transformer — 手算一遍注意力

亲手实现温度、top-k 与 top-p

在 TT Lab 中继续学习

目标

从一组 logits 到挑出下一个令牌(token),把分布经过的每一个环节都只用标准库做出来。用温度相除之后的稳定 softmax、熵、top-k、top-p、按规定顺序把四者连起来的函数、逆累积分布抽取,直到 greedy 和温度扫描。最后把用同一个种子抽两次是否得到同样的序列、温度调低的一侧是否与 greedy 相同,留成记录。

为什么重要

Transformer 块所做的事,到给出一个与词表一样大的实数列表为止。实际要用的令牌由模型之外的规则来挑,同一个模型每次说得不同的原因,和每次重复同样话的原因,都在那里。 本实验不调用模型。这个 Pod 里没有 transformers 也没有 torch,numpy 只在 /opt/onnx-lab/bin/python 里,所以在系统 Python 中 import numpy 是不行的。取而代之的是自己写出一组 logits,之后的部分亲手来做。所以这里出现的数字,全都是在你写的 logits 上测出来的。 难的是细节。温度除的位置是在 softmax 之前还是之后、为什么要减去最大值、top-k 的平局怎么打破、top-p 是否包含达到 p 的那一项、温度、top-p、top-k 按什么顺序作用。只要有一处不同,同样的配置就成了另一个系统。 评分器不会相信你写下的说明。它会真正导入你的模块,每次用不同的 logits 和不同的温度检验函数,并与评分器另外计算的概率向量对照。它不判定随机抽取本身——只看固定了种子的编号序列和概率向量。

步骤

  1. 在 /root/work/tf-sample/sample.py 中创建 VOCAB、LOGITS 以及 softmax_t(logits, temperature)。先把 logits 除以温度,再减去最大值,然后做 softmax。
  2. 增加 entropy_bits(probs),用以 2 为底的熵来测量分布的分散程度。概率为 0 的位置按 0 计。
  3. 增加 top_k_filter(probs, k),只保留概率最大的 k 个,其余恰好设为 0.0,再重新归一化。平局时编号小的一方保留。
  4. 增加 top_p_filter(probs, p),保留到降序累积和第一次达到 p 的那一项为止。如果去掉那一项,和就达不到 p。
  5. 创建 filtered_probs(logits, temperature, top_k=None, top_p=None),按 温度 → softmax → top-k → top-p 的顺序连起来。为 None 则跳过那一步。
  6. 创建 sample_index(probs, u) 和 sample_sequence(logits, temperature, top_k, top_p, n, seed)。用逆累积分布抽取,并从 random.Random(seed) 调用 n 次 random(),使同一个种子得到同样的序列。
  7. 创建 greedy(logits) 和 sweep(logits, temps, p)。greedy 是最大 logit 的编号(平局时取小的编号),sweep 对每个温度测量 (온도, 엔트로피, top-p 가 남기는 개수)(占位符依次为温度、熵、top-p 保留的个数)。
  8. 用规定的配置测量,并把结果记录到 /root/work/tf-sample/sample_report.json 和 /root/work/tf-sample/sample_report.md 中。

参考

用温度相除后再做 softmax

在 /root/work/tf-sample/sample.py 中放入 VOCAB(不少于 12 个互不相同的短字符串)和 LOGITS(长度相同,值互不相同,第一名与第二名之差 0.5 以上,最大值与最小值之差 3.0 以上),并创建 softmax_t(logits, temperature)。把 logits 除以 temperature 之后,减去最大值,取指数并归一化。temperature 小于等于 0 时要抛异常。

要点在于除的位置。如果先求出概率再作用温度,会得到完全不同的结果。减去最大值在温度小时显出作用——除以 0.01,logit 5.2 就变成 520,math.exp(520) 会直接溢出。因为是从同样的值里减去同一个数,概率不会改变。温度 0 做不了除法,所以抛 ValueError 更好——greedy 不是“温度 0”,而是另一条规则。

用比特来测量分散程度

增加 entropy_bits(probs)。是以 2 为底的熵 -sum(p * log2(p))。概率为 0 的位置按 0 计。均匀分散到 n 个时得到 log2(n)。

math.log2(0) 会抛异常。所以 0 的位置必须跳过,这不是取巧,而是正确的处理——因为 p * log2(p) 在 p 趋向 0 时的极限是 0。提高温度并测量这个值,可以看到它变大。这里是把分布有多分散概括成一个数字的地方。

只保留前 k 个

增加 top_k_filter(probs, k)。只保留概率最大的 k 个,其余恰好设为 0.0,再把剩下的归一化使和为 1。平局时编号小的一方保留。k 大于等于列表长度时什么都不丢,只做归一化,k 小于 1 时要抛异常。

把编号按概率降序排序,把前 k 个放进集合,剩下的只要看这个集合即可。把排序键设为 (-확률, 번호)(韩文,意为“(-概率, 编号)”),平局规则也能写进一行。忘了归一化,和就不是 1,那么后面抽取时会偏向最后一个位置。不能把丢弃的位置设成很小的值——它会以很低的概率复活。

到累积和达到 p 的那一项为止

增加 top_p_filter(probs, p)。把概率按降序(平局时编号小的在前)排开并计算累积和,包含累积和大于等于 p 的第一项之后停下。其余恰好是 0.0,对剩下的归一化。p 小于等于 0 或大于 1 时要抛异常。

包含还是去掉,就是这一步的全部。如果把让累积和越过 p 的那一项去掉,剩下的总和就达不到 p——那样“保留 p 这么多的质量”这句话本身就不成立。累积和达到 p 的那一刻,把那个编号已经放进去之后再停下就行。无论 p 多小,至少会留下一个。

按规定的顺序连起四者

创建 filtered_probs(logits, temperature, top_k=None, top_p=None)。按 温度 → softmax → top-k → top-p 的顺序连起来,为 None 的环节跳过。

顺序会改变结果。先作用温度,分布本身就变了,所以即使 p 相同,保留的个数也不同;先做 top-k,幸存下来的会被重新归一化而使概率变大,所以 top-p 会更早停下。函数四行就够——把前面做的三个函数按这个顺序调用就行。别忘了判断 top_k 或 top_p 是否为 None 的条件。

同一个种子得到同一个序列

创建 sample_index(probs, u) 和 sample_sequence(logits, temperature, top_k, top_p, n, seed)。sample_index 按编号顺序累加概率,返回第一次超过 u 的位置,sample_sequence 只构造一次分布,调用 n 次 random.Random(seed) 的 random(),返回 n 个编号。

就是 u < 누적합(韩文,意为“u < 累积和”)第一次为真的位置。概率为 0.0 的位置不会增加累积和,所以绝对不会被抽中——意思是被截掉的令牌不会复活,这就是这个结构的安全装置。random.Random(seed) 只在函数里创建一次。每次抽取都创建新的生成器,就会得到 n 个同样的编号。分布也只在循环之外构造一次。

greedy 与温度扫描

创建 greedy(logits) 和 sweep(logits, temps, p)。greedy 是最大 logit 的编号,平局时取小的编号。sweep 对 temps 中的每个温度返回 (온도, 엔트로피, top-p 가 남기는 개수)(占位符依次为温度、熵、top-p 保留的个数)三元组的列表。

greedy 甚至不需要求概率——因为 softmax 不改变顺序。如果搞不清 max 在平局时给出哪一个,就自己扫描,只在更大时才更新。这样就会留下小的编号。sweep 的个数是经过 top_p_filter 之后不为 0 的位置数。请亲眼看看,温度越高,熵越大,保留的个数也越多。

留下拧动旋钮的结果

用固定的配置来测量。种子 20260917,抽取次数 24,top_k 为 5,top_p 为 0.9,热的一侧温度 1.0,冷的一侧温度 0.2。把结果以 vocab_size、greedy_index、greedy_token、top_prob_t1、entropy_t1、sweep、nucleus、topk_mass、seed、draw_count、hot_draws、cold_draws、cold_is_greedy、hot_distinct、cold_distinct、repeat_matches 写入 /root/work/tf-sample/sample_report.json,并在 /root/work/tf-sample/sample_report.md 中用 ## 무엇을 쟀나(韩文,意为“测量了什么”)、## 온도가 분포를 어떻게 바꾸나(韩文,意为“温度如何改变分布”)、## top-k 와 top-p 가 남기는 것(韩文,意为“top-k 和 top-p 留下的内容”)、## 같은 시드는 같은 수열을 준다(韩文,意为“同样的种子给出同样的序列”)四节写下。

数字不要手写,要用实际运行你的代码得到的值来填。sweep 是温度 0.25、0.5、1.0、2.0、4.0,p 是 0.9。nucleus 是在温度 1.0 下把 p 换成 0.5、0.8、0.9、0.95 测得的 [p, 남은 개수](占位符依次为 p 与剩余个数),topk_mass 是在温度 1.0 的原始概率中,对 k 取 1、3、5、10 测得的前 k 个所占的总和 [k, 합](占位符依次为 k 与总和)——不是截断后再归一化的值,而是截断之前的总和。cold_is_greedy 是冷的一侧的序列是否全部与 greedy(LOGITS) 相同,repeat_matches 是用同一个种子再抽一次时是否得到同样的序列。根据你写的 logits,cold_is_greedy 也可能是假——那就如实写下,并在正文中解释为什么。