ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

模型评估指标详解:准确率、精确率、召回率与F1的实战应用

模型评估指标详解:准确率、精确率、召回率与F1的实战应用 跑模型的时候我经常被问到一个问题你这个准确率都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.0recall 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.7recall 0.6把阈值提高到0.8precision可能升到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 知识检索场景的特殊性排序指标与评估指标的配合严格来说知识检索系统返回的是一串排序后的候选文档而不是一个单纯的分类标签。这时候四个基础指标虽然能用但还有更贴合场景的排序指标比如PK前K个结果的精确率、MAP平均精确率均值和nDCG归一化折损累计增益。PK 本质上就是只看前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(dim1) 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(taskmulticlass, num_classesself.num_classes) self.precision torchmetrics.Precision(taskmulticlass, num_classesself.num_classes, averagemacro) self.recall torchmetrics.Recall(taskmulticlass, num_classesself.num_classes, averagemacro) self.f1 torchmetrics.F1Score(taskmulticlass, num_classesself.num_classes, averagemacro) self.f1_weighted torchmetrics.F1Score(taskmulticlass, num_classesself.num_classes, averageweighted) 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, dim1)是对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这些基础概念和它们的聚合方式弄明白比盲目刷一千轮训练更值得。项目上线之前我建议你问自己一句我盯着的这个指标真的能反映业务需要的结果吗
返回列表