外观
评估实践
一句话定义:评估是把"模型表现"转成"业务可决策证据"的过程——好的评估不仅回答"模型多少分",还回答"这个分数可不可信、错在哪里、值不值得上线"。
离线测试集上的一个准确率,只是评估的起点。本文从指标选型开始,一路走到线上评估,覆盖评估设计、统计严谨性与失败分析。评估指标的理论定义见深度学习评估与实验,实验纪律见DL 设计原则。
一、指标选型:先问业务要什么
指标必须由业务代价决定,而不是默认套准确率。三类典型场景:
| 场景 | 常见指标 | 为什么 |
|---|---|---|
| 分类(类别不平衡) | precision / recall / F1 / PR-AUC | 准确率会被多数类淹没;漏报/误报的代价通常不对称 |
| 分类(多类别) | macro-F1、per-class 报告、混淆矩阵 | 看清楚每个类,而不是一个平均值 |
| 回归 | MAE / RMSE / Huber 损失 | RMSE 对大误差敏感(业务想要?),MAE 稳健 |
| 排序/推荐 | NDCG@k、HitRate@k、MAP | 业务关心"前 k 个是否命中"(见深度学习推荐系统) |
| 生成 | BLEU/ROUGE + 人工评估 | 自动指标与人类判断相关性弱,需双轨 |
| 二分类 + 可调阈值 | ROC-AUC、PR-AUC | 选阈值是业务决策(灵敏度 vs 特异度),不是模型决策 |
一个反直觉的坑
类别不平衡时别用准确率当主指标。99% 是负类时,全猜负类就有 99% 准确率,但业务价值为零。医疗筛查、欺诈检测、搜索排序几乎都是这种场景——此时 precision/recall 曲线和业务成本函数才是有意义的标尺。
二、置信区间与多次运行
深度学习有随机性(初始化、数据顺序、增强采样),单次运行的成绩是随机变量,不是定论。
1. 多次运行的标准做法
固定一个超参配置,用不同随机种子跑 N 次(N≥5),报告均值 ± 标准差:
python
import numpy as np
from scipy import stats
def run_experiment(seed):
set_seed(seed)
model = train() # 完整训练
return evaluate(model) # 返回测试集指标
accs = [run_experiment(seed) for seed in range(5)]
mean, std = np.mean(accs), np.std(accs, ddof=1)
# 95% 置信区间(t 分布,小样本更准确)
ci = stats.t.interval(0.95, df=len(accs) - 1, loc=mean, scale=std / np.sqrt(len(accs)))
print(f"acc = {mean:.4f} ± {std:.4f} (95% CI: {ci[0]:.4f} ~ {ci[1]:.4f})")2. 比较两个模型时,误差小于差异没意义
模型 A 准确率 90.0%、B 90.1%,如果各自标准差是 ±0.3%,这 0.1 个点的差异在噪声之内,不能宣称 B 更好。正确的做法是:
- 配对检验:用同一组测试样本、同样的种子对两个模型做配对比较(如 McNemar 检验用于分类错误计数)。
- 报告误差:任何关键比较都带上置信区间,让读者自己判断差异是否在噪声之外。
三、评估集构建纪律:防泄漏
评估结果只有两种:可信的和自欺的。泄漏是自欺的最常见来源(机理展开见常见陷阱与反模式):
| 泄漏类型 | 例子 | 后果 |
|---|---|---|
| 数据泄漏 | 归一化均值/标准差用全量数据(含测试集)算 | 测试指标虚高 |
| 时间泄漏 | 用未来的新闻预测昨天的价格 | 线上崩盘 |
| 身份泄漏 | 同一用户的不同照片同时出现在训练/测试 | 指标不可信 |
| 增强泄漏 | 增强参数的随机种子在训练/测试间共用 | 不可复现 |
| 调参泄漏 | 反复用测试集选模型 | 测试集变成验证集 |
构建纪律:
- 划分顺序:拿到数据第一步就划分 train / val / test(并固定),之后任何操作只碰 train 与 val。
- 时序数据:按时间切分,禁止打乱;考虑"滞后验证"(用历史窗口预测未来窗口,见数据与数据工程)。
- 同源样本分组:同一实体(用户/患者/会话)的所有样本必须进同一分区,防止身份泄漏。
- 测试集只碰一次:调参、选模型、早停全部在验证集上进行;测试集只在最终验收时用一次。
四、失败分析与错误集复盘
指标是"多少分",失败分析回答"为什么扣分"。步骤:
python
# 收集所有错误样本的元信息(预测、真值、置信度、样本来源)
errors = []
model.eval()
with torch.no_grad():
for x, y, meta in test_loader: # 假设 dataset 返回 (x, y, meta)
logits = model(x)
probs = torch.softmax(logits, dim=-1)
pred = probs.argmax(-1)
for i in range(len(y)):
if pred[i] != y[i]:
errors.append({
"true": y[i].item(), "pred": pred[i].item(),
"conf": probs[i].max().item(), "meta": meta[i],
})
# 分析模式:
# 1. 按类别分组:哪些类互相混淆?(混淆矩阵)
# 2. 按置信度分组:高置信错误 = 模型"自信地错",最危险
# 3. 按样本来源分组:错误集中在某个数据批次/时段/群体?错误集复盘的标准动作:
- 画混淆矩阵看系统性混淆模式(如"4 与 9""猫与狗")。
- 分桶看错误率:按置信度、按时长、按人群切分,找出模型盲区。
- 人工读 50 个错误样本:标注是否"错得合理"(人类也分不清)还是"错得离谱"(模型/数据问题)。
- 错误 → 行动:修数据(标注错误)、加数据(盲区类别)、改模型(误把背景当地标,见可解释性与公平性)。
五、A/B 测试与线上评估
离线指标与线上表现之间常有鸿沟(分布漂移、延迟、交互影响)。上线前的金标准是 A/B 测试:
| 阶段 | 问题 | 方法 |
|---|---|---|
| 离线 | 模型候选好不好 | 测试集 + 置信区间 |
| 影子模式 | 新模型线上表现如何 | 新模型与现网并行跑,只记录不决策 |
| 小流量 A/B | 业务指标是否提升 | 分流 5%–10%,跑 1–2 周 |
| 全量 | 长期效果 | 监控漂移与回退开关 |
A/B 测试要点:
- 提前定好成功指标与最小效果(例如转化率提升 0.5% 且置信区间不含 0),避免"看数据讲故事"。
- 注意辛普森悖论:总体没提升但每个分群都提升(或反之)——务必分层分析(按新老用户、地区等)。
- 关注长尾:平均指标好但 p99 变差的模型不能上线。
- 准备回退开关:新模型出问题要能一键回滚。
线上持续评估涉及监控、漂移检测与模型更新节奏,属于MLOps 与模型部署的范围。
六、生成模型的人工评估
生成模型(文本、图像、语音)的自动指标(BLEU、FID、Inception Score)与人类观感相关性有限,必须补充结构化人工评估(基础见生成式模型与扩散模型与生成式 AI):
- 维度化打分:不评"好不好",而是分维度打分(相关性、流畅度、事实性、有害性各 1–5 分),标注评分准则,多人打分并报告一致性(如 Cohen's kappa)。
- 配对偏好:让评估者二选一(模型 A vs B 的输出),比绝对分更稳定,也最接近 A/B 的形态。
- 错误分类法:把生成错误归类(幻觉、重复、指令偏离),统计占比——这比单一分数更有行动价值。
- 规模与成本:人工评估贵且慢,合理抽样(如每配置 200–500 条)+ 用 LLM 做第一轮粗筛(见大语言模型(LLM))。
七、案例:图像分类的完整评估流程
把以上全部落到一段可运行代码里:
python
import numpy as np
import torch, torch.nn as nn
from torch.utils.data import DataLoader
from sklearn.metrics import (confusion_matrix, classification_report,
precision_recall_fscore_support)
import matplotlib.pyplot as plt
def full_evaluation(model, loader, device):
"""返回 (probs, preds, labels),并打印完整评估报告"""
model.eval()
probs_list, preds_list, labels_list = [], [], []
with torch.no_grad():
for x, y in loader:
logits = model(x.to(device))
probs = torch.softmax(logits, dim=-1)
probs_list.append(probs.cpu().numpy())
preds_list.append(probs.argmax(-1).cpu().numpy())
labels_list.append(y.numpy())
probs = np.concatenate(probs_list)
preds = np.concatenate(preds_list)
labels = np.concatenate(labels_list)
return probs, preds, labels
def evaluate_with_report(model, test_loader, classes, device, n_runs=5):
"""多次运行 + 置信区间 + 逐类指标 + 混淆矩阵 + 错误分析"""
all_run_accs = []
for seed in range(n_runs):
torch.manual_seed(seed)
# 注意:同一种子重跑仅评估阶段的随机性;训练阶段随机性已在实验脚本中处理
probs, preds, labels = full_evaluation(model, test_loader, device)
all_run_accs.append((preds == labels).mean())
if seed == 0:
print("=== per-class report ===")
print(classification_report(labels, preds, target_names=classes, digits=4))
cm = confusion_matrix(labels, preds)
plt.imshow(cm, cmap="Blues")
plt.colorbar()
plt.title("Confusion matrix")
plt.savefig("confusion_matrix.png")
mean_acc = np.mean(all_run_accs)
std_acc = np.std(all_run_accs, ddof=1)
print(f"\naccuracy over {n_runs} runs: {mean_acc:.4f} ± {std_acc:.4f}")
# 错误分析:高置信度的错误样本("自信地错")
wrong = preds != labels
conf_wrong = probs[wrong].max(axis=-1)
top_idx = np.argsort(conf_wrong)[-10:] # 置信度最高的 10 个错误
print("top confident mistakes (index, true, pred, conf):")
for i in top_idx:
print(f" {i}: true={labels[i]}, pred={preds[i]}, conf={conf_wrong[i]:.3f}")
return mean_acc, std_acc, cm使用要点:
- 评估与训练用同一个
test_loader定义(同样的变换、同样的数据),否则评估的不是训练时的那个模型。 - 多次运行取均值 ± 标准差,让"±0.3%"这类误差可见。
- per-class 报告直接暴露不平衡类别的短板。
- 高置信错误清单是错误分析的种子,人工看这 10 个样本往往能发现数据问题或模型盲区。
这套流程的产物(分数、置信区间、混淆矩阵、错误样本)就是模型"是否值得上线"的证据包,也是作品集项目中"结果"一节的素材。
延伸阅读
- 深度学习评估与实验——指标的理论定义与统计基础
- 常见陷阱与反模式——评估泄漏的完整症状清单
- 从零构建一个深度学习项目——评估的载体项目
- MLOps 与模型部署——线上评估与监控
- 可解释性与公平性——错误分析的下游
- 数据集与工具档案——评估集构建的数据源
参考资料
- Dietterich. Approximate Statistical Tests for Comparing Supervised Classification Learning Algorithms (Neural Computation 1998)——配对检验与分类器比较
- Kohavi et al. Online Controlled Experiments at Large Scale (KDD 2013)——A/B 测试工程实践
- scikit-learn. Model evaluation: quantifying the quality of predictions——指标 API 权威参考