1. 先搞清楚一件事:参数量和计算量不是一回事
我最早整理这份卷积核参数计算公式,是因为一次模型文件大小对不上账。当时我在做一个轻量级分类网络,层结构很简单,卷积核就是常规的3×3和1×1,按理说参数量应该很好算。结果模型导出之后,文件大小比我手算的多了不少,我一度怀疑是框架在导出时往模型里塞了什么额外结构。排查了一圈才发现,问题不在框架,在于我把某个分组卷积当成普通卷积来算了,输入通道和输出通道在分组情况下被我先入为主地代错了。
从那天起,卷积核参数计算对我来说就不再是一行公式的事,而是一套需要区分场景、逐项拆解的速查体系。很多人看到这个问题会觉得简单,毕竟卷积核参数不就是"核的宽乘高、再乘输入通道、再乘输出通道,最后加个偏置"嘛。但实际一深入,分组卷积怎么算、深度可分离卷积为什么要分成两段算、空洞卷积参数到底变不变、全连接层和1×1卷积在参数上是什么关系,每一处都有小坑。
这篇文章就把我日常真正在用的那套速查方法完整写出来。适合这么几种人看:刚入门CNN不久、想把手算参数量和实际模型对上的学习者;需要快速评估网络体积、在边缘设备上判断模型能不能跑得动的工程师;以及准备面试时想把这部分基础概念彻底理清的人。
1.1 我在模型大小对不上账之后才开始整理这份速查
先说说那次翻车经历的具体情况。我用的是PyTorch框架,网络结构里有一段深度可分离卷积,我图省事,直接套用了标准卷积的参数公式,把通道数整个代进去算。当时想的是3×3核、128输入、128输出,也就是3×3×128×128再加上偏置,算出来一个不小的数字。但实际上深度可分离卷积是拆成深度卷积和逐点卷积两层的,参数应该分开算再相加,我一口咬定的"标准公式"在结构上就套错了。
结果模型文件大概比我手算的少了差不多7倍,这个差距很明显,也正是因为差距过大我才意识到公式出了问题。所以后来我把"什么结构套什么公式"整理成了固定表格,不仅仅是记公式本身,也记录公式适用的前置条件。这是我最想强调的一点:卷积核参数计算从来不是一个公式走天下,而是按结构分支走多条路径,边界条件比公式本身更容易出错。
1.2 参数量、显存占用、FLOPs三者之间的关系
在往下展开公式之前,必须先把三个概念分清楚,否则后面很容易把参数计算和性能估算搅到一起。
参数量(Parameters)衡量的是模型存储维度,指的是这个模型里所有可训练权重的总数。它只跟网络结构有关,跟输入分辨率无关——你用320×320还是640×640输入,参数量是一样的。显存占用是模型运行时占的内存,除了参数量本身,还包括中间特征图、梯度、优化器状态等,它跟输入分辨率强相关,所以只能由参数量粗略推导,不能直接画等号。FLOPs(浮点运算次数)衡量的是单次前向推理的计算量,它既依赖网络参数,又依赖输入特征图的空间尺寸。
我见过不少朋友拿着参数量公式去估FLOPs,方向是对的,但少了一半的乘法加法转换,估出来的数字往往会差2倍左右。这篇文章后面会专门写一套从参数量推导FLOPs的快速估算法,但那套方法之所以能用,前提是你已经分清了这三个概念。先把概念理清,再进入公式部分,你会顺很多。
提示:如果你只是在做模型选型,先看参数量;如果你在评估推理延迟,重点看FLOPs;如果你在算训练显存,参数量和FLOPs都要看,但还得额外考虑batch size和优化器类型。
2. 标准卷积核参数公式:一个词一个词拆开看
标准卷积是所有卷积结构里最基础的形态,也是判断其他衍生结构时的参照系。它的参数来源非常直观:一批卷积核在输入特征图上滑动提取特征,每个核负责产生一张输出特征图。
2.1 公式本身和最容易忽略的偏置项
标准卷积的参数量计算公式写成这样:
P = (K_h × K_w × C_in) × C_out + C_out逐项拆开看:
- K_h 和 K_w 是卷积核的高和宽,比如3×3卷积核,这两个数都是3;
- C_in 是输入特征图的通道数;
- C_out 是输出通道数,也就是这一层用了多少个卷积核;
- 最后的 C_out 是偏置项,每个卷积核配一个偏置,所以有几个输出通道就有几个偏置。
括号里的 K_h × K_w × C_in 是一个卷积核需要的权值数量,乘以 C_out 就是全部卷积核的权值,再补上偏置,得到总参数量。
最容易被人忽略的其实是那个"加 C_out"。有些教程写的是 P = K_h × K_w × C_in × C_out,在偏置普遍存在的现代网络里,这个写法严格来说不完整,会少算一笔账。偏置本身数量不大,在小网络里可能只占总参数的0.1%都不到,但如果你在做一个有几十上百层卷积的网络,逐层累加下来的偏置误差也不算小,最后和实际模型对比时就会发现差了几个K。
2.2 三个经典例子:3×3、1×1、5×5
我在速查本上固定放了三个算例,分别对应小核、中等核、以及特殊的1×1核。这三个例子一旦记住,基本可以覆盖日常开发里绝大多数卷积配置。
第一个,3×3卷积,输入是RGB图片,C_in = 3,输出64个通道。代入公式:
P = 3 × 3 × 3 × 64 + 64 = 1,728 + 64 = 1,792第二个,5×5卷积,输入256通道,输出256通道:
P = 5 × 5 × 256 × 256 + 256 = 1,638,400 + 256 = 1,638,656这个数字可以直观看出一个现象:卷积核尺寸稍微变大,参数量就迅速上涨,因为核尺寸是平方级影响。
第三个,1×1卷积,输入512通道,输出128通道:
P = 1 × 1 × 512 × 128 + 128 = 65,536 + 128 = 65,6641×1卷积的参数公式和全连接层是一样的,这个特殊点后面会展开讲。这里先记住它没有空间感受野,只在通道维度上做混合,所以参数量计算中 K_h × K_w 直接就是1。
2.3 没有偏置的卷积怎么算
卷积层可以配置 bias = False,这在很多轻量化网络里很常见。没有偏置时,公式就直接砍掉最后加的那一项:
P = K_h × K_w × C_in × C_out还是用刚才第一个例子,不启用偏置的话,3×3卷积、3通道输入、64输出,参数量就是1,728而不是1,792。
这里有个容易被忽略的细节:有些算子融合方案(比如卷积和批归一化融合)会把偏置吸收进前面的缩放系数里,这时模型文件里的参数计数不含偏置导出,但推理算子里仍然有等效偏置。如果你拿手算结果和框架打印的numel对不上,可以看看偏置开关是不是被框架层自动处理了。
3. 输出特征图尺寸公式:和参数计算配套使用
既然标题是"卷积核参数计算",看起来跟输出特征图尺寸没关系。但实际工程里这两条公式几乎总是成对出现。原因很简单:参数计算只告诉你模型有多重,输出尺寸计算才告诉你这层卷积之后特征图变成了多大,而下一层卷积的输入通道和空间尺寸都依赖这个结果。你算一个多层的网络总参数量时,几乎必然走到输出尺寸这道工序。
3.1 公式和边界情况处理
输出特征图尺寸的标准公式是:
H_out = (H_in + 2P - K) / S + 1 W_out = (W_in + 2P - K) / S + 1其中 H_in、W_in 是输入高和宽,P 是填充(padding),K 是卷积核尺寸,S 是步长。如果填充在左右或上下不对称,Padding 要分开算,但绝大多数框架里都是对称填充。
注意,这个公式在 S 大于1时经常出现除不尽的边界情况。很多深度学习框架默认采用向下取整(floor),也就是余数直接舍弃。比如输入是14×14,卷积核3×3,步长2,填充1,代入算一下:
H_out = (14 + 2 × 1 - 3) / 2 + 1 = 13 / 2 + 1 = 6 + 1 = 7这里 13/2 向下取整得到6,输出是7。如果你用的是向上取整,结果会变成8。框架之间的差异就在这里,PyTorch里默认是floor,TensorFlow里如果显式配置了ceil_mode = True,才会变成向上取整。做手算对比时,最好先确认你用的框架在哪一种模式。
3.2 步长、填充对参数量的间接影响
步长和填充不会直接改写卷积核参数公式,因为卷积核本身还是那个尺寸、那个通道数。但它们会通过改变输出特征图的H_out和W_out,间接影响下一层卷积的输入空间尺寸,进而影响下一层的C_in和整体结构。
打个比方,你用步长2的3×3卷积替换掉步长1的卷积,参数量完全不变,但特征图尺寸直接减半,后续层接进来的计算量会大幅下降。这也是为什么很多轻量化网络喜欢用步长卷积做下采样而不是池化——下采样效果类似,但参数上不增加额外开销,网络还能保持整体可训练。
我在实际搭网络时,一定会先手算每层的输入输出尺寸,再决定下一层的填充和步长。一旦某一层尺寸不符合预期,后续层的参数公式虽然还成立,但整个网络的通道配比会错位,模型初始化阶段就会报维度不匹配错误。这类错误排查起来非常麻烦,不如在设计阶段就逐层核对。
提示:推荐把输出尺寸公式写在参数公式旁边,每次算完参数顺手把特征图尺寸也算一下,两步一起做能省掉很多后面调试维度的时间。
4. 分组卷积、深度可分离卷积、空洞卷积:衍生家族的参数速查
标准卷积公式只是基础,实际网络里大量使用各种衍生卷积。这一节是整篇文章的速查重点,我把每个衍生结构的公式和适用场景都列清楚,并配上算例。
4.1 分组卷积:参数减少G倍,别算错除法
分组卷积把输入通道分成G组,每组拥有独立的一批卷积核。直观理解是:原来一个卷积核要跨全部C_in通道做卷积,现在只在 C_in/G 个通道上做,之后各组结果在通道维度拼接起来。
分组卷积的参数量公式是:
P = (K_h × K_w × (C_in / G)) × C_out + C_out注意,是把C_in除以G,不是把C_out除以G,这个顺序错一点点,结果就会天差地别。从公式看,G越大,参数越少,G等于C_in时就变成了极端情况——每组只有一个输入通道,卷积只在单通道上做空间卷积,这就是深度卷积,是深度可分离卷积的一半。
举一个具体算例。输入256通道,输出512通道(约等于将通道翻倍),卷积核3×3,分组数G = 8:
P = 3 × 3 × (256 / 8) × 512 + 512 = 3 × 3 × 32 × 512 + 512 = 147,456 + 512 = 147,968如果同一配置不做分组:
P = 3 × 3 × 256 × 512 + 512 = 1,179,648 + 512 = 1,180,160可以看出,分组卷积的参数量正好大约是普通卷积的1/8,和G值一致。
分组卷积在实际网络中出现得非常多,最典型的是某个轻量化网络结构里的通道混洗操作,里面就广泛用了分组卷积来控制参数规模。手算这类网络时,最容易踩的坑就是把输入通道和输出通道混在一起想成"总共256通道,分成8组,每组32通道",然后顺手把输出也除8了。记住,输出通道数 C_out 是整个卷积层决定的输出规模,不用分组再分一次。
4.2 深度可分离卷积:DW和PW分开算
深度可分离卷积由两部分组成:深度卷积(Depthwise Convolution,简称DW)和逐点卷积(Pointwise Convolution,简称PW)。
深度卷积的每个卷积核只作用在一个输入通道上,相当于分组数等于输入通道数的分组卷积。它的参数公式:
P_dw = K_h × K_w × C_in + C_in(有偏置)逐点卷积本质是1×1卷积,作用是在深度卷积之后把各通道信息结合起来。它的参数公式:
P_pw = 1 × 1 × C_in × C_out + C_out所以深度可分离卷积的总参数量:
P_dsconv = P_dw + P_pw用一个完整的算例来演示。输入128通道,输出128通道,使用3×3卷积核。
深度卷积部分:
P_dw = 3 × 3 × 128 + 128 = 1,152 + 128 = 1,280逐点卷积部分:
P_pw = 1 × 1 × 128 × 128 + 128 = 16,384 + 128 = 16,512总参数量:
P_dsconv = 1,280 + 16,512 = 17,792而标准3×3卷积,输入128、输出128:
P_standard = 3 × 3 × 128 × 128 + 128 = 147,456 + 128 = 147,584深度可分离卷积的参数大约是标准卷积的12%,差距正好在8倍量级附近。这也是为什么它成为移动端网络里的常客。
我在实战中总结出的经验是:算这一类网络时,最好先分别写出DW层和PW层的参数,再相加,不要合并成一步。合并过程中你很容易漏掉DW层的偏置,或者把PW层的C_in误写成上一层的输出尺寸。分开算虽然显得啰嗦,但多写两步能避免后面全盘对不上账。
4.3 空洞卷积与转置卷积:参数不变,别被输出尺寸迷惑
空洞卷积(Dilated Convolution)通过在卷积核元素之间插入空洞,扩大感受野,但它没有新增任何可学习参数。它的参数公式和标准卷积完全一样。区别在于等效卷积核尺寸变了:
K_eff = (K - 1) × dilation + 1这会影响输出特征图的尺寸计算,但不会影响参数量。比如3×3卷积核,dilation = 2,等效核尺寸是:
K_eff = (3 - 1) × 2 + 1 = 5感受野范围是5×5,但参数量还是9个权值加1个偏置。这个"感受野变大但参数不涨"的特性,让空洞卷积特别适合在语义分割任务里用,既能保证感受野足够大,又不让模型体积失控。
转置卷积(Transposed Convolution/Deconvolution)的情况也类似:它的参数量公式还是标准卷积那一套,因为权重维度并没有变化,只是计算方向相当于正向卷积的"反向"。转置卷积的输出尺寸公式不同,常规写法是:
H_out = (H_in - 1) × S - 2P + K但参数这件事,它跟标准卷积完全一致。换句话说,你看到"反卷积"这个名字时千万不要以为它参数会翻倍,它本质上只是在信号的流向上做了反转,可学习权重的数量并没有增加。
加上常见的注意力结构,比如Squeeze-and-Excitation模块里的两个全连接层,实际上也在用1×1卷积或全连接参数公式,可以直接套之前的标准公式来算,不需要额外记忆。
5. 从参数量到模型大小:存储换算与FLOPs估算
参数量算出来之后,下一步通常是估算模型文件大小。这个换算比很多人以为的要简单,但精度问题容易踩坑。
5.1 不同精度下参数占用的字节数
模型参数在磁盘上的存储大小取决于精度。最常用的精度和字节数对应关系如下:
| 精度 | 每个参数占字节数 | 备注 |
|---|---|---|
| FP32 | 4 bytes | 训练默认精度 |
| FP16 | 2 bytes | 推理常用,可减少一半存储 |
| BF16 | 2 bytes | 训练加速常用,动态范围更大 |
| INT8 | 1 byte | 量化推理,压缩最明显 |
假设某个模型总参数量是25M,用FP32存,文件大小大约是 25 × 4 = 100MB;用FP16存,直接降到50MB;量化到INT8,就只有25MB左右。这个换算方法很实用,每次我在评估一个预训练模型能不能塞进目标设备时,脑子里就是跑一遍这个乘法。
注意,实际模型文件往往还包含优化器状态、元数据等信息,所以文件大小会比纯参数量算出来的略大。但在模型导出后做纯推理时,优化器状态会被剥离,这个时候参数计算和文件大小的对账关系就比较准了。
5.2 从参数量估算FLOPs
FLOPs衡量的是模型做一次前向推理需要的浮点运算次数。它的估算公式在卷积层里可以写成:
FLOPs = 2 × K_h × K_w × C_in × H_out × W_out × C_out乘2是因为一次卷积要做一次乘法和一次加法。很多论文里直接报的是MACs(乘加次数),也就是不加2那倍的K_h×K_w×C_in×H_out×W_out×C_out。这两者容易混淆,看论文的数字时先搞清楚它说的是FLOPs还是MACs。
还是用那个3×3卷积的例子,输入3通道,输出64通道,假设输入图像224×224,步长1,填充1,那么输出也是224×224:
MACs = 3 × 3 × 3 × 224 × 224 × 64 = 86,671,360 FLOPs = 2 × 86,671,360 = 173,342,720这个数字说明,FLOPs和参数量的关系不是简单的倍数,因为H_out和W_out会跟着输入分辨率走。所以不要试图用一个固定的系数从Paras直接换算FLOPs。正确做法是先确定输出特征图尺寸,再代入公式计算。
5.3 一个简洁的FLOPs速查公式
我日常速查时其实不一项一项算,而是用这个简写公式:
FLOPs_conv = 2 × Paras_per_kernel × H_out × W_out × C_out其中 Paras_per_kernel = K_h × K_w × C_in,也就是每个卷积核的权值数。这样就把前面算参数时的中间结果直接复用了,减少重复劳动。
对于全连接层,FLOPs计算和卷积层类似,只是没有空间维度:
FLOPs_fc = 2 × C_in × C_out算一个例子:全连接层从4096降到1024,MACs是 4096×1024=4,194,304,FLOPs是8,388,608。
提示:参数量以KB/MB衡量,FLOPs以百万(M)或十亿(G)为单位,二者不能放在同一个数量级里比较。我在网上看到过不少"参数量50M"和"FLOPs 50M"搞混的讨论,抓住单位就能避开这种坑。
6. 完整手算一遍:一个分类小网络的参数全流程
前面公式拆了很多,这一节把它们合起来使用:从零手算一个很小的分类网络,逐步算出每一层参数,再累加得到总参数量。这就像把速查表从头到尾实际跑一遍,后面你换成自己的网络时,替换数字就行。
6.1 网络结构定义
假设网络输入是 3×32×32 的RGB图像(比如某个小型图像分类任务)。网络结构如下:
- Conv1: 3×3卷积,3通道输入,16通道输出,padding=1,步长=1,带偏置
- ReLU + MaxPool: 2×2池化,步长2
- Conv2: 3×3卷积,16通道输入,32通道输出,padding=1,步长=1,带偏置
- ReLU + MaxPool: 2×2池化,步长2
- 全局平均池化(Global Average Pooling)
- 全连接层:输出10类
池化不会引入任何可学习参数,这一点先记住,遇到池化直接跳过参数计算。
6.2 逐层手算过程
先看Conv1:
P_conv1 = 3 × 3 × 3 × 16 + 16 = 432 + 16 = 448同时算输出尺寸:输入32×32,padding=1,卷积核3,步长1:
H_out1 = (32 + 2×1 - 3) / 1 + 1 = 32所以Conv1输出 16×32×32。池化后变成 16×16×16。
Conv2:
P_conv2 = 3 × 3 × 16 × 32 + 32 = 4,608 + 32 = 4,640输出尺寸同样保持16(padding=1,步长1),池化后变成 32×8×8。
全连接层输入维度是 32×8×8 = 2048,输出维度10:
P_fc = 2048 × 10 + 10 = 20,480 + 10 = 20,490总参数量:
P_total = 448 + 4,640 + 20,490 = 25,578换算FP32文件大小:25,578 × 4 = 102,312 bytes,约等于100KB。这个模型非常小,说明手算得和实际框架打印出来的数字一致时,基本可以确认公式没问题。
6.3 与框架实际测算结果对比的思路
如果你用PyTorch,跑这么一段代码可以拿到实际参数量:
def count_parameters(model): return sum(p.numel() for p in model.parameters()) model = YourSmallCNN() total = count_parameters(model) print(f"Total parameters: {total}")注意,如果模型启用了偏置,parameters()里会把权重和偏置都包含进去。你要同时打印每一层的参数形状:
for name, param in model.named_parameters(): print(name, param.shape, param.numel())这样就能和手算的每一层数值逐项核对。我遇到这种情况时,先看Conv1是不是448,再看Conv2是不是4,640,最后看全连接是不是20,490。哪一层对不上,问题就出在哪一层,不需要整个模型重新查。
我自己反而建议初学者先在纸上把每层参数都算出来,再跑代码确认,因为手算过程中你会发现很多对维度理解的偏差,而这些偏差在代码里往往会被框架自动抹平,反而不容易暴露。
7. 踩坑复盘与速查技巧:我整理成了一张表
这一节把我在实际项目里遇到的问题和最终沉淀的速查方式分享出来。每一条都是实实在在的对账经验。
7.1 三个最容易出错的位置
第一个坑是"偏置到底有没有"。很多预训练模型的卷积层确实不带偏置,因为后面接了批归一化层,批归一化的可学习参数可以覆盖偏置的功能。你算参数时如果给这一层加了偏置,最后模型总参数就会比实际多出C_out个。解决方法很简单:看模型定义里conv层的bias参数是True还是False,然后决定公式里加不加末尾的C_out。
第二个坑是分组卷积的通道除法。分组卷积的C_in除以G这个操作,我见过反着除的、也见过把C_out也一起除的,结果都和真实数量差得离谱。判断自己有没有算错,可以先看分组数G是不是整数地除尽C_in,如果除出来不是整数,那公式一定代错了。
第三个坑是深度可分离卷积的合并式求法。有些人觉得"反正DW加PW最终等价于一种卷积",图省事直接拿标准公式算,结果偏差巨大,原因就是没有把C_in和C_out分开对待。我现在的习惯是深度可分离卷积永远列两行,先算DW,再算PW,最后求和,不写合并式。
表格速查放这里,平时不确定就瞄一眼:
| 卷积类型 | 参数量公式 | 备注 |
|---|---|---|
| 标准卷积 | (K_h×K_w×C_in)×C_out + C_out | 无偏置则去掉+C_out |
| 分组卷积 | (K_h×K_w×(C_in/G))×C_out + C_out | G为分组数 |
| 深度卷积 | K_h×K_w×C_in + C_in | 即分组数=C_in的分组卷积 |
| 逐点卷积 | 1×1×C_in×C_out + C_out | 本质为1×1卷积 |
| 深度可分离 | DW参数 + PW参数 | 绝不可合并成一行 |
| 空洞卷积 | 同标准卷积 | 感受野变大但参数不变 |
| 转置卷积 | 同标准卷积 | 参数不因"反向"而翻倍 |
7.2 我的速查方式:把公式记成一句话
说一个我常用的记忆方式。我记标准卷积公式时,脑子里只记一句话:"一个卷积核有多少参数,就看他能接触到多少输入值。"一个核滑过一个输出位置时,要对输入特征图上的一个 K_h×K_w×C_in 的小块做加权求和,这一个小块里的元素数量就是单个卷积核的权值数。有了这句话,其他变体都很容易推导:分组卷积只是让每个核只接触1/G的通道,深度卷积是让每个核只接触一个通道,1×1卷积是让每个核只接触一个像素。
输出尺寸公式我也有个记法:"有效窗口尺寸减掉之后,除以步长,加回自己。"先把填充后的尺寸想清楚,再把卷积核想象成一个在图上滑动的窗口,步长决定你隔多远取一个值,公式就顺带背下来了。
这套速查方式陪我过了好几个不同框架的项目,从PyTorch到TensorFlow、从MobileNet系列到轻量语义分割网络,手算结果和框架打印数值都能对得上。
最后说一句个人习惯:遇到不确定的结构,我不会靠记忆硬算,而是把这一层单独拉出来跑一个小测试,打印参数形状和数量,再回头比对公式。宁可多花两分钟写一段三行的确认代码,也不想让一个偏置项的错误藏在几十个卷积层里,等最后模型训练起来才发现。这个习惯帮我省下的排查时间,远比当初整理这张速查表花的时间多。