Skip to content

第 17 章 小模型微调:先把一项任务做可靠 ​

17.1 问题足够具体,小模型才有可检验的优势 ​

假设系统每天收到大量投诉文本,需要输出姓名、地址、邮箱和投诉内容。模型不必写文章,也不必解数学题;它需要识别四个字段,保留原文证据,并输出可解析的 JSON。字段有没有漏、内容是否来自原文、JSON 能不能解析,都可以逐项检查。有了这些标准,才有办法判断小模型微调是否值得做。

本章伴读 Happy-LLM 的 小模型微调补充教程 与同目录笔记本。上游使用合成投诉数据和 Qwen3-0.6B,展示 LoRA 微调链路。下面沿着数据准备、训练和推理这条链路,检查输入输出要满足哪些约定、损失究竟算在哪些位置,以及评估能支持多大的结论。

笔记本共 34 个单元,索引为 0—33;其代码单元没有保存执行输出。README 中的示例回答属于上游展示;本章引用的配套检查记录只覆盖离线计算,不能作为模型训练复现的证据。

选模型之前,上游先问了三个问题:数据如何保密、任务有多难、调用要花多少钱。沿着这些问题比较时,需要把风险和成本的条件说清楚。数据发给经过授权的供应商,不自动等于数据泄漏;风险取决于使用目的、合同、留存、访问控制及是否确有授权。本地部署也不自动安全,日志、备份、模型检查点和监控平台都可能接触原始文本。

使用 API 省去了部分算力运维工作,上下文长度上限仍然存在。

成本比较应使用同一工作量。设一次性适配成本为 F,本地处理每条文本的平均成本为 cl,API 每条成本为 ca,共处理 N 条。暂不考虑质量差异时,本地成本为 F+Ncl,API 成本为 Nca。若 ca>cl,盈亏平衡量为 N>F/(ca−cl)。

举一个纯假设的例子:F=6000 元、两种每条成本分别为 0.002 元和 0.02 元,则约 33.3 万条之后本地方案才开始便宜。这里没有引用实时价格;人工标注、失败重试、设备闲置和运维都应计入 F 或 cl。当小模型准确率明显更低,经济账还要加上人工复核成本。

选择输入和输出要求适配成本常见失败
正则与规则字段边界稳定,例如标准邮箱低,规则需要维护地址、姓名与上下文关联难处理
大模型提示词少量示例即可,任务经常改变标注较少,有推理服务成本格式波动、事实补全、调用限制
小模型微调大量重复任务,输出模式稳定需标注、训练和独立评估记住模板、分布外输入失效

微调值得尝试的条件是任务足够窄、数据足够代表真实使用、质量门槛明确。参数少只是减少部分计算需求,不是准确性或高并发的保证。

17.2 数据集的四个字段其实定义了一个协议 ​

确定要尝试微调后,先检查每条训练数据会怎样进入模型。笔记本单元 2—8 依次安装依赖、下载合成数据、读 JSON、构造 Dataset 并加载 tokenizer。pd.read_json 后调用 Dataset.from_pandas,并没有像注释所说的那样写出 CSV。GenerationConfig 被导入但后文没有使用。

下载方式也要单独检查。单元 3 的下载命令关闭证书校验,本书不执行,也不推荐沿用;应使用有效 HTTPS 来源、保留文件哈希,并先检查授权和字段结构。

单元 9 讲的是 instruction、input、output 三字段,实际 process_func 还访问 system。因此,数据检查要覆盖全部四个字段,缺少其中一个就可能触发 KeyError。instruction + input 直接连接也可能粘连语义,最好给两者加清晰边界。对于抽取任务,output 最好先作为 JSON 对象检查,再用固定序列化策略生成训练文本,避免混用单引号、字段别名或缺失值格式。

一个自编样例是:“我叫林舟,邮件 lin@example.com,楼下施工到凌晨。”如果地址没有出现,标签应遵循预先约定,例如 address: null,而不能从姓名或城市习惯猜出来。否则模型学到的“完成所有字段”会压过“忠于证据”。

标签确定后,还要检查数据怎样划分。同一姓名在多个合成模板中重复时,应按模板或实体分组划分训练集与测试集;随机切行容易让近重复样本同时进入两边,使测试成绩显得过于乐观。

上游单元 0 的案例干净,单元 32 则加了空格和无关字符。后者是一个有用的扰动样例,但不能代表噪声鲁棒性。测试集还应包含:缺字段、多个姓名、转述他人、邮箱拼写错误、重复投诉、否认语句,以及“忽略上面要求”这类出现在待抽取原文中的指令。系统应把投诉内容当数据,不能让其中的命令修改输出协议。

17.3 模板、可见性和监督位置是三件事 ​

单元 10—11 用 apply_chat_template 展示 Qwen3 的角色边界和关闭思考模式后的空思考段。这里 tokenize=False 返回字符串,add_generation_prompt=True 追加下一次 assistant 发言的起始部分。因为样例已包含一条 assistant 回答,打印结果会出现“完整对话后再追加 assistant 前缀”。这适合观察模板,不应直接当作每条 SFT 样本的最终格式。

观察完模板,再看真正用于训练的序列。单元 13 的 process_func 手工拼接 system、user、assistant,再把回答接在后面。输入 ID 和标签都长为 T,批处理后形状为 [B,T];模型 logits 形状为 [B,T,V],其中 B 是批大小,V 是词表大小。

这里要分清“模型能看到什么”和“哪些位置计入损失”。标签中的 -100 表示不计算该位置的交叉熵;系统提示和用户文本仍以 attention mask 为 1 的状态进入模型,供回答使用。

设完整序列为 x0,…,xT−1,第 t 个位置的输出预测 xt+1。令 mt=1 表示标签 xt 是需要监督的回答 token,损失为

L=−1∑t=1T−1mt∑t=1T−1mtlog⁡pθ(xt∣x0,…,xt−1).

多数因果语言模型的实现会在模型内部移位 logits 与 labels;外部再移位一次会错开目标。用三个提示 ID [11,12,13]、两个回答 ID [21,22] 和结束 ID 2,完整标签是 [-100,-100,-100,21,22,2]。真正训练的相邻关系是 13→21、21→22、22→2。第一枚回答 token 由最后一个提示位置预测,这恰好说明“提示不计 loss”和“提示不参与计算”完全不同。

检查这段函数时,可以沿着序列的前缀、结尾和截断位置逐个核对。

手写前缀带 <s>,它是否属于该 tokenizer 的模板不能凭习惯决定,应与 apply_chat_template 的实际输出核对。

再看结尾:注释说追加 EOS,代码却追加 pad_token_id,同时标签也监督这个 pad。结束符与填充符承担不同职责,不能默认为同一个 ID。应使用模型模板所约定的结束符,并在后续批处理的填充位置设 -100。

最后是截断。超过 1024 token 就保留左端,如果提示本身已达上限,所有回答都被截掉,标签可能全为 -100。这类样本没有有效监督信号,应过滤或重新分配提示与回答预算,不能只检查张量长度合法。

单元 14 用 map 批量应用函数并删除原始列,便于训练,但排错阶段应保留能追溯原文的样本 ID。单元 15 解码完整输入,单元 16 过滤 -100 后解码标签,这是必要的目视检查。两段分别使用第 0 和第 1 条样本,不能拿它们直接作逐 token 对照;应对同一样本同时打印输入、标签和监督计数。

17.4 LoRA 参数账本可以从层尺寸逐项算出来 ​

单元 18—24 加载模型、启用输入梯度、设置 LoRA 并打印可训练参数。对于一个线性层,采用列向量记法,原权重 W∈Rdo×di,LoRA 更新为

y=Wx+αrBAx,A∈Rr×di,B∈Rdo×r.

r 是低秩维度,新增参数为 r(di+do),原矩阵有 dido 个参数。上游取 r=8,α=32,缩放为 4,dropout 为 0.1。dropout 作用于适配路径的输入,不等于每次丢掉 10% 的 LoRA 参数。inference_mode=False 是训练配置;task_type=CAUSAL_LM 用于选择合适的任务包装。

目标模块不仅有注意力的 q_proj/k_proj/v_proj/o_proj,还有 FFN 的 gate_proj/up_proj/down_proj。README 打印的模型有 28 层,每层的输入输出维度依次是:1024→2048、1024→1024、1024→1024、2048→1024、1024→3072、1024→3072、3072→1024。因此新增参数为

28×8×[(1024+2048)+2(1024+1024)+(2048+1024)+3(1024+3072)]=5046272.

配套 CPU 检查记录中的参数计数也是这个值,与 README 一致。再除以上游给出的包装后总参数 601,096,192,约为 0.8395%。这里核对的是参数数量,没有复现训练。

这个占比也不能直接当作显存占比。可训练参数虽然只占 0.8395%,但冻结权重仍要驻留,激活、注意力、梯度检查点及优化器状态也有不同的内存成本。

enable_input_require_grads() 使嵌入输出具备梯度条件,常用于冻结参数与特定梯度检查点组合;它不等于解冻整个 embedding 参数表。

配置好可训练参数之后,还要核对精度与设备。单元 20 的 model.dtype 仅检查模型 dtype,仍应核对各参数和输入的实际类型。device_map="auto" 与 bf16 加载描述了上游环境选择,并不能直接保证多卡训练布局正确。

17.5 训练器要接通,也要能解释每一步 ​

单元 25—30 使用 TrainingArguments 和 Trainer。每卡 batch 为 4,梯度累积 4 步,单卡一次优化更新约汇集 16 个样本。设卡数为 D,通常有效 batch 为 4×4×D,这是按样本计数;末尾不完整批次和不同长度样本会使“每次更新有效 token 数”不同。梯度累积允许把大批次拆成多次前向反向,但每次计算仍有激活成本。

训练 3 个 epoch、学习率 10−4、每步日志、每 50 步保存,都是这个案例的配置,不是抽取任务的通用最优值。output_dir 指定产物目录,save_on_each_node=True 会在多节点场景影响保存行为,应避免不同节点写入同一共享路径发生冲突。

内存和批处理则由另外几项设置控制。梯度检查点通过重新计算中间激活减少保存量,代价是更多计算。DataCollatorForSeq2Seq 在这里承担动态 padding,并非把模型改成 encoder-decoder;必须确认标签 padding 仍为 -100,输入 padding 的 attention mask 为 0。

report_to="none" 关闭自动集成,不会撤销单元 29 显式传入的 SwanLabCallback。单元 27—28 的实验名还出现 8B 与 0.6B 的不一致示例,记录时应使用真实模型、数据版本与配置。

若业务含敏感投诉,日志平台本身也是数据接收方,应先审计哪些字段会上传。本书没有登录 SwanLab,没有创建远端实验。

Trainer 只提供了训练集,没有独立验证集。因此即使 trainer.train() 的 loss 持续下降,也不能回答真实投诉的字段准确率是否变好。

训练结束后,保存 LoRA 权重之外,还要保存基础模型版本、tokenizer、模板和标签策略。以后加载适配器时,需要靠这些信息恢复同一套输入和输出约定;单独一个权重文件无法描述完整系统。

17.6 推理成功不等于抽取可靠 ​

单元 31—32 用模板编码提示,送到 CUDA,生成后裁掉输入前缀,只解码新输出。这一步裁剪是正确的,因为生成接口通常返回提示与续写拼接后的序列。

生成参数也需要按实际行为理解。max_length=2500 限制总长度;若意图限制回答长度,应明确使用新生成 token 的预算。do_sample=True, top_k=1 的候选集只剩最高分 token,在常规非并列条件下与贪心选择一致,不能把它作为采样多样性的实验。

推理时还应调用 model.eval();torch.no_grad() 只关闭梯度记录,不会关闭 dropout。

格式检查和内容检查应分开计数。JSON 能解析,只说明语法有效;四字段齐全,只说明 schema 合法;姓名和邮箱有原文证据,才说明抽取忠实。

因此,评估至少要分别报告 JSON 有效率、逐字段准确率、整条完全正确率、缺失字段幻填率,并按噪声和文本长度分组。地址可同时报告严格字符串匹配与预先定义的规范化匹配,不能看到结果后再随意放宽标准。

缺乏证据时输出 null,遇到多个候选时按协议输出列表或拒绝自动决策,通常比总是填满更可靠。一个案例回答正确,也不支持“保证并发、保证效果、保证安全”这类结论。

17.7 本章的真实检查与练习 ​

配套程序 examples/17_19_contract_checks.py 的检查记录使用 Python 3.12.14、PyTorch 2.6.0+cpu、float64、seed 42、单线程。运行命令为 python examples/17_19_contract_checks.py,结果记录在 qa/extras/17_19_contract_checks.json。记录覆盖三组相邻监督关系、提示截断到长度 3 时没有任何有效标签,以及 5,046,272 个 LoRA 参数。这些离线检查不涉及加载 Qwen 权重、下载数据、SFT 或 GPU 推理。单元 33 是空代码单元,没有可复现行为。

以下是本书补充练习。

  1. 一个线性层为 1024→3072,LoRA 的秩从 8 改到 16,新增参数变化多少?缩放参数固定为 32 时,比例如何变化?
  2. 为什么把提示的 attention mask 也改成 0,会破坏原任务?
  3. 100 条样本都输出合法 JSON,但 20 条猜出了原文没有的地址。应该怎样报告?
  4. 为什么关闭自动日志仍需检查显式 callback?

答案:

  1. 新增参数从 8(1024+3072)=32768 变为 65536,缩放从 4 变为 2;参数更多和更新幅度更大不是同一件事。
  2. 会让模型失去完成抽取所需的条件信息,标签屏蔽只应取消预测这些 token 的损失。
  3. JSON 有效率是 100%,但至少应另报地址幻填率 20%,不能称任务准确率 100%。
  4. callback 是另一条独立的记录路径,report_to 不控制已显式挂载的对象。

17.8 来源、归属与覆盖说明 ​

  • Datawhale Happy-LLM,固定提交 1e0b3a1e6f8936b8ba4bbd0673e8e2227d196c96:README、34 单元 notebook。项目根 README 将本篇署名列为“不要葱姜蒜”(KMnO4-zx);本目录未另列不同作者,归属一并保留。
  • 本章为原创解释、纠错和离线数学验证,依上游 CC BY-NC-SA 4.0 以相同许可提供;不声称官方认可,也不将上游代码改标 MIT。
  • 单元 0 对应 17.1—17.2;1—9 对应 17.2;10—16 对应 17.3;17—24 对应 17.4;25—30 对应 17.5;31—32 对应 17.6;33 对应 17.7。逐文件、定义与单元的机器可读台账见配套覆盖记录。Notebook 单元 27 引用的 images/05-2.png 不在该目录已跟踪资产中,本书未把它当作已取得证据。

保留各书来源与许可 · 示例运行条件以正文为准