第2014篇:微调模型的评估体系——防止过拟合的量化验证方法
2026/4/30大约 4 分钟
第2014篇:微调模型的评估体系——防止过拟合的量化验证方法
适读人群:正在进行LLM微调的工程师 | 阅读时长:约18分钟 | 核心价值:建立科学的评估体系,避免微调后模型"表面更好但实际变差"的陷阱
我犯过一个经典错误。
微调完一个模型,在测试集上跑了一遍,准确率从68%提升到了89%。很开心,直接上线了。
上线一周后发现:模型对训练集覆盖的问题确实更准了,但对一些之前没见过的新问法,回答质量反而变差了。更糟糕的是,它开始输出一种特定的啰嗦格式——那是训练数据里某个质量差的样本的格式,模型"学坏了"。
这就是微调评估不充分的后果。
评估的几个维度
好的微调评估需要检查:
1. 目标任务性能:在你专门优化的任务上是否更好了? 2. 通用能力保留:基础推理、常识等能力是否被破坏了? 3. 格式一致性:输出格式是否符合预期,是否稳定? 4. 边界行为:对异常输入、不熟悉的问法如何处理?
评估数据集的构成
不能只用与训练数据同分布的测试集:
# evaluation_sets.py
# 1. 核心任务测试集(与训练数据同分布)
core_test = load_dataset("data/test_core.jsonl") # 200条
# 2. 分布外测试集(换一种问法,测泛化能力)
ood_test = load_dataset("data/test_ood.jsonl") # 100条
# 3. 通用能力测试集(不相关的任务,测是否灾难性遗忘)
general_test = load_dataset("data/test_general.jsonl") # 100条
# 4. 边界案例测试集(异常输入、刁钻问题)
edge_test = load_dataset("data/test_edge.jsonl") # 50条
# 5. 对比测试:基础模型能答但微调模型可能更差的问题
regression_test = load_dataset("data/test_regression.jsonl") # 100条自动化评估管线
@Service
@Slf4j
@RequiredArgsConstructor
public class ModelEvaluationPipeline {
private final ChatClient baseModel;
private final ChatClient fineTunedModel;
private final ChatClient judgeModel; // 用于LLM-as-Judge评估
public FineTuningEvaluationReport evaluate(
List<EvaluationCase> coreTests,
List<EvaluationCase> oodTests,
List<EvaluationCase> generalTests) {
log.info("开始评估微调模型...");
// 并行评估各数据集
CompletableFuture<DatasetResult> coreFuture =
CompletableFuture.supplyAsync(() -> evaluateDataset("core", coreTests));
CompletableFuture<DatasetResult> oodFuture =
CompletableFuture.supplyAsync(() -> evaluateDataset("ood", oodTests));
CompletableFuture<DatasetResult> generalFuture =
CompletableFuture.supplyAsync(() -> evaluateDataset("general", generalTests));
DatasetResult coreResult = coreFuture.join();
DatasetResult oodResult = oodFuture.join();
DatasetResult generalResult = generalFuture.join();
// 生成综合报告
return buildReport(coreResult, oodResult, generalResult);
}
private DatasetResult evaluateDataset(String name, List<EvaluationCase> cases) {
List<CaseComparison> comparisons = cases.stream()
.map(testCase -> {
String baseAnswer = getAnswer(baseModel, testCase.getInput());
String ftAnswer = getAnswer(fineTunedModel, testCase.getInput());
// 计算相对改进
double baseScore = judgeScore(testCase.getInput(), testCase.getExpected(), baseAnswer);
double ftScore = judgeScore(testCase.getInput(), testCase.getExpected(), ftAnswer);
return CaseComparison.builder()
.input(testCase.getInput())
.expected(testCase.getExpected())
.baseAnswer(baseAnswer)
.ftAnswer(ftAnswer)
.baseScore(baseScore)
.ftScore(ftScore)
.improved(ftScore > baseScore + 0.1)
.regressed(ftScore < baseScore - 0.1)
.build();
})
.collect(Collectors.toList());
double baseAvg = comparisons.stream().mapToDouble(CaseComparison::getBaseScore).average().orElse(0);
double ftAvg = comparisons.stream().mapToDouble(CaseComparison::getFtScore).average().orElse(0);
long improved = comparisons.stream().filter(CaseComparison::isImproved).count();
long regressed = comparisons.stream().filter(CaseComparison::isRegressed).count();
return DatasetResult.builder()
.name(name)
.totalCases(cases.size())
.baseScore(baseAvg)
.fineTunedScore(ftAvg)
.improvementRate((ftAvg - baseAvg) / baseAvg)
.improvedCases((int) improved)
.regressedCases((int) regressed)
.comparisons(comparisons)
.build();
}
private double judgeScore(String input, String expected, String actual) {
String judgePrompt = """
评估AI回答的质量,与参考答案对比,打分0.0到1.0:
问题:%s
参考答案:%s
AI回答:%s
评分要求:
- 1.0:完全正确,达到或超过参考答案质量
- 0.8:基本正确,有小瑕疵
- 0.5:部分正确
- 0.2:基本错误
- 0.0:完全错误或无关
只返回小数分数:
""".formatted(input, expected, actual);
try {
String response = judgeModel.prompt().user(judgePrompt).call().content().trim();
return Double.parseDouble(response);
} catch (NumberFormatException e) {
return 0.5; // 解析失败,给中间分
}
}
private FineTuningEvaluationReport buildReport(
DatasetResult core, DatasetResult ood, DatasetResult general) {
// 判断是否通过评估
boolean passed = true;
List<String> warnings = new ArrayList<>();
List<String> failures = new ArrayList<>();
// 规则1:核心任务必须有提升
if (core.getImprovementRate() < 0.05) {
failures.add("核心任务提升不足5%,微调可能没有效果");
passed = false;
}
// 规则2:通用能力退化不超过10%
if (general.getImprovementRate() < -0.1) {
failures.add(String.format("通用能力下降了%.1f%%,存在灾难性遗忘风险",
-general.getImprovementRate() * 100));
passed = false;
}
// 规则3:OOD泛化不能太差
if (ood.getFineTunedScore() < core.getFineTunedScore() * 0.7) {
warnings.add("OOD测试集表现明显差于核心测试集,模型可能过拟合了训练分布");
}
// 规则4:回退案例不能太多
if (core.getRegressedCases() > core.getTotalCases() * 0.2) {
warnings.add(String.format("有%d个核心测试案例出现回退(%.1f%%),需要关注",
core.getRegressedCases(),
100.0 * core.getRegressedCases() / core.getTotalCases()));
}
return FineTuningEvaluationReport.builder()
.passed(passed)
.warnings(warnings)
.failures(failures)
.coreResult(core)
.oodResult(ood)
.generalResult(general)
.recommendation(passed ? "建议上线" : "不建议上线,请根据失败原因调整训练数据或参数")
.build();
}
}过拟合的常见表现与诊断
| 症状 | 原因 | 解决方法 |
|---|---|---|
| 测试集好但OOD差 | 过拟合训练分布 | 增加数据多样性 |
| 输出格式过于一致 | 训练数据格式太统一 | 加入多样化格式的样本 |
| 拒绝回答新问题 | 训练数据缺乏边界处理 | 添加"不确定时说不确定"的样本 |
| 通用能力下降 | 训练轮数过多 | 减少epochs,增加正则化 |
微调结束后运行这套评估,不通过就打回修改,直到真正确认提升了才上线。
