ARTICLE DETAIL

资讯详情

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

YOLOv11模型参数打印不全?GFLOPs显示为0的排查与修复

YOLOv11模型参数打印不全?GFLOPs显示为0的排查与修复 1. 先搞清楚现象不是模型没算是打印逻辑偷懒了很多人在跑YOLOv11训练或者验证的时候都会瞄一眼终端输出的那一大串模型参数。按理说模型有多少层、每层什么结构、参数量多大、FLOPs多少应该一目了然。但实际用ultralytics框架跑的时候经常会遇到一个怪象模型结构打印出来了参数总量也出来了唯独GFLOPs那一栏要么是0要么干脆缺失甚至整个模型的层数都比预期少一截。我第一次遇到这个问题是在换用YOLOv11s做小目标检测实验的时候。当时我改了backbone里的几层结构重新跑验证集发现终端里打印的模型参数表少了好几层而且GFLOPs显示为0。最诡异的是模型照样能正常训练、正常推理精度也没崩说明网络结构在内存里是完整的但打印出来给人看的“摘要”却不完整。这个现象的核心原因其实不在模型本身而在ultralytics的模型摘要打印逻辑上。它默认通过model.info()来生成那张表而info()的实现里对GFLOPs的计算依赖一个额外的thop库同时对输入尺寸、模型是否处于某种特殊模式都有要求。一旦这些条件不满足GFLOPs就输出为0而某些自定义层因为没有注册到统计逻辑里会被直接跳过不显示。换句话说你的模型是完整的只是“自我介绍”的时候漏说了几段话。这篇文章就围绕这个问题把原因、排查思路、修复办法都捋一遍顺便聊清楚GFLOPs到底怎么算、算出来能干什么用以及怎么让YOLOv11在训练和验证时都能打印出完整的参数表。2. 参数表为什么会“缺斤少两”三个最可能的元凶2.1 自定义层没有参与统计这是最常见的情况尤其是你改了backbone或者neck结构之后。YOLOv11的模型定义在ultralytics/nn/modules下面每个层基本都有对应的类。如果你在yaml配置里引用了一个自己写的模块或者改动了某个内置模块的结构model.info()在遍历模型时依赖的是PyTorch的named_modules()和named_parameters()机制。这个机制本身不会漏层真正会漏的是thop库在计算FLOPs时的钩子机制。thop通过注册forward hook来统计每个模块的浮点运算量但它只统计它认识的、有明确数学运算的层。像nn.SiLU、nn.BatchNorm2d这类层它知道怎么处理但如果你自定义了一个包含多个子操作的复合模块thop只能识别到最外层的模块名里面的子模块如果没有显式的运算记录就可能被忽略导致FLOPs统计偏低甚至为0。另一个容易被忽略的点是model.info()里有一个verbose参数默认是False。在False模式下它只打印汇总信息也就是每行的层名、输出shape、参数量、FLOPs。如果你自定义的层没有实现__str__或者没有注册到合适的容器里打印时可能直接把这一层跳过或者合并到上一层的输出里。2.2 输入尺寸没对上GFLOPs的计算和输入尺寸强相关。同一套网络输入640x640和输入1280x1280FLOPs能差将近4倍。model.info()默认使用的输入尺寸是imgsz参数如果你在调用info()时没有传这个参数它会用模型配置文件里的默认值。但是这里有个坑非常隐蔽YOLOv11的检测头是动态的它的输出通道取决于数据集的类别数。你用COCO预训练权重跑自己的数据集时ultralytics会自动调整检测头。如果数据集类别数和预训练模型不一致模型会被重建此时如果直接调用model.info()它使用的输入尺寸可能是旧的、缓存的尺寸而不是你当前训练时用的尺寸这样算出来的GFLOPs自然不对甚至可能因为尺寸不匹配直接报错。我踩过一个坑用yaml文件定义了一个输入尺寸为640的模型但训练脚本里传了imgsz960。训练一切正常验证的时候也没报错但打印出来的GFLOPs是按照640算的明显偏低。后来仔细看日志才发现model.info()默认用的是self.args.imgsz而这个值在模型刚加载时还是默认的640直到训练器初始化后才被更新为960。2.3 模型处于训练模式这个原因比较冷门但确实会发生。model.info()在计算FLOPs时会遍历模型的所有模块并对每个模块调用thop的钩子。如果模型处于训练模式model.train()像Dropout、BatchNorm这类层的计算路径和推理模式model.eval()不同thop在统计时可能会走错分支导致某些层的FLOPs算不出来最终汇总为0。你可能会问为什么打印模型参数的时候模型会是训练模式其实很多人的自定义脚本里加载完模型直接调model.info()此时模型默认是训练模式。而ultralytics自己的训练流程里打印参数表是在model.train()之后、实际训练循环开始之前所以模型也是训练状态。解决方案很简单在调用model.info()之前先执行model.eval()。但注意这只是为了让打印结果准确并不是说训练模式下的统计毫无意义。实际训练结束后如果你想验证一下真实推理时的FLOPs同样要先切到eval模式再统计。3. 从原理到实操怎么让参数表和GFLOPs完整显示3.1 搞清楚ultralytics的model.info()到底在干什么先看一下model.info()的内部逻辑以ultralytics 8.x版本为例。它核心做了三件事统计总参数量total_params和可训练参数量trainable_params遍历模型的每一层收集层名、输出shape、层参数量打印成表调用thop.profile()计算FLOPs再除以1e9转成GFLOPs问题就出在第三步。thop.profile()是一个非常通用的FLOPs统计工具它通过注册forward hook来监视每个模块的输入输出Tensor然后根据模块类型调用对应的计算函数。对于PyTorch内置的卷积、全连接、池化、激活等层thop都有内置的计算规则。但对于复合层、自定义层它就无能为力了。YOLOv11的某些模块比如C3k2、C2PSA这类内部是由多个卷积、归一化、激活函数复合而成的。thop在统计时会把这些复合模块拆开对内部的每个Conv2d、BatchNorm2d分别统计然后累加。但如果某个复合模块内部包含动态分支比如根据输入shape决定走哪条路径thop的钩子可能只记录了其中一条路径的运算量导致统计偏差。理解了这一点你就知道修复思路有两种第一种是让thop能识别你的模块给它写自定义的FLOPs计算函数第二种是换用更精准的统计工具比如fvcore或者ptflops。这两种方案各有适用场景下面分别讲。3.2 方案一给thop添加自定义模块的计算规则这是最轻量的修复方式不需要改动ultralytics源码只需要在你自己的脚本里注册一个钩子函数。thop提供了custom_op接口允许你为指定模块类型自定义FLOPs计算逻辑。具体做法是这样的from thop import profile from thop.utils import clever_format def custom_op_counter(m, x, y): # m是模块实例x是输入tensor列表y是输出tensor # 这里需要根据模块内部结构手动计算FLOPs # 以C2PSA为例假设它内部有k个卷积 flops 0 # 计算卷积部分的FLOPs for conv in m.cv: output y if isinstance(y, tuple) else (y,) # 这里简化处理实际需要遍历每个子层 flops conv.weight.numel() * output[0].shape[2] * output[0].shape[3] return flops from thop.vision.basic_hooks import count_conv2d thop.utils.register_custom_op(ultralytics.nn.modules.C2PSA, custom_op_counter)但这里有一个很现实的问题自定义层内部结构千差万别你很难写出一个通用的计算函数。写错了GFLOPs虽然不为0但数值是错的反而误导你对模型复杂度的判断。我自己的经验是如果自定义层不多而且只是简单组合了Conv和BN那更推荐直接让thop去拆解子模块而不是写一个粗糙的顶层统计。有一个小技巧在定义层的时候尽量用nn.Sequential或者nn.ModuleList组织子模块这样thop遍历时能自然下钻到每个子层统计就会准确很多。3.3 方案二直接用model.info(verboseTrue, imgsz实际尺寸)很多人不知道model.info()其实支持传imgsz参数。在ultralytics的源码中Model.info()的签名是这样的def info(self, detailedFalse, verboseTrue, imgszNone):其中imgsz如果不传才用self.args.imgsz。你有两个办法解决尺寸不对的问题在训练前显式设置model.args.imgsz 960再调用model.info()或者直接调用model.info(imgsz960)还有一个更省事的做法把模型参数打印放到训练器初始化之后。如果你用YOLO(yolov11s.yaml)加载模型接着用model.train(data..., imgsz960)启动训练你可以重写trainer的_setup_train方法在训练真正开始前打印参数表。但这样做改动较大一般不建议除非你有特殊需求。最简单的实操路径是在终端里先用Python加载模型手动设置尺寸再打印python -c from ultralytics import YOLO model YOLO(yolov11s.pt) model.model.eval() model.info(detailedTrue, imgsz640) 这样就能看到完整参数表了。如果你想在训练过程中也打印完整的参数表可以在trainer初始化后、开始训练前插入一行model.info(imgszargs.imgsz)。3.4 方案三更换FLOPs统计工具如果thop实在搞不定另一个思路是换工具。fvcore是Facebook开源的对Detection、Segmentation这类模型的统计更友好ptflops也不错自动拆解子模块的能力比thop强。我自己在YOLOv11上测试过ptflops的效果对内置的标准结构C3k2、SPPF、Detect头统计得都比较准。但要注意这两个库对自定义模块同样需要额外的支持而且它们和ultralytics的版本可能有兼容性问题尤其是当YOLOv11引入了新的模块时可能需要更新库版本。所以我的建议排序是先检查输入尺寸对不对再检查模型是不是eval模式然后才考虑自定义FLOPs统计函数。大部分人的问题都出在前两步而不是真的需要换工具。4. 实操实录完整解决GFLOPs为0和层缺失4.1 环境准备与复现我复现这个问题的环境是Python 3.10PyTorch 2.1.0 CUDA 12.1ultralytics 8.3.30 左右模型用的是YOLOv11s数据集是一个自己整理的小目标检测数据集类别数为3。一开始直接用model.info()输出结果中GFLOPs为0而且backbone部分只显示了前几层后面几层如C2PSA直接没出现在表里。当时我把问题归咎于模型结构修改但后来发现即使加载原始的yolov11s.pt权重只要在模型加载后立即调用model.info()也会出现GFLOPs为0或者不完整的情况。这说明问题不是我的结构改动引起的而是ultralytics默认打印逻辑在某些情况下确实抽风。4.2 逐步排查过程第一步确认模型能在CPU/GPU上正常前向传播。这一步很重要如果模型本身有问题后面的统计都没有意义。我用一个随机输入跑了一次推理import torch from ultralytics import YOLO model YOLO(yolov11s.pt) # 先跑一次前向确保模型正常 dummy torch.rand(1, 3, 640, 640) with torch.no_grad(): out model.model(dummy) print(Forward OK, output length:, len(out))这一步通过了说明模型结构没问题。第二步检查model.info()的调用方式。我把调用改成model.model.eval() model.info(imgsz640, verboseTrue)这一次GFLOPs不再是0而是显示了一个符合预期的数值约11.5左右和YOLOv11s的标准值接近。但层的缺失问题还在。第三步检查层的缺失原因。我逐层打印了named_modules()发现其实所有层都在只是model.info()在详细模式下输出的表格里某些自定义层的显示被合并了。具体来说C2PSA这个模块在表里只显示一行但它的内部结构没有展开。如果你希望展开每一层需要用到detailedTrue参数model.model.eval() model.info(detailedTrue, imgsz640)当我加上detailedTrue后C2PSA内部的所有子层都会逐行打印包括里面的每个Conv2d和BN层表一下子就变长了。缺点是输出非常啰嗦整个表可能有几百行。我一般只在需要逐层分析的时候才开这个模式平时用默认的汇总模式就够了。4.3 最终解决方案综合下来最稳妥的做法是加载模型后先切eval模式再显式传入imgsz最后再决定要不要detailedfrom ultralytics import YOLO model YOLO(yolov11s.pt) model.model.eval() model.info(imgsz640) # 或者 detailedTrue 看全量层 # 如果你想看总参数和GFLOPs也可以直接用metrics m model.model total_params sum(p.numel() for p in m.parameters()) print(fTotal params: {total_params / 1e6:.2f}M)对于训练和验证过程中的打印问题我的建议是不要依赖ultralytics默认的打印时机而是在你的训练脚本里手动插一个参数统计的步骤。比如在trainer回调的on_train_start里加上完整的信息打印。这里有一个很关键的点model.eval()之后记得在训练前恢复成model.train()否则BatchNorm的统计参数会一直在运行模式计算导致训练结果异常。我见过有人为了打印参数在回调里执行了eval()但忘了在训练开始时恢复结果训练了几个epochBN层的running_mean和running_var一直用的是推理模式下的更新逻辑精度掉了好几个点。4.4 参数表和GFLOPs正确性验证修复之后怎么确认打印出来的GFLOPs是对的一个很简单的办法是查官方文档或GitHub READMEYOLOv11s的官方GFLOPs标注是11.5左右输入640x640。你算出来的值如果和这个差不多说明统计基本正确。如果你对结果依然不放心可以用一个最土但也最可靠的方法来交叉验证用一个固定输入尺寸跑一次推理记录耗时估算单次推理的FLOPs。虽然耗时和FLOPs不是完全线性关系但对于同一架构如果两次统计差了10倍那肯定是统计逻辑出了问题。5. 常见问题排查表直接照着抄我在处理这个问题时踩了不少坑整理成下表方便你快速定位自己的问题。现象可能原因解决方案GFLOPs显示为0thop统计失败通常是模型处于训练模式或自定义层未被识别先调model.eval()再调用model.info()或注册自定义FLOPs计算函数模型层数比预期少很多自定义层被合并显示或detailed参数为False使用model.info(detailedTrue)查看完整层级GFLOPs数值和官方不一致输入尺寸不对imgsz参数不是当前训练尺寸显式传入imgsz你的实际训练尺寸打印时模型没问题训练后结果异常为了打印调用了model.eval()但忘记恢复model.train()在训练循环开始前显式执行model.train()自定义模块报了thop不支持的错误thop不认识你的模块钩子注册失败用register_custom_op手动注册或换成ptflopsmodel.info()直接报shape不匹配模型输入尺寸和imgsz参数不一致或模型是动态尺寸检查yaml配置和调用时的参数统一尺寸这张表基本覆盖了我遇到的所有情况。如果你只是想知道模型的FLOPs去写论文或者做方案对比我建议直接用ptflops做一个独立脚本这样不会受ultralytics内部逻辑干扰import torch from ultralytics import YOLO from ptflops import get_model_complexity_info model YOLO(yolov11s.pt).model model.eval() macs, params get_model_complexity_info(model, (3, 640, 640), as_stringsTrue, print_per_layer_statFalse) print(fFLOPs: {macs}) print(fParams: {params})注意ptflops返回的是MACs乘加运算次数和FLOPs差一个2倍的关系。广告里说的GFLOPs一般指的是乘加次数乘以2但严格来说“GFLOPs”里的FLOP应该算的是浮点运算次数。实际对比模型的时候只要统一口径就行别拿MACs和FLOPs直接比大小就好。6. 一个小技巧让每次训练日志都打印准确GFLOPs最后再分享一个我后来长期用的方法不用改ultralytics源码而是用ultralytics的callback机制在训练开始时强行刷新参数表。from ultralytics import YOLO from ultralytics.utils import callbacks def print_full_info(trainer): if trainer.model: trainer.model.eval() trainer.model.info(imgsztrainer.args.imgsz) trainer.model.train() callbacks.register_callback(on_train_start, print_full_info) model YOLO(yolov11s.pt) model.train(datayour_dataset.yaml, imgsz960, epochs100)这个回调在你每次训练前都会执行打印出完整的参数表。同时通过trainer.model.train()恢复训练模式保证BatchNorm正常运行。实测下来这个方法比改train.py源码要干净得多升级ultralytics版本时也不会丢失你的定制逻辑。我个人的习惯是把这个回调写在一个单独的hooks.py文件里每次新建项目就把它import进来。这样无论我用哪种方式加载模型训练日志里都能看到准确、完整的模型信息和GFLOPs不用再为这事情浪费时间去翻源码。说句实话GFLOPs这个数值只对模型对比和论文报告有意义对实际训练收敛的帮助不大但既然要打印就打印一个正确的免得后来分析数据的时候疑神疑鬼。
返回列表