You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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()

关键改动说明

  1. 移除f1_avg = AverageMeter(name='f1'),不再手动平均批次F1
  2. 用f1_score.update(out.cpu(), target.cpu())替代直接计算批次F1,让TorchMetrics内部累积TP/FP/FN统计量
  3. 调用f1_score.compute()获取当前累积的F1值,用于进度条展示(该值是基于已处理所有样本的真实F1)
  4. 最后再次调用compute()得到全局F1,调用reset()重置指标以备后续使用

额外说明

  • 对于损失这类可加性指标,用AverageMeter按样本量加权平均是正确的;但F1、精确率、召回率这类聚合类指标,必须基于全局统计量计算,不能直接平均批次结果。
  • TorchMetrics的所有分类指标都支持update/compute/reset的工作流,这是官方推荐的正确用法。

内容的提问来源于stack exchange,提问作者Omid Erfanmanesh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 05:15:00