Skip to content

评估实践

本页速览 准确率不是评估的全部。本文讲透评估设计:按业务场景选指标、置信区间与多次运行、A/B 测试与线上评估、失败分析与错误集复盘、评估集构建的防泄漏纪律、生成模型的人工评估,并给出一份图像分类完整评估流程的可运行代码。

评估实践

一句话定义:评估是把"模型表现"转成"业务可决策证据"的过程——好的评估不仅回答"模型多少分",还回答"这个分数可不可信、错在哪里、值不值得上线"

离线测试集上的一个准确率,只是评估的起点。本文从指标选型开始,一路走到线上评估,覆盖评估设计、统计严谨性与失败分析。评估指标的理论定义见深度学习评估与实验,实验纪律见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 检验用于分类错误计数)。
  • 报告误差:任何关键比较都带上置信区间,让读者自己判断差异是否在噪声之外。

三、评估集构建纪律:防泄漏

评估结果只有两种:可信的和自欺的。泄漏是自欺的最常见来源(机理展开见常见陷阱与反模式):

泄漏类型例子后果
数据泄漏归一化均值/标准差用全量数据(含测试集)算测试指标虚高
时间泄漏用未来的新闻预测昨天的价格线上崩盘
身份泄漏同一用户的不同照片同时出现在训练/测试指标不可信
增强泄漏增强参数的随机种子在训练/测试间共用不可复现
调参泄漏反复用测试集选模型测试集变成验证集

构建纪律

  1. 划分顺序:拿到数据第一步就划分 train / val / test(并固定),之后任何操作只碰 train 与 val。
  2. 时序数据:按时间切分,禁止打乱;考虑"滞后验证"(用历史窗口预测未来窗口,见数据与数据工程)。
  3. 同源样本分组:同一实体(用户/患者/会话)的所有样本必须进同一分区,防止身份泄漏。
  4. 测试集只碰一次:调参、选模型、早停全部在验证集上进行;测试集只在最终验收时用一次。

四、失败分析与错误集复盘

指标是"多少分",失败分析回答"为什么扣分"。步骤:

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 个样本往往能发现数据问题或模型盲区。

这套流程的产物(分数、置信区间、混淆矩阵、错误样本)就是模型"是否值得上线"的证据包,也是作品集项目中"结果"一节的素材。

延伸阅读

参考资料