做模型落地这几年,被问得最密集的一个问题就是:手上攒了几百上千条业务数据,是不是丢进去做一轮 Fine-Tuning,模型就懂我的业务了?问这话的人里,有做客服工单的、有做合同审阅的、也有做工业质检报告的,场景五花八门,但底层期待是同一个——希望微调像给模型装一个插件,插上就灵。实际情况要朴素得多:大模型微调(Fine-Tuning)本质是在已有基座能力上做分布偏移,它能把输出格式、语气风格、任务范式、领域术语习惯拉到你想要的位置,但它不擅长凭空灌入大量事实性知识,也不负责修补基座本身的推理短板。这篇文章是我自己踩过几轮坑之后整理的第一部分内容,从"该不该微调"讲到"怎么把一次 LoRA 微调完整跑通并验证",包含显存估算、数据构造、参数配置、评估方法和一排排查清单。适合刚接触大模型微调、准备动手但不想白白烧算力的同学,也适合已经把流程跑通、但效果总差一口气的同行对照着看。
1. 先搞清楚:这次微调到底要解决什么问题
动手之前最贵的不是显卡,是把方向判错。我见过太多团队一上来就拉数据、配环境、跑三天训练,最后发现需求用一段结构化提示词加检索就能解决八成,白白浪费了两周。所以第一步永远是定性:你面对的这个问题,属于能力缺口、知识缺口,还是表达缺口。这三类缺口的解法完全不同,微调只对其中一部分有效。
1.1 提示词、检索增强、微调三者的真实分工
我用一个比较生活化的类比来说明。基座模型像一个刚毕业的高材生,知识面广、理解力强,但对你的公司一无所知。提示词工程相当于你当面把任务交代清楚,告诉他"这份工单请按紧急、一般、咨询三个等级分类,只输出等级词";检索增强相当于给他配一个可以随时翻查的资料柜,遇到公司内部制度、产品型号、历史案例就去查;微调则是让他在你这个岗位上实习一段时间,把做事的手感、输出的口径、行业黑话的用法内化成直觉。
三者的成本结构和失效模式差异很大。提示词改动成本最低,改一句就能上线,但受限于上下文长度和模型的指令跟随稳定性,样本一多就容易飘;检索增强解决了知识时效性和可溯源问题,代价是要维护索引、处理召回质量,而且它改变不了模型的输出风格和推理路径;微调一旦训完,行为固化在权重里,推理时零额外开销、格式极稳,但更新知识要重训,且数据质量差会直接把模型带偏。
判断口诀我总结成一句:"能说清楚的用提示词,需要查资料的用检索,需要改肌肉记忆的才上微调。"所谓肌肉记忆,指的是那些你很难用自然语言精确描述、但一看输出就知道对不对的东西——比如特定行业的报告结构、某种垂直领域的术语搭配习惯、或者一个多步骤抽取任务里各字段之间的隐性约束关系。
1.2 微调真正擅长的四类任务
第一类是输出格式强约束。要求模型稳定输出特定结构的 JSON、固定字段的报告、带特定标记的文本,且字段数量多、嵌套深。这类任务靠提示词能做到七八成,但剩下那两三成的格式崩坏在批量处理里会被放大成灾难,微调能把稳定性拉到接近百分之百。
第二类是风格与语气对齐。比如让模型的回复贴合某个品牌的表达规范,或者把技术文档改写成面向非专业读者的说明。这类需求的特点是你很难穷举规则,但可以给大量正例让模型去模仿。
第三类是垂直领域的任务范式迁移。比如把通用模型改造成专门做法律条款比对、医疗记录结构化、工业故障描述的专用模型。注意这里迁移的是"怎么做这件事"的流程感,不是"这件事的事实"。
第四类是小模型能力蒸馏。用一个很强的模型生成高质量输入输出对,拿去微调一个参数量小得多的模型,让它在特定任务上逼近大模型的表现,从而把推理成本压下来。这是目前工业界最主流的微调动机之一,后面第 3 节会展开讲怎么造这种数据。
1.3 三类不该用微调硬扛的需求
第一类是高频变动的知识问答。产品价格、库存、政策条款这类一天一变的信息,训进权重里第二天就过期,而且模型会用非常自信的语气说出过时答案,比不知道更糟。这类必须走检索。
第二类是需要精确计算的场景。微调不会让模型学会算术,它只会学会"看起来像在算"。涉及金额、税率、工时统计的,务必把计算拆出来交给代码或规则引擎,模型只负责抽取和组装参数。
第三类是基座完全没有的能力。比如让一个不支持长上下文的基座去处理超长文档,或者让纯文本模型理解图片。这类属于架构层面的缺失,微调解决不了,硬上只会得到一堆似是而非的输出。
注意:判断是否该微调,有个很实用的小测试——先手工写 20 条测试样本,用最好的提示词方案跑一遍,记录失败样本。如果失败集中在"格式不稳"和"风格不对",微调收益大;如果失败集中在"不知道这个事实"和"算错了",微调基本无效。
2. 方案选型:全参微调与参数高效微调怎么选
方向定了,接下来是选技术路线。这块的讨论经常变成名词混战:全参、LoRA、QLoRA、Adapter、Prefix-Tuning、P-Tuning v2……其实对绝大多数团队来说,真正要在脑子里过的只有两个问题:显卡够不够,以及效果需不需要动基座的全部参数。把这两个问题回答清楚,选型基本就定了。
2.1 全参微调的显存账,先算再动手
全参微调意味着所有权重参与梯度更新,显存开销可以按"每参数字节数"来估。以混合精度训练为例,一份参数在训练过程中同时存在四份副本:fp16 的权重(2 字节)、fp16 的梯度(2 字节)、fp32 的 Adam 一阶和二阶动量(各 4 字节,共 8 字节)、fp32 的主权重副本(4 字节)。加起来约16 字节/参数。
按这个公式算,一个 70 亿参数的模型,光模型状态就要 7B × 16 ≈ 112 GB,这还没算激活值和临时缓冲区。如果把优化器动量也降到 bf16,能压到 12 字节/参数左右,也就是 84 GB 上下。再加上激活值,单卡根本放不下,必须上分布式切分。切分策略上,数据并行只切 batch 不切模型状态,救不了显存;张量并行切单层内部,通信开销大;流水并行切层,有空泡;真正能把优化器状态摊开的是分片式数据并行,把参数、梯度、优化器状态切到各卡上,用的时候再聚合。
实际算一笔:8 张 80 GB 卡,总显存 640 GB,跑 7B 全参微调,模型状态 112 GB 加激活和碎片,理论上是够的,但 batch size 会比较憋屈。如果是 13B 模型,模型状态就 208 GB 起,余量更紧。我个人的经验阈值是:参数量超过 10B,优先考虑参数高效方案;确实需要全参微调,先准备好 8 卡以上并预留至少 30% 显存余量,因为实际跑起来的峰值比你纸面算的高,尤其是长序列场景。
2.2 LoRA 的低秩直觉与秩的取值
LoRA 的思路不复杂,用一句人话说:冻结原来的大权重矩阵,在旁边挂两个瘦长的小矩阵,让它们的乘积去逼近这次任务需要的权重增量。数学上就是把权重更新 ΔW 分解成 B×A,其中 A 是 r×d 的形状,B 是 d×r 的形状,r 远小于 d。训练时只更新 A 和 B,主干一动不动。
为什么这样能work?一个直观解释是:针对特定任务做的权重调整,其内在"自由度"其实不高,不需要动整个高维空间,一个低秩子空间就够表达了。你可以理解成给模型调音,不是重造乐器,而是拧几个旋钮。
秩 r 的取值是最常被问的参数。我的经验区间是这样的:
| 秩 r | 可训练参数占比(7B 量级) | 适用场景 | 备注 |
|---|---|---|---|
| 4 | 约 0.13% | 单一格式对齐、语气微调 | 见效快,容量有限 |
| 8 | 约 0.26% | 常规任务微调,最稳妥的起点 | 我八成项目用这个 |
| 16 | 约 0.52% | 多任务混合、结构复杂的抽取 | 数据量需跟上 |
| 32 | 约 1.0% | 领域范式迁移、接近全参效果 | 小数据集易过拟合 |
| 64 以上 | 1.5% 以上 | 数据量极大且任务复杂 | 收益递减明显 |
参数量计算可以自己动手核对。以一个隐藏维度 4096、前馈维度 11008、32 层的模型为例,r=16,作用在注意力的四个投影矩阵和前馈的三个矩阵上:注意力的每个矩阵贡献 2×16×4096 ≈ 13.1 万参数,前馈的前两个矩阵各 13.1 万,第三个是 2×16×11008 ≈ 35.2 万。单层合计约 114 万,32 层约 3640 万,占 70 亿参数的 0.52%,和表格对得上。
还有一个常被忽略的参数是缩放系数。LoRA 的输出要乘上一个缩放因子,通常取"alpha / r"。很多实现里 alpha 直接固定成 16,那 r 从 8 变到 16 时缩放系数就从 2 降到 1,等于变相调小了有效学习率。所以改 r 的时候记得同步看一眼 alpha,我的习惯是让 alpha = 2r 保持缩放恒定,或者干脆固定 alpha=16 只调 r,两者不能同时乱动。
2.3 QLoRA:把门槛压到单卡消费级
如果手上只有一张 24 GB 的卡,又想做 7B 微调,那 LoRA 之外还得叠加量化。QLoRA 的做法是把冻结的主干权重压到 4 bit 存储,训练过程中反量化到计算精度参与前向,梯度只回传给 LoRA 分支。
显存账会变得很好看:7B 参数按 4 bit 存储约 3.5 GB,加上反量化时的临时缓冲、激活值和 LoRA 自身的优化器状态,单卡 24 GB 跑序列长度 2048、batch 1 是稳稳的,甚至能开一点梯度累积把等效 batch 做大。
代价是速度。4 bit 权重在前向时要反复反量化,训练吞吐通常比纯 LoRA 慢 30% 到 50%,具体取决于实现是否用了针对性的算子优化。另一个代价是精度损失,量化误差会让最终效果比同配置的纯 LoRA 略微下降,这个差距在简单任务上几乎看不出来,在精细的数值抽取或长链条推理任务上会有感知。
我的建议是:探索阶段用 QLoRA 快速验证数据和参数,验证通过后如果不是显存受限,切回纯 LoRA 重训一遍。前面那轮探索的价值在于确认数据格式对不对、loss 降不降、效果有没有起色,验证完再用更快的方案做正式训练,整体时间反而更省。
2.4 一张表看清四种方案
| 方案 | 显存门槛(7B,seq 2048) | 训练速度 | 效果上限 | 适用判断 |
|---|---|---|---|---|
| 全参微调 | 100 GB+,需多卡 | 慢,通信开销大 | 最高 | 数据量 10 万条以上且算力充足 |
| LoRA | 约 18–22 GB | 快 | 接近全参的 90%–95% | 绝大多数场景的默认选项 |
| QLoRA | 约 10–14 GB | 中等,慢 30%–50% | 略低于 LoRA | 单卡验证、显存受限 |
| 纯 Adapter / Prefix 类 | 约 20 GB | 快 | 略低于 LoRA | 多任务切换频繁、需要插拔式模块 |
还有一类方案是"只训部分层",比如只解冻最后几层或者只解冻 MLP 层。这种做法的显存开销和全参接近(因为大部分状态还在),效果却不如 LoRA 灵活,我基本不推荐,除非有很特殊的需求。
3. 数据准备:微调效果的天花板在这里
如果只能给一条建议,那就是:把 80% 的精力花在数据上,20% 花在参数上。我做过对比,同一份配置下,把数据从"网上随手爬的一万条"换成"人工精修的一千条",任务准确率能差出二十多个百分点。参数调优的边际收益远小于数据质量。
3.1 样本结构:一条合格数据长什么样
指令微调的标准样本是三元组:指令、输入、期望输出。落到具体实现上,最终要拼成一条完整的对话序列,其中只有回答部分参与损失计算,指令和输入部分的标签要屏蔽掉。这个细节听起来小,但做错的人非常多,症状是 loss 看起来正常下降,实际效果稀烂。
屏蔽的实现方式是把非回答区域的标签全部设成 -100,损失函数会自动跳过这些位置。如果不屏蔽,模型会把大量算力花在学习"如何复述用户的问题"上,学出来的模型特别喜欢重复输入,问它一句它先给你复述三句。
一条合格的样本应该满足几个条件:指令描述清晰无歧义;输入部分包含完成任务所需的全部信息,不依赖外部上下文;输出部分格式统一,同类任务的字段顺序、标点习惯、单位写法完全一致;整体长度在目标序列长度的合理区间内,别全都是超长样本把显存吃光,也别全都是两三个字的短样本浪费吞吐。
3.2 清洗环节的六个必做动作
我自己的流程里固定有六道清洗,每一步都对应过实际踩过的坑:
- 去重。精确去重之外还要做近似去重,同一句话换个说法出现几十遍,会让模型对这句话产生过强偏好,表现为输出复读。用相似度阈值 0.85 左右做一轮模糊去重,能干掉大部分模板化的重复样本。
- 长度分布检查。统计样本长度直方图,把超长和超短的尾部切掉。我遇到过一次,数据集里混进了几百条超长样本,训练时把序列截断了,导致模型学到的回答全是半截话。
- 输出空值与非目标语言过滤。有些采集数据会留下空回答、纯标点、或者夹杂大量其他语种的噪声样本,这些必须清掉。
- 格式归一化。全角半角、中英文标点、日期格式、金额单位,全部统一到同一套规范。这一步直接影响模型输出的稳定性,因为模型会忠实模仿数据里的不一致。
- 敏感与风险内容筛查。用规则加模型双重过滤,把不该出现的内容剔干净,这是上线前的必要环节,别指望模型自己学会拒绝。
- 人工抽检。随机抽 100 条逐条看,我几乎没有一次抽检没发现问题。这一步不能省,而且要抽检清洗后的数据,不是原始数据。
提示:去重的时候不要只对输入去重。有类样本是输入完全相同、输出略有差异(比如同一个问题不同专家的不同答法),这种样本保留少量可以增加模型输出的多样性,保留太多会让模型学到"这个问题的答案不确定",表现成回答含糊、来回摇摆。我的处理是同一输入最多保留 2 到 3 条,且要求输出之间的差异是实质性的。
3.3 数据量与配比的经验值
数据量没有绝对标准,但有几个经验区间可以参考:
| 任务类型 | 建议样本量 | 说明 |
|---|---|---|
| 单一格式对齐 | 300–1000 | 重点是格式一致性,不是数量 |
| 风格/语气模仿 | 1000–5000 | 需要覆盖足够的表达变体 |
| 结构化抽取 | 3000–10000 | 字段组合的覆盖面决定上限 |
| 领域范式迁移 | 10000–50000 | 同时需要混入通用数据防遗忘 |
| 小模型能力蒸馏 | 50000 以上 | 覆盖面比单条质量更关键 |
另一个容易被忽略的是配比。如果你的微调数据全是业务样本,模型会逐渐"忘掉"通用能力,表现为闲聊能力下降、常识问答变差、甚至原有语言能力退化——这就是灾难性遗忘。我的做法是在训练集里混入 5% 到 20% 的通用指令数据,比例按任务偏移程度调,偏移越大混得越多。混入的那部分不需要精修,用公开的通用指令集抽样即可,作用相当于给模型一个"锚",防止它跑太远。
还有个实操细节:数据要在训练前打乱,但打乱之后建议把长度相近的样本聚在一起,配合按长度分桶的采样策略,能显著减少 padding 浪费,吞吐提升往往有 20% 以上。
3.4 没有标注团队时,数据怎么造
现实情况是大部分团队没有专门的标注人力。我常用的三条路径:
路径一,从业务日志里挖。客服对话、工单记录、审阅意见、系统日志里天然存在大量"输入—处理结果"对。把历史记录整理成指令格式,是最低成本的高质量数据来源。关键在于筛选,只保留那些当时被采纳、被确认、没有后续返工记录的处理结果。
路径二,强模型蒸馏。用能力更强的模型批量生成回答,再人工筛选。这里有个提高效率的技巧:不要让人逐条审,而是先让模型对同一输入生成 3 条候选,再让另一个模型做交叉打分排序,人工只看排名靠前和排名靠后的各一批,中间的丢弃。这样人工工作量能压到原来的三分之一。
路径三,规则加模板半自动生成。对于结构固定的任务(比如从合同里抽几个固定字段),可以用规则生成输入,用模板生成标准输出,再用模型改写输入的表达方式增加多样性。这条路径生成的数据质量上限受模板限制,但用于格式对齐类任务效果很好。
注意:蒸馏数据一定要做"去味"处理。不同模型有各自的口头禅,比如特别爱用某些连接词、特别爱在开头加一句总结。如果直接拿去训,你的模型会把这种口头禅学过来,输出风格变得很别扭。处理方法是在生成提示里明确禁止某些表达,或者在清洗阶段做一轮模式替换。
4. 实操:一次完整的 LoRA 微调流程
前面都是判断和准备,这一节进入真正的执行。我用一个虚构的工单结构化场景来走完全程:输入一段客户描述,输出包含问题类型、紧急程度、涉及产品模块、建议处理动作四个字段的结构化结果。这个任务足够典型,格式约束强、有领域术语、需要一定理解能力,正好是微调的主场。
4.1 环境与显存预算
先做显存预算,我的习惯是在纸上算完再动手。目标是 7B 量级模型、LoRA、序列长度 2048、单卡 24 GB。
- 冻结的主干权重,bf16 存储:7B × 2 字节 ≈ 14 GB
- LoRA 分支参数,约 0.5%:可忽略
- LoRA 的优化器状态,按 16 字节/参数算:3640 万 × 16 ≈ 0.6 GB
- 激活值,开启梯度检查点后约为 序列×batch×隐藏维度×2×层数 字节量级:2048 × 1 × 4096 × 2 × 32 ≈ 0.5 GB,加上临时缓冲按 2–3 GB 估
- 框架和碎片开销:留 2 GB 余量
合计约 19–20 GB,24 GB 卡跑 batch 1 有余量,可以尝试 batch 2 或者把序列长度提到 4096。这个账算清楚之后,你就能预判"调大 batch 会不会炸",而不用靠试。
软件环境上有几个选择需要说明理由:优先用 bf16 而不是 fp16,因为 bf16 的数值范围和 fp32 一致,不容易出现梯度溢出导致的 NaN,代价是精度略低,但对训练影响很小,现代显卡基本都支持。梯度检查点要开,它用约 30% 的额外计算时间换掉大半个激活显存,是单卡训练必开的选项。注意力实现尽量选优化的内核,长序列下能省下可观的显存和时间。
4.2 关键参数逐项配置
下面是一份我常用的配置骨架,字段命名参照主流参数高效微调库的通用叫法,具体实现里字段名可能略有差异:
# LoRA 结构配置 lora: r: 16 lora_alpha: 32 # 缩放系数 = alpha / r = 2 lora_dropout: 0.05 target_modules: # 挂载位置 - q_proj - k_proj - v_proj - o_proj - gate_proj - up_proj - down_proj bias: none # 不训练偏置项 # 训练超参 training: learning_rate: 2.0e-4 # LoRA 常用 1e-4 ~ 3e-4 num_train_epochs: 2 per_device_train_batch_size: 1 gradient_accumulation_steps: 16 # 等效 batch 16 lr_scheduler_type: cosine warmup_ratio: 0.03 weight_decay: 0.0 max_grad_norm: 1.0 bf16: true gradient_checkpointing: true max_seq_length: 2048 logging_steps: 10 save_steps: 200 packing: false # 短样本多时再开逐项说一下为什么这么设。r 取 16、alpha 取 32,缩放系数 2,是这类任务里比较稳的组合,任务有一定复杂度但不是范式重构,16 够用且不易过拟合。dropout 0.05是防过拟合的轻量手段,数据量上万条时可以降到 0.02 甚至关掉。target_modules 挂满注意力四个投影加前馈三个矩阵,只挂 q、v 是老做法,现在显存充裕的情况下挂满效果更稳,参数占比也只多零点几个百分点。
学习率 2e-4是个关键点。LoRA 的学习率要比全参微调大一个数量级左右,因为可训练参数少、更新路径短。全参微调用 1e-5 到 2e-5 是常规,LoRA 用这个量级会几乎不动,这也是"loss 死活不降"最常见的原因之一。epoch 取 2,指令微调通常 1 到 3 轮就够,超过 3 轮基本都在过拟合。梯度累积 16是为了在 batch 1 的显存约束下拿到 16 的等效 batch,让梯度更稳。
packing 先关,因为工单样本长度差异大,打包时如果没做好注意力隔离,不同样本之间会互相"看见",学出来的模型会莫名其妙地把上一个样本的内容带到下一个输出里。等流程跑通、确认效果后,再开打包提吞吐,并且务必验证隔离是否生效。
4.3 训练过程中的显存与吞吐调优
训练启动后先看三件事:loss 前 50 步的走势、显存峰值、每秒处理的 token 数。
loss 的正常形态是从 1.5 到 2.5 附近起步,前几十步快速下降,然后进入缓慢下降的平台。如果前 50 步几乎是平的,先查三处:学习率是不是设成了全参的量级、标签屏蔽是不是把所有位置都设成了 -100、数据拼接模板是不是和模型训练时用的格式差太远。最后这条最隐蔽,有些基座对特定的对话标记敏感,模板不对等于让模型在陌生格式上学习,效率极低。
显存峰值如果贴着上限,按这个顺序降:先减 batch(但保持等效 batch 不变,靠增加累积步数补回来),再降序列长度,再开更激进的检查点策略,最后才考虑上量化。反过来的顺序是错的,先上量化会牺牲精度,而那些只是显存紧一点的情况,减 batch 就够了。
吞吐低的时候,先看是不是 padding 浪费严重。样本长度从 50 到 2000 混在一起,不做分桶的话每条都要 pad 到 2048,有效计算量可能只有三成。按长度分桶采样加动态 padding,通常能提升 20% 到 40% 的吞吐。再往上就是开打包和换更快的注意力内核,这两个都属于工程优化,不影响训练结果。
训练过程中我还会盯一个指标:梯度范数。它被裁剪到 1.0 说明训练基本稳定;如果长期低于 0.1,说明学习率偏小或者数据太简单;如果频繁触顶,说明学习率偏大或者数据里有异常样本。这个指标比 loss 更能反映训练健康度。
4.4 权重合并与灰度验证
训练完成后有两个选择:保留 LoRA 分支做"基座 + 适配器"的两段式推理,或者把 LoRA 权重合并回主干得到一个完整模型。
保留分支的好处是灵活,一个基座可以挂多个适配器,按业务切换,存储成本也低(适配器通常几十到几百 MB)。坏处是推理时多一层计算,且部署链路要支持动态加载。
合并的好处是推理链路简单、速度略快,适合最终上线。合并的关键是必须用和训练时一致的计算精度做加法,常见做法是在 fp32 下做权重相加再转回 bf16 存储,否则精度损失会累积。合并完务必做一次对比验证:同一批输入,合并前后输出应该高度一致,如果差异明显,说明合并过程出了问题,常见原因是缩放系数没乘对或者适配器加载的层名不匹配。
灰度验证这一步很多人跳过,代价很大。我的做法是准备 50 条"刁钻样本"——格式边界、超长输入、空输入、混合语言、含干扰信息——在合并前后各跑一遍,逐条比对。这一步能抓出大部分部署环节的问题,比在线上一批批翻车再回滚划算得多。
5. 评估:怎么判断这次微调成功还是白干
训练 loss 降到了 0.6,看起来很漂亮,然后上线发现效果不如提示词方案。这事我遇到过不止一次。原因很简单:loss 衡量的是模型对训练分布的拟合程度,不是任务完成质量。评估体系必须在训练之前就搭好,而不是训完再想怎么看。
5.1 三套评估集,缺一不可
留出集从训练数据里切,比例 5% 到 10%,作用是看模型有没有过拟合。留出集的 loss 如果从某个点开始持续上升,同时训练 loss 还在降,就是典型的过拟合信号,该停或者该加数据了。
分布外测试集是另一批同任务但不同来源、不同表达习惯的样本,比如训练数据来自 A 渠道的工单,测试集用 B 渠道的。这套集合衡量的是泛化能力,也是最能反映真实上线表现的。我一般要求这套集合的得分不低于留出集得分的 85%,差距太大说明模型在死记硬背。
回归测试集是一批通用能力样本,闲聊、常识问答、指令跟随、拒答边界各若干条。这套集合的作用是监控灾难性遗忘,我见过微调之后业务指标涨了三个点、通用问答能力掉了三成的案例,如果不做回归测试根本发现不了。
三套集合的规模不用大,各 100 到 200 条就够,但必须人工确认过标准答案的正确性。用模型生成的答案当标准答案,评估就变成了自己跟自己比。
5.2 自动指标与人工盲评的组合
自动指标这块,结构化输出任务最好算:字段级完全匹配率、整体 JSON 解析成功率、关键字段的准确率和召回率。这三个数组合起来能说明大部分问题。其中解析成功率是最刚性的指标,它直接决定你的下游程序会不会崩。
开放式生成任务就麻烦些,BLEU、ROUGE 这类指标和人类判断的相关性很低,我基本不看。替代方案是要点覆盖率:人工给每条测试样本标注 3 到 5 个必须命中的要点,评估时用规则或模型判断要点是否被覆盖,算一个覆盖率。这个指标构造简单、解释性强,比一堆看不懂的相似度分数实用。
人工盲评是最后一道关。方法是从微调模型和基线模型的输出里各随机抽 50 条,打乱顺序、隐去来源,让评估人按 1 到 5 分打分。评分维度我一般拆成三项:准确性(内容对不对)、格式合规(结构对不对)、可读性(人读起来顺不顺)。三项分开打,比打一个总分有信息量得多——经常出现准确性提升但可读性下降的情况,那就得回头调数据里的表达多样性。
5.3 回归测试:别把原来的能力训没了
防遗忘这件事值得单独说。微调的本质是让权重向你的数据分布偏移,偏移必然带来原能力的损失,问题只是损失多少、损失的是不是你在意的部分。
我的操作是:训练前先跑一遍回归测试集记录基线分数,训练后同集再跑一遍。如果某个维度的分数下降超过 5 个百分点,就要处理。处理手段有优先级:先增加混入的通用数据比例(从 5% 提到 15% 试试);再降低学习率或减少训练轮数;最后才考虑调整 LoRA 的挂载层——把挂载位置从"全部层"缩到"后三分之二的层",对通用能力的破坏会小一些,因为浅层更多承载通用语言能力。
还有一个低成本技巧:在训练数据里保留一部分"什么都不用改"的样本,即输入和输出基本一致、模型原本就能答对的样本。这类样本相当于告诉模型"遇到这种情况保持原样",对抑制遗忘有奇效,占比 10% 左右即可。
6. 踩坑记录:常见异常与排查速查
这一节是我自己整理的排查手册,按症状分类。每次训练出问题,我基本都能在里面找到对应的条目,省下大量瞎试的时间。
6.1 loss 曲线异常的五种形态
| 症状 | 最可能的原因 | 处理动作 |
|---|---|---|
| loss 一直平在 2.0 以上不动 | 学习率用了全参量级;标签全被屏蔽;模板与基座不匹配 | 学习率提到 2e-4;检查标签中 -100 的分布;核对对话模板 |
| loss 快速掉到 0.1 以下 | 数据重复严重;样本过于简单;训练集与验证集有重叠 | 重新去重;检查数据难度分布;重切数据集 |
| loss 出现 NaN 或 inf | fp16 溢出;学习率过大;梯度爆炸 | 切 bf16;学习率减半;开启梯度裁剪 1.0 |
| 训练 loss 降但验证 loss 上升 | 过拟合 | 减 epoch;加 dropout;补数据;降 r |
| loss 周期性剧烈震荡 | 数据未打乱;batch 太小;有异常长样本 | 重新 shuffle;增大梯度累积;按长度分桶 |
第一行那个"loss 平着不动"是最高频的问题,我统计过自己遇到的问题,接近一半是这个。而其中又有大半不是学习率问题,而是标签屏蔽做错了——有的实现里模板拼接后没正确对齐标签位置,导致整个序列都被设成 -100,损失恒为 0 或者恒定值。排查方法很简单:打印一条样本的标签序列,看非 -100 的位置是不是恰好对应回答部分。
6.2 显存与速度问题的排查顺序
显存 OOM 的时候,按这个顺序排查,不要一上来就上量化:
- 确认序列长度是不是设成了训练数据的最长值而不是分位数,用 95 分位长度往往能省下三分之一显存
- 确认梯度检查点是否真的生效,有些实现需要在模型上额外调用一次启用方法,光配参数不生效
- 减小 micro batch,同步增加梯度累积步数保持等效 batch
- 检查是否有日志或评估逻辑在训练中额外驻留了计算图
- 最后才考虑 4 bit 量化
速度慢的排查顺序不一样:先看 GPU 利用率,如果长期低于 70%,瓶颈在数据加载,把数据预处理提前做好、用多进程加载;如果利用率接近 100% 但吞吐还是低,看 padding 比例,做长度分桶;再往下就是换更快的注意力实现和开打包。
提示:有个特别隐蔽的显存问题——评估函数在训练循环里被调用时,如果没有包在无梯度上下文里,会把整个评估过程的中间激活都存下来,显存直接翻倍。症状是平时不炸、一到评估步就炸。这个坑我踩过两次才记住。
6.3 效果类问题:复读、答非所问、风格漂移
输出复读。模型反复输出同一句话或同一个短语。原因通常是数据里这类表达出现频率过高,或者解码时温度设得太低。先查数据重复度,再适度提高采样温度、加一点重复惩罚。如果只在长输出时复读,多半是训练时超长样本被截断,模型没学到"怎么结束",需要在数据里补足完整的结尾样本。
答非所问。输入理解没问题但输出跑偏,八成是训练数据的输入输出对齐有问题。我遇到过一批数据,清洗脚本把输入和输出错位了一行,训出来的模型答得头头是道但完全不对题,loss 却降得很漂亮。这种问题只能靠人工抽检发现,所以前面强调抽检不能省。
风格漂移。微调后模型变得特别啰嗦,或者特别喜欢在开头加一句"好的,我来帮你分析"。这是把训练数据里的模板化表达学进去了。处理方法是在数据清洗阶段把这些套话统一删掉,让回答直接从实质内容开始。如果已经训完了,可以补一批"简洁回答"的样本做第二轮轻量微调,学习率调小、只训一轮。
格式在长输入下崩坏。短输入格式完美,输入一长就开始漏字段或者提前结束。这是训练数据里长样本覆盖不足导致的,补 200 到 500 条长输入样本重训通常能解决。顺便检查一下训练时的截断策略,如果长样本被硬截断而标签还在,模型会学到"输出到一半就停"。
特定字段总是抽错。比如四个字段里有一个准确率明显低。先看这个字段在训练数据里的取值分布,如果类别极不平衡(某个值占了 80%),模型会倾向于一直输出那个值。处理方法是补充少数类样本,或者调整采样权重。如果分布均衡还是错,看这个字段的判定是不是依赖输入里较远位置的信息,那可能是序列长度或注意力的问题。
最后分享一个我个人觉得最有用的习惯:每次微调都留一个"最小可复现实验"。挑 200 条数据、训 200 步、十分钟内跑完,专门用来验证数据格式、模板拼接、标签屏蔽这三件事对不对。正式训练之前先把这个小实验跑通,看一眼 loss 是否正常下降、输出是否还是人话,能挡掉八成以上的低级错误。我见过太多人直接上全量数据跑八个小时,第二天发现模板拼错了,只能重来。这套小实验的配置我会单独存一份,换任务、换基座、换框架的时候都先拿它过一遍,比任何文档都可靠。