exp_005 到 exp_009:六轮微调的试错全记录
/ 10 min read
Table of Contents
这是我实习那个内容合规模型项目(项目总览)的实验流水账。它不是一条直线——六轮微调,没有一轮是一次就对的。把每一轮的真实数字和踩的坑都记下来,因为这些”一版版逼出来”的过程,比最后那个部署版本本身更说明问题。
基础设定:基座 Qwen2.5-14B-Instruct,LoRA(rank=64, alpha=128, target=all),覆盖四平台(小红书/抖音/视频号/公众号),硬指标溯源率 90%。推理是三阶段:全量扫描 → 逐条复核 → 按引用去重。
exp_005:一个”太完美”的 loss
接手后第一版从基座重训。单卡 4090,3 epoch,1287 步,22 分 45 秒。最终 train_loss = 0.0137。
这个数字低得反常。我没敢信,去查了配置——cutoff_len=1024。而公众号样本的 system prompt(塞了规则)能长到 9000 多 token,被从中间截断,助手输出落在 1024 之外、不参与损失计算。难 token 大量缺席,平均 loss 被算到 0.0137。这是个假象。
更要命的是评估:首轮评估一次性把全平台规则塞进一个 prompt,但模型训练时每条只见过 1-3 条规则的格式,从没见过”一次几十条”。它在一个完全陌生的输入分布上被测,合规判断 93-95% 看着好看,但 title 匹配率(也就是溯源率)只有 35-52%——溯源不行的信号第一次出现。
exp_006:把 loss 修真,代价是慢 7 倍
把 cutoff_len 提到 4096 重训。同样的数据、同样的模型,train_loss 变成 0.245——这才是所有 token 都参与计算后的真实值,和 0.0137 不可比。
代价很直接:4096 的序列长度让吞吐从 exp_005 的 15 samples/s 掉到 2.03 samples/s,慢约 7 倍,一轮训练从 22 分钟变成 2 小时 48 分。ZeRO-2 扛住了 4096,显存 37GB/49GB,没 OOM。
但训完发现:改 cutoff 对训练数据其实没多大意义。一查训练样本,99.9% 都在 1024 以内,最长才 1070——超长的是塞进 prompt 的规则,不是数据本身。exp_006 几乎白训。真正的问题不在训练,在评估格式。
认知转折:评估格式错了,不用重训
这是整个实验链最重要的一个判断:模型学的是”逐规则检查器”(给你这几条规则,判断违没违反),不是”一次扫全量”的扫描器。之前的评估和 webui 都用错了格式(一次塞几十条),所以那 93% 没意义。
改成按规则类别逐组推理、再合并结果(per-category),对齐训练分布——不用重训。这一改,溯源率才测得准,也才暴露出真问题:per-category 召回高(小红书/抖音 75%)但精确率塌到 5-8%,模型严重过度预测,某轮抖音 TP=9 / FP=166。
exp_007:换个结构,更糟
试了一条结构性重构:把任务改成”一次只判一条规则、输出是/否”配 RAG 检索,16056 条数据(6027 正 + 10029 负),843 步,loss 从 4.9 一路降到 0.085,3 小时 15 分,收敛很漂亮。
但它失败了。RAG 召回只有 5-35%——正确的规则根本没被检索进模型的视野,模型学得再好也没用。而且这个”单规则 + RAG”的格式和最终的三阶段推理对不上,学的东西用不上。漂亮的 loss 配上没用的格式,等于白训。
三阶段推理:发现”结构比模型更重要”
放下微调,先用 base 模型 + 三阶段提示工程(全量扫描 → 精准复核 → 去重)跑了一轮,得到一个核心洞见。base 模型 30 样本的真实数字:
| 平台 | 精确率 | 规则数 / 类别数 |
|---|---|---|
| 小红书 | 58.8% | 49 / 14 |
| 抖音 | 38.5% | 107 / 21 |
| 视频号 | 27.8% | 135 / 11 |
| 公众号 | 11.1% | 53 / 1 |
同一套方案,小红书 59% 而公众号 11%,差 5 倍——就因为公众号 53 条规则全挤在一个类别里,没法”按类别精准过滤”。规则的分类结构对效果的影响,比模型能力大得多。 而且去重这一步很关键,两阶段加到三阶段,抖音精确率从 20.6% 提到 38.5%。
exp_008:最终部署版
把训练数据改成和三阶段推理对齐的 batch 格式(给 20 条规则 + 文案 → 输出违规 JSON),6889 条,QLoRA 4-bit,430 步 1 epoch。merge 之后用 AWQ INT4 把 28GB 的模型压到约 10GB,跑在 24G 卡上,vLLM(--enforce-eager)+ FastAPI 部署。
修复了评估格式后第一次真实评估:整体 R=63.6% / P=50.3%,分平台小红书 78.9% / 抖音 64.2% / 视频号 52.8%(最低,135 条规则最复杂)/ 公众号 76.3%。这是 exp_008 的真实水平,也是最终上线的版本。
exp_009:一次把召回干到 46% 的灾难
为了冲 90%,上 Hard Negative——在 system prompt 里同时放正确规则 + 2-3 条相似但不正确的规则,逼模型学辨别。这次用自费的 48G 卡跑 BF16(去掉量化)、r64/a128、cutoff 3072 + packing、2 epoch、158 步、2 小时 4 分,loss 从 4.458 降到 0.355,收敛漂亮。
结果评估出来是灾难:平均 Recall 从 77.1% 暴跌到 46.3%,掉了 30.8 个点。 各平台全崩,视频号从 70.4% 掉到 37.9%。
根因抽 5 条训练样本人工看才挖到,而且很隐蔽:make_hard_neg.py 在去重时只比规则的短名(“第四条 违禁物品”)、没比完整标题(带平台前缀),结果不同前缀的”第 X 条”被当成同一个、去重去错了。后果是——有的样本里,正确答案本身被当成 Hard Negative 放了进去。小红书一条样本里,三条 GT 规则同时出现在”正确答案是这三条”和”这三条别选”两个位置。模型收到自相矛盾的信号,学成了”什么都别选”,召回自然暴跌。28.3% 的样本有这种标题碰撞。
方向其实没错(模型的病确实是过度预测,该让它更挑剔),但数据有毒,好心办了坏事。我准备了一个 5 分钟的回滚脚本,退回 exp_008。
这一串实验教给我的
- 改超参前先实测数据分布。exp_006 花了近 3 小时改 cutoff,结果数据根本没超 1024。脑补的瓶颈不算瓶颈。
- 漂亮的 loss 不等于有用的模型。exp_007 收敛到 0.085,但格式和推理对不上、RAG 召回崩,照样白训。
- 结构有时比模型更决定上限。公众号 11% vs 小红书 59%,差距来自规则分类结构,不是模型不行。
- 冲新高前先钉死可回退基线。exp_009 一把把 77% 干到 46%,幸亏有 5 分钟回滚脚本。激进实验必须有退路。
- 指标崩了,去抽训练样本人看。exp_009 的真因是”正确答案被当负样本”,这种 bug 在 loss 曲线上完全看不出来(loss 还很漂亮),只有把数据摊开一条条看才发现。
六轮下来,最终上线的是 exp_008。但真正长本事的不是那个能跑的版本,是中间这五次”训得漂亮却没用”和”一改就崩”——它们逼着我把”训练成功”和”真的有用”这两件事彻底分开看。