ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

卷积神经网络特征图尺寸计算:公式推导与 LeNet-5 实战

卷积神经网络特征图尺寸计算:公式推导与 LeNet-5 实战 把 LeNet-5 的结构图摊在桌上一层一层往下推特征图尺寸输入 32×32第一层 5×5 卷积核、步长 1、不加填充输出 28×28接着 2×2 池化输出 14×14……这种手推看着很基础但卷积神经网络里绝大多数张量维度对不上skip connection 相加失败全连接层输入特征数算错的报错根子都在这几步计算上。特征图大小计算这件事说穿了只有一个公式但它牵扯到卷积、池化、空洞卷积、转置卷积等一整套层的尺寸规则还连锁影响感受野、参数量、显存占用和计算量。这篇内容就是把这套规则从头讲透公式怎么来的、边界情况怎么处理、3D 卷积和图卷积为什么不能照搬、以及在真实项目里怎么用几行代码把自己的手算结果验证一遍。不管你是刚看完 CNN 基本结构图、准备复现 LeNet-5 的新手还是已经在调 ResNet 变体、经常被尺寸问题绊住的老手都能从里面找到能直接抄走的做法。1. 别急着背公式先搞清楚它是怎么数出来的1.1 一次维度报错引出的核心问题我印象最深的一次排查是帮同事看一个残差网络训练不起来的报错。日志只给了一句张量尺寸不匹配两个特征图的通道和空间尺寸都对不上加法做不了。翻了半天代码问题出在一个 3×3、步长 2 的卷积上——同事默认输出尺寸会整除到整数但实际上输入是 27×27除以 2 之后向下取整输出变成了 13×13和另一条分支的尺寸差了 1。改成步长 1 加池化或者把输入补到偶数问题立刻消失。这类问题的本质是卷积和池化的输出尺寸不是连续可分的它必须取整。取整方向、取整后丢掉的那部分像素去了哪里、下一层还能不能对上这些都得算清楚。所以特征图大小计算不是一个考试知识点而是排查网络结构问题的第一把螺丝刀。你只要能把每一层的输入输出尺寸写成一列绝大多数结构 bug 在三分钟内就能定位。1.2 用能放几次的思路把公式数出来很多人记公式是靠背但背下来的东西一旦遇到空洞卷积或者转置卷积就失效了。我更推荐从滑窗能放几次的角度去理解。假设一维输入长度是 $L$卷积核长度是 $k$步长是 $s$两侧各补 $p$ 个位置。补完之后总长度变成 $L 2p$。卷积核第一次覆盖的位置起始于第 1 个点之后每次向右移动 $s$ 个位置。那么卷积核的起始位置最多能取到哪里最后一个合法的起始位置必须满足核的右端不超过补零后的总长度也就是起始位置 $i$ 要满足 $i k - 1 \le L 2p$。把所有合法起始位置列出来是 $1, 1s, 12s, \dots$这是一个等差数列。设共有 $n$ 个位置则最后一个位置是 $1 (n-1)s$代入约束$$1 (n-1)s k - 1 \le L 2p$$整理得 $(n-1)s \le L 2p - k$于是$$n \left\lfloor \frac{L 2p - k}{s} \right\rfloor 1$$这就是那个大家都见过的公式。我之所以要把它推一遍是因为推导过程里藏着两个关键信息第一下取整是天然的不是人为规定的因为不完整的滑窗位置被丢弃了第二决定结果的不是单独的 $p$ 或 $k$而是 $L 2p - k$ 这个有效可用长度理解这一点之后你就能凭直觉判断这个配置会不会丢像素。1.3 二维卷积为什么宽高可以分开算二维卷积看起来复杂但它的滑窗在高度和宽度两个方向上是独立运动的。高度方向只和输入高度、核高度、高度方向步长、高度方向填充有关宽度方向同理两者互不干扰。所以二维卷积的输出可以直接写成两个一维公式的乘积$$H_{out} \left\lfloor \frac{H_{in} 2p_h - k_h}{s_h} \right\rfloor 1, \quad W_{out} \left\lfloor \frac{W_{in} 2p_w - k_w}{s_w} \right\rfloor 1$$通道维度的规则更简单输出的通道数等于这一层卷积核的个数和输入的通道数无关。输入通道数只影响每个卷积核的深度——一个 $3 \times 3$ 的卷积核作用在 64 通道输入上时它实际是一个 $3 \times 3 \times 64$ 的三维张量。这个区分很重要新手最容易把输入通道和输出通道的角色搞混然后在算参数量的时候出错。理解了这一点你就能明白为什么卷积层可以接受任意尺寸的输入只要空间尺寸满足公式能算出至少 1 的输出网络就能跑通这也是全卷积网络能处理不同分辨率图像的原因。2. 不同类型层的尺寸规则逐个拆开看2.1 标准卷积层填充和步长的配合套路标准卷积层直接用上面那个公式没什么特殊之处但实际写代码时有几个固定套路值得记住。当步长 $s1$ 时如果想让输出尺寸和输入保持一致需要 $L 2p - k \ge L$也就是 $p \ge (k-1)/2$。对于奇数核最自然的取值是 $p (k-1)/2$3×3 配 15×5 配 27×7 配 3。这就是所谓的same padding也是为什么主流网络的卷积核几乎清一色用奇数尺寸——偶数核没法在两侧对称填充后保持尺寸不变只能一边多补一个位置平白引入不对称性。我用过的所有稳定训练的图像模型里空间卷积核全是 3×3 或 5×5没有例外。当步长 $s2$ 时情况就不那么干净了。代入 $k3, p1, s2$$$out \left\lfloor \frac{L 2 - 3}{2} \right\rfloor 1 \left\lfloor \frac{L-1}{2} \right\rfloor 1 \left\lceil \frac{L}{2} \right\rceil$$也就是说偶数输入会被精确减半奇数输入会向上取整。这个特性在实现下采样时非常常用但代价是输入尺寸的奇偶性会影响输出网络前后两段的尺寸可能对不齐。我的做法是只要用了 stride2 的卷积做下采样就保证输入特征图的高宽是偶数从数据预处理阶段就把输入裁剪到 2 的幂次附近能省掉后面一大堆麻烦。提示PyTorch 官方文档给出的卷积输出公式是 $\lfloor (H_{in} 2p - d(k-1) - 1)/s 1 \rfloor$比我上面写的多了一个 dilation 项。当 $d1$ 时两者完全等价可以放心使用同一个记忆版本。2.2 池化层公式相同但取整策略可以调池化层的尺寸公式和卷积完全一样因为它在数学上就是一个固定权重的滑窗操作。常用的池化配置是核 2、步长 2、不填充代入公式$$out \left\lfloor \frac{L - 2}{2} \right\rfloor 1 \left\lfloor \frac{L}{2} \right\rfloor$$所以 32 变 16、28 变 14、14 变 7、7 变 3。注意最后一步7 会变成 3 而不是 3.5被丢掉的那一行一列像素就永久消失了。这在浅层网络里没什么感觉但在做逐像素分割或者需要恢复分辨率的任务里丢掉的位置信息会让上采样后的边缘对不上。我见过不少分割模型在验证集上边缘一直糊最后查出来是下采样路径用奇数尺寸输入四次池化之后尺寸和上采样路径错开了一个像素靠插值硬凑回去的。PyTorch 的池化层提供了一个ceil_mode参数设为 True 时会向上取整公式变成 $\lceil (L-k)/s \rceil 1$。什么时候该用它当你希望输出尺寸尽量大、不想丢边界像素的时候。什么时候不该用当你的下采样路径和上采样路径必须严格对称的时候向上取整会让尺寸变化不再单调可预测反而更难对齐。我的经验是默认用向下取整把输入尺寸调成能被整除的数值是最省心的方案。2.3 空洞卷积和转置卷积公式要改两个地方空洞卷积的引入是为了在不增加参数量的前提下扩大感受野。它的做法是在卷积核的相邻元素之间插入空洞等效核尺寸变成$$k_{eff} d \times (k - 1) 1$$其中 $d$ 是膨胀率。3×3 的核膨胀率为 2 时等效核是 5×5膨胀率为 4 时等效核是 9×9。但参数量始终是 3×39 个权重这就是它的价值所在。把等效核代回原公式$$out \left\lfloor \frac{L 2p - d(k-1) - 1}{s} \right\rfloor 1$$实战里踩过的坑是为了让空洞卷积保持尺寸不变填充量也要跟着放大。3×3、膨胀率 2 的卷积如果还按 $p1$ 去配输出尺寸会缩水 2。正确的填充量是 $p d(k-1)/2$。我在做语义分割的多尺度上下文模块时几条并行的空洞卷积分支如果填充量不按这个规则配输出的特征图尺寸就各不相同最后根本没法拼接或者相加。记住一句话膨胀率变了填充量必须同步变化。转置卷积也叫反卷积但这个名字不够准确走的是反方向。它的作用是让输出比输入大常见于上采样路径。公式是$$out (L - 1) \times s - 2p k output_padding$$这个公式之所以长得不一样是因为它把前向卷积中哪个位置被覆盖了几次的过程反转了输入中的每个点会按步长散布到输出上散布区域重叠的部分累加。用 $L4, s2, p1, k3$ 试一下输出是 $(4-1)\times 2 - 2 3 7$正好是输入的两倍少 1。如果你想让输出精确等于输入的 2 倍就需要把 $output_padding$ 设为 1输出变成 8。注意转置卷积非常容易出现棋盘格artifacts原因是不同输出位置的覆盖次数不均匀。规避方法一是保证 $k$ 能被 $s$ 整除比如 4×4 核配步长 2二是干脆用最近邻插值 3×3 卷积替代。我在最近两年的项目里几乎不再直接用转置卷积换成插值加卷积之后输出图像的网格感基本消失了。2.4 3D 卷积和图卷积不能照搬二维的思路3D 卷积在二维公式的基础上多了一个时间或者深度维度。设三维核为 $k_t \times k_h \times k_w$步长为 $s_t \times s_h \times s_w$填充为 $p_t \times p_h \times p_w$那么三个方向各自套用一维公式即可$$T_{out} \left\lfloor \frac{T_{in} 2p_t - k_t}{s_t} \right\rfloor 1$$以此类推。处理视频或者医学体数据时我通常把时间维的步长单独设成 2 或者 3空间维保持 1这样能在保留空间分辨率的同时压缩时间维度。这里要注意的是3D 卷积的参数量和显存占用是二维的三次方级别增长一个 3×3×3、64 通道的 3D 卷积参数量接近 11 万和二维的 3×3 卷积完全不是一个量级。所以做体数据处理时第一层往往用较大的空间核配小时间核比如 $3\times7\times7$控制住显存。图卷积神经网络的尺寸概念和上面这些完全不是一回事它是理解难点。图卷积里的节点数量由图的邻接结构决定卷积操作实际上是在邻接矩阵和特征矩阵之间做乘法节点数在传播过程中保持不变所以不存在滑窗滑几次的问题。真正变化的是每个节点的特征维度那才对应着常规卷积里的通道数。如果拿二维卷积的公式去套图卷积会用错方向。图卷积里需要关心的是聚合范围——几阶邻居参与计算以及是否引入池化来减少节点数这需要图池化或者图粗化操作不是简单滑窗。3. 手算加代码验证把 LeNet-5 逐层走一遍3.1 手推 LeNet-5 的每一层理论说得再多不如完整走一遍。LeNet-5 是结构图里出现频率最高的小网络参数少、层数浅非常适合练手。原始结构约定输入是 32×32 的单通道图像后面接 C1 卷积、S2 池化、C3 卷积、S4 池化、C5 卷积再接两个全连接层。逐层算下来层输入尺寸核步长填充输出尺寸计算过程输入----1×32×32-C11×32×325×5106×28×28(32-5)/1128S26×28×282×2206×14×14(28-2)/2114C36×14×145×51016×10×10(14-5)/1110S416×10×102×22016×5×5(10-2)/215C516×5×55×510120×1×1(5-5)/111F6120---84全连接输出84---10全连接关键在于 C5 这一层5×5 的卷积核正好和 5×5 的输入等大输出塌缩成 1×1。这在结构上等效于全连接层但权重共享方式不同。我第一次看论文时没意识到这一点以为 C5 后面还有空间维度结果算全连接层的输入特征数时多乘了一个 5×5直接导致参数量对不上。顺便算一下参数量这是检验你是否真的理解通道维度的好办法。C1 的每个卷积核是 $5\times5\times1$共 6 个加偏置$5\times5\times1\times66156$。C3 的核是 $5\times5\times6$共 16 个$5\times5\times6\times16162416$。C5 是 $5\times5\times16$共 120 个$5\times5\times16\times12012048120$。全连接 120 到 84 是 $120\times848410164$84 到 10 是 $84\times1010850$。全部加起来 61706 个参数和公开资料里 LeNet-5 约 6 万参数的说法完全吻合。能把这串数字算到和公开值一致说明你的尺寸和通道逻辑已经通了。3.2 用几行代码把自己的手算结果验一遍手算容易错最好的习惯是写完就验证。下面这几行代码可以直接拿去用把每一层的实际输出形状打出来import torch import torch.nn as nn def conv_out(size, k, s1, p0, d1): 一维卷积/空洞卷积输出尺寸 return (size 2 * p - d * (k - 1) - 1) // s 1 def pool_out(size, k, s, p0, ceil_modeFalse): 池化输出尺寸 if ceil_mode: return -(-(size 2 * p - k) // s) 1 return (size 2 * p - k) // s 1 class LeNet5(nn.Module): def __init__(self): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 6, 5), # C1 nn.Tanh(), nn.AvgPool2d(2, 2), # S2 nn.Conv2d(6, 16, 5), # C3 nn.Tanh(), nn.AvgPool2d(2, 2), # S4 nn.Conv2d(16, 120, 5), # C5 nn.Tanh(), ) self.classifier nn.Sequential( nn.Linear(120, 84), nn.Tanh(), nn.Linear(84, 10), ) def forward(self, x): for i, layer in enumerate(self.features): x layer(x) print(flayer {i:02d} {layer.__class__.__name__:12s} - {tuple(x.shape)}) return self.classifier(x.flatten(1)) model LeNet5() _ model(torch.randn(1, 1, 32, 32)) # 顺便验证手算公式 assert conv_out(32, 5) 28 assert conv_out(14, 5) 10 assert pool_out(28, 2, 2) 14 assert pool_out(10, 2, 2) 5 print(手算与代码一致)跑一遍就能看到输出依次是 6×28×28、6×14×14、16×10×10、16×5×5、120×1×1和自己手推的表格一模一样。我更推荐用torchinfo这类工具直接总结整个模型的逐层形状、参数量和计算量输入尺寸设成你要用的实际尺寸一次就能看到全貌# pip install torchinfo from torchinfo import summary summary(LeNet5(), input_size(1, 1, 32, 32))实操心得验证时输入的 batch size 一定要用真实的数值不要用 1 蒙混过关。因为有些自定义层里写死了 batch 维度的操作比如某些实现不规范的全连接或者自注意力的 reshapebatch1 时恰好不报错换到 32 就炸。我在项目里养成了一开始就用真实 batch 跑一次 shape 检查的习惯。3.3 从目标尺寸反推填充量实际工作中更常见的场景是反过来我已经知道输入尺寸也定好了输出尺寸需要求填充量该设多少。把公式变形一下就得到$$p \frac{s \times (out - 1) - L k}{2}$$如果算出来不是整数说明这组配置达不到你要的输出尺寸需要调整核、步长或者输出尺寸。举个真实例子输入 224×224我要用步长 2、核 3 的卷积把它降到 112×112代入$$p \frac{2 \times 111 - 224 3}{2} \frac{1}{2}$$不是整数差一点点。这就是为什么很多网络在 stride2 的卷积前后都会检查一下输入尺寸——224 这种标准尺寸看起来是 2 的倍数但除以 2 之后变成 112再除以 2 是 56一路除到 7 就变成奇数了再往下就需要配合池化或者AdaptiveAvgPool。我的处理方式是深层特征图不要依赖逐层手算直接在最末层用自适应平均池化指定输出尺寸让框架自己处理剩下的取整问题。这比纠结每一层的填充量靠谱得多。代码上就是一句nn.AdaptiveAvgPool2d((1, 1))无论输入是 7×7 还是 8×8输出都是 1×1。4. 尺寸算完之后感受野和计算量怎么接着算4.1 感受野的递推关系特征图尺寸算清楚了感受野就能顺着往下推。感受野的含义是输出特征图上的一个点对应回原始输入图像上的区域有多大。递推规则是设到第 $n$ 层为止的感受野为 $RF_n$累乘步长为 $S_{cum}$那么$$RF_n RF_{n-1} (k_n - 1) \times S_{cum, n-1}, \quad S_{cum, n} S_{cum, n-1} \times s_n$$初始时 $RF_0 1$$S_{cum,0} 1$。注意这里用的是核尺寸如果有膨胀要换成等效核尺寸 $d(k-1)1$。拿 LeNet-5 走一遍会发现一个很漂亮的结论。C1 后 $RF5$累乘步长 1S2 后 $RF 5 1\times1 6$累乘步长变 2C3 后 $RF 6 4\times2 14$累乘步长仍是 2S4 后 $RF 14 1\times2 16$累乘步长变 4C5 后 $RF 16 4\times4 32$。最后一层输出 1×1 时感受野恰好是 32正好等于整个输入图像的大小。这不是巧合是网络设计者刻意安排的结果——输出层的每一个神经元都应该看到完整图像。这个自洽性检验非常好用如果你设计完一个分类网络算出来最后一层的感受野明显小于输入尺寸说明网络根本没看全图像性能上限就被锁死了。相反的情况也值得警惕。我做过一个医学图像分类的任务输入 512×512但网络只有五层下采样算下来感受野不到 300。模型在训练集上表现不错验证集一直上不去换成更深的骨干网络之后才正常。后来复盘根本原因就是有效感受野覆盖不了病灶的完整范围。4.2 参数量和计算量的估算方法卷积层的参数量公式很直接$$Params k_h \times k_w \times C_{in} \times C_{out} C_{out}$$最后那一项是偏置如果biasFalse就去掉。计算量以乘加次数计则是$$FLOPs H_{out} \times W_{out} \times C_{out} \times k_h \times k_w \times C_{in}$$把 LeNet-5 的三层卷积代进去看看C1 是 $28\times28\times6\times25\times1 \approx 11.8$ 万次C3 是 $10\times10\times16\times25\times6 24$ 万次C5 是 $1\times1\times120\times25\times16 4.8$ 万次。总共约 40 万次乘加放到今天连手机的零头都算不上。可就是这个小网络在当年把手写数字识别的错误率压到了很低。对比一下现代网络就能感受到量级差异。一个 3×3 卷积输入输出都是 256 通道特征图 56×56计算量是 $56\times56\times256\times9\times256 \approx 1.85$ 亿次乘加比整个 LeNet-5 大了四百多倍。所以当你发现显存不够的时候优先砍的应该是特征图空间尺寸和通道数同时较大的那些层而不是无脑减层数。4.3 为什么 3×3 堆叠比大核更划算这个结论值得单独说因为它直接决定了现代网络的形态。一个 5×5 卷积输入输出通道都是 $C$参数量是 $25C^2$。换成两个 3×3 卷积串联参数量是 $2 \times 9C^2 18C^2$少了将近三成感受野是 $3 2 5$和单个 5×5 完全一样。再往上三个 3×3 堆叠感受野是 7参数量 $27C^2$而单个 7×7 要 $49C^2$省了将近一半。除了参数更省堆叠还有一个更重要的好处每两个卷积之间都夹着一层非线性激活。单个 5×5 卷积只做了一次线性变换加一次激活而两个 3×3 做了两次表达能力更强。这就是 VGG 之后几乎所有主流骨干网络都采用 3×3 小核堆叠的原因。不过也有例外。在网络的浅层尤其是第一层输入通道数很小1 或者 3参数量本来就不大这时候用 7×7 或者 5×5 的大核配上较大步长能更快地压缩空间尺寸、扩大感受野性价比反而更高。我自己的经验是第一层用大核大步长做快速下采样中间主体用 3×3 堆叠最后一层用全局池化收尾这个组合在大多数视觉任务上都很稳。5. 尺寸对不上怎么办常见问题速查5.1 六种典型的尺寸错位原因现象常见原因排查方法处理建议两条分支相加报错stride2 的卷积遇到奇数输入取整后差 1打印两条分支的输出形状保证输入为偶数或改用池化下采样拼接时通道对不上把输入通道当成了输出通道检查卷积层的 out_channels记住输出通道等于卷积核个数全连接层输入维度算错漏乘了空间尺寸或误算 C5 为 5×5把卷积输出 shape 打印出来用flatten(1)让框架自己算空洞卷积后尺寸缩小填充量没随膨胀率同步调整检查 p 是否等于 d(k-1)/2膨胀率变了填充必须跟着变上采样后尺寸差 1转置卷积的 output_padding 没设反推公式验证补齐 output_padding 或用插值替代池化后奇数尺寸丢像素默认向下取整看输入尺寸奇偶性输入调成偶数或开 ceil_mode推理时尺寸和训练不符用了固定尺寸的全连接层对比两个阶段的输入尺寸换自适应池化支持动态输入5.2 几条踩坑换来的实操经验第一条先写尺寸表再写代码。我在设计任何新网络时第一件事是在纸上或者表格里把每一层的输入尺寸、核、步长、输出尺寸列出来确认最后能收敛到期望的大小然后才开始写代码。这个习惯让我省下的调试时间比写这张表花的时间多十倍都不止。第二条把形状检查当断言用。关键分支合并的地方加一行形状断言一旦尺寸不匹配立刻报错而不是等到训练几个小时后才发现 loss 异常。位置成本几乎为零收益极高。第三条动态尺寸输入要谨慎。如果你的部署场景需要处理不同分辨率的输入网络里所有依赖固定尺寸的操作固定输入的全连接层、某些自定义 reshape、位置编码都得改。最稳妥的做法是全卷积化加全局池化让网络对输入尺寸天然免疫。但要注意全卷积化之后浅层 padding 较多的卷积在边界处的计算会引入一些误差尺寸越小这个影响越明显所以也别把输入缩得太小。第四条注意框架之间的差异。同样的配置不同框架的默认行为可能不一样。有的框架是通道在前有的是通道在后打印形状时一定要看清楚维度的顺序否则你会把 batch 维度当成通道维度然后困惑为什么通道数对不上。我自己就因为这个在跨框架迁移权重时白白折腾了半个下午。第五条不要迷信工具输出的计算量数字。不同工具对 FLOPs 的定义不一样有的算一次乘加为 1 次有的算 2 次有的把激活层的开销也算进去有的不算。做模型对比的时候一定用同一个工具、同一个设置去测否则数字之间没有可比性。5.3 常用配置速查需要保持尺寸不变时直接查这张表核尺寸步长填充输出相对输入典型用途110不变通道变换、瓶颈结构311不变主力卷积层512不变浅层大核713不变网络第一层321约减半向上取整下采样220减半向下取整池化下采样421精确减半转置卷积上采样需要扩大到指定倍数时转置卷积的配置反推一下想让输出精确等于输入的 2 倍$k4, s2, p1$ 是最干净的组合因为 $k$ 能被 $s$ 整除不会产生棋盘格。3 倍上采样同理用 $k6, s3, p2$。最后分享一个我平时用得最多的检查套路不管多复杂的网络先在纸上把尺寸链算一遍然后用真实 batch size 跑一次前向把每层形状打出来逐行核对最后再算一遍感受野确认覆盖范围。这三步做完网络结构层面的问题基本就清零了剩下的才轮到数据和优化器的问题。我前后带过几个新人让他们养成这个习惯之后卡在形状报错上的时间明显少了很多能把精力放到真正重要的地方去。
返回列表