对应实践:
course/practice/labs/lab08-step7/
主要修改文件:course/practice/labs/lab08-step7/framework/student.c
验证命令:make clean && make test
到了这里,模型已经具备了一个重要前提:
它不再只是随机把一堆模块拼起来,而是已经有了“通过 logits 表达偏好”的能力。
但 logits 还不是文本。
一个语言模型真正“开口说话”的瞬间,不在 model_forward 结束的时候,而在你决定:
这一拍到底选哪一个 token 作为下一个输出。
这就是生成章的核心。
本章不会再引入新的大模型结构。它要处理的是另一类非常工程化、但又直接决定模型表现的问题:
- 是永远选分数最高的那个 token,还是允许一点随机性?
- 如果允许随机性,随机性的范围该怎样控制?
- 如果词表很大,是否应该只在最有希望的少数候选里抽样?
这些问题的答案,最终都会落到三个需要你实现的函数上:
student_sample_greedystudent_sample_temperaturestudent_sample_top_k
这三种策略加在一起,就构成了后面所有生成行为的基本决策层。
8.1 为什么生成章不是“附属技巧”
很多初学者会把采样策略误看成模型主体之外的一点“调味料”,好像只是在前向后面多接几行代码。
但从用户实际看到的效果来说,采样几乎直接决定了模型表现的性格。
同样一组 logits:
- 用 greedy,模型会更稳定,也更容易复读;
- 用高温度采样,模型会更发散,也更容易出怪话;
- 用 top-k,模型会只在高分候选里活动,表现更受约束。
所以这一章不是在教一个外围技巧,而是在教:
模型内部的概率分布,怎样被转化成外部可见的文本行为。
8.2 本章你要建立哪些判断
这一章结束后,你应当能够清楚说出:
- greedy 为什么本质上就是
argmax; - 温度为什么等价于“先把 logits 除以一个系数,再做 softmax”;
- 为什么温度越小,分布越尖锐;温度越大,分布越平坦;
- top-k 为什么能够砍掉大量低概率尾部候选;
- 为什么“未训练模型生成乱码”是预期,而不是 bug。
如果这些直觉建立起来,后面无论是 REPL、HTTP 服务还是 BPE 对话,你都能更清楚地判断“问题到底出在模型学得差,还是出在采样策略不合适”。
8.3 先看 practice target:这章改哪里
本章实践目录是:
course/practice/labs/lab08-step7/
├── TASK.md
├── Makefile
└── framework/
├── student.c <- 主要修改这里
├── student.h
├── verify.c <- 自动验证,不改
└── verify.h
这一章需要你实现的函数,正好对应三种采样策略:
student_sample_greedystudent_sample_temperaturestudent_sample_top_k
它们的关系不是三道孤立小题,而是一层层往上搭:
- greedy 是最简单的基线;
- temperature 是在完整词表上按概率抽样;
- top-k 是先缩小候选范围,再调用温度采样。
这也是一个很好的工程设计范例:更复杂的策略最好建立在更简单的策略之上,而不是每一题都从头另写一遍。
8.4 greedy 为什么是最好的第一步
greedy 采样的定义非常直接:
选 logits 中最大的那个索引
也就是 argmax。
它的好处是确定性强、最容易调试,也最适合作为采样逻辑的起点。
因为只要 greedy 都选不对,后面的 temperature 和 top-k 就更没有可信度。
这也是为什么 Lab08 的第一组测试会优先检查:
- 最大值位置是否被正确选出;
- 平局时如何处理;
- logits 本身是否被无意修改。
这些看起来简单,但正是在守住最基本的决策层。
8.5 温度真正改变的是什么
温度不是“直接改概率”,而是先改 logits 的尺度。
如果写成公式,就是:
scaled_logits[i] = logits[i] / temperature
然后再对 scaled_logits 做 softmax。
这里最值得讲清楚的是直觉:
temperature < 1时,所有差距都被放大,分布更尖;temperature = 1时,保持原始分布;temperature > 1时,差距被压缩,分布更平。
所以温度本质上是在调“模型有多犹豫”。
温度越低,模型越像在说“我只认最可能那个”;温度越高,模型越像在说“几个候选都可以试试”。
8.6 为什么 top-k 是对尾部分布的约束
语言模型的词表通常很大。即使前几个 token 概率已经远高于其它候选,完整 softmax 仍然会给大量低概率 token 留下一点点质量。
如果你让抽样直接在完整词表上发生,就会出现一个问题:
那些几乎不可能、但仍不为零的候选,仍然有机会偶尔被抽中。
top-k 的思路就是先粗暴地做一层裁剪:
- 只保留 logits 最高的前
k个; - 其余候选全部视作不可选;
- 再在剩下的这些候选里做 temperature 抽样。
它的作用不是让模型“更聪明”,而是让生成空间更受约束、更不容易掉进词表尾部噪声。
8.7 为什么“模型学得差”和“采样策略差”要分开看
本章一个非常重要的课程目标,是让学员别把所有生成问题都归因给模型本身。
如果模型没训练好,那么:
- 不管你怎么调采样,输出都可能很差。
但反过来,如果模型其实已经学到一点东西,而采样策略选得极端,例如:
- 温度过高;
- top-k 太大;
- 没有任何约束;
那么输出同样可能显得混乱。
所以从 Chapter 8 开始,课程要学员建立一个新的调试分层:
- 先问 logits 有没有学到东西;
- 再问采样策略是不是把好分布抽坏了。
8.8 本章实践步骤
task 8.1:先读 student.c 和 verify.c
进入:
cd course/practice/labs/lab08-step7
建议先读:
framework/student.hframework/student.cframework/verify.c
当前验证器会分别检查:
- greedy 是否真的选出最大位置;
- 温度采样在低温和高温下的统计行为是否合理;
- top-k 是否真的只在前
k个候选里抽样; - 一些边界条件,例如
temperature <= 0、k <= 0或k >= vocab_size时怎样退化。
这说明本章不是只看单次输出,而是在看抽样行为是不是符合概率直觉。
task 8.2:实现 student_sample_greedy
这一题建议完全手写一次 argmax。
重点不是代码长短,而是形成习惯:
采样函数不应该偷偷改输入 logits,本质上它只是读 logits,然后做决策。
task 8.3:实现 student_sample_temperature
这一题的关键步骤有三个:
- 按温度缩放 logits;
- 做 softmax;
- 按累积分布抽样。
建议你在写的时候不断提醒自己:
temperature 的真正作用发生在 softmax 之前,而不是 softmax 之后。
task 8.4:实现 student_sample_top_k
这一题的重点不是排序技巧,而是清楚:
- 哪些候选应该保留;
- 哪些候选应该彻底屏蔽;
- 筛完以后,仍然要回到 temperature 抽样那套逻辑。
也就是说,top-k 的本质不是另一种全新抽样,而是“先裁剪,再抽样”。
task 8.5:运行当前真实基线
在还没补完三个函数前,先运行:
make clean && make test
当前 Lab08 的真实初始状态是:
- 8 通过,12 失败。
已经通过的部分,主要来自一些边界测试和“空实现也不会破坏输入”的检查。
失败的部分则集中在真正与采样决策相关的逻辑上,例如:
- greedy 没有选对最大值;
- 温度采样没有体现出预期的分布趋势;
- top-k 没有把采样范围约束住。
这正说明这章当前的实验起点是健康的:
框架可以跑,但采样核心逻辑还在等学员完成。
task 8.6:完成后重新验证
当你补完三个函数后,再执行:
make clean && make test
如果实现正确,你应该看到:
- greedy 的位置选择正确;
- 低温时几乎总选最大项;
- 高温时非最大项获得更多采样质量;
- top-k 只在规定候选里活动。
到这一章结束,模型才真正具备“把内部概率转成外部文本选择”的能力。
8.9 常见错误与排查顺序
最常见的错误通常是:
- greedy 写成了取最小值或平局规则错误;
- temperature 忘了除温度,或者温度为 0 时没有退化到 greedy;
- softmax 概率没正确归一化;
- top-k 没有真正屏蔽词表尾部候选;
- 抽样函数修改了输入 logits。
建议排查顺序是:
- 先看 greedy;
- 再看 temperature 分布;
- 最后看 top-k 的筛选范围。
因为 top-k 本质上依赖前面两层直觉。
8.10 思考题
- 为什么温度趋近于 0 时,采样行为越来越像 greedy?
- 为什么温度升高时,低分 token 会获得更多机会?
- 为什么 top-k 能抑制尾部噪声,但也可能让模型更容易陷入复读?
8.11 本章小结
Chapter 8 把课程重心从“模型内部怎样算”推到了“模型外部怎样表现”。
从现在开始,学员应该逐渐形成一个重要判断:
模型输出的文本,不只是由权重决定,也同样由采样策略决定。
这一步铺好以后,下一章才有意义。因为多轮对话本质上就是把“单次生成”放进带上下文的连续交互里。