【YOLOv5】FLOPS计算实战:从profile函数到模型效率评估

1. 为什么我们需要关心YOLOv5的FLOPS?

大家好,我是老张,一个在AI和嵌入式设备上折腾了十多年的工程师。今天咱们不聊那些高大上的理论,就聊聊一个非常实际的问题:当你手上有好几个YOLOv5模型,比如nano、s、m、l、x,或者你自己魔改了一版,你怎么知道哪个模型“更省力”?这里的“省力”,指的就是计算量,也就是我们常说的FLOPS。

FLOPS,全称是Floating Point Operations Per Second,每秒浮点运算次数。但在模型评估里,我们通常指的是完成一次前向推理所需要的总浮点运算次数。这个数字直接关系到你的模型跑起来快不快、费不费电、能不能塞进边缘设备里。我见过不少新手朋友,光看mAP(平均精度)高了零点几个点就兴冲冲地选型,结果部署到实际设备上,推理速度慢如蜗牛,功耗还高得吓人,项目直接卡壳。这就是忽略了计算效率评估的后果。

那么,怎么算FLOPS呢?最直接的想法就是用现成的工具,比如thop库里的profile函数。这听起来很简单,对吧?但坑就藏在这里。我刚开始用的时候也以为profile(model, input)一下,除个1E9得到GFLOPS就完事了。结果发现,不同人算出来的同一个YOLOv5s的GFLOPS值能差一倍!有的论文里写的是4GFLOPS,有的报告里又是8GFLOPS。这还怎么比?到底谁是对的?

问题的核心就在于计算标准不统一thop.profile函数本身只是个“计数器”,它按照一定的规则去遍历模型的计算图,数出乘法和加法的次数。但关键在于,我们怎么定义“一次运算”?是把一次“乘加”(一个数乘以另一个数再加上第三个数)算作1次操作,还是算作2次(乘法和加法分开算)?另外,YOLOv5官方代码里计算FLOPS的方式还有点“小动作”,它引入了一个stride(步长)的概念,并且用了一个很小的输入(比如32x32)来计算基准FLOPS,再按比例放大到你想要的输入尺寸(比如640x640)。这又是为什么?

如果你对这些问题感到困惑,那么这篇文章就是为你准备的。我会带你一步步拆解YOLOv5官方计算FLOPS的代码,然后对比直接使用profile函数的几种不同方式,看看结果到底差在哪里。更重要的是,我会结合我这些年踩过的坑,告诉你在不同的场景下——比如写论文、做内部模型选型、或者为具体硬件部署做准备——到底应该采用哪种计算方式,才能得到最有参考价值的“效率成绩单”。我们的目标不是追求一个绝对“正确”的数字,而是找到一个可靠、可比、能真实反映模型效率的评估方法。

2. 拆解YOLOv5官方的FLOPS计算“黑盒”

很多朋友拿到YOLOv5的代码,想看看它的计算量,直接去utils/torch_utils.py里找model_info函数或者相关的打印信息。你会发现它的计算逻辑并不是简单的profile调用,里面有不少设计考量。我们以v5.0版本的代码为例,把它掰开揉碎了看。

2.1 第一步:确定那个神秘的“基准输入尺寸”

官方代码第一行就有点意思:

stride = max(int(model.stride.max()), 32) if hasattr(model, 'stride') else 32

这行代码在干嘛?它在确定一个用于计算FLOPS的基准输入图像的边长model.stride是YOLOv5中下采样倍率的集合。我们知道,YOLO网络会不断对特征图进行下采样,产生不同尺度的输出。model.stride记录了这些下采样的步长,通常是[8, 16, 32]。这里取其中最大的值,并且和32比较,取更大的那个。

为什么要这么做? 这是为了计算上的鲁棒性和简便性。YOLOv5的设计中,最大的stride(比如32)对应着网络最深层、分辨率最低的特征图。用这个值作为输入的宽高,可以确保输入图像经过所有下采样层后,得到的特征图尺寸仍然是整数(因为32能被8、16、32整除)。同时,设置一个最小值32,可能是为了避免输入尺寸过小导致某些层无法正常工作,或者是为了得到一个标准化的、较小的计算基准,方便后续缩放。

2.2 第二步:构造一个“最小化”的测试输入

确定了stride(假设是32)后,代码构造了一个输入张量:

img = torch.zeros((1, model.yaml.get('ch', 3), stride, stride), device=next(model.parameters()).device)

这个张量的形状是 [1, 3, 32, 32]。意思是:批量大小为1,通道数为3(RGB),高度和宽度都是32像素。这是一个非常小的图片!用全零(torch.zeros)而不用随机数,是因为FLOPS计算只关心操作的类型和数量,不关心具体的数值,零值输入完全够用,还能避免不必要的随机性。

这里的关键点在于:官方并没有直接用你想要的推理尺寸(比如640x640)来计算FLOPS,而是用一个极小的、与模型stride相关的尺寸(32x32)先算出一个“基准FLOPS”

2.3 第三步:计算基准FLOPS并引入“乘2”规则

接下来是核心计算:

flops = profile(deepcopy(model), inputs=(img,), verbose=False)[0] / 1E9 * 2

这一行干了三件事:

  1. profile(deepcopy(model), inputs=(img,), verbose=False)[0]: 调用thop.profile函数,传入模型的深拷贝(防止计算影响原模型)和那个32x32的输入图片。这个函数会返回两个值,第一个是FLOPS,第二个是参数量。我们取第一个。
  2. / 1E9: 把单位从“次运算”转换成“十亿次运算”(GFLOPS)。
  3. * 2: 这是一个争议点。它把profile函数数出来的操作次数翻倍了。

为什么要乘以2? 这涉及到对“一次浮点运算”的定义。thop.profile函数在默认情况下,将一次“乘加运算”(Multiply-ACCumulate, MAC)计为1次操作。一次MAC包含一次乘法和一次加法。但在学术界的很多论文和报告中,习惯将乘法和加法分开计数,这样一次MAC就被算成了2次浮点运算(FLOPS)。YOLOv5官方代码选择了后一种更“传统”的计数方式,所以乘了2。这就导致了数值上的直接翻倍。

2.4 第四步:按比例缩放至实际输入尺寸

你训练和推理用的肯定是比如640x640的图片,不是32x32。所以需要一个缩放:

img_size = img_size if isinstance(img_size, list) else [img_size, img_size]
fs = ', %.1f GFLOPS' % (flops * img_size[0] / stride * img_size[1] / stride)

假设img_size是640,stride是32。那么缩放因子就是 (640/32) * (640/32) = 20 * 20 = 400。之前算出的基准GFLOPS(基于32x32)会乘以400,得到最终640x640输入下的GFLOPS。

这种缩放成立的前提是什么? 前提是模型中绝大部分层的计算量(尤其是卷积层)与输入特征图的宽高成正比。对于标准的卷积层,其FLOPS确实等于 输出通道数 * 输出高度 * 输出宽度 * 卷积核高度 * 卷积核宽度 * 输入通道数。当输入图片尺寸等比例放大时,输出特征图尺寸也等比例放大,因此计算量大致按面积(宽乘高)的倍数进行缩放。YOLOv5的这种计算方法巧妙地避免了直接用大尺寸输入进行profile可能带来的内存或计算负担,同时通过线性缩放得到一个近似值。

总结一下官方的思路:它定义了一套自己的“标准流程”:用小尺寸基准输入(由模型stride决定)配合thop.profile计算,采用“乘加分开计次”(*2)的统计口径,最后按面积比例线性放大到目标尺寸。这套方法在YOLOv5社区内部比较一致,但当你拿这个数字去和其他模型、其他论文对比时,就要格外小心了。

3. 实战对比:几种profile函数调用方式的差异

了解了官方的“黑盒”,我们现在把它打开,看看如果不用官方那套,直接使用thop.profile函数,不同的调用方式会带来多大差异。这是我们模型选型时最容易踩坑的地方。

3.1 方法一:完全仿照官方,但拆解步骤

我们先按照官方的逻辑,但一步步手动实现,看看中间值。

import torch
import thop
from copy import deepcopy

# 假设我们已经有一个YOLOv5s模型 `model`
device = next(model.parameters()).device

# 1. 确定 stride 和基准尺寸
stride = max(int(model.stride.max()), 32)  # 通常是32
base_size = stride
print(f"基准输入尺寸: {base_size}x{base_size}")

# 2. 创建基准输入
base_input = torch.zeros(1, 3, base_size, base_size).to(device)

# 3. 计算基准FLOPS (不除1E9和不乘2,看原始值)
flops_base, params_base = thop.profile(deepcopy(model), inputs=(base_input,), verbose=False)
print(f"profile直接输出的基准FLOPS: {flops_base}")
print(f"官方方式基准GFLOPS (flops_base/1E9*2): {flops_base / 1E9 * 2:.2f} GFLOPS")

# 4. 缩放至目标尺寸,例如640x640
target_size = 640
scale_factor = (target_size / stride) * (target_size / stride)
flops_final = flops_base * scale_factor
print(f"缩放至{target_size}x{target_size}后的FLOPS: {flops_final}")
print(f"官方方式最终GFLOPS: {flops_final / 1E9 * 2:.2f} GFLOPS")

运行这段代码,你会得到几个关键数字。对于YOLOv5s,flops_base(32x32输入,未乘2)可能大约在2-3G左右(即20-30亿次运算)。乘以2再缩放后,640x640的GFLOPS大概在16-17左右。这就是你常看到的“YOLOv5s约16GFLOPS”说法的来源。

3.2 方法二:直接使用目标尺寸输入,不乘2

这是最“朴素”的想法:我要测640x640的模型,就直接给个640x640的输入。

# 直接计算目标尺寸下的FLOPS
target_input = torch.randn(1, 3, target_size, target_size).to(device)
flops_direct, params_direct = thop.profile(deepcopy(model), inputs=(target_input,), verbose=False)
print(f"直接profile({target_size}x{target_size})输出的FLOPS: {flops_direct}")
print(f"直接转换的GFLOPS (不乘2): {flops_direct / 1E9:.2f} GFLOPS")
print(f"直接转换的GFLOPS (乘2): {flops_direct / 1E9 * 2:.2f} GFLOPS")

你会发现一个有趣的现象flops_direct的值,大概率并不等于 flops_base * scale_factor。它们可能很接近,但不会完全相等。这是因为线性缩放是一个近似。模型中的某些操作(例如某些激活函数、归一化层)的计算量可能并不严格与输入尺寸的平方成正比。直接用大尺寸输入profile得到的是更精确的数值,而官方缩放方法是一种高效的估计。

更重要的是,如果你采用“不乘2”的口径,那么flops_direct / 1E9的值,大概只有官方最终GFLOPS值的一半(约8-9 GFLOPS)。这就是差了一倍的根源所在

3.3 方法三:考虑非标准输入尺寸的影响

官方方法用stride相关的尺寸做基准,一个好处是避免了输入尺寸不是stride整数倍的问题。如果我们粗暴地用一个非32倍数的尺寸,比如650x650,去直接profile会怎样?

odd_size = 650
odd_input = torch.randn(1, 3, odd_size, odd_size).to(device)
try:
    flops_odd, _ = thop.profile(deepcopy(model), inputs=(odd_input,), verbose=False)
    print(f"{odd_size}x{odd_size}输入下的GFLOPS (不乘2): {flops_odd / 1E9:.2f}")
except Exception as e:
    print(f"可能出错: {e}")

在实际中,模型可能能正常推理,但thop.profile在遍历计算图时,对于特征图尺寸出现小数(如650/32=20.3125)的情况,其内部计数逻辑可能会产生误差或警告。而官方方法通过先在小尺寸整数特征图上计算,再按理论上的比例缩放,巧妙地规避了这个问题,保证了计算结果的稳定性和可重复性。

我们来做个表格对比一下这几种方法对YOLOv5s模型(640x640输入)的计算结果差异:

计算方法核心操作得到的GFLOPS值(示例)特点与注意事项
YOLOv5官方方法32x32基准输入 -> profile -> *2 -> 按面积缩放至640x640~16.4 GFLOPS社区标准,结果稳定,便于内部对比。使用了“乘加分开计数”(*2)。
直接Profile (乘2)640x640输入 -> profile -> *2~16.0 GFLOPS结果最精确反映该尺寸下的实际运算次数(按乘加分开计)。但输入尺寸需为stride整数倍。
直接Profile (不乘2)640x640输入 -> profile~8.0 GFLOPS反映硬件更真实的MAC操作数。与许多深度学习框架和硬件厂商的评估方式一致。
错误方法示例650x650输入 -> profile数值可能异常输入尺寸非模型设计倍数,可能导致特征图尺寸非整数,profile计数不准,结果不可靠。

注意:以上GFLOPS值为示意,实际运行因版本和具体实现会有细微波动,但比例关系(乘2与否差一倍)是确定的。

看到这里,你应该明白了,没有哪个数字是“错的”,它们只是基于不同的规则。直接对比不同规则下的数字,就像比较摄氏度和华氏度而不说明单位一样,毫无意义。

4. 如何选择?不同场景下的FLOPS计算建议

知道了各种差异,那在实际工作中到底该怎么用呢?我的经验是,没有一刀切的标准,要根据你的使用场景来决定。

4.1 场景一:撰写学术论文或技术报告

如果你需要发表论文,或者在正式的技术报告中对比模型复杂度,首要原则是:与对比文献保持一致

  • 查阅基准论文:首先去看你领域内公认的基准论文(比如YOLOv5的原论文、或者你研究方向的主流论文)他们用的是哪种口径。如果他们都用“乘加分开计数”(即报告了~16 GFLOPS for YOLOv5s),那你也应该采用同样的方式,并在报告中明确写明“FLOPS计数包含乘法和加法运算”。
  • 明确声明:在你的论文“实验设置”或“模型分析”部分,必须用一句话明确说明FLOPS的计算方式。例如:“本报告中的FLOPS均采用乘加运算分开计数的方式,使用thop库在640x640输入分辨率下测得。” 或者 “We report FLOPs in the unit of multiply-adds (MACs).”。透明化是学术可比性的基石
  • 推荐做法:在计算机视觉领域,近年来越来越多的论文倾向于报告MACs(乘加运算次数)而不是传统的FLOPS(乘加分开)。因为MACs更贴近硬件(如GPU、NPU)实际执行的操作。如果你有选择权,我建议你报告GMacs(十亿次乘加运算),即不乘以2的profile结果除以1E9。这样既避免了歧义,也更符合工业界的习惯。

4.2 场景二:内部模型选型与优化

当你在公司或项目组内部,为了选择或优化一个模型时,目标是为了预测真实部署时的性能

  • 一致性优先:选定一种计算方式(例如,直接profile目标尺寸,不乘2),然后所有候选模型都用这套完全相同的流程去计算。这样得到的数字虽然绝对值可能和论文里的对不上,但在你们内部是绝对可比的。A模型8 GMacs,B模型12 GMacs,那么A的计算量就是比B少50%,这个结论是可靠的。
  • 贴近真实输入:你的profile输入尺寸应该尽可能接近实际应用中的图像尺寸。如果你的应用里图片会被预处理成640x640,那就用这个尺寸去测。不要用官方的32x32缩放,直接用640x640输入得到的结果更能反映真实负载。
  • 关注相对值,而非绝对值:内部优化的核心是看变化。比如你剪枝了一个模型,用同一套方法测出来FLOPS从10 GMacs降到了7 GMacs,那就是减少了30%的计算量。这个优化比例是准确的,比单纯追求一个“标准”的FLOPS数值更有意义。

4.3 场景三:为特定硬件部署进行评估

这是最需要小心的一步。硬件(如手机芯片、边缘计算盒子、自动驾驶域控制器)的算力通常以 FLOPSTOPS (Tera Operations Per Second) 来标称。这里的关键是搞清楚硬件厂商说的“操作”是什么。

  • 与硬件厂商对齐定义:联系硬件厂商的工程师或查阅其SDK文档,确认他们的算力指标(如 4 TOPS)中的“O”(Operation)指的是什么。是指一次MAC算一个操作?还是指一次乘法或加法各算一个操作?这一点至关重要。
  • 实测验证:即使对齐了定义,理论FLOPS和实际端到端推理速度也往往有差距。因为模型在硬件上的性能还受到内存带宽、数据布局、算子优化程度、调度开销等诸多因素影响。最可靠的方法永远是端到端的基准测试。你可以用理论FLOPS作为初步筛选(例如,排除那些理论计算量就远超硬件算力的模型),但最终一定要把模型转换成硬件支持的格式(如ONNX、TFLite、特定引擎),在真实硬件上跑通,测量其每秒处理帧数(FPS)和功耗。
  • 使用硬件厂商的工具:许多芯片厂商会提供自己的模型分析工具(如NVIDIA的TensorRT、高通SNPE、华为MindSpore Lite等)。这些工具给出的“操作数”或“计算量”评估通常更贴合其硬件架构的实际执行方式,比通用的thop更有参考价值。

在我经历的一个安防摄像头项目里,我们就曾因为FLOPS计算口径问题吃了亏。早期用论文里乘2后的数据去评估芯片算力,觉得绰绰有余,结果实际部署后发现帧率不达标。后来才发现,芯片厂商的TOPS是以MACs来计算的。重新用不乘2的口径评估后,才发现模型计算量已经接近芯片瓶颈。所以,在硬件部署阶段,理论计算量评估一定要和硬件平台的实际计量方式挂钩。

5. 超越FLOPS:更全面的模型效率评估视角

最后,我想说FLOPS是一个非常重要的指标,但它绝不是模型效率的全部。如果你只盯着FLOPS,可能会掉入另一个陷阱。

参数量(Parameters):这个指标影响模型大小和内存占用。对于存储空间紧张(如手机APP)或需要快速加载模型的场景,参数量至关重要。一个FLOPS很低但参数量巨大的模型,可能因为内存访问频繁而实际推理速度很慢。

实际推理速度(Latency/FPS):这是终极指标。它受限于计算硬件(CPU/GPU/NPU)、软件框架、内存带宽、批处理大小(Batch Size)等。我见过FLOPS更低的模型,因为算子不被某个推理引擎友好支持,反而跑得比FLOPS高的模型还慢。一定要在目标平台上做端到端的速度测试。

内存占用(Memory Footprint):包括模型权重占用的静态内存和推理过程中中间激活值占用的动态内存。在内存受限的边缘设备上,过高的内存占用会导致程序崩溃或无法与其他任务共存。

能量消耗(Energy Consumption):对于电池供电的设备(如无人机、手机),能耗直接决定了续航。计算密集型操作固然耗电,但频繁的内存读写(访存)也可能是耗电大户。

一个负责任的模型效率评估,应该是一份包含多个维度的“体检报告”:

  1. 理论复杂度:FLOPS/GMacs(明确口径)、参数量(Params)。
  2. 部署友好度:模型文件大小、是否包含非常规算子(如深度可分离卷积、动态上采样等,这些在某些推理引擎上可能优化不佳)。
  3. 实测性能:在目标硬件上的平均推理延迟(毫秒)、吞吐量(FPS)、峰值内存占用。
  4. 精度表现:在验证集上的mAP、精度、召回率等。毕竟,我们不能为了效率无限牺牲精度。

我的习惯是,在项目初期用FLOPS和参数量做快速筛选,排除明显不合适的模型。在中期,对2-3个候选模型,进行目标平台上的原型部署和速度测试。在最终决定前,一定做一个完整的“效率-精度”权衡分析,画出折线图,看看为了提升一点精度,需要付出多少计算量和速度的代价。这套组合拳打下来,你选出的模型才既能在纸上谈兵时站得住脚,也能在真刀真枪的部署中扛得住压力。模型评估从来不是找一个完美的数字,而是建立一套可靠的、贴合业务需求的比较体系。

内容概要:本文围绕“基于改进秃鹰算法的微电网群经济优化调度”展开研究,提出了一种改进的秃鹰搜索算法(BES),旨在解决微电网群在复杂运行环境下的多目标、强约束、非线性及高维经济调度问题。通过引入特定优化策略,增强了基础算法的全局搜索能力和收敛效率,克服了传统智能算法易陷入局部最优的缺陷。研究构建了一个包含分布式电源、储能系统与多元负荷的微电网群调度模型,以最小化系统综合运行成本为核心目标,综合考虑功率平衡、设备出力能力、储能运行特性等多重约束条件。通过仿真实验验证了所提算法在调度精度、稳定性和计算效率方面相较于传统方法具有明显优势,并进一步展示了其在降低能源开支、提升可再生能源消纳水平方面的实际应用价值。; 适合人群:具备一定电力系统基础知识或优化算法背景,从事新能源调度、智能优化算法研究与应用等相关领域的研究生、科研人员及工程技术人员。; 使用场景及目标:①应用于微电网群、综合能源系统等场景下的经济调度优化;②为秃鹰算法及其他群体智能算法的改进、复现与性能对比提供参考范例;③服务于科研仿真、算法验证及工程化应用需求。; 阅读建议:建议读者结合文中提供的Matlab代码实现进行实践操作,重点关注算法改进机制与调度模型的构建逻辑,同时可借助网盘资源获取完整资料,以加深对算法性能表现与应用场景的理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值