资讯动态

卷积神经网络Padding详解:从Valid到Same,彻底搞懂卷积输出尺寸计算

发布时间:2026/10/13 0:35:32 来源:尧图企业网站定制
1. 从边缘像素被冷落说起为什么卷积网络离不开Padding如果你跟着吴恩达的深度学习课程一路学下来到了卷积神经网络这一章大概率会在某个瞬间卡住——为什么一个6×6的输入图像经过一个3×3的卷积核之后输出就变成了4×4更让人困惑的是视频里紧接着就说为了避免这个问题我们通常要加Padding。这个问题到底是什么问题Padding又到底在垫什么我当初学到这里的时候第一反应是少两个像素就少两个像素呗有什么大不了的。但后来自己动手搭网络跑图像分类任务才发现事情远没有这么简单。边缘像素被冷落只是表象背后牵扯到的是特征提取的完整性、网络深度的可扩展性以及边缘信息在多层卷积中的衰减规律。这三个问题不搞清楚后面学ResNet、学各种现代骨干网络的时候你会一直处于似懂非懂的状态。这篇笔记就是围绕Padding这个看似简单、实则暗藏玄机的操作展开。我会从为什么需要Padding这个最朴素的问题切入把卷积输出尺寸的计算逻辑、Valid卷积与Same卷积的区别、Padding的常见模式、以及实际写代码时容易踩的坑一层一层拆开来讲。适合正在跟吴恩达课程、或者刚接触CNN、对卷积尺寸变化感到困惑的朋友。读完之后你不仅能手算任意卷积层的输出尺寸还能理解为什么现代网络几乎清一色地选择Same Padding。2. 卷积输出尺寸的数学本质一个公式背后的取舍2.1 从6×6到4×4卷积核走了几步先把这个最基础的问题说透。假设输入是一张6×6的灰度图卷积核是3×3步长stride为1不加任何Padding。卷积核从左上角开始每次向右滑动一格滑到最右边时卷积核的右边缘刚好和图像的右边缘对齐。那么它能放多少个位置呢横向来看卷积核占据3列图像有6列可滑动的起始位置是第1列到第4列一共4个位置。纵向同理也是4个位置。所以输出就是4×4。用公式表达就是输出尺寸 输入尺寸 - 卷积核尺寸 1代入数字6 - 3 1 4。这个公式在步长为1、无Padding的情况下永远成立。你可以自己验证8×8的输入配5×5的卷积核输出就是8-514即4×4。这里的关键洞察是每一次卷积图像的有效尺寸都会缩小卷积核尺寸-1个像素。3×3的核让图像每层缩2个像素5×5的核让图像每层缩4个像素。这个缩小速度在浅层网络里可能无所谓但一旦网络加深问题就暴露了。2.2 深层网络的缩水危机假设你设计了一个10层的卷积网络每层都用3×3卷积核、步长1、不加Padding。那么每经过一层特征图的宽高各减少2个像素。10层之后特征图尺寸减少了20个像素。如果你输入的是32×32的CIFAR图像经过10层之后特征图尺寸会变成32 - 20 12也就是12×12。这还能接受。但如果你输入的是28×28的MNIST图像呢28 - 20 8变成8×8。再深几层特征图就变成负数了——这在数学上直接不成立程序会报错。换句话说不加Padding的卷积网络有一个隐形的深度上限这个上限由输入尺寸和卷积核尺寸共同决定。我实际做项目的时候遇到过一个典型场景用一个小型卷积网络处理64×64的医学图像网络设计到第15层的时候特征图尺寸已经缩到64 - 15×2 34看起来还行。但当我尝试把网络加深到30层以提升表达能力时特征图尺寸变成64 - 30×2 4再往下加一层就直接崩了。这就是缩水危机的真实写照。2.3 边缘像素的参与次数差异除了尺寸缩水还有一个更隐蔽的问题图像边缘像素和中心像素被卷积核覆盖的次数是不对等的。拿一个5×5的输入和3×3的卷积核举例。中心位置的那个像素第3行第3列会被多少个卷积窗口覆盖到答案是9个——因为卷积核可以放在以它为中心的9个不同位置上。而左上角那个像素第1行第1列只能被1个卷积窗口覆盖到就是卷积核放在左上角的那一次。这意味着什么意味着边缘信息在卷积过程中被采样的次数远少于中心信息。从特征提取的角度看边缘像素对输出的贡献天然就被削弱了。如果网络有多层这种削弱会逐层累积最终导致网络对图像边缘区域的特征学习不充分。这个问题在图像分类任务里可能影响不大因为分类主要看整体。但在语义分割、目标检测、边缘检测这类对空间位置敏感的任务里边缘信息的丢失是致命的。我自己做缺陷检测项目时就深有体会不加Padding的模型对图像四角的缺陷漏检率明显高于中心区域后来加了Padding之后才把这个偏差纠正过来。3. Padding的两种模式Valid与Same的本质区别3.1 Valid卷积不做任何填充的裸奔Valid卷积就是不加Padding的卷积输出尺寸严格遵循输入 - 核 1的公式。它的特点是输出特征图比输入小边缘信息参与度低每一层的空间维度都在收缩在早期的卷积网络比如LeNet-5的某些层里Valid卷积是默认选择。因为那时候网络浅输入尺寸也小缩一点无所谓。但到了深层网络时代Valid卷积就变成了奢侈品——你得有足够大的输入才能撑得住。3.2 Same卷积让输出和输入长得一样Same卷积的目标很明确让输出特征图的空间尺寸和输入保持一致在步长为1的前提下。为了实现这个目标需要在输入图像的四周填充一定数量的像素。填充多少呢这取决于卷积核的尺寸。对于奇数尺寸的卷积核3×3、5×5、7×7填充量有一个简洁的公式Padding (卷积核尺寸 - 1) / 23×3卷积核Padding (3-1)/2 1四周各填1圈5×5卷积核Padding (5-1)/2 2四周各填2圈7×7卷积核Padding (7-1)/2 3四周各填3圈填充之后输入尺寸从n×n变成(n2p)×(n2p)再经过卷积输出尺寸就是(n2p) - k 1 n 2p - k 1。把p (k-1)/2代入得到n (k-1) - k 1 n。完美输出尺寸和输入一致。这就是Same卷积的数学原理。它之所以能保尺寸本质上是因为填充的像素补偿了卷积核带来的尺寸损失。3.3 为什么现代网络偏爱Same Padding你去看现在的主流网络架构——ResNet、DenseNet、EfficientNet——几乎清一色地在3×3卷积上使用Same Padding。原因有三第一残差连接的需要。ResNet的核心是跳跃连接skip connection它要求主分支的输出和跳跃分支的输出能够直接相加。如果主分支的卷积改变了空间尺寸跳跃分支就必须也做相应的下采样才能对齐这增加了设计的复杂度。Same Padding让主分支的尺寸保持不变跳跃连接可以直接相加干净利落。第二特征图尺寸的可预测性。当所有卷积层都保持尺寸不变时只有池化层或步长大于1的卷积层才会改变空间维度。这让网络设计变得非常清晰你只需要关注下采样的次数而不用逐层计算尺寸变化。第三边缘信息的保留。填充的像素虽然不是真实数据但它让原本的边缘像素有了更多被卷积核覆盖的机会。从信息论的角度看这相当于给边缘信息加权让它在多层卷积中不至于被完全淹没。注意Same Padding在步长为1时能保证输出尺寸等于输入尺寸。如果步长大于1输出尺寸会变成输入尺寸除以步长向上取整这时候的Same指的是输出尺寸是输入尺寸的1/stride而不是严格相等。4. Padding的常见填充方式与选择逻辑4.1 零填充最常用但也最粗暴零填充Zero Padding就是在图像四周补上值为0的像素。这是深度学习框架里最默认的填充方式Keras的paddingsame、PyTorch的padding1底层做的都是零填充。零填充的好处是简单、无副作用、不引入额外参数。但它的缺点也很明显在图像边缘人为制造了一条黑色边框。如果输入图像的边缘本身是亮色比如白色背景上的物体那么这条黑色边框会在卷积时产生一个强烈的边缘响应可能干扰网络对真实边缘的判断。我做过一个实验用零填充的卷积网络处理白底黑字的文档图像结果网络在图像最外圈学到的特征明显异常可视化出来能看到一圈伪边缘。后来改成镜像填充这个问题就消失了。4.2 镜像填充与复制填充更温柔的方案镜像填充Reflect Padding是把图像边缘的像素像照镜子一样翻折出去。比如图像最右边一列是[3, 5, 7]镜像填充后右边会补上[7, 5, 3]。复制填充Replicate Padding则是直接复制边缘像素补上[3, 3, 3]或者[7, 7, 7]。这两种填充方式的好处是不会引入突兀的零值边界填充出来的像素和真实边缘像素在数值上是连续的。在图像风格迁移、超分辨率重建这类对边缘质量敏感的任务里镜像填充往往能带来肉眼可见的改善。不过它们也有代价计算稍微复杂一点而且不是所有框架都默认支持。PyTorch里需要用nn.ReflectionPad2d显式指定Keras则需要自定义层或者用tf.pad手动实现。4.3 填充方式对比与选型建议填充方式实现难度边缘伪影适用场景框架支持零填充极低较明显通用分类、检测所有框架默认镜像填充中等几乎无风格迁移、超分、生成PyTorch需显式指定复制填充中等轻微语义分割、边缘检测需自定义常数填充非零低取决于常数特殊预处理场景需手动设置选型的时候我的经验是如果没有特殊需求零填充就够了。现代网络在训练过程中会逐渐学会忽略那条零值边界的影响尤其是在大数据集上。但如果你做的是生成类任务或者对边缘质量有肉眼可见的要求那就值得花时间换成镜像填充。5. 动手算一遍从公式到代码的完整验证5.1 手算卷积输出尺寸的通用公式把前面的讨论汇总成一个通用公式。对于输入尺寸n×n、卷积核尺寸k×k、填充p、步长s的情况输出尺寸 floor((n 2p - k) / s) 1这个公式涵盖了所有情况Valid卷积p0输出 floor((n-k)/s) 1Same卷积s1p(k-1)/2输出 floor((n k - 1 - k)/1) 1 n带步长的Same卷积输出 floor((n 2p - k)/s) 1我建议你在学这一章的时候拿纸笔把几个典型组合都算一遍。比如n32, k3, p1, s1输出是32n32, k3, p0, s2输出是floor(29/2)115。算多了之后看到任何卷积层你都能在脑子里快速估算出尺寸变化。5.2 PyTorch中的Padding实操在PyTorch里Padding有两种设置方式。第一种是在卷积层里直接指定padding参数import torch.nn as nn # 方式一在Conv2d中直接指定padding conv nn.Conv2d(in_channels3, out_channels64, kernel_size3, padding1, stride1) # 输入32x32输出仍然是32x32第二种是使用独立的Padding层这种方式更灵活可以指定填充模式# 方式二使用独立的Padding层 pad nn.ZeroPad2d(1) # 四周各填1圈零 # 或者 pad_reflect nn.ReflectionPad2d(1) # 镜像填充 conv nn.Conv2d(3, 64, kernel_size3, padding0) # 卷积层本身不填充 # 前向传播时先填充再卷积 x pad(x) x conv(x)两种方式在数学上等价但第二种方式让你可以自由选择填充模式。我个人的习惯是标准卷积用第一种需要特殊填充时用第二种。5.3 Keras/TensorFlow中的Same与ValidKeras的接口更直观直接用一个字符串参数控制from tensorflow.keras import layers # Same Padding输出尺寸等于输入尺寸stride1时 conv_same layers.Conv2D(64, kernel_size3, paddingsame, strides1) # Valid Padding不填充输出尺寸缩小 conv_valid layers.Conv2D(64, kernel_size3, paddingvalid, strides1)这里有一个容易踩的坑Keras的paddingsame在步长大于1时填充量是动态计算的目标是让输出尺寸等于ceil(输入尺寸/步长)。这和PyTorch手动指定padding的行为不完全一致。如果你在PyTorch里想复现Keras的Same行为需要根据输入尺寸动态计算padding值不能简单地固定为(k-1)/2。5.4 一个完整的尺寸验证脚本下面这段代码可以直接跑验证不同配置下的输出尺寸import torch import torch.nn as nn def check_output_size(n, k, p, s): 计算并验证卷积输出尺寸 conv nn.Conv2d(1, 1, kernel_sizek, paddingp, strides, biasFalse) x torch.randn(1, 1, n, n) out conv(x) formula (n 2*p - k) // s 1 print(f输入{n}x{n}, 核{k}x{k}, 填充{p}, 步长{s} - f实际输出{out.shape[2]}x{out.shape[3]}, 公式计算{formula}x{formula}) # 测试几种典型配置 check_output_size(6, 3, 0, 1) # Valid: 输出4x4 check_output_size(6, 3, 1, 1) # Same: 输出6x6 check_output_size(32, 3, 1, 1) # Same: 输出32x32 check_output_size(32, 3, 0, 2) # Validstride2: 输出15x15 check_output_size(32, 5, 2, 1) # Same: 输出32x32跑一遍这个脚本把结果和你的手算对一下。如果全部吻合说明你对卷积尺寸的理解已经到位了。6. 那些年我在Padding上踩过的坑6.1 坑一以为Same Padding永远保持尺寸不变这是我初学阶段最大的误解。paddingsame在步长为1时确实保持尺寸但一旦步长变成2输出尺寸就变成输入的一半向上取整。我曾经在设计一个下采样模块时想用步长2的卷积代替池化同时保持尺寸不变结果发现输出尺寸直接减半导致后面的跳跃连接对不上。正确的做法是如果你用步长大于1的卷积做下采样就要接受尺寸变化并相应调整跳跃连接分支比如在跳跃分支上也加一个步长2的1×1卷积。6.2 坑二Padding值算错导致尺寸差1对于偶数尺寸的卷积核比如2×2、4×4(k-1)/2不是整数无法通过对称填充实现Same卷积。这时候框架的处理方式各不相同有的会在右边和下边多填一个像素有的会报错。我遇到过最隐蔽的一次bug是用4×4卷积核配padding1输入是奇数尺寸比如7×7输出变成了(72-4)16和输入的7差了一个像素。这个差异在浅层网络里不明显但传到后面和跳跃连接相加时直接报维度不匹配。排查了半天才发现是卷积核尺寸选得不合适。经验法则尽量用奇数尺寸的卷积核3×3、5×5、7×7这样Same Padding的填充量永远是整数尺寸计算不会出幺蛾子。6.3 坑三零填充在归一化后的副作用如果你的输入图像做了归一化比如减均值除标准差那么零填充的0在归一化后的空间里可能并不代表黑色。举个例子如果图像均值是0.5归一化后像素值在[-1, 1]之间那么填充的0实际上对应的是原始空间里的0.5也就是灰色。这会导致填充区域和真实边缘之间产生一个灰色边框而不是黑色边框。这个问题在Batch Normalization之后更加微妙。我的建议是如果输入做了归一化要么用镜像填充要么在填充前把数据反归一化回[0,1]空间再填充。当然最省事的办法还是用镜像填充一劳永逸。6.4 坑四Padding和池化的顺序搞反有些网络设计里池化层和Padding层的顺序会影响最终效果。标准做法是卷积→激活→池化Padding是卷积的一部分在卷积之前完成。但如果你手动插入Padding层一定要确保它在卷积之前而不是在池化之后。我曾经在一个自定义模块里把Padding放在了池化后面结果池化已经把尺寸降了一半再Padding再卷积尺寸变化完全乱套。后来养成了一个习惯每次写自定义层先用一个随机张量跑一遍打印每一层的输出尺寸确认无误再接入完整网络。7. 从Padding延伸出去那些相关的设计决策7.1 Padding与感受野的关系Padding不仅影响尺寸还间接影响感受野。感受野是指输出特征图上的一个像素对应输入图像上的区域大小。不加Padding时边缘像素的感受野比中心像素小加了Padding之后边缘像素的感受野被撑大了和中心像素更加接近。这对于需要全局理解的任务比如场景分类是有利的因为每个输出像素都能看到足够大的输入区域。但对于需要精确定位的任务比如关键点检测Padding带来的感受野均匀化可能反而模糊了位置信息。这时候就需要在Padding和定位精度之间做权衡。7.2 空洞卷积与Padding的配合空洞卷积Dilated Convolution通过在卷积核元素之间插入空洞来扩大感受野它和Padding经常一起使用。在空洞卷积中Padding的计算公式会变成Padding dilation × (k - 1) / 2比如3×3的卷积核dilation2那么Padding 2×(3-1)/2 2。如果不考虑dilation直接填1输出尺寸就会比输入小。这个细节在实现DeepLab系列网络时特别重要我当初复现的时候就因为忽略了dilation对Padding的影响导致输出尺寸和论文对不上。7.3 不同框架的Padding默认行为对比框架默认PaddingSame Padding实现特殊填充支持PyTorch0Valid手动计算或指定ReflectionPad, ReplicationPadTensorFlow/Kerasvalidpaddingsame需自定义MXNet0paddingsame有限支持JAX/Flax0paddingSAME需手动实现跨框架迁移模型的时候Padding行为的差异是最容易出问题的地方之一。我的建议是迁移前先用一个固定尺寸的输入跑一遍逐层对比输出尺寸确认一致后再做权重转换。8. 我个人的几条实操建议学完Padding这一节之后我在实际项目里逐渐总结出几条习惯分享出来供参考。第一默认用Same Padding。除非有明确的理由比如需要快速降维否则3×3卷积一律配padding1。这样网络设计简单尺寸可预测跳跃连接好写。第二输入尺寸尽量选2的幂次。32、64、128、256这样的尺寸经过多次下采样后仍然是整数不会出现奇数尺寸带来的取整问题。如果原始图像尺寸不是2的幂次预处理时resize一下。第三写自定义层时先做尺寸测试。用一个torch.randn(1, C, H, W)跑一遍打印每层输出尺寸确认和预期一致再接入网络。这个习惯帮我省了无数调试时间。第四边缘敏感任务考虑镜像填充。做分割、检测、生成的时候如果发现边缘区域的效果明显差于中心不妨把零填充换成镜像填充试试往往有惊喜。第五不要过度纠结Padding的理论最优解。现代网络在训练中会自适应地处理填充带来的影响零填充在绝大多数场景下都够用。把精力放在数据质量、网络深度、训练策略上收益更大。Padding这个操作本身只有一行代码但它背后的设计逻辑贯穿了整个卷积网络的发展史。从Valid到Same从零填充到镜像填充每一次选择都对应着对边缘信息如何处理这个问题的不同回答。理解了这个你再看那些现代网络架构就会发现很多设计决策都变得顺理成章了。

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

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

免费获取报价 →
↑