资讯动态

CNN卷积层与全连接层参数量计算:公式、统计与显存核对

发布时间:2026/10/28 3:04:54 来源:尧图企业网站定制
1. 算清 CNN 卷积层和全连接层的参数量是件比看起来更要紧的事我前后给不少模型做过“体检”最常被问到的问题不是准确率掉了几个点而是“这个模型到底多大、能不能塞进目标设备”。每次到了这一步绕不开的就是CNN 卷积层和全连接层的参数量这本账。它不像训练曲线那样直观但只要算错一次后面显存估算、部署切分、量化方案全都会跟着歪。我见过一个挺典型的场面某开发者拿着剪枝后的模型跟我说参数降了 40%我把named_parameters()打出来一看他统计的是权重张量个数漏掉了偏置和批归一化里的仿射参数实际只降了 31%后面按错误数字去排显存预算跑到第三轮就 OOM 了。这篇东西想解决的就是这类问题。它不针对某一个特定网络而是把“参数量怎么来的、为什么这么算、算出来之后怎么用”这条线完整讲一遍。适合刚接触 CNN 结构、想搞明白summary()里那些数字从哪来的同学也适合已经在做模型压缩、端侧部署需要快速心算一个层甚至整个网络规模的从业者。我会从公式推导讲到手算核对再讲到框架统计、显存换算和踩坑排查中间穿插深度可分离卷积、分组卷积、全局平均池化这些最容易被算错的结构。先把最核心的认知摆出来参数量衡量的是模型“记住”了多少东西计算量衡量的是模型“干了多少活”这两件事完全不同。一个 3×3 卷积把 64 通道压到 128 通道参数量约 7.4 万但如果输入是 224×224它要做的乘加次数是 7.4 万乘上输出空间位置数量级直接飙到三亿多。反过来一个 4096×4096 的全连接层参数是 1677 万比很多小网络的整体还大可它只处理一个向量。所以后面讲参数量的时候我会反复提醒你把它和 FLOPs、显存分开看别混成一团。2. 卷积层参数量的拆解从一行公式到八种变体2.1 普通卷积的参数量公式是怎么长出来的普通二维卷积的参数量公式只有一行Params (k_h × k_w × C_in 1) × C_out括号里那个1是偏置k_h × k_w × C_in是单个卷积核的权重数量。理解这个式子最直观的方式是把一个输出通道想象成“一张完整的特征图”它由C_in张输入特征图分别做二维卷积再相加得到。每张输入图对应的那一片权重就是k_h × k_w个数所以一个输出通道要吃掉k_h × k_w × C_in个权重再加上一个偏置多个输出通道就再乘以C_out。举个具体例子输入 64 通道输出 128 通道卷积核 3×3padding 设为 1 保持尺寸不变那么参数量是(3 × 3 × 64 1) × 128 577 × 128 73856。这个数字非常有代表性你在 ResNet、VGG 的中段会反复看到类似量级。如果嫌算得慢有个心算口诀k² × C_in × C_out 就是权重部分偏置直接加 C_out 个零头。因为偏置的占比通常是千分之一量级做快速估算时可以先忽略真要精确对账时再补上。这里有个特别容易被忽略的事实卷积层的参数量和输入图像的分辨率完全无关。输入从 224×224 变成 512×512这层的参数量一个字节都不会变变的只有计算量和中间激活值的显存。这正是卷积相对全连接最大的优势也是为什么同样的分类网络能直接吃不同尺寸的输入——参数是共享的同一个卷积核在整张图上滑动位置换了权重不变。用生活里的类比卷积层像一个印章盖在纸上哪个位置都是同一个图案全连接层像一屋子人两两握手人数一多握手次数直接爆炸。2.2 偏置、批归一化与那些不叫“参数”的显存占用严格来说bias是可训练参数的一部分但它的存在感常常被两件事冲淡。第一现代网络在卷积后面接批归一化BatchNorm时很多实现会把卷积的bias关掉因为 BN 的平移项会把偏置的效果吸收掉留着纯属浪费。第二如果你用框架自带的统计接口它默认会把 bias 算进去但如果你自己手写累加很容易漏掉。我在做层级别对账时养成了一个习惯先打印m.bias is None确认这层到底有没有偏置再决定公式里加不加C_out。批归一化的参数更需要单独拎出来说。一个BatchNorm2d(C)在affineTrue时有2 × C个可训练参数分别是缩放系数gamma和平移系数beta各占 C 个。但它的state_dict里还会多出running_mean、running_var和num_batches_tracked这三样东西前两个各 C 个元素最后一个是标量。这三样不是可训练参数不参与梯度更新但它们实打实地占显存、占模型文件体积做部署打包的时候一个都跑不掉。所以在“模型到底多大”这个问题上通常要把它们算进去在“多少参数需要更新”这个问题上要把它们排除。两种口径并存不是谁错了是回答的问题不同。我把这三类东西整理成一张表做统计时对着看一眼基本不会再漏项目数量是否可训练计入模型体积备注卷积权重k²×C_in×C_out是是主体卷积偏置C_out是若存在是常被 BN 吸收后关闭BN 的 gamma/beta2×C是是affineTrue 才有BN 的 running_mean/var2×C否是推理必需训练时也在显存里BN 的 num_batches_tracked1否是标量容易忽略但确实在文件里注意不同框架对num_batches_tracked的默认处理不完全一致导出模型时最好实际看一下文件里到底存了哪些 key不要凭印象估。2.3 深度可分离、分组卷积与 1×1 卷积的账怎么变真正让参数量计算变复杂的是卷积的各种变体。先看深度可分离卷积它拆成两步第一步 depthwise每个输入通道单独用一个 k×k 核卷参数量是k² × C_in再加C_in个偏置第二步 pointwise用 1×1 卷积做通道混合参数量是C_in × C_out C_out。合起来是k² × C_in C_in C_in × C_out C_out。拿刚才那组数字对比一下3×3、64 进 128 出普通卷积是 73856深度可分离是(9 × 64 64) (64 × 128 128) 640 8320 8960差不多是前者的八分之一。这个比例不是巧合理论上的压缩比约等于1/C_out 1/k²通道越多、核越大省得越狠。但我要提醒一句参数少不等于跑得快。深度可分离卷积的 FLOPs 确实也降了可它的问题是访存密度低、并行度差在很多硬件上实际推理时间只能省三四成远没有参数降得那么夸张。这是部署时最容易产生预期落差的点。分组卷积是另一个高频场景公式是k² × (C_in / g) × (C_out / g) × g C_out化简后权重部分是k² × C_in × C_out / g也就是参数随分组数线性下降。g C_in C_out时它退化成 depthwise 卷积。分组卷积最早是为了在有限显存下堆更深的网络现在更多出现在轻量结构里做通道间的稀疏交互。计算时唯一容易出错的地方是通道数必须能被 g 整除如果代码里写了groups4但通道数是 30框架会直接报错不会偷偷给你补零。1×1 卷积看着简单但它恰恰是很多网络参数的大头。因为它没有空间维度参数量就是C_in × C_out C_out可当C_in和C_out都是 512 时一层就是 26 万参数。ResNet 的 bottleneck 结构里三个卷积依次是 1×1 降维、3×3 变换、1×1 升维参数量分别约C²/4、9C²/16、C²/4加起来17C²/16而一个直接 3×3 的 C 到 C 卷积是9C²前者大约是后者的八分之一多点。这就是 bottleneck 设计的算术依据用 1×1 先降维把昂贵的 3×3 放在窄通道上做再用 1×1 把维度还回去。很多人背下了这个结构却没算过这笔账面试被问到“为什么是四分之一而不是八分之一”就卡住了其实答案就在这个比例里。3. 全连接层参数最贵、也最容易被冤枉的一层3.1 公式很短但“最后一层为什么这么贵”值得说透全连接层的参数量公式比卷积还短Params C_in × C_out C_out这个式子没有任何空间共享的概念输入向量的每一个元素都要和输出向量的每一个元素单独连一条边每一条边一个权重所以是纯粹的乘积关系。命名上也直接C_in是输入特征维度C_out是输出维度偏置再加C_out个。贵在哪拿一个经典结构算一下就明白了。假设卷积主干输出256 × 6 × 6 9216维的特征接一个 4096 的全连接层参数量是9216 × 4096 4096 37,756,928将近 3776 万。而它前面那五层卷积加起来可能也就 300 多万一层全连接直接把总量翻了十倍。第二个全连接 4096 到 4096又是 1678 万。最后 4096 到 1000410 万。三层全连接接近 5800 万占整个网络九成以上。这个现象在早期网络里非常普遍因为当时的思路是“卷积负责提特征全连接负责做决策”决策部分要足够宽才能拟合复杂的类别边界。代价就是参数全堆在了没有空间共享的稠密连接上。更麻烦的是全连接层把输入尺寸焊死了一旦输入分辨率变了C_in就变权重形状对不上直接报错。这也是为什么后来的网络几乎都用全局平均池化替换掉最后几层全连接。3.2 用全局平均池化换掉全连接到底省了多少全局平均池化GAP的做法是对每个通道的整张特征图取平均得到长度等于通道数的向量再直接接一个分类层。假设主干输出是C × H × WGAP 之后变成C × 1 × 1分类层是 C 到类别数 N 的全连接参数量只有C × N N。对比一下同样是 512 通道、7×7 特征图、1000 类如果走传统全连接中间还得插一个512 × 7 × 7 25088到 4096 的层参数量 1.03 亿走 GAP 则直接是512 × 1000 1000 513,000差了整整两百倍。当然现实中不会这么极端通常中间还会有一层 4096但量级差距依然巨大。省下来的不只是参数。GAP 没有需要学习的权重所以它对输入空间尺寸不敏感网络可以吃任意分辨率它把每个通道压成一个标量天然带一点正则效果过拟合会轻一些它还让特征图和类别之间建立了更直接的可解释对应关系——哪个通道响应强就倾向判成哪一类。我在实际项目里做过一次对照把一个小网络的最后两层全连接换成 GAP 加单层分类头参数量从 1200 万降到 180 万验证集精度掉了不到 0.5 个点但模型文件从 48MB 缩到 7MB推理延迟降了三成。这笔买卖在端侧场景基本是稳赚的。3.3 冻结、微调与“可训练参数”这笔另账做迁移学习的时候参数量会出现第二个口径总参数量和可训练参数量。你把主干冻住只训练新加的分类头模型文件一点没变小但需要计算梯度的参数可能只剩百分之几。这时候如果有人拿“参数量”去估训练显存就会严重高估。这两个数在代码里很好区分sum(p.numel() for p in model.parameters())拿的是总数加上if p.requires_grad才是可训练数。我一般会在训练脚本开头把这两个数都打出来因为优化器状态比如 Adam 的一阶、二阶矩只跟着可训练参数走冻结主干之后显存占用会明显下降这个下降幅度是可以提前算出来的。举个例子一个 2500 万参数的模型冻结到只剩 200 万可训练用 Adam 的话优化器状态从2500万 × 8 字节降到200万 × 8 字节省下来的接近 180MB这在显存吃紧的卡上就是能不能多开一个 batch 的区别。还有一个坑requires_gradFalse只是不更新前向的激活值和中间结果照样要占显存。所以冻结主干能省的显存远比想象中少别指望靠它把一个超大头模型塞进小显存。4. 动手实操把每一层的数字都对上4.1 用一段脚本把逐层参数量打干净框架自带的汇总工具很方便但它给的往往只有一个总数出问题的时候不知道错在哪一层。我习惯自己写一小段统计把卷积、全连接、归一化分开列一眼就能看出哪层不对劲import torch import torch.nn as nn def layer_report(model, show_bnTrue): total 0 rows [] for name, m in model.named_modules(): if isinstance(m, (nn.Conv2d, nn.Linear)): n sum(p.numel() for p in m.parameters()) elif show_bn and isinstance(m, (nn.BatchNorm2d, nn.BatchNorm1d)): n sum(p.numel() for p in m.parameters()) else: continue total n rows.append((name, type(m).__name__, n)) width max(len(r[0]) for r in rows) if rows else 10 for name, kind, n in rows: print(f{name:{width}} {kind:14} {n:12,d}) print(- * (width 32)) print(f{TOTAL:{width}} {:14} {total:12,d}) return total # 用法 # net YourModel() # layer_report(net)这段脚本的关键在于把 BN 单独拎出来。很多人统计完发现总数比预期多了一截差值恰好是通道数的两倍那就是 BN 的 gamma 和 beta 被漏掉了。反过来如果你只想看“有多少权重需要学”把 BN 那一路去掉就行。再给一个更硬核的核对方式直接把模型存成文件看实际大小。torch.save(model.state_dict(), tmp.pt)之后取文件字节数除以总参数量得到的每个参数占用字节数如果是 4说明全是 fp32如果是 2说明做过半精度转换如果明显大于 4通常是因为文件里还存了running_mean这些非参数 buffer。这个交叉验证我几乎每次交付前都会做一遍因为它能抓到手算和框架统计都对、但实际存盘时被悄悄转换精度的情况。4.2 手算和框架结果对不上时的排查顺序对不上是常态关键是排查要有顺序别一上来就怀疑框架。我的习惯是从差值的大小反推原因因为不同的疏漏产生的差值特征非常明显。差值特征最可能的原因快速验证恰好差 C_out偏置被关闭了公式里却加了打印m.bias is None恰好差 2 × C漏算 BN 的仿射参数检查层后是否紧跟 BatchNorm恰好差 C_in把 depthwise 当普通卷积算了检查m.groups m.in_channels差值是整数倍分组卷积的 g 没除打印m.groups差一个数量级1×1 卷积的通道维度算错核对in_channels/out_channels总数差几千漏了最后的分类层或 Embedding遍历named_parameters()全打一遍有一次我帮人核对一个轻量网络手算比框架少了整整 12 万按上面的表逐个排最后发现是一个 depthwise 层的 groups 写成了 1代码跑起来没报错因为通道数正好能整除但实际执行的是普通卷积参数量差了六十多倍。这种错误不带表格逐项排靠盯代码是很难看出来的。4.3 把参数量换算成模型体积和训练显存参数量算出来只是中间结果真正要用的是两个衍生数字模型文件多大和训练要多少显存。文件体积的换算很直接每个参数占的字节数取决于精度fp32 是 4 字节fp16 和 bf16 是 2 字节int8 量化后是 1 字节。一个 2500 万参数的模型fp32 存下来约 95MBfp16 约 48MBint8 约 24MB。实际文件会略大一些因为还要存结构信息和非参数 buffer但量级不会错。训练显存的账要复杂得多因为除了权重还有梯度、优化器状态和激活值。以 Adam 为例每个可训练参数要占四份 fp32权重本身、梯度、一阶矩、二阶矩合计 16 字节。2500 万参数全量训练就是 400MB 左右这还没算激活值。混合精度训练稍微绕一点通常的做法是权重存一份 fp16 加一份 fp32 的主副本梯度存 fp16优化器状态仍用 fp32摊下来每个参数大约 16 到 18 字节省得没有想象中多。真正省显存的大头在激活值而激活值和 batch size、输入分辨率成正比跟参数量反而没直接关系。所以看到“模型才 100MB 却要 24GB 显存”这种事不要惊讶那就是激活值和中间缓存的锅。精度方案权重梯度优化器状态每参数合计fp32 SGD440动量另算 48 ~ 12fp32 Adam44816混合精度 Adam2 4主副本28约 16 ~ 18纯推理 fp162002纯推理 int81001提示做显存预算时先用“参数量 × 每参数字节数”估一个下限再按经验加 1.5 到 3 倍的激活值开销。如果算出来刚好卡在显卡容量边缘实际大概率跑不起来。5. 高频错算场景与几条压箱底的经验5.1 那些看起来对、其实错得很离谱的算法有一类错误特别隐蔽公式没错数字也对但统计的对象错了。比如有人统计的是“权重张量的个数”而不是“元素总数”对一个形状是(128, 64, 3, 3)的卷积核他会报 4实际应该是 73728。这个错误在写自定义脚本时特别容易犯因为len(module.parameters())返回的是张量个数要拿元素总数必须用numel()。我见过有人在论文复现里报了个“模型只有 200 个参数”就是这个原因。另一类是把计算量和参数量搞混。卷积层的 FLOPs 大致是2 × H_out × W_out × k² × C_in × C_out乘加各算一次前面那个 2 有人算有人不算导致同一个模型在不同论文里 FLOPs 差一倍。参数量则完全不含输出尺寸这一项。所以出现“参数量很小但 FLOPs 巨大”是完全正常的比如输入分辨率很高、通道很窄的网络反过来“参数量巨大但 FLOPs 很小”也正常比如全连接层堆出来的结构。判断一个模型轻不轻这两个指标得一起看只看一个必然误判。还有一类是忽视了结构里的隐藏参数。位置编码、类别 token、LayerNorm 的仿射参数、Embedding 表这些在很多“只看卷积和全连接”的统计里会被整个跳过。纯 CNN 里它们占比很小但只要网络里混进了一点点注意力结构Embedding 和归一化的参数就可能反超卷积部分。做混合结构统计时我建议直接遍历named_parameters()全量求和再按模块名前缀分组别预设“只有卷积和全连接才有参数”。5.2 我在实际项目里攒下的几条判断准则第一条先估算再动手。拿到一个新结构别急着写代码跑先用k² × C_in × C_out把主要几层口算一遍心里有个总量级。这个过程通常只要两三分钟但能帮你提前发现设计上的问题。比如你想在一个 512 通道的网络上堆十个 3×3 卷积口算一下就是10 × 9 × 512 × 512 ≈ 2360 万如果目标设备只有几十兆的模型预算这个设计当场就该改不用等到训完才发现塞不进去。第二条参数量的分布比总量更有信息量。同样是 2000 万参数均匀分布在二十层里和集中在两个全连接层里是完全不同的两种模型。前者通常更好压缩、更容易剪枝后者一旦动刀精度就崩。我习惯在统计完之后顺手算一下“最大单层占比”如果某一层超过 30%就要警惕这个模型的结构是否合理尤其是那层如果是全连接基本可以判定是早期设计值得考虑换成 GAP。第三条通道数设计的本质是在参数量上做乘除法。卷积参数量对通道数是二次关系通道翻倍参数变四倍这个杠杆比核大小还猛核从 3 到 5 只是 9 到 25约 2.8 倍。所以在有限预算下调整结构优先动通道数其次动核大小最后才考虑层数。反过来如果想让模型变宽提升表达力也要清楚每加一倍宽参数就是四倍别指望线性增长。第四条同参数量下的性能差异往往来自参数怎么用而不是用了多少。两个都是 500 万参数的模型一个把所有参数堆在稠密连接上一个用深度可分离卷积铺满空间维度后者在图像任务上的表现通常好得多因为它把参数花在了“空间共享”这个更高效的地方。这也是为什么我算参数量从来不只看总数一定会把卷积部分和全连接部分分开报——这两个数字的比例基本能反映出一个网络的设计年代和设计取向。最后分享一个我常用的小技巧给模型加一个极简的param_breakdown()方法返回三个数——卷积参数量、全连接参数量、其他。每次改结构就跑一次把这组数字记在笔记里。时间长了你会发现看这三个数的变化趋势比看总参数量更能判断一次改动是“有效加宽”还是“白堆参数”。这个习惯帮我省下的调试时间远比写统计脚本花的时间多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑