Appearance
第10章 · 后训练与对齐
toy SFT/LoRA/DPO 更新不等于工业 RLHF 或真实偏好质量。
先修:第8章的训练循环和第9章的推理对拍。
本章目标
- 区分 pretraining、SFT、preference data、reward model、RLHF 和 DPO 的目标。
- 实现 assistant-only loss mask、极小 SFT、LoRA adapter、可解释的 reward/advantage toy path 和 DPO toy path。
- 用固定评估集比较 base/SFT/LoRA/DPO 行为,同时报告遗忘、数据污染和不确定性边界。
公式与 shape
SFT 只在 assistant token 上计算 loss:
LoRA 将冻结权重写成
DPO toy loss 可写成
| 对象 | shape | 约束 |
|---|---|---|
| conversation ids | (B, T) | system/user/tool token 可以存在但不一定监督 |
| loss labels | (B, T) | 未监督位置使用 ignore index |
LoRA A/B | (r, d_in) / (d_out, r) | r 明显小于 full rank |
| chosen/rejected log-prob | (B,) | pair 顺序固定 |
关键概念与论文绑定
| 概念 | 论文/参考 | 动手输出 / 判断问题 | 不可替代的边界 |
|---|---|---|---|
| instruction data 与 assistant-only mask | InstructGPT;Raschka ch07 | 给 system/user/assistant/padding 标出监督位,说明 shifted labels 与 ignore index | prompt 格式正确不等于模型学会遵循指令 |
| LoRA 的低秩更新、初始化、merge | LoRA;Raschka Appendix E | 计算可训练参数量;证明零初始化 no-op、merge 前后对拍 | toy adapter 不代表大模型质量或灾难性遗忘已解决 |
| preference pair、reference policy 与 DPO | DPO | 用一对 chosen/rejected 检查 margin 方向,故意交换符号并记录失败 | DPO toy loss 不等于 reward model、PPO 或工业 RLHF |
| SFT、reward model、RLHF、DPO 的关系 | InstructGPT 的流程图与 DPO 公式 | 画四者的数据/优化目标关系,口述“谁产生 reward、谁直接优化 policy” | 课程只实现可解释最小路径,不宣称训练了 reward model |
| scalar reward 与 normalized advantage | policy-gradient 基础公式 | 对 3 个固定样例计算 baseline、centered advantage 和标准化值;交换 reward 符号必须改变更新方向 | 这是奖励记账 fixture,不是 reward model、PPO 或 RLHF |
| 遗忘与数据污染 | 固定 holdout 与训练 ID 清单 | 报告样例 ID、训练/评估集合交集和行为差异;不能只报平均分 | 一组 synthetic fixture 不能外推真实用户偏好 |
Karpathy 的 nanochat 仅用于观察对话模板、conversation render 和 loss mask 的问题切分;本课只抽取问题与接口,不复制实现。中文阅读可选 LLMs-from-scratch-CN 的对应章节和 dive-into-llms documents 的对齐/推理说明;它们是词汇与复习入口,作业仍须独立重写并记录来源、版本/commit 和许可状态。
数学桥接:第2章 → 第10章
本章的后训练与对齐建立在一组你可能已经陌生的概率和优化概念之上。每个桥接点对应第2章的具体函数:
SFT loss mask 与 cross_entropy:第2章 §6 的 cross_entropy(p_dist, q_dist) 衡量两个分布的"信息差距"。SFT 的 assistant-only mask 等价于对每个 token 位置乘以一个二元权重
当 cross_entropy([1,0], [0.8,0.2]) ≈ 0.322 是单 token 版本;SFT 把它推广到带 mask 的序列平均。
DPO 与 kl_divergence:第2章 §6 的 kl_divergence(p, q) = Σ p(x)·log₂(p(x)/q(x)) 衡量两个分布的"距离"。DPO 的封闭形式解(Eq. 7 in the paper)等价于在满足偏好约束的前提下最小化 π_θ 和 π_ref 之间的 KL 散度:
其中
LoRA 与 gradient_descent_step:第2章 §7 的 gradient_descent_step(param, grad, lr) = param - lr · grad 每次更新一个标量。LoRA 冻结 W 的全部参数,只对低秩增量 α/r · B · A 做梯度更新——可训练参数量从 O(V·D) 降到 O(r·(d_in + d_out))。W' = W + ΔW 的 merge 操作在 save/load 时把 ΔW 折叠回 W,推理时没有额外开销。
Reward / Advantage 与 policy gradient:第2章 §7 的梯度更新需要一个标量 reward 信号。advantage = reward - baseline 衡量"这个 action 比平均水平好多少";标准化后(零均值、单位方差)让不同样本间的更新步长可比。DPO 绕开了显式 reward model,但 chosen/rejected 的对数比差值就是隐式 reward。
前端类比:SFT loss mask = CSS 的 pointer-events: none——指定区域不响应点击(不产生梯度)。LoRA = Vue 的 shallowRef vs ref——base 权重是深层不可变响应式对象,adapter 是浅层可变代理,merge 时合并回同一对象。DPO = A/B 测试的 Bayesian update——chosen 组胜出时更新 belief(policy),rejected 组则降低该策略的概率。
交互观察
交互:Softmax + Temperature
把 logits 看成「未归一化分数」。Temperature 越高分布越平,越低越尖。
用温度变化复习“偏好排序”和“采样随机性”不是一回事;DPO/SFT 的真实更新、assistant-only mask 和固定评估集仍必须在 Python 中验证。
交互:LoRA 合并动画
LoRA 训练时只新增一个低秩增量 ΔW = (B·A)·α。推理前把这个增量「焊」进 W,就回到单矩阵前向。
y = (W0 + α/r · B·A) x① 训练时冻结 W0,只更新 A、B(r 远小于 dIn×dOut)
W₀(冻结的基础权重)
1.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
1.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
1.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
1.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
1.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
1.0
0.0
0.0
+
α·(B·A) · 0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
=
W'(合并后)
1.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
1.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
1.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
1.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
1.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
1.00
0.00
0.00
LoRA 增量参数:28(r × (dIn + dOut))
原权重参数:48(dIn × dOut)
比例:58.3%
$$W' = W_0 + \Delta W,\quad \Delta W = \frac{\alpha}{r}\,B\,A,\quad A\in\mathbb R^{r\times d_{in}},\ B\in\mathbb R^{d_{out}\times r}$$
教学要点(红旗实验):把 r 调到 1、α 调到 32 —— 增量看起来不小,但参数只有 14 个。 这就是 LoRA「便宜微调」的本质:把表达能力锁在低秩流形上,换取极高的参数效率。 合并后推理图就少两个矩阵算子(省启动开销 / 少算一次 matmul),代价是这一份 LoRA 不能再热切换。
拖动 rank 与 α,观察低秩增量 α/r·BA 如何叠加到冻结权重上;把 merge 滑块从 0 推到 1,确认「LoRA 在路上」与「合并进 W」两种形态输出一致——这正是下方故障表前两行(adapter 初始非零、merge 缩放/顺序错误)的直观版。
动手任务与验收(从零实践)
构造带 system/user/assistant 的最小 batch,逐位置打印
input_ids、labels和loss_mask。先训练一个极小 SFT/adapter,确认 adapter 或权重变化,再在固定 prompt 集上比较 base 与适配后输出。
用极小 reward fixture 计算逐样例 scalar reward、baseline、advantage 和 normalized advantage;故意交换符号,确认方向故障可见。
用极小 preference fixture 验证 DPO 的 chosen margin 方向;不要把 toy loss 称作工业 RLHF。
运行当前入口:
bashPYTHONPATH=python python -m pytest \ python/tests/test_post_training_eval.py \ python/tests/test_post_training.py \ python/tests/test_quantization.py -q生成一份不含真实对话的对照表:
base → SFT → LoRA → DPO,并附 reward/advantage 逐样例 JSON;每行绑定固定 prompt/样例 ID、观察量、运行命令和局限;未运行的指标写<未测量>,不能借用上游仓库或论文里的数字。从上面任一参考资料挑一个概念,先关闭参考实现,再用本仓库接口重写一个最小测试;提交只保留自己的代码、测试输出和链接,不复制上游文字、图片或代码。
当前 post_training.py 已提供 build_sft_batch、可训练的 LoRABigramPolicy、LoRAAdapter、compute_reward_advantages、dpo_batch_loss 和量化相关接口。固定 reward fixture 的 baseline 为 0.0,chosen/rejected 的 normalized advantage 分别为 ±1.224744871391589,交换符号会反转方向;逐样例机器快照见 evidence/10-reward-advantage-v1.json。post_training_eval.py 在固定合成集上真实比较四条路线:base accuracy 为 0.375,full-base SFT 与 LoRA 均为 0.875,DPO preference rate 为 1.0;固定 holdout 遗忘为 0/9,训练 ID 与评估 ID 污染为 0/8。这些是可复现的 toy 行为证据,不是生产模型质量、reward model、PPO 或 RLHF 结论,报告 hash 为 4036dbd2ce6953e5a8706203860f9da37ac51e7597b624de8c027f77660c02a6。
故障注入与预期信号
本表是本章唯一的故障注入权威清单;「动手实验」一节不再另列第二份。
| 注入 | 预期失败信号 | 修复后证据 |
|---|---|---|
| LoRA 初始 adapter 非零 | base 输出被无意改变 | no-op forward 和 merge 对拍 |
| merge 忘记缩放或矩阵顺序错 | merge 前后输出不一致 | frozen weight 对拍 |
| 损失掩码覆盖了 prompt | 模型学会复读用户输入、对话变自说自话 | 只在 assistant 段构造目标 token,逐位置打印 mask 验证 |
| LoRA 忘记冻结 base 参数 | 参数量与显存指标假象、破坏适配语义 | 显式关闭 base 的可训练标记,训练一步后 base 逐元素不变 |
| DPO 的 reference 模型未切 eval 且未 no_grad | 显存翻倍、优化目标漂移 | reference forward 包在 eval/no_grad 下 |
| chosen 与 rejected 标注顺序颠倒 | 对齐方向反向、奖励信号相反 | 数据加载层固化标注 schema 并断言,交换符号的故障题可见 |
论文与延伸
- Training language models to follow instructions with human feedback(Ouyang 等,2022)
- LoRA: Low-Rank Adaptation of Large Language Models(Hu 等,2021)
- Direct Preference Optimization: Your Language Model is Secretly a Reward Model(Rafailov 等,2023)
前端/Agent 迁移
对齐改变行为分布,不等于事实性、安全性或产品责任已经解决。可以把 SFT/LoRA 看成改变组件默认状态,把 DPO 看成偏好排序;Agent 仍需要独立的工具权限、证据和人工决策边界,不能用模型分数代替系统政策。
- SFT assistant-only mask ≈ 条件渲染(v-if):只对 assistant 角色渲染 loss,system/user 角色不参与梯度——就像 Vue 的
v-if="role === 'assistant'"。 - LoRA ≈ 热更新 / HMR:冻结 base 权重,只更新 adapter;merge 时把增量折叠回主权重,推理时零开销——就像 Webpack HMR 在运行时替换 module,不刷新页面。
- DPO ≈ A/B 测试的 Bayesian 更新:chosen/rejected pair 提供偏好信号,policy 朝 chosen 方向更新——就像 A/B 测试中胜出版本获得更多流量。
- Reward model ≈ 单元测试评分函数:给每个输出打分,但不直接决定行为;DPO 绕开显式 reward model,就像 E2E 测试直接根据用户行为优化,不经过中间评分。
口述与自测(不看资料,5–10 分钟)
- 用同一条 prompt 的
base → SFT/LoRA → DPO对照解释各阶段改变了哪个参数/目标;说明 assistant-only loss mask 为什么不能漏到 user token。 - 结合 InstructGPT、LoRA 与 DPO,区分 reward-model/RLHF 流程和直接偏好优化;说明 toy reward/advantage 结果为什么不能推出安全、事实性或工业对齐质量。
- 用第2章的
cross_entropy([1,0], [0.8,0.2]) ≈ 0.322解释 SFT 的 assistant-only mask:CE loss 只在 assistant token 位置计算,system/user token 的 loss 被 mask 为 0,等价于乘以。 - 用第2章的
kl_divergence(p, q)解释 DPO 的隐式目标:DPO 的 closed-form 解等价于最小化 π_θ 和 π_ref 之间的 KL 散度;chosen 的对数概率高于 rejected 时 margin 为正,loss 下降。 - 用第2章的
gradient_descent_step解释 LoRA 为什么只更新 B 和 A:冻结 W 后,可训练参数从 O(V·D) 降到 O(r·(d_in+d_out));merge 时 ΔW = α/r · BA 被折叠回 W,推理无额外开销。
实验与参考
动手实验
把后训练三件事钉牢:SFT 的损失只由 assistant 区域贡献、prompt 不参与梯度;LoRA 训练中 base 权重冻结、只有 adapter 参数变化;DPO 让 chosen 相对 rejected 的对数比单调上升。三者任何一个失守都会让对齐方向跑偏。
环境准备
bash
cd <仓库根>
export PYTHONPATH="$PWD/python"命令与预期输出
bash
python -m pytest python/tests/test_post_training.py \
python/tests/test_post_training_eval.py \
python/tests/test_align_kv.py -qtext
............................. [ 100% ]
3 passed in 2.04s
判定信号:
prompt 位置 loss 被掩码为 0,assistant 位置 loss 大于 0
LoRA 训练一步后 base 权重逐元素不变,adapter 权重更新
chosen-rejected 对数比在优化后单调上升
chosen 与 rejected 配对顺序与标签方向一致概念图
图:第10章 SFT → LoRA → DPO 三种对齐方法 — SFT 用交叉熵让模型模仿对话格式,LoRA 通过低秩矩阵冻结基座做高效微调,DPO 直接在 chosen/rejected 对上优化偏好。三种方法共享第2章的交叉熵和 KL 散度作为数学基础,对齐后的模型作为第11章 RAG 的生成器、第13章 Agent 的推理后端和第18章 Eval 的评测对象。
故障注入清单
故障注入以正文「故障注入与预期信号」一节为唯一权威清单(6 项,覆盖 LoRA 初始化/冻结/merge、loss mask、reference 模型模式和标注方向),此处不再另列第二份;做故障题时逐项对照该表的预期信号与修复后证据。
资源 / 成本 / 隐私
本章只运行极小 synthetic preference fixture 和本地 PyTorch,预计 gross cost 为 0;不调用闭源 API。偏好数据不得包含真实人物、客户或私人对话,LoRA/微调权重仅保存在本地。
Evidence
仓库当前机器证据(只读快照)
evidence/module-manifest-v1.json 中 10.evidence 指向当前文件:evidence/10-runtime-v1.json。这是当前 checkout 的脱敏机器运行记录,只覆盖该 JSON 记录的命令、指标、产物和已知失败;它不是学习者提交,也不能推出学习者已完成本章。后训练评估的补充本地报告见 evidence/10-post-training-eval-v1.json,同样不替代学习者独立证据。
学习者提交模板(待填写,不是当前机器证据)
复制下面模板并填写自己的真实运行结果。所有 <...> 都是未填写状态;actual 和 artifacts 尤其不能被当作已运行或已通过。artifacts 必须替换为本次提交中真实存在的仓库相对路径。
yaml
schema: learn-llm.evidence.v1
module: 10-post-training
commit: <learner-commit-sha>
verified_at: <iso-date>
environment: <sanitized-python-device>
seed: 8
commands:
- PYTHONPATH=python python -m pytest python/tests/test_post_training.py python/tests/test_quantization.py -q
metrics:
- name: assistant_only_mask_accuracy
expected: 1.0
actual: <recorded-value>
- name: adapter_parameter_count
expected: <versioned-bound>
actual: <recorded-value>
- name: chosen_vs_rejected_margin
expected: '>0'
actual: <recorded-value>
artifacts:
- <learner-repo-relative-artifact-path>
cost:
gross_usd: 0
credit_usd: 0
licenses:
- source: <source>
version: <version>
license: <license>
attribution: <attribution>
redistribution: <redistribution>
known_failures:
- <sanitized-failure-or-none>没有 mask/merge/sign 故障证据时,本章保持 gate;本地 toy 更新和固定集结果不冒充工业 RLHF。