Lab 3 · 微调一个大语言模型
本实验对应第 6、9 讲:加载一个预训练语言模型,用参数高效微调(LoRA)在受限显存下把它适配到一个下游任务,并用自回归采样评估输出。它是「小算力撬动大模型」的标准流程。
1. 环境与模型
- 库:
transformers+peft(HuggingFace 生态),或等价的 LoRA 实现。 - 基座模型:选一个能在你的显存里加载的预训练模型(先跑通小模型,再考虑更大的)。本书不绑定具体型号;关键是理解流程。
- 数据:一个指令-回答格式的小数据集(几百到几千条即可)。格式通常是:
<instruction> 把下面这句话翻译成英文。 </instruction>
<input> 今天天气很好。 </input>
<response> The weather is nice today. </response>
第一次务必用最小配置(小模型、小数据、几步训练)确认整条链路能跑通,再逐步放大。LLM 微调里「环境没配好、显存不够、数据格式解析失败」占失败原因的绝大多数,与算法本身无关。
2. 为什么要“参数高效“微调
全参数微调一个模型要存 4 份东西(第 9 讲 9.1):参数、梯度、优化器的一阶与二阶矩。一个 参数的模型在 Adam 下约需 字节——大模型单卡放不下。
LoRA 的思想:冻结原权重 ,只训练一个低秩增量 (,,),前向变成
只训练 两个小矩阵,可训参数量降到不到 1%;且推理时 可合并回 ,不增加推理开销。
从预训练基座跑到下游任务,权重的改变往往落在低维子空间里——不需要动整个大矩阵,改一个低秩部分就够了。这与第 9 讲「显存瓶颈在优化器状态」正好对上:不训练 ,就不存 的梯度和优化器状态,显存骤降。
from peft import LoraConfig, get_peft_model
cfg = LoraConfig(r=8, lora_alpha=16, target_modules=["q_proj", "v_proj"],
lora_dropout=0.05, task_type="CAUSAL_LM")
model = get_peft_model(base_model, cfg)
model.print_trainable_parameters() # 例:trainable 0.1% | total 100%
target_modules 通常选注意力里的投影矩阵();r 是秩,越大容量越大、越贵。
3. 数据格式与分词
- 分词(tokenize):把文本切成 token id。必须用与基座模型配套的分词器,否则 id 与模型学过的含义错位。
- 拼接与掩码:把 instruction / input / response 拼成一条序列;只对 response 部分计算损失(prompt 部分 mask 掉),这样模型学的是“如何回答“,而不是“复述问题“。
- 截断与填充:设最大长度,超长截断、不足填充;注意 padding token 不能参与损失。
预训练对所有 token 算下一个词预测损失;指令微调只对答案部分算损失——这才是"教模型按格式回答"的关键。mask 写错(把 prompt 也算进去)会让模型倾向于复述问题而不是回答。
4. 训练
与第 1 讲的循环完全一致,只是模型大:
out = model(**batch) # 前向
loss = out.loss # 交叉熵(对 response token,第 1 讲 1.4)
loss.backward() # 反向(定理 1.2)
opt.step(); opt.zero_grad() # 更新(定义 1.3)
受限显存时可叠加(第 9 讲):梯度累积模拟大批量、混合精度降占用、梯度检查点用计算换显存。
5. 评估:怎么判断“微调有用“
| 方式 | 说明 |
|---|---|
| 定性 | 对若干留出问题采样,人工/对照检查是否按格式作答 |
| 定量 | 在留出集上算 token 级困惑度(perplexity),或任务专属指标(如精确匹配率) |
| 对照 | 必须对比微调前,否则无法判断提升来自微调还是运气 |
用自然语言采样很容易产生"看起来像那么回事"的输出。评估要盯任务指标(是否答对、格式对不对),而不是输出的流畅度。范式化评估(固定问题集、固定解码参数)才能做前后对比。
6. 排错清单
- 分词器不匹配:用了非配套分词器 → 输出乱码;
- 损失 mask 写错:prompt 也算进损失 → 学成复述;
- padding 参与损失:loss 被 padding 拉偏;
- 显存 OOM:先降 batch,再上梯度累积;开混合精度;必要时减小
r或 max_length; - 过拟合小数据:几百条数据训太多轮会死记硬背,用早停/验证集。
7. 提交与自检
- 一段微调前后同一问题的回答对比;
- 一张可训练参数量与显存占用的对比(全参数 vs LoRA);
- 一句结论:在你这个任务上,LoRA 用多少可训参数达到了怎样的效果。
自检:能解释 LoRA 为何只训练极少参数却有效(低秩假设 + 冻结主干);能说清指令微调为何只对 response 算损失;能为 OOM 给出三条具体的缓解手段。
8. 代码 ↔ 讲次对照表
| 代码 / 步骤 | 对应结论 |
|---|---|
加载 tokenizer 与 input_ids | 6.1 大语言模型:BPE 分词决定 token 边界与序列长度 |
| 只在 response 上算损失(mask prompt) | 6.1:监督微调的目标是「学会回答」而非「复述问题」 |
LoraConfig(r=..., target_modules=...) | 9.5 PEFT:低秩增量 |
| 4-bit 量化基座 + LoRA(QLoRA) | 9.5:显存降到 量级 |
| 梯度累积达到有效批大小 | 1.6 优化器、9.2 数据并行 |
| 微调前后对比 + 留出集评测 | 6.7 评估:口径要对齐,且要防污染 |
| 若继续做偏好优化 | 5.6 RLHF 与 DPO:成对偏好 → DPO 损失 |
9. 扩展任务(选做 1–2 项)
- LoRA vs 全参:在你能拿到的硬件上对比两者的显存、吞吐与效果,并给出「什么条件下值得全参」的判据(9.5)。
- 秩与模块扫描:固定基座,扫 与不同
target_modules,画「可训参数—指标」曲线,找性价比拐点(附录 A 选题 4)。 - 偏好优化:用 200–500 对偏好数据做一次 DPO,与 SFT 结果对照;特别观察风格与格式的变化和「过度优化」的迹象(5.6)。
- 量化部署:把模型导出并在
llama.cpp或vLLM上跑,报告吞吐(tokens/s)与首 token 延迟,解释瓶颈在哪(6.7、工具箱)。 - 污染自查:自建 30 道题,做「同义改写 / 选项打乱」对照,比较分数落差并讨论证据的局限(6.7)。