ARTICLE DETAIL

建站实战干货

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

卷积神经网络特征图大小计算:公式、手算与避坑

2026/9/18 18:37:26 拓冰建站 浏览量
卷积神经网络特征图大小计算:公式、手算与避坑 搞卷积神经网络的人早晚都会撞上特征图大小计算这道坎。你按教程搭好一个CNN信心满满敲下训练命令结果第一轮就弹出一串红色报错说张量维度对不上或者模型勉强跑通了中间某层的特征图却莫名其妙缩成了 1×1后面接全连接层直接崩掉。这类问题的根子九成以上都出在对卷积层输入输出尺寸的推导不够熟练——不是不会写网络而是没把每一步的尺寸变化算清楚。我打算把卷积神经网络特征图大小计算这件事从参数含义、公式推导、特殊层处理到实战手算一层层拆开讲透让刚入门的朋友也能自己推导让做过几年的人也能把踩过的坑再对一遍。不管你是刚学完 CNN 基本结构、正准备复现 LeNet-5 的学生还是天天调网络结构、被 shape mismatch 折磨的工程师把尺寸计算这一关过了后面搭网络会顺很多。1. 特征图尺寸算错代价比你想的大1.1 从一次真实的报错回溯说起我第一次正经搭 CNN 的时候用的是最朴素的做法照着一篇博客抄了个三层卷积加两层全连接的结构输入 32×32 的灰度图改完数据加载部分就直接跑。前向传播没问题反向传播时报错说矩阵乘法维度不匹配。我当时的反应是全连接层输入维度写错了随手改了个数结果又报新错。折腾大半天之后我才老老实实从第一层开始把每一层的输出尺寸用纸笔列了一遍才发现中间某层卷积用了 5×5 核、步长 1、无填充我以为输出还是 28×28实际上算下来是 24×24后面所有层的尺寸都是错的。这件事之后我养成了一个习惯任何一个新建的卷积网络在写代码之前先用一张表格把每一层的输入尺寸、卷积核、步长、填充、输出尺寸列清楚。这张表格花不了五分钟但能省下几个小时甚至一整天的调试时间。尺寸计算本身只是四则运算真正贵的是你因为算错而反复试错浪费掉的时间。从工程角度看尺寸问题之所以频繁出现是因为卷积层的输出尺寸同时受四个参数影响输入尺寸、卷积核大小、步长、填充。这四个量任意一个填错输出就会偏差而这个偏差会沿着网络往后传播被后面的层放大。你在第 3 层少算了 2 个像素到第 10 层可能就变成了整整一层特征图凭空消失。1.2 尺寸问题的三种典型表现根据不同人的踩坑经历尺寸算错通常有三种表现认得出症状排查起来就快。第一种是硬报错。这类最好处理框架会直接告诉你哪一层维度对不上比如 PyTorch 里报mat1 and mat2 shapes cannot be multiplied或者 Keras 里报negative dimension size。看到negative dimension基本可以直接断定某一层的卷积核比输入还大公式减出了负数。第二种是静默缩水。网络能跑但性能莫名其妙很差或者中间某层输出的特征图已经变成 1×1相当于这一层的空间信息全丢了后面的卷积实际上退化成了全连接。这种最阴险它不报错你要专门去看每一层的输出形状才能发现。第三种是转置卷积/上采样对不上。做分割、生成类任务时编码器下采样多少次解码器就要上采样多少次中间还要做跳跃连接。如果编码器输出是 13×13解码器对应层输出是 12×12拼接concat的时候就会报错。这类问题在 U-Net 类结构里特别常见因为奇偶尺寸经过下采样后容易出现奇数需要靠填充或裁切来对齐。提示现代框架大多支持动态图print(x.shape)随时可看但这不代表可以不算。搞清楚怎么算才能在写代码之前就设计出尺寸自洽的结构而不是边跑边改。2. 卷积层四个核心参数先分清楚谁影响谁2.1 卷积核大小、步长、填充、膨胀率卷积层里影响输出尺寸的参数主要就这四个我一个个说清楚它们各自的作用。卷积核大小kernel size记作 k就是滑动窗口的边长常见的有 3×3、5×5、7×7。核越大单次看到的范围越大感受野增长越快但参数量按平方增长计算量也上去了。现在主流做法是用堆叠的小核比如三层 3×3替代一个大核比如一层 7×7在感受野相当的前提下参数量更少、非线性更强。步长stride记作 s滑动的跨度。步长为 1 时输出尺寸大致等于输入尺寸步长为 2 时输出尺寸大约减半这也是下采样最常用的手段。步长越大输出越小计算量越低但细节丢得也越多。填充padding记作 p在输入特征图四周补的圈数通常补 0。填充的目的是控制输出尺寸防止每过一层图像就缩小一圈。取 p(k-1)/2 且步长为 1 时输出尺寸和输入完全相等这就是所谓的 same padding。膨胀率dilation记作 d也叫空洞卷积参数让卷积核的元素之间插入间隔从而在不增加参数量的前提下扩大感受野。膨胀率默认是 1也就是普通卷积。这四个参数里k 和 s 是设计网络时的主动选择p 往往是配套确定的d 只在需要大感受野又不想下采样时才会用到。理解它们对输出尺寸的作用方向比死记公式重要得多k 增大让输出变小s 增大让输出变小p 增大让输出变大d 增大让输出变小。2.2 输入通道、输出通道与卷积核数量的关系这里有个初学者特别容易混淆的点必须单独拎出来说通道数不影响特征图的空间尺寸。一个卷积层的输入形状通常写成(N, C_in, H, W)其中 N 是批量大小C_in 是输入通道H、W 是空间高宽。卷积核本身的形状是(C_out, C_in, k, k)也就是说每个输出通道对应一个完整的C_in × k × k的卷积核输出通道数 C_out 决定了有多少个这样的卷积核。关键在于不管你有 6 个还是 64 个卷积核输出特征图的高和宽在空间维度上的变化只由 k、s、p、d 决定跟通道数完全无关。输出形状是(N, C_out, H_out, W_out)只有第二维通道维由卷积核数量决定。我见过有同学以为64 个卷积核会让特征图变成原来的 64 倍大这显然是把通道数和空间尺寸搞混了。通道维度的计算很简单输出通道 卷积核个数仅此而已。另外值得一提的是一维、二维、三维卷积的区别。一维卷积处理序列输出尺寸按序列长度算二维卷积处理图像按高宽两个维度算三维卷积处理视频或体数据按深度、高、宽三个维度算。它们的尺寸公式形式完全一致只是维数多寡的问题这一点在第 7 节会具体展开。3. 二维卷积输出尺寸公式的完整推导3.1 从一维情形推导通用公式先看最简单的场景一条长度 L 的序列用一个长度 k 的一维卷积核去卷步长为 1不填充。核的起点可以从位置 1 滑动到位置 L-k1所以输出长度是 L-k1。这个式子的几何含义很直观核要完整地放在有效区域内最多能滑动的起点数是 L-k1。现在加上步长 s起点每次跳 s 步能取的起点数是 floor((L-k)/s) 1。加上填充 p 之后有效长度从 L 变成 L2p于是out floor((L 2p - k) / s) 1推广到二维就是最常用的那个公式H_out floor((H_in 2p - k) / s) 1W_out floor((W_in 2p - k) / s) 1注意这里的 floor 是向下取整。为什么是向下取整因为核必须完整落在有效区域内边界上多出来的那一小段不足以再放下一个完整的核所以要舍掉。这个舍掉的行为有个副作用输出尺寸对输入尺寸的奇偶性变得敏感。步长 2 的情况下输入 224 和输入 223 的输出可能都是 112但再往后推几层就会出现差异。注意很多框架里比如 PyTorch只要你的结构在每个维度上都能整除一般不显式写 floor但一旦尺寸不能整除floor 的行为就决定了你最终拿到的是哪个数。算不准的时候宁可自己拿计算器确认一遍。用这个公式回头验证一下经典配置输入 32核 5步长 1不填充输出 (32-5)/11 28下一层池化 2×2 步长 2输出 (28-2)/21 14。这就是 LeNet-5 前两层的尺寸变化和官方结构对得上。3.2 三种常见填充策略下的公式简化实际写代码时填充策略分三种各自对应不同的计算公式分清楚会省很多事。填充策略填充量输出尺寸公式典型场景无填充 validp0floor((H-k)/s)1想让特征图逐步缩小等长填充 same自动ceil(H/s)想保持分辨率不变手动填充自己指定floor((H2p-k)/s)1精细控制每层输出valid无填充每卷一次尺寸就缩。装 LeNet-5 的时候用这个让空间尺寸自然衰减。same等长填充框架自动补零保证输出尺寸等于 ceil(H/s)。注意same 条件下输出尺寸只跟输入和步长有关和核大小无关——这一点经常被忽略。步长为 1 时输出尺寸等于输入步长为 2 时输出约为输入的一半向上取整。手动填充想要输出尺寸等于输入且步长为 1就取 p(k-1)/2。3×3 核配 p15×5 核配 p27×7 核配 p3。这个关系要记住因为绝大多数 backbone 的第一层都是这么配的。再强调一个容易踩的坑p(k-1)/2 只在步长为 1 时成立。如果步长是 2想让输出正好是输入的一半需要 p 和 k、s 配合用通用公式反推不能套这个简化式。我见过有人把 3×3 卷积、步长 2、padding 1 当成标准的减半操作结果是输出 (H2-3)/21 floor((H-1)/2)1输入为偶数时是 H/2输入为奇数时是 (H1)/2奇偶敏感的问题就出来了。3.3 反向求解从目标输出推参数工程上更常见的需求是反过来的我想要输出是输入的 1/2该用什么参数或者我想让输出固定为某个尺寸填充该填多少求填充量由 H_out (H_in 2p - k)/s 1 反解得 p ((H_out - 1)·s k - H_in)/2。举例输入 28希望输出 14核 3步长 2代入得 p ((14-1)×2 3 - 28)/2 (263-28)/2 0.5。出现小数说明这组参数无法精确达成目标要么调整步长要么接受 13 或 14 的输出。求步长s (H_in 2p - k)/(H_out - 1)。同样要求结果接近整数。这套反向推导在做网络裁剪、自定义下采样倍率时特别有用。比如你想把输入 224 精确降到 14也就是 16 倍下采样用四层步长 2 的卷积每层减半224→112→56→28→14这就是 ResNet 类结构的典型设计思路非常干净。但如果用步长 3224 是除不尽的后面就必须靠填充或裁切来兜底。实操心得设计网络时尽量让输入尺寸是 2 的高次幂比如 224、256、512这样经过多次步长 2 下采样之后仍能整除不容易出奇偶问题。如果输入尺寸没法改就宁可多留一点填充也不要让它截断。4. 池化、空洞卷积、转置卷积的尺寸计算4.1 池化层的尺寸变化池化层的尺寸公式和普通卷积完全一样H_out floor((H_in 2p - k) / s) 1最常见的是 2×2 窗口、步长 2、无填充效果就是把高宽各减半。也见过 3×3 窗口、步长 2、padding 1 的配置输出是 ceil(H/2)能保证奇数输入也能凑出合理输出。池化的作用和卷积不一样它没有可学习参数只是做空间上的降维和聚合。尺寸上要特别注意的是池化的下采样是不可逆的信息损失所以分割类任务通常不再用池化而是用步长卷积来代替这样既降维又保留可学习能力。全局平均池化Global Average Pooling是个特例它把 H×W 直接压成 1×1公式上就是核大小等于输入尺寸。这一步的尺寸变化经常被忽略但它在把特征图接到分类头之前非常关键因为它的输出形状是 (N, C, 1, 1)展平后正好是 C 维不需要你再操心全连接层的输入维度。这也是为什么现在很多分类网络都拿 GAP 替掉了原来的大全连接层。4.2 空洞卷积的有效核计算空洞卷积也叫膨胀卷积的尺寸公式和普通卷积形式一致但要先把核大小换成有效核大小k_eff k (k - 1) × (d - 1)然后代入通用公式H_out floor((H_in 2p - k_eff) / s) 1举个数3×3 核膨胀率 2有效核 3 2×1 5相当于一个 5×5 的核但参数量还是 9 个。这就是空洞卷积的价值——用少量参数换取大感受野同时避免因为池化而丢分辨率。膨胀率 3 时有效核是 7膨胀率 4 时是 9。空洞卷积在 DeepLab 系列语义分割网络里是核心组件。它的问题是会产生网格效应膨胀率越大核元素之间的空洞越大某些像素永远不会被采样到导致局部信息丢失。常见的解法是叠加使用不同膨胀率比如 1、2、3 交替或者用混合空洞卷积结构让感受野覆盖更均匀。尺寸计算上要留个心要保证空洞卷积的输出尺寸等于输入尺寸same 行为填充量要按 k_eff 来算不是按 k。3×3 核、膨胀率 2 想保持尺寸需要 p(5-1)/22而不是 1。写成一个 3×3、padding 1、dilation 2 的卷积输出会缩小很多人在复现 DeepLab 的时候就是在这里第一次翻车。4.3 转置卷积的输出尺寸反推转置卷积常被叫作反卷积虽然严格说它只是一个反向的尺寸映射用于上采样是分割和生成网络里的标配。它的尺寸公式是H_out (H_in - 1) × s - 2p k output_padding这个式子看起来别扭其实用反过来想的方式更好理解正卷积把大图变小图转置卷积把公式倒过来解。已知正卷积的映射是 H_in → H_out floor((H_in 2p - k)/s) 1那么从 H_out 恢复 H_in就得到上面那个式子。这里出现了三个参数s、p、k 都对应正卷积的配置output_padding 则用来消除多解的歧义。举个经典例子输入 2×2用 3×3 核、步长 2、padding 1、output_padding 1输出是 (2-1)×2 - 2 3 1 4。也就是说2×2 上采样到 4×4正好是两倍。注意 output_padding 只会影响输出尺寸不会往特征图里补实际的零它只是让输出多长那么几行几列。当你在设计编码器-解码器结构时最稳的做法是让解码器每一层的输出尺寸严格等于编码器对应层的输出尺寸跨层拼接才不会出问题。实操上我一般会先把编码器每层的尺寸列表算出来再反推解码器的参数保证一一对上。5. 手算 LeNet-5把公式用一遍5.1 LeNet-5 原始结构拆解LeNet-5 是绕不开的起步例子结构简单、层数少、每层尺寸变化都很典型用它把公式完整走一遍再合适不过。原始版本的输入是 32×32 的单通道手写数字图整体结构是C1卷积层6 个 5×5 卷积核步长 1无填充S2池化层2×2 窗口步长 2C3卷积层16 个 5×5 卷积核步长 1无填充S4池化层2×2 窗口步长 2C5卷积层120 个 5×5 卷积核步长 1无填充F6全连接层84 个神经元输出层10 类注意原始论文里 C3 与 S2 之间的连接是稀疏的只在部分通道间做卷积这是那个年代为了省算力做的设计现在复现时一般简化成全连接式的标准卷积尺寸计算不受影响。5.2 逐层尺寸计算表把每一层代进公式结果如下表。这张表我建议手推一遍推完对卷积的尺寸变化就有感觉了。层类型输入尺寸ksp计算过程输出尺寸C1Conv32×32×1510(32-5)/1128×28×6S2Pool28×28×6220(28-2)/2114×14×6C3Conv14×14×6510(14-5)/1110×10×16S4Pool10×10×16220(10-2)/215×5×16C5Conv5×5×16510(5-5)/111×1×120F6FC1×1×120---展平120OutFC120----10看 C5 这一行输入 5×5核也是 5×5一算正好输出 1×1核刚好把整个特征图盖住。这正是原始设计有意为之——C5 名义上是卷积层实际输出已经是 1×1等价于一个全连接层所以后面接 F6 时直接展平就行。这种用卷积实现全连接的技巧现在依然常见好处是不用关心输入分辨率。再往后看如果输入不是 32×32 而是 28×28MNIST 原始尺寸C1 输出就变成 (28-5)124S2 变 12C3 变 8S4 变 4C5 时 5×5 核根本放不下——直接报负维度。所以复现 LeNet-5 时必须把 MNIST 图填充到 32×32这不是可选项是结构上的硬要求。5.3 用框架验证手算结果手算完之后用代码验证一下既确认结果也熟悉怎么打形状。下面这段用 PyTorch 写import torch import torch.nn as nn model nn.Sequential( nn.Conv2d(1, 6, kernel_size5, stride1, padding0), # C1 nn.Tanh(), nn.AvgPool2d(kernel_size2, stride2), # S2 nn.Conv2d(6, 16, kernel_size5, stride1, padding0), # C3 nn.Tanh(), nn.AvgPool2d(kernel_size2, stride2), # S4 nn.Conv2d(16, 120, kernel_size5, stride1, padding0), # C5 nn.Tanh(), nn.Flatten(), nn.Linear(120, 84), nn.Tanh(), nn.Linear(84, 10), ) x torch.randn(1, 1, 32, 32) for i, layer in enumerate(model): x layer(x) print(f第{i}层 {layer.__class__.__name__:12s} 输出形状: {tuple(x.shape)})跑一遍输出会依次是(1,6,28,28)、(1,6,14,14)、(1,16,10,10)、(1,16,5,5)、(1,120,1,1)、(1,120)、(1,84)、(1,10)和手算表格完全一致。我最开始学的时候就是这么对的手算一遍、代码跑一遍两边对上公式才算真记住了。实操心得如果懒得逐层跑PyTorch 里可以用torchsummary或torchinfo库一行代码打印整个模型的每层输出形状。但别依赖它来猜它只是验证工具不能替代推导。工作里我见过有人完全靠试错改全连接层维度改了七八次才碰对这种习惯一旦养成后面搭复杂网络会吃大亏。6. 尺寸计算里的高频坑与排查手法6.1 常见问题速查表现象可能原因排查方法解决思路报错 negative dimension某层核大于输入逐层打印形状找出缩小的那层减小核、加填充或提前上采样全连接层维度不匹配卷积输出尺寸算错手算一遍卷积输出用 GAP 替代大 FC或按公式修正拼接 concat 失败编码解码尺寸不一致比对两侧每层形状用 output_padding 或裁切对齐输出尺寸和预期差 1floor 取整或奇偶问题换偶数输入尺寸再试改输入为 2 的幂或显式算填充量same 填充没生效框架默认不同检查 padding 设置和框架约定显式指定 padding 数值空洞卷积输出缩小填充按 k 而非 k_eff 算复核有效核大小填充量按 k_eff 重算这张表基本覆盖了日常会遇到的尺寸问题。我建议把它贴在显示器边上报错的时候先对着查一遍绝大多数 shape 相关的错误都能在几分钟内定位到。6.2 几个我踩过的坑第一个坑是框架间的卷积约定不一致。PyTorch 的nn.Conv2d里padding是明确指定补多少圈而 TensorFlow/Keras 里paddingsame是自动补到输出尺寸等于 ceil(输入/步长)。同样的参数写在两个框架里输出尺寸可能不一样。我最早跨框架迁移代码时就吃过这个亏每一层的形状都要重新对一遍。迁移模型的时候最保险的做法是把same全部显式展开成具体的填充数字。第二个坑是把 padding 和 stride 的关系搞反。有段时间我以为只要 padding 够大输出就一定更大实际公式里 padding 增加确实让输出变大但如果步长也跟着上去两者会互相抵消。判定方向的时候看的是 (2p - k) 和 s 的相对关系单看某一个参数容易误判。第三个坑是忽略了池化的取整效应。输入 7×72×2 池化步长 2输出是 floor((7-2)/2)1 3不是 3.5。这个 0.5 被舍掉之后解码器那边如果按 3.5 去设计上采样尺寸就对不上。处理奇数尺寸时我一般会在下采样前先做一次填充把尺寸凑成偶数或者在下采样后用output_padding把尺寸补回来两种方式都行关键是编码器和解码器的尺寸表要一致。第四个坑是没意识到 batch 维和通道维不参与空间计算。有同学看到输入形状是(32, 3, 224, 224)误以为 32 是通道数算尺寸时把 32 也代进去。记住(N, C, H, W)的顺序尺寸计算只动 H 和 W前两维按原样走除非是全连接层要把 C×H×W 一起展平。注意展平flatten这一步是尺寸计算里最容易被忽略的环节。卷积部分的空间尺寸算对了但展平后的维度算错全连接层照样报错。展平后的长度就是 C×H×W接 FC 之前务必自己乘一遍。7. 3D卷积与图卷积的尺寸处理7.1 3D卷积的尺寸公式3D 卷积处理的是体数据输入形状是(N, C, D, H, W)卷积核是(C_out, C_in, kd, kh, kw)。尺寸公式就是把二维形式在三个空间维度上各写一遍D_out floor((D 2p_d - k_d) / s_d) 1H_out floor((H 2p_h - k_h) / s_h) 1W_out floor((W 2p_w - k_w) / s_w) 1三个维度可以共用同一组参数也可以各自不同。视频理解里常见的配置是时间维度用较大的核或较大的步长空间维度用 3×3 核比如时间上步长 2 做快速降帧空间上步长 1 保留细节。这时候的时间维尺寸下降会明显快于空间维设计时要把三个维度的尺寸表都列出来不能只看空间部分。3D 卷积的参数量和计算量是二维的立方级增长同样的通道配置下3D 卷积的 FLOPs 大概是 2D 的 kd 倍。所以做视频任务时经常用 (21)D 分解卷积——把一个 3D 卷积拆成空间二维卷积加时间一维卷积尺寸计算逻辑不变只是分两步算。例如先做 kd1 的二维卷积时间维不变再做 khkw1 的一维时间卷积空间维不变最后把结果组合起来输出尺寸和标准 3D 卷积一致但参数量大幅降低。7.2 图卷积的尺寸到底指什么图卷积神经网络GCN的尺寸概念和图像卷积完全不是一回事。图像卷积处理的是规则的网格有明确的高、宽、通道图卷积处理的是节点和边组成的图结构节点之间连接的数目不固定所以没有特征图高宽这个说法。GCN 里数据的基本形状是(N, C)N 是节点数C 是每个节点的特征维数。一层图卷积做的事情是把所有节点的特征聚合起来按邻接关系做加权求和再过一个线性变换。输出形状仍然是(N, C_out)——节点数不变特征维度可以变。换句话说图卷积改变的是特征维度不是节点数目。对比来看图像卷积改变的是空间尺寸H、W特征维度由卷积核数量决定图卷积不改变节点数特征维度由权重矩阵的形状决定。把这两者的差异弄清楚就不会拿图像卷积的公式去套图网络了。如果一定要在图卷积里谈尺寸那对应的量是节点数和每个节点的特征维度以及邻接矩阵的形状(N, N)。邻接矩阵决定了信息传播的范围一阶邻域还是一阶加二阶会直接影响计算的感受范围。这跟图像里用卷积核大小和层数来扩大感受野是同一个道理只是表现形式不同。7.3 顺带说说感受野的递推计算尺寸计算和感受野计算经常一起出现虽然严格说它们是两件事但实际搭网络时往往要同步考虑。感受野指的是输出特征图上一个像素对应到原始输入上的区域大小。递推公式是RF_i RF_{i-1} (k_i - 1) × stride_prod_{ji}其中 stride_prod 是前面所有层步长的累乘。举个例子连着三层 3×3、步长 1 的卷积第一层感受野 3第二层 32×15第三层 52×17正好等于一个 7×7 卷积的感受野。这就是小核堆叠代替大核的理论依据。反过来如果在中间插入一层步长 2 的卷积后面的感受野增长会加倍。所以设计网络时下采样的位置会显著影响最终的感受野。我一般在算出每一层的输出尺寸之后顺手把感受野也推一遍两列数据放在一起能很直观地看出哪一层的感受范围出现了跳变。这一步多花几分钟对后面调结构和做可视化都有帮助。实际操作里如果你觉得推导太繁琐可以直接用一个简单的小程序把每层的 k、s、p 填进去让它帮你把输出尺寸和感受野一起算出来。我自己的习惯是维护一个文本文件每设计一个新网络就在里面列一遍参数表跑通之后再落成代码。这个习惯让我少踩了很多形状的坑也让我对哪里该下采样、哪里该保分辨率有了更清楚的手感。