聊到 CNN 卷积网络,很多人会把注意力放在卷积核大小、步长、激活函数这些参数上,真正把 padding 的两种方式彻底吃透的人并不多。我第一次意识到 padding 是个大问题,是在搭建一个图像分割网络时:模型结构看起来完全正常,可每次跑完一个 block,特征图尺寸就莫名其妙少一圈,叠加几次之后,下采样路径竟然把边缘信息丢得干干净净,最后分割结果糊成一片。排查了半天才发现,问题根本不在结构设计,而在每一层卷积的 padding 选择上。
这篇内容主要围绕卷积网络里的 valid padding 和 same padding 展开,说清楚它俩各自解决什么问题、怎么计算、框架里怎么实现、实际选型时该考虑哪些因素。适合正在学习卷积网络原理的初学者,也适合自己动手搭模型但总被特征图尺寸问题困扰的开发者。我会结合自己踩过的坑、调试过的模型,把这两个概念从数学推导到工程实践完整串一遍。
1. Padding 到底在解决什么问题:三个我实际遇到的现象
1.1 特征图不停“缩水”,整个网络结构被悄悄改变
先从一个最常见的现象说起。假设输入是一张分辨率为 224×224 的图像,卷积核大小是 3×3,步长设为 1,不加 padding 的话,每个卷积层都会把特征图尺寸变成 222×222。你可能会说,才缩了 2 个像素,算什么大问题?可一旦网络有十几层卷积,累积效应就很明显:20 层下来,特征图直接缩到 184×184,空间分辨率丢失了接近两成。
在很多任务里,空间分辨率就是信息量,尤其是分割、检测、人脸关键点这类需要精确定位的任务。缩小的不仅是尺寸,更意味着图像边缘处的结构被更快地“压扁”,高层特征对边缘区域的表达能力被削弱。更隐蔽的副作用是:当特征图尺寸在浅层不断变化时,后续池化、归一化、跳层拼接都会受到影响。比如你用了全局平均池化做分类,那还好;但如果你在中间层做多尺度融合,或者需要把浅层特征和深层特征拼接,尺寸不一致就直接报错,或者需要额外加插值层来对齐。这些被迫插入的额外层,都会改掉你最初设计的感受野分布。
我自己的经验是:如果一个网络结构设计出来,每一层输出尺寸都在变化,模型的内在计算逻辑会变得很难追踪。真正稳定的结构,往往在关键路径上保持空间尺寸不变,只在明确需要降采样的位置才缩小分辨率。这种“显式降采样”的设计思路,远比“每层都无意中缩一点”要清晰得多。而 padding 正是控制这种缩水行为的最直接手段。
1.2 边缘像素的话语权被无视,中心区域天然占便宜
第二个问题更隐蔽,也更容易被忽略:卷积操作本质上是滑动窗口求和,边缘区域的像素参与卷积计算的次数远少于中心区域。以 5×5 输入、3×3 卷积核、无 padding 为例,左上角那个像素只出现在第一次卷积窗口中,参与计算 1 次;而正中心位置的像素,会因为窗口滑过它周围的所有可能位置,参与计算 9 次左右。你感受一下这个差距:同样是图像里的一个像素,中心点对最终特征图的贡献是角落点的几倍甚至十几倍。
这种“位置不均衡”直接导致卷积网络天然更关注图像中心,边缘信息的利用效率极低。如果输入图像的边缘恰好是重要目标,比如医学影像中的病灶贴着图像边界,或者遥感图像里目标就在切片边缘,不加 padding 的训练结果就会非常不稳定:同样的目标,放在图像中心学习得很好,放到边缘就检测不出来。
零填充(zero padding)最直接的作用就是给边缘像素创造额外的“参与机会”。填充了几行零之后,原本在角落的像素会重新出现在多个卷积窗口的覆盖范围之内,相当于把它从“边缘居民”变成了“普通居民”,参与度得到补偿。虽然这个补偿不是完全均匀的,但相比完全不填充,位置差异性已经大幅缩小。理解这一点,比单纯记住“same padding 能保持尺寸”要重要得多,因为这才是 padding 在特征层面上的真实价值。
1.3 跳层连接和多尺度任务中的对齐问题
第三个现象出现在我在做类似编码器-解码器结构的时候。跳跃连接(skip connection)需要把编码器某一层的特征图和解码器对应层的特征图拼接或相加。在原始设计里,我原本期望两侧尺寸完全一致,结果因为没有统一 padding 策略,编码器某一层输出是 56×56,解码器对应层却是 54×54,直接在拼接环节报维度错误。
除了这种明显的维度错误,还有一种更隐蔽的问题:即使尺寸一样,如果两侧特征边界补零的方式不对称,拼接之后也会在边界处留下明显的“接缝”。在做图像生成、超分这类像素级任务时,这种接缝经常被网络理解为真实结构,导致生成结果出现奇怪的边界纹理。
多尺度任务更麻烦。特征金字塔等结构经常需要把不同层级的特征图相互融合,一层的尺寸可以在降采样后任意变化,但同一分支内部的尺寸一致性是必须保障的。如果每一步卷积都随手用不同的 padding 方式,整个金字塔的尺寸关系就会乱成一团。所以我现在搭网络时有一种基本习惯:先明确哪些层负责保持尺寸,哪些层负责降采样,然后统一制定 padding 策略,而不是每一层临时决定。
2. Valid 与 Same 的本质差异:从 5×5 输入一步步推导
2.1 输出尺寸公式与三个关键参数
不管 padding 怎么设置,卷积输出的尺寸都可以用一条统一公式表达:
输出尺寸 = floor((输入尺寸 + 2×padding − 卷积核尺寸) / 步长) + 1
这里的 padding 指每侧补零的圈数。三个关键参数是:卷积核尺寸 k、步长 s、padding 圈数 p。理解这个公式之后,很多“为什么我算出来的尺寸和框架里报错不一样”的问题都能迎刃而解。
需要注意一点:这里用的 padding 是“单侧补零圈数”。比如 padding=1,意思是上下左右各补一圈零,那么输入尺寸在公式里就加 2。不同的开源框架对 padding 的定义有时不太一样,有些参数直接传总补零量,有些传单侧量,这个我在第 3 部分会展开讲。
2.2 Valid Padding:不做处理,让特征自由缩减
Valid padding 就是 p=0,不添加任何额外像素,卷积窗口只在原始输入范围内滑动。也正因为窗口无法覆盖到边界之外的区域,输出尺寸一定会小于或等于输入尺寸。
还是拿 5×5 输入、3×3 卷积核、步长 1 来算:输出尺寸 = (5 + 0 − 3) / 1 + 1 = 3。输出是 3×3 特征图。如果换成 7×7 输入,输出就是 (7 − 3) + 1 = 5。规律很直观:每经过一层 valid 卷积,尺寸就减少 k−1。
Valid padding 的立场是:我只在真实存在的像素上做计算,不引入任何人为构造的虚拟像素。它适合那些希望特征图尺寸逐层收缩,或者边界信息本身可信度不高的场景。做某些关键点检测时,如果图像边缘存在大量背景干扰,直接用 valid 卷积把边缘区域逐渐丢弃,反而能减少无效信息对网络的影响。代价是特征图尺寸会越来越小,高层特征的感受野虽然变大,空间分辨率却变低。
2.3 Same Padding:让输出尺寸与输入持平
Same padding 的目标是:在步长为 1 的前提下,让输出特征图尺寸和输入保持一致。要达到这个效果,通常取 p = (k − 1) / 2,也就是让卷积核覆盖范围在输入边界外侧各多出半个核的大小。
依旧用 5×5 输入、3×3 卷积核、步长 1:p = (3 − 1) / 2 = 1。输出尺寸 = (5 + 2×1 − 3) / 1 + 1 = 5。输入输出都是 5×5,尺寸保持不变。换成 5×5 卷积核时,p = 2,输入输出同样保持 5×5。
这里有一个常见误区:很多人以为只要 padding 层数够多,任何卷积核都能“保住尺寸”。实际上关键在于补零量要和卷积核尺寸协同:奇数卷积核配合 p=(k−1)/2 是最自然的选择,因为左右、上下可以对称分配。像 3×3 配 p=1、5×5 配 p=2、7×7 配 p=3,都是标准搭配。
如果卷积核是偶数,比如 4×4,要让输出和输入一致,就需要在一边补 2 个零、另一边补 1 个零,具体哪边多补由框架内部决定。这种不对称补零虽然一般不影响数值结果,却会让特征图在空间上产生一个微弱的偏移,虽然神经网络可以学到这种偏移的补偿,但在高精度定位任务里仍然建议尽量避开偶数卷积核。
2.4 步长不为 1 时,Same 的含义开始模糊
严格来说,same padding 的定义在步长为 1 时最清晰:输出等于输入。可当步长变成 2,输出尺寸就不再可能等于输入尺寸,因为输出大致变成输入的一半。这种情况下,“same”实际指的是“输出 = 输入 / 步长”,也就是和“不补零但步长为 2”的输出结果相比,保持相同的缩放关系。
举例:输入 5×5,卷积核 3×3,步长 2。valid 卷积的输出是 (5 − 3) / 2 + 1 = 2,也就是 2×2。如果用 p=1 做 padding,输出是 (5 + 2 − 3) / 2 + 1 = 3,也就是 3×3。两种结果差了整整一圈。如果你在框架里用 padding='same',底层会自动算出一组补零量,确保输出尺寸等于 ceil(输入尺寸 / 步长)。理解这个行为,比背结论更重要,因为很多人在做步长为 2 的降采样时,对 padding 的预期往往和实际不符。
2.5 两种方式的快速对照表
| 对比维度 | Valid Padding | Same Padding |
|---|---|---|
| 补零操作 | 不补零,p=0 | 补零,p 根据卷积核大小确定 |
| 输出尺寸(步长 1) | 输入 − k + 1 | 等于输入 |
| 边缘信息利用 | 直接丢弃边缘区域像素 | 通过补零保留边缘参与度 |
| 计算量 | 更小 | 略大,但差距通常可忽略 |
| 感受野增长速度 | 每层增长更快 | 每层增长稍慢 |
| 典型使用场景 | 边界干扰大、希望特征收缩 | 分割检测、跳层结构、保持尺寸稳定 |
这张表不是让你死记硬背,而是帮助你在设计网络时形成明确判断:需要保持空间尺寸、做密集预测时优先 same;希望特征图逐步收缩、或者输入边界不需要太多关注时可以用 valid。没有绝对的好坏,只有适不适合当前任务。
3. 框架实现里的细节:PyTorch 与 TensorFlow 的规则并不完全一致
3.1 PyTorch 的 padding 参数和 padding_mode
PyTorch 的 Conv2d 接口里,padding 参数是整数或元组,默认是 0,对应的就是 valid padding。设成 1 表示上下左右各补一圈。还有一个重要参数 padding_mode,默认是 zeros,也就是补零;也可以设置成 reflect、replicate、circular。
reflect 模式是镜像填充,边缘像素值会被翻折到边界外侧;replicate 是复制边缘像素值;circular 是循环填充。三种非零模式在图像生成类任务里偶尔会用到,尤其是 CCD 类图像修复任务中,复制边缘比补零造成的伪影更少。但我必须提醒一句:大多数主流卷积网络仍然使用默认的零填充,不是因为其他模式完全没用,而是因为零填充配合 BN 等归一化层时,数值行为最稳定、最可控。边缘的零值在卷积计算过程中会被当作“无信息”处理,不会引入额外的梯度信号。
PyTorch 里没有直接类似 padding='same' 的字符串参数,你需要自己计算 p。所以在 PyTorch 中写网络时,我通常定义一个函数,根据卷积核尺寸和步长自动算 p:
import torch.nn as nn def conv3x3_basic(in_channels, out_channels, stride=1): # 保证步长为1时,3x3卷积输出尺寸不缩水 return nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)这种方式简单直接,3×3 卷积配 padding=1 已经成为我在 PyTorch 里最常用的默认设置。
3.2 TensorFlow/Keras 的 Same 特殊行为
TensorFlow/Keras 的 Conv2D 接受 padding='valid' 或 padding='same'。表面上只是字符串选择,底层却藏着一个容易踩坑的细节:当步长大于 1 时,padding='same' 会自动决定在哪一侧多补像素。比如输入尺寸 5、步长 2、卷积核 3,为了让输出是 ceil(5/2)=3,框架会把总补零量分成两部分,通常额外的一列会补在右侧,额外的一行补在下侧。这种“右下偏好”在大多数任务中没有影响,但在需要严格对齐坐标的任务里,可能会造成一个像素级别的偏移。
TensorFlow 内部计算补零量的方法是:需要的总 padding = (步长 − 1) × 输入尺寸 + 卷积核尺寸 − 步长,然后按“顶部 = 总 padding // 2,底部 = 总 padding − 顶部”的方式分配。我做跨框架迁移时踩过这个坑,同一份模型结构和权重,从 Keras 换到 PyTorch,因为 padding 分配策略不同,输出结果有几处像素误差。所以如果要在不同框架之间对齐模型,不能只盯着“same”这个名字,最好打印每一层实际输出尺寸,逐层比较。
3.3 我用来做形状自检的最小代码片段
无论是哪个框架,我写好网络后的第一件事,永远是跑一段“形状自检”代码。给一个假输入,看每一层输出尺寸是否符合预期。这个习惯帮我发现了大量结构错误,而且比肉眼查网络定义高效得多。
import torch def shape_check(model, input_size=(1, 3, 224, 224)): model.eval() with torch.no_grad(): x = torch.randn(*input_size) for name, layer in model.named_children(): x = layer(x) print(f"{name:30s} -> {tuple(x.shape)}")这里只列模型顶层子层,如果你需要逐层打印更细的信息,可以用支持嵌套模块遍历的统计工具。重点不是工具本身,而是形成“写网络必查形状”的条件反射。很多 padding 错误,在形状打印面前会原形毕露。
3.4 padding 对参数量和计算量的真实影响
很多人担心加了 padding 会让模型参数量显著增加——其实不对。padding 本质是给输入特征图边缘补像素,它不改变卷积核本身的参数量,也不改变权重个数。参数量只由输入通道数、输出通道数和卷积核尺寸决定,和 padding 没有直接关系。
计算量确实会略有增加,因为输入尺寸变大了,卷积窗口滑动的次数变多。以 3×3 卷积为例,same padding 比 valid padding 单层计算量大约多出 (输入尺寸+2)² / (输入尺寸−2)² 倍,输入尺寸越大,这一比例越接近 1。当输入是 224×224 时,增加约 4%;当输入是 32×32 时,增加约 25%。小分辨率特征图上的 padding 计算量影响更明显,但相比之下这点开销通常远小于模型容量的灵活性收益。设计网络时,我一般不会因为这一点点计算量差异而放弃 same padding,真正需要考虑计算预算的场合,优先从通道数和卷积核大小上做文章。
4. 选择 Valid 还是 Same:不止是“保不保尺寸”那么简单
4.1 什么时候该用 Valid:设计上就要舍弃边界
不是所有任务都需要完整保留边缘信息。比如一些图像分类网络,输入图像的边缘往往是最不稳定的区域:一个目标物偶尔贴着边,偶尔完全在取景框内部;边缘像素在不同样本之间的相关性极低。这种情况下,用 valid 卷积逐步丢弃边缘区域,反而能提高模型对目标主体位置的鲁棒性。
另外,如果你使用的输入本身已经经过了严格的预处理,比如已经将目标居中、裁剪到固定大小,边缘区域包含的有效信息很低,那么 valid padding 就是一种合理的设计。它相当于告诉模型:你不需要花费额外的计算资源去理解边界那一圈像素之间的关系。
我还见过一种用法:在模型的第一层就用 valid 卷积做一个简单的边缘平滑,把边界区域收紧,后续层全部使用 same padding 保持尺寸稳定。这种做法适用于输入分辨率较高、而目标区域集中在中部的场景。要注意的是,这种策略会让模型的输出尺寸缩小,对于分类任务无所谓,但对于分割任务,最终特征图还要上采样回原分辨率,多一套对齐逻辑,并不划算。
4.2 什么时候该用 Same:保持空间结构、配合跳层连接
Same padding 最大的价值在于维持空间结构。分割、检测、关键点定位这类任务,输出就是和输入逐像素对齐的密集预测,如果每一层都随意缩尺寸,最后就得靠多次上采样恢复,而每次上采样都伴随信息损失。
更实际的理由是跳层连接。在编码器-解码器结构里,编码器第 n 层和解码器第 n 层往往需要拼接。如果编码器各层用 same padding 保持尺寸,就可以在降采样发生时明确记录尺寸变化点,和解码器上采样结构精确对齐,省去大量尺寸匹配的麻烦。我在做分割网络时,整个骨干网络几乎完全采用 3×3 + padding=1 + 步长控制降采样的策略,结构清晰、对齐简单,输出尺寸的推导一目了然。
4.3 池化与感受野:padding 对高层特征的间接影响
池化层本身不涉及 padding,但池化之前的卷积层如果用了 valid padding,特征图缩小后,池化窗口覆盖的范围也在改变,进而影响有效感受野。
举一个例子:两层 3×3 valid 卷积之后接一个 2×2 池化,感受野是 6×6;两层 3×3 same 卷积之后接一个 2×2 池化,感受野同样是 6×6,但第二种情况下,特征图尺寸更大,池化之前保留的中间特征更多。表面看感受野一样,但在边界区域,valid 卷积已经把边缘信息提前丢弃,边界处的感受野并不完整;same 卷积却通过补零让边缘处也拥有完整感受野。这个差异在深层特征上会持续放大,最终影响模型对边缘小目标的敏感度。
如果你做的是小目标检测,尤其是目标经常出现在图像边缘的任务,尽量用 same padding 保持特征完整。如果是做整体图像分类,类别主要由中心大物体决定,那 valid 造成的边缘信息损失通常无伤大雅。
4.4 一个实用框架:先用“不缩水”跑通,再做边界取舍
我个人的设计流程是这样的:先在所有卷积层统一使用 same padding,把网络结构跑通,确认逻辑无误;然后分析任务特性,如果确实有理由减少边缘信息,比如输入边界是纯背景,再把对应层的 padding 改成 valid,同时观察输出尺寸变化对下游结构的影响。先把网络做对,再把它调好,这个顺序能显著减少因 padding 导致的低级错误。
反过来,如果一开始就在各层混用 valid 和 same,结构出错时你会很难判断问题是出在 padding 选择,还是出在网络的其他逻辑部分。统一默认值,再定点调优,是实践中效率最高的做法。
5. 边界假象和大坑:反卷积、注意力、检测头里的 padding 问题
5.1 转置卷积的“边缘重叠加法”和 output_padding
很多人以为 padding 只在普通卷积里有讲究,做转置卷积(反卷积)时同样会遇到边界问题,只不过表现方式不同。转置卷积会把输入特征图的每个像素扩展成一个局部区域,相邻像素的扩展区域会相互重叠,重叠部分做累加。如果 padding 设置不当,边缘区域的累加次数会明显多于中心区域,产生类似“棋盘格伪影”的纹路,尤其是在图像生成任务里非常明显。
为了解决这个问题,转置卷积提供了一个 output_padding 参数,它的作用是调整输出一侧的额外补零量,让尺寸刚好能对上普通卷积的逆过程。注意 output_padding 并不是在输出上补零,而是增加输出的行数和列数,通常取值是 0 或 1。在编码器-解码器结构里,我怎么判断 output_padding 该取多少呢?最简单的办法就是先用形状公式推一遍:输出尺寸 = (输入 − 1) × 步长 − 2×padding + 卷积核尺寸 + output_padding。把公式凑到和目标尺寸一致,output_padding 就出来了。
5.2 注意力机制会放大位置偏见
近几年 transformer 类结构越来越常见,很多人喜欢在卷积特征图上叠加注意力模块。注意力模块通常会对所有空间位置计算相关权重,但卷积层输出的特征图如果因为 padding 而带有位置偏置,注意力模块会放大这种偏置。
我做过一个小实验:对同一个图片分别用 valid 和 same 卷积提取特征,然后接同一个全局注意力模块。结果 same 卷积的特征经过注意力后,注意力的分布更均匀;valid 卷积的特征则在中心区域出现明显的注意力集中。原因不难理解:valid 卷积让中心像素在特征里更“强壮”,注意力机制会进一步把这些强响应点的权重拉高。在目标检测、跟踪这些需要同时关注多个位置的场景里,这种偏置会伤害模型表现。如果你发现自己的注意力模块总是关注固定位置,除了检查数据,也值得检查一下前面卷积层的 padding 策略。
5.3 检测头里的中心偏置
检测任务里还有一种常见现象:模型对图像中心目标的检测效果明显好于边缘目标。排除数据集本身中心分布偏多的情况之后,padding 的“中心偏置”是重要嫌疑。即便使用了 same padding,零填充也会让边缘区域的信息“纯度”下降:边缘自然像素旁边多了一圈零,卷积计算时会把零值顺带融入特征,相当于给边缘特征引入了一种特殊的污染。这一点在目标较小、上下文依赖较强的检测场景下尤其突出。
缓解方法有几个:一是在输入层面做随机裁剪、平移增强,让目标出现在不同位置,迫使网络学会位置无关的特征;二是在骨干网络后期适当使用 valid 卷积丢弃一部分边缘特征,牺牲边缘但换回中心的稳定性;三是使用可学习的 padding 方式,比如用反射填充替代零填充,减少零值引入的伪信息。这些方法没有绝对优劣,但意识到“边缘位置天然吃亏”这个事实,是调试检测模型的前提。
6. 我的调试习惯:怎样快速判断 padding 没有选错
6.1 形状日志:每层都打印,不合预期立刻停
前面提过形状自检,这里再补充一个更细的实践。我把所有会改变特征图尺寸的操作统一封装:卷积、池化、步长、上采样,都在一个函数里完成形状打印。开发阶段这个函数会输出每一层的输入输出尺寸和当前有效感受野估算值。一旦某层输出尺寸偏离预期,立刻就能定位是哪一层、由哪个参数导致,而不是等整个模型跑完报一个维度错误,再回头翻半天代码。
日志里我最关注三个量:尺寸是否按预期降采样、边缘是否因为 valid 卷积被异常截断、同一条分支上同一分辨率的层是否保持一致。如果这三个量都正常,padding 基本就不会出问题。
6.2 边界响应检查:用“角落激活”测试验证 padding 效果
想更直观地验证 padding 对边缘的影响,我会构造一张特殊输入:只有一个像素点有值,其他全部为零。把这个点分别放在左上角、中心、右下角,跑一遍网络,观察特征图输出中哪些位置有强响应。如果使用 valid padding,放左上角时响应可能会在某些层消失;如果使用 same padding,不同位置产生的响应强度会更接近。这种测试虽然不能直接量化模型的精度,但能帮你迅速理解当前 padding 策略对空间信息的保留程度,比读公式直观得多。
6.3 用同一模型不同 padding 做消融实验
最后一种方法最朴素也最有说服力:在验证集上,把模型分别设置为 valid 和 same padding,各训练一轮或几轮,观察指标差异。很多初学者怕麻烦,总想从理论上推导出最优 padding。但 padding 的最优选择和数据集分布、任务目标、网络深度都有关,理论推导只能给出方向,最终抉择还是得交给实验。
我在做消融实验时,会把控制变量做到极致:只切换 padding 参数,学习率、数据增强、训练轮数完全一致,然后记录验证集上不同区域的指标。比如分割任务里,我会按距离图像边界远近划分几个区域,分别看 IoU。如果边缘区域指标在 same padding 下明显更好,那这个选择就值得保留。如果相差不大,优先用更简单的 valid,减少计算量。
最后补充一个和 padding 相关的操作细节:在搭建网络时,我习惯先在草稿本上画出整个网络的尺寸变化表,每一层后面注明输入尺寸、输出尺寸、padding 圈数、步长。这个过程看起来很笨,但实际帮我避开了大量维度错误,尤其是做多分支结构时,这张表就是网络设计的地图。等把尺寸变化表画顺了,再写代码基本一遍过。如果你也被特征图尺寸问题折腾过,我建议你先试试这个笨办法。