跑模型的时候,我经常被问到一个问题:"你这个准确率都90多了,是不是就能上线了?"每次听到这种话,我都得耐着性子解释半天:准确率高了不代表模型真的好,更不代表它适合你的业务场景。尤其是做知识检索、文本分类、视频动作识别这类任务的时候,光盯着准确率一个数字,大概率会被带进沟里。
这就要聊到知识检索和分类任务中最核心的四个评估指标:准确率(Accuracy)、精确率(Precision)、召回率(Recall)和F1分数(F1 Score)。这四个词你可能已经听过无数遍了,但说实话,我见过太多人把精确率和准确率搞混,把F1当成一个"万能分数"来用,却说不清楚它到底在平衡什么。
这篇文章我会把这四个指标从公式到直觉、从代码到实战全部掰开揉碎讲清楚,并且结合最近在UCF101视频动作分类任务上用PyTorch调模型的过程,分享一下用这些指标评估模型时踩过的坑和避坑经验,最后给出可直接复用的指标计算源码。
1. 四种指标:从公式到直觉
1.1 准确率:最直观但最危险的数字
准确率的数学定义非常简单:
accuracy = (TP + TN) / (TP + TN + FP + FN)
翻译成人话就是:所有样本中,预测正确的比例。
这个公式看起来人畜无害,但它藏着一个大坑:当类别严重不平衡的时候,准确率会变成一块"遮羞布"。我举个极端例子:假设你的知识检索系统里有10000篇文档,其中只有100篇是相关的(正类),9900篇是无关的(负类)。这时候你写一个"无脑返回全部不相关"的模型,它的准确率是99%。听起来很高对吧?但这个东西等于什么都没做。
这就是准确率的"直觉陷阱":它把所有类别的错误一视同仁。在分类任务里,如果你关心的是"从一堆东西里找出少数有用的那部分",准确率就不是一个好用的指标。
在UCF101这种视频动作分类任务里,情况也类似。101个动作类别,训练数据分布略微不均,如果某个类别的样本数量明显偏多,你什么都不做、全部预测成这个类别,整体准确率也能刷到不低的水平,但这显然不是我们想要的行为。
1.2 精确率与召回率:一对天生的矛盾
精确率(Precision)和召回率(Recall)合在一起,才是评估检索/分类质量的正确打开方式。
精确率衡量的是"你预测为正类的样本中,有多少是真的正类":
precision = TP / (TP + FP)
召回率衡量的是"真正的正类样本中,有多少被成功找出来了":
recall = TP / (TP + FN)
还是用文档检索的例子。你搜"知识图谱",系统返回了10篇文档,其中6篇是真正相关的,那么精确率就是0.6。整个文档库里有20篇跟知识图谱真正相关的文档,系统只召回了其中6篇,那么召回率就是0.3。
这两个指标天然互斥:把返回结果的数量从10篇扩大到100篇,召回率大概率会上升(因为捞回真相关的概率变大),但精确率会下降(因为混进来的噪声也变多了)。反过来,如果只返回最有把握的3篇,精确率可能冲到1.0,但召回率就惨不忍睹了。
这种"鱼与熊掌不可兼得"的关系,决定了你必须根据业务场景做取舍。搜索引擎倾向高召回率,因为宁可多展示一些相关结果,也不能漏掉用户想找的东西;垃圾邮件过滤器倾向高精确率,因为把一封正常邮件丢进垃圾箱的代价,远高于放进来一封不痛不痒的广告邮件。
1.3 F1分数:精准与全面的调和平均
F1分数把精确率和召回率压成一个数字:
F1 = 2 * (precision * recall) / (precision + recall)
注意它是调和平均数,不是算术平均数。为什么用调和平均而不是简单平均?因为调和平均对小的值更敏感。如果 precision = 1.0,recall = 0.0,算术平均是0.5,看起来还挺好,但这时模型要么把所有正类全漏掉了,F1分数算出来是0,这才是真实情况的正确反映。
F1的价值在于:当你需要用一个数字来对比不同模型时,它比准确率可靠得多。它不会允许某个指标一枝独秀而另一个指标崩掉的情况蒙混过关。
我之前在视频动作分类任务里做过一次对比实验:模型A的准确率是85.7%,模型B是84.9%,只看准确率会认为A更好。但把F1分数拉出来对比,模型A的F1反而低了2.3个百分点。原因就是A在头部几个高频动作类别上表现特别好,但在中低频类别上召回率严重偏低,整体F1被拖下去了。这就是为什么不看F1很容易被单点指标蒙骗。
2. 四个指标背后的深层逻辑
2.1 为什么准确率会骗人——类别不平衡问题
把准确率的问题单独拿出来说,是因为它太常见了,而且太容易被忽略了。
在分类任务里,类别不平衡意味着某一类样本远多于其他类别。知识检索场景中,通常相关的正类样本只占极小的比例;UCF101视频动作分类中,虽然官方数据集的类别分布相对均衡,但你做数据增强、采样、筛选之后,实际训练/验证集可能已经不再均衡了。
如果此时你只监控准确率,模型的优化方向就会跑偏。SGD在做梯度下降的时候,并没有"关心"准确率这个概念,它关心的是损失函数。但当类别极其不均衡时,模型只要学会"把所有样本预测成多数类",训练loss就会非常低,因为你样本里99%都是多数类。
这就是为什么在推理阶段准确率看起来很高,但实际上一点用都没有的原因。
我的建议是:任何分类项目起步阶段,先打印一份类别分布的直方图。如果发现最少的类别样本数不足最多类别的十分之一,准确率这个指标基本可以降级为"仅供参考"了,核心看F1或者加权平均召回率。
2.2 精确率与召回率的业务取舍逻辑
不同业务对错误代价的定义不同,这会直接影响你选择优化哪个指标。
举两个极端场景:
内容推荐系统里,给用户推了10篇文章,8篇他喜欢,2篇他不喜欢,这个体验其实没太大问题。但如果推荐文章系统只找到了3篇喜欢的、漏掉了70篇真正喜欢的内容,那用户很快就会觉得"这推荐没意思"。这种场景需要尽量提高召回率,因为"无法触达用户兴趣"的代价大于"偶尔推错一篇"的代价。
反过来看医疗辅助诊断,模型说"疑似病变"的样本里必须尽可能都是真病变,宁可漏掉一些轻微病例,也不能把大量健康样本标记为阳性——因为假阳性会引发不必要的复检和心理焦虑,消耗大量医疗资源。这种场景精确率优先。
在视频动作分类里也一样:如果想做"跌倒检测"报警系统,精确率低会导致频繁误报,用户很快就把报警功能关掉了;如果想做"关键行为复盘系统",召回率低会导致漏掉重要片段,那这套系统就失去了存在价值。
所以,不要一上来就问"F1是多少",先问自己:"假阳性带来的代价大,还是假阴性带来的代价大?"这个问题有了答案,你才知道哪个指标才是你真正要优化的目标。
2.3 阈值到底怎么选:PR曲线与ROC曲线的配合使用
分类模型的输出通常是概率或者置信度分数(比如PyTorch里softmax之后的输出),只有当分数超过某个阈值时,才被判定为正类。阈值的选择直接影响四个指标。
举个例子:0.5阈值时,precision = 0.7,recall = 0.6;把阈值提高到0.8,precision可能升到0.85,但recall会掉到0.35。所以"调模型"不只是调整网络结构,也包含在验证集上寻找最优阈值这个环节。
标准做法是绘制精确率-召回率曲线(PR Curve),横轴为召回率,纵轴为精确率。曲线上的每个点对应一个阈值。理想模型是右上角的矩形:召回率1.0,精确率1.0。实际模型是一条向右下倾斜的曲线,你需要在曲线上找到业务可接受的那个操作点。
ROC曲线也是一种常见工具,但注意:当类别极不均衡时(比如正类只有1%),ROC曲线可能会给人过度乐观的印象,因为它受负类数量影响很大。PR曲线对不均衡数据更敏感,也更直观。
我一般在验证集上同时打印这两条曲线,ROC附件参考,但最终以PR曲线为准。
3. 多分类与知识检索场景的指标扩展
3.1 多分类中的macro、micro与weighted
前面所有例子都在讲二分类,但知识检索和视频动作分类本质上都是多分类问题。多分类场景下,每个指标都有三种聚合方式,很多初学者在这块特别容易混乱。
macro(宏平均):对每个类别分别计算precision / recall / F1,然后取算术平均。它把每个类别当成平等的,不管这个类别的样本量有多少。如果某个罕见类别表现很差,macro-F1会被明显拉低。UCF101这类任务报告macro指标时,中低频类别的表现会成为主要瓶颈。
micro(微平均):把所有类别的TP、FP、FN分别加总,再统一计算precision / recall / F1。样本量大的类别会主导这个指标。当你的任务对样本量大的类别更敏感时,micro-F1更贴合实际表现。
weighted(加权平均):计算每个类别的指标后,按该类样本占比加权求平均。这是实际工作中用最多的方式,因为它在"关注每个类别的表现"和"尊重真实分布"之间取了折中。
我自己的习惯是同时报告macro-F1和weighted-F1。如果两个数字差距很小,说明模型在不同类别上的表现相对均衡;如果差距很大,说明模型偏向采样较多的类别,需要针对小类别做数据增强或重采样。
3.2 知识检索场景的特殊性:排序指标与评估指标的配合
严格来说,知识检索系统返回的是一串排序后的候选文档,而不是一个单纯的分类标签。这时候四个基础指标虽然能用,但还有更贴合场景的排序指标,比如P@K(前K个结果的精确率)、MAP(平均精确率均值)和nDCG(归一化折损累计增益)。
P@K 本质上就是"只看前K个返回结果"的精确率,它跟业务中"用户只看第一页"的行为高度吻合。MAP 是所有查询的"精确率-召回率曲线下面积"的平均值。nDCG 更进一步考虑了排序位置,排在前面的相关文档得分更高。
那这四个基础指标还有没有用?当然有。因为在知识检索系统里,你通常不会只用一个模型。召回阶段(粗排)用双塔模型,精排阶段用交叉编码器,每个阶段都有二分类子任务:"这个文档跟查询相关吗?"这时候precision / recall / F1就直接用来评估每个阶段的模型质量。
所以不要觉得"我做的是检索不是分类,所以分类指标跟我没关系"。实际上,检索系统内部的每一个中继节点都在做分类决策,分类指标是基础能力评估,排序指标是最终效果评估,二者缺一不可。
4. 手把手写评估指标源码:PyTorch实现
4.1 从零手写四个指标:10分钟搞定一个Evaluation类
很多人习惯直接调sklearn的classification_report,这当然没问题。但如果你想在PyTorch训练循环里实时监控指标,尤其是每个epoch在验证集上打一次,你会发现自己写一个小工具类会灵活得多。这里我给出一个简洁可靠的实现。
import torch class Metrics: def __init__(self, num_classes): self.num_classes = num_classes self.reset() def reset(self): self.tp = torch.zeros(self.num_classes) self.fp = torch.zeros(self.num_classes) self.fn = torch.zeros(self.num_classes) self.tn = torch.zeros(self.num_classes) def update(self, preds, labels): # preds: [B, C] 原始logits # labels: [B] 整数标签 pred_classes = preds.argmax(dim=1) for cls in range(self.num_classes): pred_mask = (pred_classes == cls) label_mask = (labels == cls) self.tp[cls] += (pred_mask & label_mask).sum().item() self.fp[cls] += (pred_mask & ~label_mask).sum().item() self.fn[cls] += (~pred_mask & label_mask).sum().item() self.tn[cls] += (~pred_mask & ~label_mask).sum().item() def compute(self): eps = 1e-6 accuracy = (self.tp.sum() + self.tn.sum()) / (self.tp.sum() + self.tn.sum() + self.fp.sum() + self.fn.sum() + eps) precision = self.tp / (self.tp + self.fp + eps) recall = self.tp / (self.tp + self.fn + eps) f1 = 2 * precision * recall / (precision + recall + eps) macro_precision = precision.mean().item() macro_recall = recall.mean().item() macro_f1 = f1.mean().item() weight = (self.tp + self.fn) / (self.tp + self.fn).sum() weighted_f1 = (f1 * weight).sum().item() return { "accuracy": accuracy.item(), "macro_precision": macro_precision, "macro_recall": macro_recall, "macro_f1": macro_f1, "weighted_f1": weighted_f1, }这段代码做了三件事:
update在验证循环里不断累积TP / FP / FN / TN计数;compute在epoch结束时计算四个指标;- 同时输出macro和weighted版本的F1。
代码里加了一个eps = 1e-6,防止分母为0的情况。这在数据处理不干净、某些类别的TP为0时尤其重要。我第一次写这个类的时候就因为没加eps,遇到某个类别的precision表达式里出现0/0,直接返回NaN,然后整个训练日志就变成了"NaN全家桶"。
4.2 用TorchMetrics减少重复造轮子
当然,如果你不想每次自己维护这些计数,直接用现成的库会更快。PyTorch生态里最常用的就是TorchMetrics这个库,接口设计得很干净。
import torchmetrics class Validator: def __init__(self, num_classes): self.num_classes = num_classes self.reset() def reset(self): self.accuracy = torchmetrics.Accuracy(task="multiclass", num_classes=self.num_classes) self.precision = torchmetrics.Precision(task="multiclass", num_classes=self.num_classes, average="macro") self.recall = torchmetrics.Recall(task="multiclass", num_classes=self.num_classes, average="macro") self.f1 = torchmetrics.F1Score(task="multiclass", num_classes=self.num_classes, average="macro") self.f1_weighted = torchmetrics.F1Score(task="multiclass", num_classes=self.num_classes, average="weighted") def update(self, preds, labels): self.accuracy(preds, labels) self.precision(preds, labels) self.recall(preds, labels) self.f1(preds, labels) self.f1_weighted(preds, labels) def compute(self): return { "accuracy": self.accuracy.compute().item(), "macro_precision": self.precision.compute().item(), "macro_recall": self.recall.compute().item(), "macro_f1": self.f1.compute().item(), "weighted_f1": self.f1_weighted.compute().item(), }TorchMetrics的好处是它内部实现了很好的分布式同步和多卡逻辑,在DistributedDataParallel训练模式下不需要额外处理指标同步问题。如果你只是在单卡上跑实验,自己手写的版本完全够用。
有一点我要提醒:TorchMetrics类的average参数直接决定了是macro、micro还是weighted。默认是micro,如果你在训练日志里发现某个指标不对劲,先检查一下自己有没有改这个参数。
5. UCF101视频动作分类实战:如何真正提升准确率
5.1 UCF101数据集与评估流程
UCF101是目前视频动作分类领域最常用的中等规模数据集之一,包含101个动作类别,总计约13000个视频片段,来自YouTube。类别覆盖了人体动作、人机交互、体育运动和演奏乐器等。
在UCF101上做评估,标准做法是使用官方提供的三种数据集划分(split1、split2、split3),分别训练并测试,最后报告三次结果的平均值。这样做的目的很明确:减少随机划分带来的偏差,让模型性能对比更加稳定。
视频动作分类模型的常规流程可以用这个流水线概括:
- 视频抽帧(常用25帧/s采样率)
- 空间采样(常见做法是随机裁剪到224×224,或者用多尺度裁剪做数据增强)
- 时间采样(用均匀采样抽8帧或16帧作为一段输入)
- 送入3D卷积网络(如I3D、SlowFast、X3D)或者视频Transformer(如VideoMAE、TimeSformer)
- 输出101维logits,用交叉熵做监督训练
5.2 提升准确率的几个关键改进点
分享几个我在UCF101上实测有效的改进手段,每一个都不是花哨技巧,而是扎实的提升点。
第一,关键帧采样比均匀采样效果好。均匀采样8帧容易漏掉动作变化最剧烈、信息量最大的部分。我的做法是先用一个轻量级模型对整段视频做帧级评分,选出信息量最高的8帧再送入主模型。这个方案在多个动作类别上都带来了大概2到4个百分点的准确率提升。代价是预处理阶段多了一次前向推理,但换来的是主模型更容易学到有效特征。
第二,多视角测试比单中心裁剪稳定。推理阶段不做随机裁剪,而是从视频中均匀抽3段,每段再做中心裁剪、顶部裁剪、底部裁剪,得到9个view,分别推理后取平均softmax分数。这个"多视角投票"策略几乎免费带来1到2个百分点的准确率提升,在视频分类里属于性价比极高的操作。
第三,预热学习率(warmup)很重要。视频模型通常很大,批量又受显存限制,冷启动阶段loss震荡非常厉害。我一般安排5个epoch的线性预热,从1e-5逐渐爬到设定的初始学习率,然后再用余弦退火(cosine annealing)降到接近0。这个操作基本能稳定提升1到3个百分点。
第四,用类别平衡采样器解决长尾。虽然UCF101总体均衡,但个别类别的样本量差异还是蛮大的。我发现直接用标准的RandomSampler训练时,低频类别上的recall会明显偏低。改成按类别均匀采样的WeightedRandomSampler之后,macro-F1提升明显,但准确率几乎不变。这里就能看出指标的差异:准确率几乎不动,但模型对稀有类别的鲁棒性变好了。
5.3 训练中的评估陷阱与应对
在UCF101上跑实验的时候,有一个非常容易踩的坑:验证集评估的时间维度不一致。
训练时的视频片段是短时间裁剪的(比如8帧),但验证集如果用完整长度的视频做评估,模型的预测行为会发生偏差。我遇到过一次:训练loss持续下降,训练准确率升到78%,但验证准确率卡在45%死活不动。排查了一天,最后发现是验证时的视频采样策略跟训练完全不一致导致的。
正确的做法是:训练阶段的时空增强策略和验证阶段的采样策略要保持"结构上的一致性"。也就是说,验证时仍然采用均匀抽帧,每段视频抽固定帧数,然后做中心裁剪。不要在验证时做跟训练时完全不同的采样方式。
另外还有一个很反直觉的现象:在验证集上准确率最高时的模型权重,并不一定是在部署时最好用的权重。我通常的做法是每个epoch保存一次checkpoint,然后额外存储"F1最高"和"loss最低"两个checkpoint,最后分别去测试集上验证,择优选用。经验上"F1最高"的权重在小类别上的表现通常更稳定,而"loss最低"的权重在整体分布上更平滑。
6. 常见问题排查与避坑指南
6.1 指标计算中的常见错误
我在实际项目里见过很多次指标计算错误,这里直接把最经典的几个坑列出来,大家做个对照检查。
坑1:多分类任务里把argmax做成softmax。计算预测类别时,torch.argmax(preds, dim=1)是对logits直接取最大值索引,不需要先softmax。因为softmax是单调函数,argmax的结果不变,但多一次运算反而可能因为精度问题引入诡异行为。这个错误本质上不改变结果,但会让代码变慢、变复杂。
坑2:混淆了sample-wise和class-wise的TP/FP/FN。在多分类里,每个类别都有自己的TP/FP/FN。有人会错误地认为"预测错的都是FP",忽略了FN也是同样重要的错误类型。看代码时,确保fn是按类别统计的,而不是在所有错误上一次性加总。
坑3:验证集和训练集分布不一致。评估指标只有在验证集能代表真实部署环境时才有意义。如果你做的是时序视频数据,直接用random_split切分训练/验证集,很容易把同一视频的相邻帧同时分到两边,导致验证集准确率虚高。UCF101官方的split已经考虑了这个问题,但如果你用了其他数据集,务必确认切分方式是否合理。
6.2 实战中的经验清单
最后整理一份我自己的经验清单,这些内容不写进论文、不出现在官方文档里,但对实际项目帮助很大。
在训练日志里同时记录loss、accuracy、macro-F1、weighted-F1四个数字。只记录loss和准确率,很多问题会被掩盖;只记录F1,可解释性又会变差。四个数字一起看,互相印证,才能快速定位问题。
理解并利用"threshold"这个隐藏超参。二分类任务中,阈值不一定要固定为0.5。在验证集上画一下PR曲线,找到那个"误报代价和漏报代价相等"的操作点。很多知识检索系统上线之后发现效果不如预期,不是模型的问题,而是阈值没调对。
每个epoch打印一次混淆矩阵(或至少保存一次)。光看指标数字你只能知道"模型变差了",但不知道"哪个类别被分到哪个类别去了"。混淆矩阵能直接告诉你模型在哪些类别上存在混淆,比如UCF101里"打高尔夫"和"挥棒球"这类动作高度相似的类别,会经常互相误判。有了混淆矩阵,你才能针对性地做数据增强或者后处理纠错。
数据增强的比例不是越高越好,要监控验证集指标的拐点。搞过视频分类的人都知道,视频数据的增强自由度比图像大很多,但滥用数据增强会引入过多的噪声,导致验证集指标开始下降。科学的做法是逐步增强并持续监控验证集F1,找到"提升-过拟合"的拐点。
永远在测试集上做最终评估,验证集只做调参。如果你反复在验证集上做阈值搜索和早停,验证集本身就变成了"训练集的一部分",指标会虚高,等你上到真实数据上时会发现大打折扣。UCF101提供了三个split,我一般在split1上调参,在split2和split3上验证最终效果。
我在实际使用中发现,评估指标这件事看起来简单,但每一层都有坑。从公式到工程实现,如果不把每个环节背后的逻辑吃透,很容易被一个漂亮的数字误导。特别是知识检索和视频动作分类这种任务,数据分布天然复杂,指标的选择直接影响你对模型优劣的判断。多花一点时间把precision、recall、F1这些基础概念和它们的聚合方式弄明白,比盲目刷一千轮训练更值得。项目上线之前,我建议你问自己一句:我盯着的这个指标,真的能反映业务需要的结果吗?