13. 采样与命令行对话
模型输出的是每个词元的分数。要把它变成一个回答,还需要采样:从这些分数中选出一个词元,接到序列末尾,再继续下一步。
这一页讲清温度、top-k、停止条件,以及 chat_cli.py 怎样把多轮对话串起来。
从 logits 到下一个词元
模型在每个位置输出词表大小的 logits:
词元 A: 2.1
词元 B: 5.4
词元 C: 1.3
...2
3
4
采样过程分三步:
- 用温度缩放 logits。
- 可选地用 top-k 只保留分数最高的 k 个候选。
- softmax 转成概率,按概率随机抽取一个词元。
nanochat/engine.py第 140–156 行↗温度
温度 T 控制分布的陡峭程度:
调整后的 logits = 原始 logits / TT = 0:不退而求其次,直接选分数最高的词元。这叫贪心解码(greedy decoding),结果最稳定,但可能重复。T < 1:分布更集中,更倾向于高概率词元。T = 1:保持模型原始分布。T > 1:分布更平坦,低概率词元也有机会被选中,输出更随机。
nanochat 的命令行默认使用 0.6,top-k 为 50。这比始终选最高分更有变化,但又不至于太随机。
温度不是“创造力”旋钮
温度高会增加随机性,不等于模型变得更聪明。数学题或代码题通常更适合较低温度,聊天和写故事可以适当提高。
top-k
top-k 只保留分数最高的 k 个候选,把其余候选的概率设为 0。
例如 k = 3:
原始候选:A, B, C, D, E
保留:A, B, C
丢弃:D, E2
3
这样可以避免模型偶尔抽到极低概率的奇怪词元,同时保留一定随机性。
代码中没有 top-p(核采样)。如果你需要,可以自己加,但要注意保持采样接口和测试一致。
随机种子
Engine 接受 seed 参数。固定种子后,在相同模型、相同提示和相同参数下,采样结果会重现。
这不是训练中的随机性控制,而是生成时的随机数发生器。chat_cli.py 没有显式暴露种子,所以每次对话的随机结果可能不同。
什么时候停止
Engine 在遇到以下词元时结束当前行:
<|assistant_end|>:正常的助手回答结束。<|bos|>:新文档开始,当前回答也结束。
另外,达到 max_tokens 也会停止。命令行默认最多生成 256 个词元。
如果模型一直没有生成结束标记,chat_cli.py 会在达到最大长度后手动补一个 <|assistant_end|>,保证下一轮对话的格式正确:
scripts/chat_cli.py第 84–96 行↗多轮对话怎样保存
chat_cli.py 不保存 Python 对象形式的历史消息,而是维护一个词元列表:
conversation_tokens = [bos]每轮用户输入后:
加入 user_start
加入用户文本的词元
加入 user_end
加入 assistant_start
生成助手词元
保存助手回答2
3
4
5
6
下一轮继续在这个列表后面追加。模型每次都能看到之前的全部对话,直到达到上下文长度。
读多轮对话主循环scripts/chat_cli.py第 43–96 行↗输入 clear 会把列表重置为 [bos],相当于开始一段新对话。
命令行参数
读命令行参数scripts/chat_cli.py第 13–21 行↗| 参数 | 默认值 | 作用 |
|---|---|---|
-i 或 --source | sft | 加载 sft 或 rl 检查点 |
-g 或 --model-tag | 自动找最大模型 | 指定模型目录 |
-s 或 --step | 自动找最后一步 | 指定检查点步数 |
-p 或 --prompt | 空 | 单次提问后退出 |
-t 或 --temperature | 0.6 | 采样温度 |
-k 或 --top-k | 50 | top-k 候选数量 |
示例:
# 单次回答
python -m scripts.chat_cli -p "Why is the sky blue?"
# 使用 RL 模型
python -m scripts.chat_cli -i rl -p "What is 12 * 3?"
# 更稳定的回答
python -m scripts.chat_cli -t 0 -p "What is the capital of France?"
# 进入交互模式
python -m scripts.chat_cli2
3
4
5
6
7
8
9
10
11
为什么模型总是重复
小模型、少量训练和较高的温度都可能导致重复。先尝试降低温度:
python -m scripts.chat_cli -t 0.2 -p "Hello"如果仍然重复,通常是模型训练不足,而不是采样参数的问题。检查:
- 是否加载了正确的 SFT 检查点?
- 训练步数是否太少?
- 分词器和模型词表是否匹配?
- 是否在 CPU 上训练了一个很小的模型?
为什么回答会在句子中间停住
可能是达到了 max_tokens,也可能是模型生成了结束词元。命令行默认最多 256 个词元,对于长回答可能不够。
可以在代码里调大 max_tokens,但要注意键值缓存的序列长度和显存占用。缓存按最大可能长度预分配,长度越大,占用越高。
为什么中文输出不如英文
nanochat 的预训练数据以英文为主,SFT 数据也以英文对话为主。分词器可以编码中文,但模型没有见过足够多的中文文本和中文对话,因此中文表现通常较弱。
这不是分词器不支持中文,而是训练数据和训练目标的问题。若要改善中文能力,需要加入中文预训练语料、中文对话数据和相应的评测。
一次生成流程的完整回顾
用户文本
→ 分词器编码
→ 加入对话特殊词元
→ 预填充提示,建立 K/V 缓存
→ 采样第一个词元
→ 把新词元送入模型,追加 K/V
→ 重复,直到结束词元或最大长度
→ 分词器解码
→ 显示给用户2
3
4
5
6
7
8
9
这条链路就是整个 nanochat 项目的最终落点。前面所有训练阶段,都是为了让这条链路产生有用的输出。
本章要记住的三件事
- 温度控制随机性,top-k 限制候选范围,固定种子可以让采样结果重现。
- 命令行通过追加词元维护多轮对话,不单独保存结构化消息。
- 小模型输出重复或答非所问时,先检查检查点和训练量,不要只调采样参数。
到这里,主课程结束。下一步可以看从 CPU 走到多卡 GPU,自己设计实验。