使用AverageMeter与TorchMetrics计算F1Score结果异常问题
问题:批次平均F1与全局F1结果差异巨大
在PyTorch项目中使用AverageMeter累加TorchMetrics的BinaryF1Score批次结果时,发现进度条显示的批次平均F1为0.553,而用全量数据计算的F1却高达0.9811,二者差异极大。
核心代码与运行结果
验证逻辑代码
from time import time import torch import tqdm from utils import TBLoggerBuilder, setup_logger import os import numpy as np from models.metrics import get_metric from utils.average_meter import AverageMeter from logging import Logger from models.utils import save_model_check_points, save_model, best_model, early_stopping_model, init from torchmetrics.classification import BinaryConfusionMatrix from torchmetrics.classification import BinaryStatScores import pandas as pd import torchmetrics def evaluation(conf, model, criterion, val_loader, device, inference, debug ): print('\nValidating...') confmat = BinaryConfusionMatrix() stats = BinaryStatScores() model.eval() # 非必须,除非模型有特定层(如Dropout) y_pred = [] y_true = [] f1_score = torchmetrics.classification.BinaryF1Score() loss_avg = AverageMeter(name='loss') f1_avg = AverageMeter(name='f1') with torch.no_grad(): with tqdm.tqdm(val_loader, unit="batch") as tepoch: for image, target in tepoch: image = image.to(device) target = target.to(device) out = model(image) target = target.unsqueeze(1) target = target.float() loss = criterion(out, target) loss_avg.update(loss.item(), image.size(0)) # 错误:计算当前批次F1后用AverageMeter平均 f1 = f1_score(out.cpu(), target.cpu()) f1_avg.update(f1.item()) y_pred.extend(out) y_true.extend(target) tepoch.set_postfix(loss=loss_avg.avg, f_score=f1_avg.avg) if debug: break y_pred = torch.tensor(y_pred) y_true = torch.tensor(y_true) print(f'\n混淆矩阵: \n{confmat(y_pred, y_true).cpu().numpy()}') stats_scores = stats(y_pred, y_true) print(f'\ntp: {stats_scores[0]}, fp: {stats_scores[1]},' f' tn: {stats_scores[2]}, fn: {stats_scores[3]},' f' sup: {stats_scores[4]}') print(f1_score(y_pred,y_true),f1_avg.avg) return loss_avg.avg, f1_avg.avg # train_one_epoch与fit代码省略...
AverageMeter实现
class AverageMeter(object): """计算并存储当前值和平均值""" def __init__(self, name, fmt=':f'): self.name = name self.fmt = fmt self.val = 0 self.avg = 0 self.sum = 0 self.count = 0 def reset(self): self.val = 0 self.avg = 0 self.sum = 0 self.count = 0 def update(self, val, n=1): self.val = val self.sum += val * n self.count += n self.avg = self.sum / self.count def __str__(self): fmtstr = '{name} {val' + self.fmt + '} ({avg' + self.fmt + '})' return fmtstr.format(**self.__dict__)
运行结果
Validating... 100%|██████████| 65/65 [01:07<00:00, 1.04s/batch, f_score=0.553, loss=0.0522] 混淆矩阵: [[520 5] [ 14 493]] tp: 493, fp: 5, tn: 520, fn: 14, sup: 507 tensor(0.9811) 0.5534822574028602
问题原因
F1分数是基于精确率(Precision)和召回率(Recall)的调和平均数,属于聚合类指标,其计算依赖全局的真阳性(TP)、假阳性(FP)、假阴性(FN)统计量。直接对每个批次的F1分数取平均,完全忽略了不同批次的样本量、类别分布差异,最终结果和全局F1没有任何统计学意义上的关联。
举个极端例子:某批次只有1个负样本,模型预测错误,该批次F1为0;其他批次F1都是1。批次平均F1会被拉低,但全局F1几乎是1——这就是你遇到的情况。
另外,你当前调用f1_score(out.cpu(), target.cpu())时,每次都会重新计算当前批次的F1,没有利用TorchMetrics内置的累积统计功能,属于用法错误。
解决方案
正确使用TorchMetrics的累积特性
TorchMetrics的指标对象本身支持逐步累积批次数据,最后统一计算全局指标,同时也能在过程中获取当前累积的指标值用于进度条展示。修改后的验证代码如下:
def evaluation(conf, model, criterion, val_loader, device, inference, debug ): print('\nValidating...') confmat = BinaryConfusionMatrix() stats = BinaryStatScores() model.eval() y_pred = [] y_true = [] # 初始化F1指标 f1_score = torchmetrics.classification.BinaryF1Score() loss_avg = AverageMeter(name='loss') with torch.no_grad(): with tqdm.tqdm(val_loader, unit="batch") as tepoch: for image, target in tepoch: image = image.to(device) target = target.to(device) out = model(image) target = target.unsqueeze(1) target = target.float() loss = criterion(out, target) loss_avg.update(loss.item(), image.size(0)) # 关键:用update累积当前批次的预测和标签,而非直接计算批次F1 f1_score.update(out.cpu(), target.cpu()) # 获取当前累积的F1值用于进度条展示 current_f1 = f1_score.compute() y_pred.extend(out) y_true.extend(target) tepoch.set_postfix(loss=loss_avg.avg, f_score=current_f1.item()) if debug: break y_pred = torch.tensor(y_pred) y_true = torch.tensor(y_true) print(f'\n混淆矩阵: \n{confmat(y_pred, y_true).cpu().numpy()}') stats_scores = stats(y_pred, y_true) print(f'\ntp: {stats_scores[0]}, fp: {stats_scores[1]},' f' tn: {stats_scores[2]}, fn: {stats_scores[3]},' f' sup: {stats_scores[4]}') # 计算全局F1 global_f1 = f1_score.compute() print(global_f1, loss_avg.avg) # 重置指标,避免后续复用污染 f1_score.reset() return loss_avg.avg, global_f1.item()
关键改动说明
- 移除
f1_avg = AverageMeter(name='f1'),不再手动平均批次F1 - 用
f1_score.update(out.cpu(), target.cpu())替代直接计算批次F1,让TorchMetrics内部累积TP/FP/FN统计量 - 调用
f1_score.compute()获取当前累积的F1值,用于进度条展示(该值是基于已处理所有样本的真实F1) - 最后再次调用
compute()得到全局F1,调用reset()重置指标以备后续使用
额外说明
- 对于损失这类可加性指标,用
AverageMeter按样本量加权平均是正确的;但F1、精确率、召回率这类聚合类指标,必须基于全局统计量计算,不能直接平均批次结果。 - TorchMetrics的所有分类指标都支持
update/compute/reset的工作流,这是官方推荐的正确用法。
内容的提问来源于stack exchange,提问作者Omid Erfanmanesh
相关产品推荐
相关产品推荐

