1. 为什么"数一遍参数量"这件事值得单独拿出来说
很多人刚接触卷积神经网络时,第一反应是搭个模型跑通就行,等到要估算显存、对比模型大小、或者判断某个轻量化方案到底省了多少参数,才发现自己对"参数量怎么算"这件事其实是一知半解的。我见过不少人在讨论模型规模时,脑子里只有一个模糊的印象:"卷积层参数应该不多,全连接层参数很多。"这话大方向没错,但一旦要精确到具体数字,或者要解释为什么某个模型明明层数很深参数却很少,就卡壳了。
参数量这个指标,说白了就是模型里所有需要学习的权重和偏置的总个数。它直接决定了几件事:模型文件有多大(按 float32 算,一个参数占 4 字节)、训练时优化器要维护多少状态(Adam 还要额外存一阶和二阶动量,相当于参数量的 2 倍)、推理时权重占用的内存下限大概是多少。注意这里说的是"权重占用",不是全部显存占用,因为推理过程中的特征图(激活值)占用往往比权重还大,这一点后面会展开说。
这篇内容想解决的问题很具体:把卷积层和全连接层的参数量计算公式讲透,让你拿到任何一个常见的网络结构,都能手算出每一层的参数量,并且理解每一项为什么这么算。适合的人群包括正在准备模型压缩相关工作的开发者、需要写模型分析报告的同学、以及单纯想把基础知识打扎实的初学者。我不打算只丢两个公式就完事,而是把公式拆开、配上具体数字推演,再把实际计算中最容易出错的几个地方单独拎出来讲。
先给一个直观的结论当作锚点:卷积层的参数量与输入图像的尺寸无关,只和卷积核大小、输入通道数、输出通道数有关;全连接层的参数量则和输入特征维度、输出特征维度绑定,并且通常是大头。这个不对称性是很多人第一次算错参数的根源,也是后面所有讨论的起点。
2. 卷积层参数量的构成拆解
2.1 权重和偏置分别对应什么
一个标准卷积层,需要学习的参数只有两类:卷积核上的权重,以及每个输出通道对应的一个偏置项。偏置这个东西很容易被忽略,因为很多框架在定义卷积层时默认bias=True,你写代码时根本没提它,但它确确实实存在,而且数量等于输出通道数。
权重的数量怎么理解?想象你有一个 3×3 的卷积核,输入是单通道图像,那这个卷积核上就有 9 个权重值。现在输入变成 3 通道(比如 RGB 图像),卷积核为了保证能同时看到三个通道的信息,就会自动扩展成 3×3×3,也就是 27 个权重。这是第一个关键点:卷积核的深度永远等于输入通道数,这不是人为设定的,而是卷积运算的定义决定的——每个输出值都是输入所有通道在同一位置的加权和。
再看输出侧。如果这一层要输出 64 个通道的特征图,就意味着有 64 个不同的卷积核,每个核都独立地在整个输入上滑动,各自产生一张特征图。所以权重总数就是:单个卷积核的权重数 × 卷积核个数 = (卷积核高 × 卷积核宽 × 输入通道数) × 输出通道数。
2.2 一个能直接套用的公式
把上面的推导整理成公式:
卷积层参数量 = 卷积核高 × 卷积核宽 × 输入通道数 × 输出通道数 + 输出通道数后半部分的"输出通道数"就是偏置项。如果这一层关闭了偏置(bias=False),把后面那一项去掉即可。这个公式在 PyTorch 和 TensorFlow 里都通用,因为卷积的数学定义是一样的。
我用一个具体例子走一遍。假设某一层卷积的配置是:输入通道 3,输出通道 64,卷积核 3×3,带偏置。那么:
- 权重数 = 3 × 3 × 3 × 64 = 1728
- 偏置数 = 64
- 总参数量 = 1792
就这么点。对比一下,如果后面接一个输入 512 维、输出 512 维的全连接层,参数量是 512×512 + 512 = 262656。差了将近 150 倍。这个对比能解释为什么很多经典网络在卷积部分堆了几十层,参数量却还没有最后几个全连接层多。
2.3 分组卷积和深度可分离卷积的修正
上面那个公式只适用于标准卷积。一旦用上分组卷积(groups > 1),每个输出通道只和一部分输入通道相连,权重数要除以分组数。公式变成:
分组卷积参数量 = (卷积核高 × 卷积核宽 × 输入通道数 / groups × 输出通道数) + 输出通道数前提是输入通道数和输出通道数都能被 groups 整除。举个典型的例子,输入 256 通道、输出 256 通道、3×3 卷积、groups=256(也就是深度卷积),那权重数就是 3×3×1×256 = 2304,偏置 256,总共 2560。如果不分组,同样的配置是 3×3×256×256 + 256 = 590080。差了 230 倍,这就是深度可分离卷积能大幅压缩参数的核心原因。
深度可分离卷积还要再加一个 1×1 的逐点卷积(pointwise convolution),它本质上就是 groups=1 的 1×1 卷积,参数量是 1×1×输入通道数×输出通道数 + 输出通道数。两部分加起来才是完整的深度可分离卷积层。这里有个细节容易搞混:逐点卷积的输入通道数等于深度卷积的输出通道数,不是原始输入通道数,中间如果做了通道数的调整,要按调整后的值算。
3. 全连接层参数量:简单但容易漏掉细节
3.1 基本公式与那个"隐形的"偏置
全连接层的计算是 y = Wx + b,权重矩阵 W 的形状是(输出维度,输入维度),偏置 b 的长度是输出维度。所以:
全连接层参数量 = 输入维度 × 输出维度 + 输出维度非常简单。输入 784、输出 128 的一层,参数量是 784×128 + 128 = 100480。那个 +128 就是偏置,占总数的比例很小,但如果你的模型里全连接层很多、每层输出维度又不大,偏置的影响就不能完全无视了。我见过有人在统计参数量时只算权重乘法部分,结果和框架打印出来的numel()对不上,排查半天就是漏了偏置。
3.2 卷积转全连接:那个"展平"操作带来的参数暴涨
真正让全连接层参数量失控的,是卷积特征图展平的那一步。假设最后一个卷积层输出 512 通道、7×7 的特征图,展平之后就是 512×7×7 = 25088 维。如果紧跟着一个 4096 维的全连接层,参数量是 25088×4096 + 4096 = 102764544,超过一亿。这就是经典图像分类网络里全连接层动辄上亿参数的来源。
这里的计算有一个隐含前提需要明确:展平后的维度 = 通道数 × 特征图高 × 特征图宽。特征图尺寸由前面的卷积和池化决定,所以全连接层的参数量其实是"间接"受到输入图像尺寸影响的——输入图像变大,经过同样多的下采样后特征图也变大,展平维度随之变大,全连接层参数就跟着涨。这一点和卷积层形成了鲜明对比,也解释了为什么很多现代网络干脆用全局平均池化替代全连接层:把 7×7 的特征图直接平均成一个数,展平维度从 25088 降到 512,后面接分类层的参数量自然也降下来了。
3.3 全局平均池化为什么能省掉一大块参数
接上文的例子,如果不用那个 4096 维全连接层,而是在 512×7×7 的特征图上做全局平均池化,得到 512 维向量,再接一个 512→1000 的分类层,参数量是 512×1000 + 1000 = 513000。和之前的一亿多相比,几乎可以忽略。
这个替换的价值不只是省参数,还减少了过拟合风险,因为全连接层那种密集连接太容易记住训练集的噪声了。不过要客观说,全局平均池化也不是万能的,它假设每个通道的全局平均值有足够判别力,在一些需要保留空间位置信息的任务上(比如密集预测类任务)就不太合适,这时候往往还是保留卷积结构而不是转全连接。
| 结构 | 输入 | 输出 | 参数量 |
|---|---|---|---|
| 全连接层 | 25088 | 4096 | 102764544 |
| 全连接层 | 4096 | 4096 | 16781312 |
| 全连接层 | 4096 | 1000 | 4097000 |
| 全局平均池化 + 全连接 | 512 | 1000 | 513000 |
这张表能直观看出,一个经典结构里全连接部分的总参数超过一亿两千万,而换成池化方案后只剩五十万级别。差距不是靠调几个超参数能补上的,是结构层面的选择。
4. 完整网络的逐层手算流程与对照验证
4.1 拿一个具体结构从头算到尾
光看公式不够,我习惯拿一个简化版的结构完整手算一遍,再和框架的输出对照。假设输入是 3×32×32 的图像,结构如下:
- 卷积层 conv1:3×3 核,输入 3 通道,输出 32 通道,步长 1,padding 1
- 池化层 pool1:2×2 最大池化,步长 2
- 卷积层 conv2:3×3 核,输入 32 通道,输出 64 通道,padding 1
- 池化层 pool2:2×2,步长 2
- 展平
- 全连接层 fc1:输入展平维度,输出 256
- 全连接层 fc2:256 → 10
先算特征图尺寸的变化。conv1 用 padding=1、kernel=3、stride=1,输出尺寸保持不变,还是 32×32。pool1 之后变成 16×16。conv2 同样保持尺寸,16×16。pool2 之后变成 8×8。所以展平维度是 64×8×8 = 4096。
逐层参数量:
- conv1:(3×3×3×32) + 32 = 864 + 32 = 896
- conv2:(3×3×32×64) + 64 = 18432 + 64 = 18496
- fc1:4096×256 + 256 = 1048576 + 256 = 1048832
- fc2:256×10 + 10 = 2560 + 10 = 2570
总计 = 896 + 18496 + 1048832 + 2570 = 1070794。可以看到,仅 fc1 一层就占了全部参数的 98%。这个比例和很多真实网络的分布是一致的,也是"卷积层参数少、全连接层参数多"这个结论的量化依据。
4.2 用代码核对,别相信手算
手算完一定要用代码验证,这是我最强调的习惯。用 PyTorch 写一小段:
import torch import torch.nn as nn model = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Flatten(), nn.Linear(4096, 256), nn.Linear(256, 10), ) total = sum(p.numel() for p in model.parameters()) print("总参数量:", total) for name, module in model.named_modules(): if isinstance(module, (nn.Conv2d, nn.Linear)): print(name, sum(p.numel() for p in module.parameters()))输出应该和手算的 1070794 完全一致。如果不一致,通常是两个原因:一是特征图尺寸算错了(padding 或 stride 理解有偏差),二是漏了偏置或者某层的输入维度填错。我建议每加一层就打印一次形状,用torchsummary之类的工具或者手动跑一个 dummy 输入,把每层的输出 shape 打出来,对着看展平维度对不对。
提示:
p.numel()返回的是张量元素个数,也就是标量参数的数量。对于权重矩阵,它等于各维度乘积,不要用p.shape手动乘,容易在多维情况下出错。
4.3 特征图尺寸的计算不能含糊
很多人算参数出错,根子不在参数公式,而在特征图尺寸算错了,导致展平维度错、全连接层输入维度错,最后差出一大截。卷积输出尺寸的通用公式是:
输出尺寸 = floor((输入尺寸 + 2 × padding - kernel_size) / stride) + 1这个公式必须记牢。几种常见配置下的结果可以背下来当直觉:kernel=3、padding=1、stride=1 时输出尺寸不变;kernel=3、padding=0、stride=1 时尺寸减 2;kernel=3、padding=1、stride=2 时尺寸减半(向下取整);kernel=2、padding=0、stride=2 时尺寸也减半。池化层同理,MaxPool2d(2) 默认 stride 等于 kernel,所以尺寸减半。
有个坑要提醒:floor 的存在意味着奇数尺寸向下取整。比如输入 7×7,经过 stride=2 的 3×3 卷积,输出是 floor((7+2-3)/2)+1 = 4,不是 3.5。这种边界情况在网络比较深的时候会累积,最后展平维度和你想的不一样,全连接层直接报维度不匹配。
5. 实际计算中最常踩的几个坑
5.1 只算权重不算偏置,对不上框架输出
这是最高频的错误。卷积层有输出通道数个偏置,全连接层有输出维度个偏置,BatchNorm 层还有可学习的缩放和偏移参数(每个通道 2 个)。如果你的模型里 BN 层很多,光是把 BN 的参数漏掉,总量就能差出可观的一截。BN 层还有个细节:训练时它会统计均值和方差,推理时用的是滑动平均后的值,这两部分在 PyTorch 里分别存在running_mean和running_var中,属于 buffer 而不是 parameter,model.parameters()不会把它们算进去。所以用parameters()统计出来的数字会比实际保存的模型文件小一点点,讨论参数量时一般以parameters()为准,但如果你在算文件大小,得把 buffer 也考虑进去。
5.2 混淆参数量和计算量
这两个概念经常被混着说,但完全是两回事。参数量是模型有多少个可学习的数,计算量(通常用 FLOPs 或 MACs 衡量)是推理一次要做多少次乘加运算。一个卷积层的参数量可能只有几千,但如果输入特征图很大,计算量可以非常吓人。反过来,全连接层参数量巨大,计算量也不小,但它的计算量和输入尺寸是线性关系,而卷积的计算量对空间尺寸很敏感。
举个具体对比:一个 3×3 卷积,输入输出都是 256 通道,参数量是 3×3×256×256 + 256 = 590080,而它作用在 56×56 的特征图上时,计算量(MACs)约为 56×56×3×3×256×256 ≈ 18.5 亿次乘加。参数量不到六十万,计算量却是十亿级别。所以只盯着参数量做轻量化设计是不完整的,这也是为什么移动端模型评估通常要同时看参数量和 FLOPs 两个指标。
5.3 把输入图像尺寸当成影响卷积参数的因素
这个误解非常普遍。我遇到过有人问:"输入图像从 224 换成 320,卷积层的参数会变多吗?"答案是绝对不会。卷积核的大小是固定的,通道数是固定的,输入图像变大只会让特征图的尺寸变大、计算量变大,参数一个都不会多。真正受输入尺寸影响的是展平之后的全连接层——如果输入变大导致最终特征图变大,展平维度就变大,全连接层的输入维度跟着变,那部分参数才会增加。而在采用全局平均池化的结构里,输入尺寸对参数量完全没有影响,这也是这类结构的一个隐性优势。
5.4 分组数不整除时的报错与检查
用分组卷积时,输入通道数和输出通道数都必须能被 groups 整除,否则框架直接报错。我在做通道剪枝实验时就踩过这个坑:剪枝后某个层的输出通道变成 63,而下一层的 groups 设的是 4,直接挂了。排查思路是先打印所有层的 in_channels、out_channels、groups,逐个检查整除关系,再确认剪枝策略是否需要按 groups 的倍数对齐通道数。这一点在做结构化剪枝时尤其重要,剪枝的粒度往往要受分组数的约束。
6. 把参数量算清楚之后能做什么
6.1 估算模型文件大小和显存下限
参数量乘以每个参数的字节数就是权重的存储大小。float32 是 4 字节,float16 是 2 字节,int8 量化后是 1 字节。一个一千万参数的模型,用 float32 存大概 40MB,量化到 int8 就是 10MB。这个换算在部署到资源受限的设备时是第一步估算。
但要注意,显存占用不等于权重大小。推理时还需要存放中间层的特征图,这部分占用和 batch size、输入分辨率强相关。一个参数只有几百万的模型,如果输入分辨率很高、batch size 又大,激活值占用可能远超权重。所以显存不够时,先降 batch size 或者输入尺寸,往往比换更小的模型更立竿见影。这个经验是我在实际部署中反复验证过的。
6.2 判断某个改动到底省了多少
有了逐层参数量的能力,你就可以定量评估结构改动的收益。比如把某个 3×3 标准卷积换成深度可分离卷积,参数量从 590080 降到 2560 + 逐点卷积的参数,具体省多少一算便知。再比如把最后的全连接层换成全局平均池化,参数直接降两个数量级。这种定量分析比"感觉变小了"靠谱得多,也方便你在方案对比时拿出有说服力的数字。
6.3 定位参数冗余集中的层
逐层算一遍参数量,按大小排序,很快就能看出参数集中在哪些层。绝大多数情况下,排名靠前的都是全连接层或者靠近输出端的大通道卷积层。知道冗余在哪里,压缩的着力点就明确了。如果一上来就盲目对所有卷积层做剪枝,很可能费了半天劲,总参数量降了不到百分之五,因为大头根本没动。
我在实际做模型瘦身时的顺序一般是:先看全连接层能不能用池化替代,再看大通道卷积能不能用分组或可分离卷积重构,最后才考虑细粒度的剪枝和量化。这个顺序的依据就是参数量的分布,先动占比最大的部分,性价比最高。当然,每改一处都要重新验证精度,结构和精度的平衡只能靠实验来定,没有一劳永逸的公式。