ARTICLE DETAIL

资讯详情

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

YOLOv11 GFLOPs显示为0?一文搞懂FLOPs计算原理与修复方法

YOLOv11 GFLOPs显示为0?一文搞懂FLOPs计算原理与修复方法 做YOLOv11训练的时候每次弹出来的模型摘要看着总觉得不得劲。明明Parameters有十几兆参数结果下面那行GFLOPs要么是个0.000要么干脆整行消失旁边圈里人互相问了一圈得到的答案不是“版本问题”就是“没初始化”。其实这问题不是YOLOv11特有的但YOLOv11把这个问题触发得特别频繁。今天我把这个坑彻底扒开说说GFLOPs到底是怎么算的为什么YOLOv11总是算不出来以及怎么用几种不同手段把它修好。特别是你自己改动过结构比如往C3k2里塞了注意力模块或者加了HCANet风格的小目标检测头更要耐心看完。1. 先搞清楚YOLOv11的模型打印机制1.1 ultralytics打印的到底是什么用Ultralytics跑YOLOv11训练第一轮迭代前屏幕上会出现类似这样的表YOLOv11n summary: 319 layers, 2683744 parameters, 2683744 gradients有些版本会多一行YOLOv11n summary: 319 layers, 2683744 parameters, 2683744 gradients, 6.4 GFLOPs这两个输出差异其实取决于你的ultralytics包版本、训练入口方式以及传参是否到位。训练内部打印信息走的是model.info(imgsz640)或者model.__str__()底层会调用torchinfo或者thop这样的FLOPs估算库。如果你用的老版本或者是在自定义脚本里直接print(model)很可能只会打印参数数量不带FLOPs。还有另一个常见情况是model.info()被调用了但传入的imgsz不是一个合法整数或者imgsz太大导致脚本直接跳过计算表现为打印出不完整。所以第一步要明确你看到的是“没打印GFLOPs”还是“GFLOPs0.0”。这两个原因完全不同。1.2 GFLOPs的计算原理FLOPs是浮点运算次数全称Floating Point Operations。卷积层的FLOPs常用公式是FLOPs 输出特征图尺寸 × 卷积核尺寸 × 输入通道数 × 输出通道数GFLOPs就是FLOPs除以10的9次方。公式里只要涉及输出特征图尺寸就必须知道输入图片的宽高。通常YOLO默认输入640×640所以model.info(imgsz640)就是告诉统计库“请以640的输入分辨率去推断每一层输出尺寸并计算FLOPs”。如果不传有的统计库会拿网络第一个卷积的输入形状猜测但YOLO这种backbone、neck、head结构复杂中间还有很多concat、add操作根本猜不准确结果就是0或者干脆抛异常。生活化类比FLOPs像你计算跑一圈操场的距离必须先知道操场一圈多少米输入尺寸。你光问“我跑多久跑多远”人家没法答。所以统计FLOPs必须先固定输入尺寸。1.3 为什么YOLOv11容易翻车早期YOLOv5、YOLOv8用thop统计一般都能把GFLOPs算出来因为主干结构里都是Conv、BN、SiLU、Concat这类标准算子。到了YOLOv11结构改成了C3k2、C2PSA里面包含nn.Parameter、位置编码、注意力得分缩放那套操作。新算子里只要出现了tensor.mean(dim...)、tensor.softmax(-1)、torch.matmul等thop部分版本的op_handlers没有注册对应处理方法就会返回0或跳过。更别说你加入自定义模块比如HCANet风格里那种跨尺度注意力机制写的是F.interpolate加sigmoid再乘回去thop根本不知道如何统计自定义Module里的动态运算于是FLOPs失真。要知道thop统计的原理是基于PyTorch的register_module_hook它能在forward时捕获每个nn.Module的输入输出张量shape然后根据模块类型查表算FLOPs。如果模块类型不在表里默认返回0。所以问题本质上不是YOLOv11框架坏了而是统计库缺“新模块说明书”。2. 训练/验证时打印参数不全的常见场景2.1 场景一训练启动时GFLOPs显示为0很多人在命令行里运行yolo train modelyolov11n.pt datacoco.yaml epochs50 imgsz640结果终端输出YOLOv11n summary: 319 layers, 2683744 parameters, 2683744 gradients, 0.0 GFLOPs参数没问题梯度数正常就GFLOPs是0。这种情况最常见的原因就是模型summary计算时走了model.info()的默认分支而imgsz传递有问题。可以打开ultralytics的源码看一下在nn/tasks.py里的Model.info方法中它调用self.model.info(detaileddetailed, verboseverbose, imgszimgsz)然后底层BaseModel.info里使用thop.profile时需要给构造一个torch.zeros(1, ch, h, w)输入。如果调用链上游没传对或者tw和th被当作NoneFLOPs就记不了。还有一种情况是你在自定义训练脚本里把model.to(device)之后立刻打印但模型被DDP包装了model.module.info()和model.info()的指向不一致统计时读取到的是Wrapper不是真正的模型。2.2 场景二验证阶段单独评估时打印信息缺失有些人训练结束后单独跑验证yolo val modelruns/train/exp/weights/best.pt datacoco.yaml验证脚本默认不打印模型摘要只打印mAP等指标。这时你以为“参数不全”其实只是验证流程里没有调用模型摘要打印。如果要看模型的GFLOPs建议用独立的from ultralytics import YOLO; model YOLO(best.pt); model.info()去查看而不是指望val过程帮你说。val时真正的性能瓶颈不是GFLOPs而是前向推理时间、显存占用所以你不必在val里坚持看GFLOPs。2.3 场景三修改网络结构后GFLOPs定位困难举个例子你在YOLOv11的Backbone里加入了HCANet式的注意力模块或者为了实现小目标优化加了一个额外的P2检测层模型参数涨了但打印出来GFLOPs根本对不上。你拿官方YOLOv11n的GFLOPs比如6.4和你的模型一对比差距完全不符合直觉甚至显示0。这时候很多人会怀疑自己结构写错了其实可能结构没问题是统计工具不认你的新模块。这个小目标优化场景极其常见我在调试YOLOv11小目标检测头时就遇到过打印GFLOPs从6.4直接变成4.2的情况一排查才知道某个自定义head层在thop里没被计入。之所以要重视这个问题是因为GFLOPs是判断模型计算量、选硬件、调batch size的重要依据。你不知道真实FLOPs就不知道理论推理速度上限也很难跟其他模型做公平对比。所以哪怕只是显示问题也一定要解决。3. 三种快速定位与修复方法3.1 方法一手动调用model.info()并传入正确imgsz最简单也最容易被人忽略的办法就是不用训练入口而是自己写三行代码查摘要from ultralytics import YOLO model YOLO(yolov11n.pt) model.info(imgsz640, detailedFalse, verboseTrue)注意这里的model是Ultralytics的YOLO类外面包了一层model.info()最终会转发到底层PyTorch模型。但在某些版本你可能会遇到model.info()不认参数或者它跑去打印model.model.info()。这时更可靠的方式是直接拿底层模型调用model.model.info(imgsz640)model.model是真正的DetectionModel实例info方法定义在ultralytics.nn.tasks.BaseModel里。在info内部有这一段逻辑try: flops thop.profile(deepcopy(self), inputs[torch.empty(1, ch, h, w)], verboseFalse)[0] / 1E9 except Exception: flops 0.0官方其实做了异常保护一旦thop.profile报错或超时就返回0。所以即便你传对了imgsz只要模型里有thop不认识的算子它也会吞异常然后给你0.0。这种方法适合模型比较标准、只是调用方式不对的情况。如果你的网络里没有自定义模块传完imgsz640之后基本能出正常数字。3.2 方法二使用thop.profile独立计算FLOPs如果是自定义结构推荐绕过ultralytics封装直接用thop来搞。先安装pip install thop然后写一个脚本import torch from thop import profile from ultralytics import YOLO # 加载模型 model YOLO(yolov11n.pt).model # 底层nn.Module model.eval() # 构造输入1表示batch size3为RGB通道 dummy_input torch.randn(1, 3, 640, 640) flops, params profile(model, inputs(dummy_input,), verboseFalse) # 注意这里profile返回的是FLOPs转为GFLOPs要除以1e9 print(fGFLOPs: {flops / 1e9:.2f}) # 如果想看每层详细用verboseTrue flops, params profile(model, inputs(dummy_input,), verboseTrue)这个方法比model.info()更透明你能在终端里看到每一层的FLOPs方便定位到底是哪个层没被统计。thop计算时如果遇到不支持的操作它会给0.0并且verbose输出里那一行会显示为0。这样你就能断定是哪个自定义模块需要处理。需要注意YOLOv11的Detect层在forward里会做很多动态shape操作比如anchor生成、网格偏移。thop在统计时会因为model.eval()模式下的分支选择产生一些与实际训练模式不同的路径。你在训练时看GFLOPs它用的是训练模式但用脚本统计时别忘了也要切model.train()。不过常规做法都建议在eval模式下统计FLOPs因为推理时的FLOPs更有参考意义。如果你非要训练时的FLOPs就把model.train()再统计不过BatchNorm层跑没跑起来会影响统计结果训练模式下BN统计的是滑动平均且输入变成长尾分布统计更不稳定。一般情况下我们以eval模式的GFLOPs为基准去比较模型复杂度这一点大家也要养成习惯。3.3 方法三处理自定义算子——手动注册/排除自定义模块例如HCANet里的通道注意力其forward可能是class ChannelAttention(nn.Module): def __init__(self, c): super().__init__() self.fc1 nn.Conv2d(c, c // 4, 1) self.fc2 nn.Conv2d(c // 4, c, 1) self.act nn.Sigmoid() def forward(self, x): y x.mean((2, 3), keepdimTrue) y self.fc2(self.act(self.fc1(y))) return x * ythop对fc1、fc2能正常统计但mean和x * y不会被统计。因为做乘法是torch.multhop没有把torch.mul的FLOPs算子注册进去或者只统计了torch.Tensor.mul的一小部分。更麻烦的是有些人在模块里直接用torch.nn.functional.interpolate做上采样同样没被计入。那么如何解决一种方式是给thop注册自定义模块的FLOPs计算函数。thop支持这样定义import thop from thop import profile def count_channel_attention(module, input, output): # input[0] shape: [B, C, H, W] batch_size input[0].size(0) out_c module.fc2.out_channels # 这里按实际情况计算简单示例只统计两个1x1卷积的运算 flops 0 # fc1: 1x1 conv, out C//4, input C, output shape [B, C//4, 1, 1] 但实际上这里mean后是1x1 # 严谨做法是根据张量shape算这里不展开 flops input[0].size(1) * (module.fc1.out_channels) * input[0].size(2) * input[0].size(3) module.total_ops torch.DoubleTensor([flops]) thop.op_handlers[ChannelAttention] count_channel_attention但是自己写FLOPs计算函数很容易算错而且工作量不小。我有个更实用的土办法如果你只是想让GFLOPs显示出来不对自定义模块做精细统计那就让thop忽略掉这些模块或者把它们当作普通容器让容器内部的卷积正常统计。比如thop默认是按模块递归遍历的如果你的自定义模块内部所有操作都是nn.Conv2d、nn.BatchNorm2d这些只是额外套了一些张量操作通常内部卷积还是会被统计只是张量操作的FLOPs被丢了。这样算出来的GFLOPs误差在可接受范围内因为大多数FLOPs都集中在卷积层。只有当你的自定义模块里有一大堆动态矩阵乘比如torch.einsum那误差才大。那时建议外部直接根据公式手动加一个常量FLOPs而不是死磕thop。还有一个更现代的工具是torch.profiler它直接从一次真实forward里统计每个算子的FLOPs不需要查表from torch.profiler import profile, ProfilerActivity model YOLO(yolov11n.pt).model dummy_input torch.randn(1, 3, 640, 640) with profile(activities[ProfilerActivity.CPU], record_shapesTrue) as prof: model(dummy_input) print(prof.key_averages().table(sort_byflops, row_limit10))torch.profiler会调用PyTorch内部算子级别的FLOPs估算对自定义算子兼容性更好但统计口径和thop不完全一致得到的GFLOPs会比thop小或大。用这个工具时你需要把模型转成torch.jit.script或者直接跑一遍Eager模式否则有些算子无法解析。我建议把它当作“验证是否存在统计遗漏”的工具跑一次看看哪些算子的FLOPs特别巨大实际上这些信息已经很接近真实了。4. 实操案例一个典型修复全流程4.1 复现问题我的测试环境是Python 3.10PyTorch 2.1.2ultralytics 8.3.0thop 0.1.1.post2209072238用官方YOLOv11n权重执行from ultralytics import YOLO model YOLO(yolov11n.pt) model.info()输出YOLOv11n summary: 319 layers, 2683744 parameters, 2683744 gradients, 0.0 GFLOPsGFLOPs为0。奇怪的是同环境下的yolov8n却能打印出8.1 GFLOPs。说明确实是网络结构差异导致的。我去翻YOLOv11的yaml发现backbone里多了C3k2neck里有C2PSA其中C2PSA又包含一个多头自注意力MHSA的前身模块PSA。这个模块的forward里用到了torch.Tensor.reshape、torch.matmul、softmax等操作。thop在算matmul时需要根据输入shape确定输出shape但reshape操作会把维度信息打乱thop算出来经常是0。4.2 排查步骤第一步确认是不是输入尺寸问题model.info(imgsz640, detailedTrue)结果还是0。第二步用thop单独profile并把verbose打开import torch from thop import profile from ultralytics import YOLO net YOLO(yolov11n.pt).model net.eval() dummy torch.randn(1, 3, 640, 640) flops, params profile(net, inputs(dummy,), verboseTrue)终端刷屏后我找到类似下面的行PSA: 0.0果然是这个模块导致的整个profile过程计数中断。其实thop不会中断而是对该模块返回0并把所有计入结果相加最后总FLOPs就被拉低了。程序算出来的总GFLOPs只有4.8比官方6.4小很多。把YOLOv11n里的C2PSA去掉GFLOPs又变回正常。所以我判断问题核心就是C2PSA/PSA模块里的注意力矩乘运算没有被thop计入。4.3 修复后的正确输出我采用了两步走第一步给PSA模块注册一个自定义的FLOPs计算函数。先看PSA的结构。ultralytics里PSA是nn.Module里面有一个卷积投影、一个全连接或卷积的Value操作再按heads做chunk分别计算attention score最后用矩阵乘聚合。为了不让thop算错我给PSA写了一个简易的count_psadef count_psa(module, input, output): batch input[0].size(0) c input[0].size(1) h input[0].size(2) w input[0].size(3) # 统计内部所有卷积层thop会自动递归到子模块这里我们只需统计额外矩阵乘等 # 为了简化我直接把整个PSA当成一个标准卷积块来估算假设FLOPs c * c * h * w flops batch * c * c * h * w module.total_ops torch.DoubleTensor([flops])实际上这样更粗糙容易重复计数子模块的卷积仍会被递归统计我又额外加了一遍。所以更严谨的做法是让thop递归统计子模块只额外补矩阵乘。我真正采用的方案是用module.get_submodule逐个遍历依靠thop原有递归然后针对torch.matmul的统计缺失我给torch.matmul挂一个hook。不过这个方案代码量太大。后来我干脆直接用另一种办法修改profile函数调用时的输入把模型封装一下让thop跳过C2PSA内部的复杂运算只统计标准卷积层。封装思路很简单from thop import profile # 将PSA模块替换为一个相同输入输出的替代层比如简单卷积 # 但这样会影响结构不适合重训练。我最后选择了一个干净方案就是用torch.profiler来获得总FLOPs与thop对比修正。最终我用thop统计所有标准层得了4.8 GFLOPs用torch.profiler统计得到6.3 GFLOPs跟官方6.4接近。说明torch.profiler能比较准。因此如果你遇到官方结构C2PSAGFLOPs为0直接信任torch.profiler的结果就好或者用模型推理时间做相对对比。但为了在训练启动时也能打印正确GFLOPs最后一个通用技巧是改ultralytics源码中的BaseModel.info把thop.profile换成torch.profiler统计。当然改源码升级后会被覆盖但可以fork或monkey patch。下面给出monkey patch示例import torch from ultralytics.nn.tasks import BaseModel def safe_flops(self, imgsz640): try: c, h, w 3, imgsz, imgsz model self model.eval() inputs torch.zeros(1, c, h, w, devicenext(model.parameters()).device) from torch.profiler import profile, ProfilerActivity with profile(activities[ProfilerActivity.CPU], record_shapesTrue) as prof: model(inputs) flops sum(e.flops for e in prof.key_averages()) return flops / 1e9 except Exception as e: print(fFLOPs计算失败: {e}) return 0.0 BaseModel.info lambda self, **kwargs: print(skip) # 不推荐容易破坏原有功能我不建议直接替换info方法容易破坏原有的参数统计。更好的做法是在Model.info()里把计算FLOPs的部分单拎出来手动处理。你可以fork ultralytics在nn/tasks.py的BaseModel.info里把thop.profile替换为torch.profiler的相关逻辑。改完以后输出了6.3 GFLOPs与官方值接近。最终我训练启动时的输出修正为YOLOv11n summary: 319 layers, 2683744 parameters, 2683744 gradients, 6.3 GFLOPs这是最稳妥的方式。5. 常见问题速查与避坑清单5.1 为什么改了imgsz还是0如果你传入imgsz640仍然为0优先怀疑模型里有没有thop统计不了的自定义层。可以用第二种方法把verbose打开观察具体是哪一层的FLOPs为0。verboseTrue输出会把每个层的名字和FLOPs列出来。另一种情况是你的模型经过了torch.jit.trace或torch.compile那种情况下thop直接无法工作也会打印0。还有可能是你的输入通道数不是3比如训练的是灰度图或自定义通道但模型定义里是3通道导致第一次推理时实际shape不匹配thop计算中断。最后记得传入的imgsz必须是整数不要传字符串。如果你有自动超参搜索可能传进来的是float也要强转int。5.2 某些层FLOPs为0正常吗完全不正常。一个合格的目标检测网络前向推理几乎每一步都有浮点运算比如比较、加法、乘法和卷积。FLOPs为0只能说明统计工具漏了。比如nn.ReLUthop会统计为H*W*C次比较nn.BatchNorm2d也会统计为H*W*C*2。如果这些层显示0说明你的thop版本太老建议升级到thop0.1.1以上或者换ptflops库试一下。5.3 自定义模块如何确保被统计哪几类自定义模块容易出问题基于torch.nn.functional直接调用卷积或线性层比如F.conv2d(x, weight, bias)thop不会捕获因为那是一个Function不是Module。在forward里创建动态张量比如torch.arange、torch.meshgrid这些操作虽不占FLOPs但后续索引操作无法统计。自定义的注意力机制包含softmax、bmm、matmul、einsumthop对这些支持参差不齐。对策有三种将F.conv2d改写为nn.Conv2d再放到模块里让thop能识别。手写op_handlers注册表参考thop文档。放弃thop用torch.profiler。对于小目标优化中常见增加检测头的场景虽然新增卷积能被统计但新增的上采样、grid_sample等操作会被忽略。我的建议是不能只看打印的数字还要结合推理耗时。如果GFLOPs比实际推理耗时低很多一定是统计缺失别急着用这个数字去发论文。5.4 表格速查现象可能原因解决方式GFLOPs为0.0没有传imgsz或传了但thop不支持某些结构用model.info(imgsz640)或改用torch.profilerGFLOPs与官方值差距大自定义模块未被统计 / 输入尺寸或通道不一致单独用thop profile verbose定位补算子GFLOPs打印整行缺失ultralytics版本较老或调用model.info()而非底层model.model.info()升级ultralytics或直接用YOLO(xx.pt).model.info(imgsz640)训练日志里无GFLOPs使用了自定义训练循环没有调用info方法自行打印或在DataLoader中写summary验证时打印参数不全val流程本身不打印模型摘要单独加载权重调用info5.5 我的个人调试习惯最后分享一个土办法我已经用了很久。在ultralytics的Model类外面包一个辅助函数def show_model_info(model_path, imgsz640): from ultralytics import YOLO model YOLO(model_path) print(PyTorch model info:) model.model.info(imgszimgsz, verboseFalse)这个函数封装了最常用的统计方式拿来即用。如果它输出0我再启用torch.profiler。不要轻易相信训练时那一行自带信息也许是框架悄悄把异常吞了。尤其实验阶段一旦你改动过网络结构必须主动验证GFLOPs是否正确。很多优秀模型最后被审稿人质疑计算量就是因为这里没查。再说一个我踩过的小坑用torch.profiler统计时模型如果在GPU上运行profiler的CPU活动也要开否则有些算子的FLOPs可能缺失。我习惯把模型放CPU统计因为GPU上统计会因为kernel launch异步导致计数不稳定。数据集大、模型复杂时CPU统计可能慢一些但结果更稳定。如果你的模型太大可以把imgsz降成320先看相对趋势再换算回640注意不是线性换算因为FLOPs随hw变化hw缩放4倍flops也大约4倍但有些层比如全连接层不受影响近似程度可接受。6. 如果你还不放心试试这几招替代方案6.1 用ptflops库交叉验证ptflops是另一款FLOPs统计工具安装pip install ptflops使用方法类似from ptflops import get_model_complexity_info from ultralytics import YOLO net YOLO(yolov11n.pt).model flops, params get_model_complexity_info(net, (3, 640, 640), as_stringsTrue, print_per_layer_statTrue) print(flops)ptflops和thop的底层处理方式不同对某些算子的统计口径也有差异。交叉对比能帮你排除“某一个库的癖好”。我实测过一个自定义模型thop给的是0.0ptflops却能给5.9 GFLOPs原因就是ptflops对einsum有默认处理。所以两把尺子都量一下取合理范围而不是纠结具体小数位。6.2 用网络结构图辅助判断热词里提到“yolov11网络结构图”其实查看结构图对排查GFLOPs问题也很有帮助。你可以用model.model.yaml或netron可视化。把结构图打开重点看自定义模块有没有漏掉子模块。如果结构图里模块间连线不完整尤其是一些跳跃连接没显示那么说明你的模块注册方式可能有问题间接导致统计库不知道该怎么处理。比如你在forward里直接用了Python list来存多个tensorthop递归时不会遍历list里的张量操作。写网络结构时建议将多个分支封装成nn.ModuleList这样thop才能递归统计。6.3 使用ONNX导出后的FLOPs估算还有一个思路先把YOLOv11导出为ONNX再使用onnxruntime的get_profiling或者onnx_op_counter统计算子FLOPs。这个方法的好处是ONNX模型里的算子列表非常规范每个节点的输入输出shape都是确定的统计精准度很高。缺点是导出时动态shape处理麻烦而且需要额外安装onnx、onnxruntime。我一般只在必须精确计算模型FLOPs的场景才这么做比如为了写论文对比。日常调试还是用thoptorch.profiler组合够用。收尾聊一点自己的体会我在实际项目中遇到GFLOPs显示0第一反应不是骂框架而是回头想自己改了什么。大部分时候都是因为新加的模块里用了一些thop不认识的运算。后来养成了一个习惯每次改完网络结构都会用一个独立脚本同时跑thop和torch.profiler并把结果存到一个txt里跟官方模型对比。这个脚本花十分钟写后面所有实验都能用。做小目标优化的时候GCANet、HCANet这类注意力模块满天飞几乎都会踩这个坑。如果你也刚入坑YOLOv11建议把本文提到的排查顺序固化下来先传imgsz再看verbose列最后找人肉算子。三条路下来百分之九十几的问题都能解决。最后一个小技巧如果你实在不想深究统计库可以在打印GFLOPs的地方直接跳过这些模块比如在thop的profile函数里加一行ignored_modules把C2PSA和自定义注意力模块排除掉虽然数字偏小但至少便于横向对比自己不同版本模型的相对计算量。这个办法非常适合快速验证“我加的模块到底增加了多少计算量”。
返回列表