ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

手推CNN复杂度公式:从FLOPs到工程实践

2026/10/3 13:11:46 拓冰建站 浏览量
手推CNN复杂度公式:从FLOPs到工程实践 1. 为什么我们要手推一遍复杂度公式很多人第一次接触卷积神经网络时看到O(N²·K²)或者O(C_in·C_out·H·W·K·K)这种式子第一反应是背下来就好。但实际做项目的时候这个公式根本不是用来背的——它是用来估算显存、判断能不能上更大的batch、决定模型剪枝后到底能省多少FLOPs、以及对比卷积和全连接计算量的关键工具。如果你只是囫囵吞枣地记住结论遇到稍微变形的情况比如空洞卷积、分组卷积、深度可分离卷积就会懵住。我自己最开始也是直接套公式直到有一次需要在一张老旧的GPU上部署一个实时检测模型发现推理帧率怎么都上不去。用torchsummary看参数量好像也没多大后来手动算了一遍某个层的FLOPs才发现瓶颈根本不在参数量而在特征图尺寸和卷积核尺寸带来的乘法次数。从那以后我养成了一个习惯每设计一个网络结构先手推一遍复杂度心里有数再上机器。这篇文章不打算列一堆复杂的数学符号吓人而是用最朴素的方式把卷积操作的复杂度公式从零推导一遍。我会用一个非常具体的例子贯穿全文同时解释每一步推导背后的物理意义以及在真实工程中这个公式会怎么帮你做决策。2. 从一次最简单的乘法开始2.1 单次卷积操作到底做了几次乘法先忘掉批量维度和通道维度只看最基础的一次二维卷积。假设输入是一张单通道的H×W灰度图卷积核大小是K×K步长为1padding为0。卷积核每次滑到一个位置做的事情是把K×K个输入像素和K×K个权重分别相乘然后把它们加起来。这个乘加操作通常被称为MACMultiply-Accumulate。一次标准的卷积位置需要K²次乘法和K²−1次加法。在工程上我们一般只统计乘法次数因为加法在硬件上几乎不增加额外延迟尤其在GPU和NPU上乘加被融合成一条指令。所以单次卷积位置的乘法次数 K²。这里面有一个经常被忽略的点偏置项bias不算乘法只算一次加法。虽然它不影响FLOPs的主导项但在手推极小的网络时别忘了它。2.2 卷积核在一个维度上能滑动多少次接下来算输出特征图的尺寸。不填充、步长为1时输出尺寸是H_out H − K 1W_out W − K 1每个输出像素对应卷积核的一个滑动位置。因此单通道输入、单卷积核的情况下整个输出特征图需要进行的卷积位置总数是H_out × W_out把这两步合起来单输入通道、单卷积核、无padding、步长为1的卷积总乘法次数总乘法数 (H − K 1) × (W − K 1) × K²这里必须注意一个常见的错误有些人会写成H×W×K²理由是输入有H×W个像素每个像素都参与了K²次乘法。这在padding0且步长为1时是不成立的——因为边缘像素被卷积核滑过的次数和中右区域像素不一样真正准确的计算方式是看输出像素数乘以K²。2.3 一个具体数字带来的直觉假设输入是32×32卷积核是5×5那么输出尺寸(32−51)×(32−51) 28×28 784单位置乘法数5² 25总乘法数784×25 19600这个数字本身不重要重要的是它和什么对比。如果是全连接层输入1024维、输出1024维乘法数是1024×1024 ≈ 1048576大约是上面卷积的53倍。这就是为什么卷积网络在图像任务上能轻松超过全连接网络——同样的输入规模卷积的计算代价低得多而且还能保持空间局部性。很多初学者会疑惑为什么卷积核越小越省计算从公式看很直接K²是平方增长3×3和5×5的差距是9比25差了近3倍。这一条在后面设计网络时会反复用到。3. 把通道维度加进来真正的CNN复杂度公式3.1 多输入通道和多输出通道的含义现实中的卷积输入不是单通道灰度图而是多通道特征图比如RGB图像是3通道网络中间层常常是128、256甚至512个通道。输出通道数等于卷积核的个数每个卷积核负责生成一张输出特征图。关键点在于每个输出通道都有一组独立的卷积核每组卷积核的深度等于输入通道数。听起来有点绕我拆开说。假设输入特征图形状是C_in × H × W输出特征图形状是C_out × H_out × W_out卷积核尺寸是K×K。对于每个输出通道o它对应的卷积核形状是C_in × K × K这个卷积核会对输入的所有C_in个通道分别做二维卷积然后把结果加在一起得到一个输出通道的特征图。因此每生成一个输出像素需要做的乘法次数是C_in × K²。3.2 完整公式的逐步推导生成一个输出像素需要C_in × K²次乘法。一张输出特征图有H_out × W_out个像素所以一个输出通道总共需要C_in × K² × H_out × W_out一共有C_out个输出通道因此总乘法次数是总FLOPsMAC计数 C_out × C_in × K² × H_out × W_out这通常就是CNN层复杂度公式的核心形式。如果输出特征图尺寸跟输入接近比如same padding且步长为1可以近似写成≈ C_out × C_in × K² × H × W3.3 用具体例子验证公式下面通过一个实际数字验证这个公式的作用。假设输入是3×224×224第一层卷积使用64个7×7卷积核padding3步长为2。先算输出尺寸H_out (224 2×3 − 7) / 2 1 112输出特征图形状是64×112×112。套用完整公式总乘法数 64 × 3 × 7² × 112 × 112 64 × 3 × 49 × 12544 64 × 3 × 614656 64 × 1843968≈ 1.18亿单层就做了1.18亿次乘法这只是ResNet或VGG这类网络几十层中的一层。从这里能直观感受到为什么深度学习训练那么吃算力——每一层都是上亿级别的乘法操作反向传播还要再乘2到3倍的量。3.4 FLOPs与MACs的区别工程中必须分清很多资料把FLOPs和MACs混着用但在工程上必须严谨区分MACs每秒或每次前向传播的乘加次数。一次乘加算1个MAC。FLOPs浮点运算次数一次乘加通常计为2次FLOPs1次乘法和1次加法。所以上面算出来的1.18亿是MACs如果换算成FLOPs要乘以2约为2.36亿。PyTorch的thop库和NVIDIA的pytorch-OpCounter默认输出的是MACs而很多论文里写的是FLOPs有时实际指的是MACs这个差异在复现论文时特别容易踩坑。建议看别人代码里统计复杂度的输出时先搞清楚它统计的是MACs还是FLOPs否则对比模型A和模型B谁更轻量时可能会出现2倍的偏差直接影响结论。4. 标准公式的三个重要变体空洞卷积、分组卷积、深度可分离卷积实际项目中你很少能用到纯粹的标准卷积。几乎每个成熟网络都会用空洞卷积、分组卷积或深度可分离卷积。这些变体的复杂度公式不能靠死记但可以用标准公式推导出来。4.1 空洞卷积Dilated Convolution的复杂度空洞卷积引入了一个参数dilation rate膨胀率记为d。它不改变卷积核的参数量但会扩大卷积核覆盖的输入区域。等效卷积核尺寸为K_eff (K − 1) × d 1比如一个3×3卷积核dilation rate2时等效感受野是5×5但真正参与乘法的仍然只有9个位置。复杂度公式变成FLOPs C_out × C_in × K² × H_out × W_outK还是原始卷积核尺寸不是等效尺寸。因为参与乘法的参数数量没有变只是它们之间的间隔变大了。输出特征图的尺寸需要按等效尺寸重新计算H_out (H 2×padding − K_eff) / stride 1这里很多人会算错把K_eff代到乘法次数里去结果高估了空洞卷积的计算量。实际上空洞卷积的FLOPs通常和普通卷积一样只是感受野更大这也是它在语义分割任务中被广泛使用的原因——在不增加计算量的前提下扩大视野。4.2 分组卷积Grouped Convolution的复杂度分组卷积的理念是把输入通道和输出通道都分成g组每组独立做标准卷积最后把结果沿通道维度拼接起来。具体来说每组输入通道数是C_in/g每组输出通道数是C_out/g卷积核尺寸不变。整个分组卷积的总FLOPs为FLOPs g × (C_out/g) × (C_in/g) × K² × H_out × W_out化简一下FLOPs C_out × C_in × K² × H_out × W_out / g从这个式子能看出来分组卷积的标准卷积相比计算量减少到1/g。这解释了为什么ResNeXt和MobileNet等轻量级网络大量使用分组卷积——同样的通道数分组数越大计算量越低。需要强调的是分组卷积虽然省算力但也会带来信息流通的问题不同组之间的特征互不往来所以很多网络在分组卷积后面加一个1×1卷积pointwise convolution来混合通道信息这正是MobileNet的核心思想。4.3 深度可分离卷积Depthwise Separable Convolution的复杂度深度可分离卷积把标准卷积分成两步先做深度卷积depthwise convolution再做逐点卷积pointwise convolution。第一步深度卷积。它对每个输入通道单独做K×K卷积不跨通道。输入是C_in个通道每通道的卷积核是K×K输出也是C_in个通道FLOPs为深度卷积FLOPs C_in × K² × H_out × W_out这一步通常不改变通道数只做空间滤波。第二步逐点卷积。它本质上是一个1×1标准卷积卷积核尺寸为1×1输入C_in通道输出C_out通道。FLOPs为逐点卷积FLOPs C_out × C_in × 1² × H_out × W_out两个加起来就是深度可分离卷积的总FLOPs总FLOPs C_in × H_out × W_out × (K² C_out)作为对比标准卷积的FLOPs是标准卷积FLOPs C_out × C_in × H_out × W_out × K²比值是深度可分离 / 标准 (K² C_out) / (C_out × K²)当C_out较大时比如256这个比值大约等于1/K²。也就是说如果用3×3深度可分离卷积替代标准卷积计算量大约能降到原来的1/9。这也是MobileNet系列能在移动端流畅运行的核心原因。4.4 变体对比表格这几个变体的复杂度差异用一张表对比更直观。为了避免分歧统一假设输入输出分辨率均为H×W输入通道数等于输出通道数记为C。卷积类型总FLOPsMACs相对标准卷积的比值标准卷积C_out×C_in×K²×H_out×W_out1空洞卷积C_out×C_in×K²×H_out×W_out1分组卷积g组C_out×C_in×K²×H_out×W_out/g1/g深度可分离卷积C_in×H_out×W_out×(K²C_out)约1/K²当C_out较大时这表格在生产环境中很有用。比如你在做模型轻量化目标是把推理耗时降低到原来的1/5那么把标准3×3卷积改成深度可分离卷积约降到1/9大概率能达标但如果只是把普通卷积改成分组数2的分组卷积降到1/2可能还不够。5. 工程中怎么用这个公式三个真实决策场景这块是重点。公式推完了如果只停留在算纸上价值不大。我更想分享的是在真实项目里这个公式如何帮我做决策。5.1 用FLOPs估算显存占用和推理延迟模型推理的显存占用大头是激活值activation不是权重。权重只占一小部分而每一层输出的特征图在反向传播时都需要保存。假设你现在设计了一个网络层输入是128×56×56输出是256×56×56中间使用3×3卷积batch size32。这一层需要保存的激活值显存大约是128×56×56×4字节 256×56×56×4字节 (401408 802816) × 4≈ 4.8MB乘以batch size 32就是154MB。这还只是一层的激活值。整个网络几十层显存一下子就爆了。如果你在写代码前先手算一下就能提前预判这个结构在当前GPU上可能跑不动应该改用分组卷积或者减少通道数而不是等到CUDA out of memory报错再抓瞎。5.2 判断模型瓶颈算力密集型还是访存密集型有了FLOPs还要除以访存量得到算术强度arithmetic intensity。这个指标能告诉你该优化什么方向。举个具体的例子。ResNet50在224×224输入下总FLOPs大约是41亿MACs权重参数约2550万个对应权重数据约102MB。算术强度41亿 / 102MB ≈ 400 FLOPs/byte对于现代GPU来说这个值属于中等偏上意味着计算密度较高瓶颈主要在计算而不是访存。因此优化方向应该是提高计算效率比如使用Tensor Core、混合精度而不是一味减少权重加载次数。反过来如果你设计的网络FLOPs很低比如1亿但每层特征图特别大重要的事就是访存量会超过计算量GPU利用率上不去。这时候需要考虑融合算子比如把卷积和激活函数融合在一起减少数据在内存和计算单元之间搬移的次数。5.3 剪枝和通道压缩能否省复杂度的直觉判断在做结构化剪枝时我们通常剪掉的是输出通道C_out。从公式看FLOPs C_out × C_in × K² × H_out × W_outFLOPs与C_out是线性关系。如果剪掉一半输出通道FLOPs直接减半。但要注意这层输出的通道数减少后下一层的输入通道数C_in也会跟着减少所以下一层的FLOPs也会缩减。这是一个连锁反应剪掉靠近输入端的通道影响范围比剪掉靠近输出端的通道更大。我经常用这个逻辑跟产品同学解释为什么剪枝到50%稀疏度模型推理速度并没有变成原来的一半因为实际部署时还受内存带宽、算子库、并行粒度的影响FLOPs只是理论上限。这就是公式之外的经验活了。6. 写代码前必知的三个复杂度计算常见坑数完了正的推导和应用最后说说我在实际写评估代码时踩过的几个坑。6.1 计数单位不统一MACs和FLOPs混用前面提到过有些库输出的是MACs有些输出的是FLOPs实际乘2。最经典的是torchsummary和thop的输出就不同。我在对比两个模型谁更轻量时曾经把thop输出的MACs直接和某篇论文里的FLOPs放在一起除以2结果数值差了4倍差点得出完全错误的结论。建议统一使用FLOPs作为论文对比口径自己心里记清楚MACs FLOPs / 2。写工作报告时明确标注单位不要含糊。6.2 忽略1×1卷积的复杂度很多人觉得1×1卷积参数量小、计算量也小不算也罢。但1×1卷积的FLOPs是FLOPs C_out × C_in × 1² × H_out × W_out C_out × C_in × H_out × W_out当输入输出通道数都是512、特征图尺寸是28×28时单层1×1卷积的FLOPs是512 × 512 × 28 × 28 ≈ 2亿这比很多3×3卷积层还要高因为在通道数很大时1×1卷积相当于在通道维度上做全连接计算量不可忽视。MobileNetV2的bottleneck结构里1×1卷积占了总FLOPs的很大比例这就是为什么它叫bottleneck——瓶颈在逐点卷积。6.3 忽略padding和stride对FLOPs的影响标准公式里的H_out×W_out必须是真实输出尺寸不能用输入尺寸代替。在padding0、stride1时输出比输入小但在same padding、stride1时输出等于输入。如果stride2输出直接减半FLOPs也减半。很多人写评估代码时直接拿输入尺寸代进去导致高估了深层网络的FLOPs。深层网络经过多次stride2下采样后特征图尺寸缩小了8倍甚至16倍FLOPs也跟着骤降。如果全用输入尺寸算最后的结果可能高出真实值几十倍。我曾做过一次实验把一个标准ResNet50的FLOPs用输入尺寸直接代公式的方式算出来结果是真实FLOPs的约3倍。这也解释了为什么有时候手算和thop对不上问了一圈才发现是这个细节。7. 手算一次完整网络的复杂度从公式到工具前面理论和坑都讲完了最后带大家完整算一个简单的卷积网络——假设这个网络只有三层卷积来感受一下整个流程。网络结构设定输入3×32×32比如CIFAR-10单张图像第1层Conv 3×3, stride1, padding1, 输出32通道第2层Conv 3×3, stride2, padding1, 输出64通道第3层Conv 3×3, stride2, padding1, 输出128通道第1层输入3通道输出32通道3×3卷积same paddingstride1特征图保持32×32。FLOPs 32 × 3 × 9 × 32 × 32 884736第2层输入32通道输出64通道3×3卷积stride2padding1。输出尺寸H_out (32 2 − 3) / 2 1 16FLOPs 64 × 32 × 9 × 16 × 16 4718592第3层输入64通道输出128通道3×3卷积stride2padding1。输出尺寸8×8。FLOPs 128 × 64 × 9 × 8 × 8 4718592三层加起来884736 4718592 4718592 ≈ 1032万这个数字意味着什么如果你在嵌入式设备上每秒只能做1亿次运算那这个网络处理一帧图像大约需要0.1秒左右——刚好能跑实时但不能跑高清。如果要跑1024×1024的高清输入FLOPs会随着分辨率平方增长直接变成数亿那就必须考虑轻量化方案了。如果你不想每次手算可以用thop这个库快速验证import torch from thop import profile # 定义一个简单网络 class SimpleNet(torch.nn.Module): def __init__(self): super().__init__() self.conv1 torch.nn.Conv2d(3, 32, kernel_size3, stride1, padding1) self.conv2 torch.nn.Conv2d(32, 64, kernel_size3, stride2, padding1) self.conv3 torch.nn.Conv2d(64, 128, kernel_size3, stride2, padding1) def forward(self, x): x torch.relu(self.conv1(x)) x torch.relu(self.conv2(x)) x torch.relu(self.conv3(x)) return x model SimpleNet() input_tensor torch.randn(1, 3, 32, 32) macs, params profile(model, inputs(input_tensor,)) print(fMACs: {macs / 1e6:.2f}M) # 单位百万 print(fParams: {params / 1e6:.2f}M)跑出来的MACs约等于1032万的一半因为thop输出的是MACs不是FLOPs。如果跟手算有细微出入多半是在bias的处理上不影响大局。8. 关于复杂度公式我一直保留的几个个人看法最后说点不太容易写进教科书、但实际很有用的体会。第一公式是近似不是精确预言。FLOPs再低也不代表模型一定能跑得飞快因为现代硬件上还受内存带宽、算子调度、并行度等影响。同样一个FLOPs为1亿的层用PyTorch的conv2d跑和用TensorRT优化过的算子跑实际耗时可能相差一个数量级。所以复杂度公式更适合用来做横向对比和瓶颈分析而不是直接预测毫秒数。第二参数量和计算量是两个维度不能混为一谈。有个经典例子MobileNet的参数量并不比普通卷积少太多但计算量小得多原因是它把通道间的全连接式混合拆成了两阶段稀疏操作。做模型压缩时要明确目标到底是减少参数为了省存储还是减少FLOPs为了省计算这两条路走的方案完全不同。第三手推一次公式能带来的直觉是任何库和工具都给不了的。当你亲手把C_out × C_in × K² × H_out × W_out展开并代入数字之后你会秒懂为什么ResNet要在下采样时加倍通道数、为什么3×3卷积是现代网络的事实标准、为什么深度可分离卷积能大幅降低计算量。这份直觉在后续调网络结构、做NAS搜索或者调试性能瓶颈时价值远超那几分钟手算的时间。如果你正在学习CNN或者准备面试建议先自己找一组真实数字比如ImageNet的3×224×224输入配一个ResNet50的某层把标准卷积、分组卷积、深度可分离卷积各算一遍再把thop的输出对一遍确认自己的推导是对的。这个过程走完了复杂度公式这一关基本就彻底过了。