首页 / 资讯中心 / 文章详情

池化层反向传播详解:MaxPooling与AvgPooling求导全解析

池化层反向传播详解:MaxPooling与AvgPooling求导全解析 ★ FEATURED ARTICLE
很多人一听到“池化层”就觉得完全不用管反向传播因为没有可学习的参数。这是我见过最多的误解甚至不少做过几年模型训练的人在被问起 MaxPooling 的梯度怎么回传时也只能含糊说一句“哦就是把梯度原样传回去吧”。实际上池化层的反向传播虽然不涉及参数更新却是整个反向链路里最容易写错的地方之一。我早期手写过一个自定义池化算子前向跑得好好的loss 一反向就满屏 NaN最后定位到是 MaxPooling 的索引记录和梯度散布没对齐。那次排查之后我才真正把这两种池化的求导过程完整梳理清楚。这篇就来把 MaxPooling 和 AvgPooling 的求导过程讲透包括数学推导、工程实现里容易忽略的边界情况、以及为什么框架里这两种池化的反向逻辑差别巨大。适合正在做自定义算子、或者面试前想把反向传播原理彻底弄明白的同学参考。1. 为什么池化层的反向传播会被卡住三个常见误区先说说我观察到的几种典型卡壳点。很多入门阶段能写出全连接层、卷积层反向传播的人一到池化层就露怯通常是因为下面这三个误区。误解一以为池化层“没有参数所以反向是直通的”。池化层确实没有权重矩阵但这不代表梯度可以直接透传。MaxPooling 和 AvgPooling 的前向运算规则不同反向时梯度分配的规则也完全不同。如果是 MaxPooling梯度只有最大值位置能拿到其他位置梯度为 0而 AvgPooling 是把上游梯度均分给窗口里的每个元素。说“直通”的人大概率把池化层和 ReLU 这类逐元素激活函数搞混了。误解二以为只要把池化公式写成 y f(x)照着链式法则就能推。MaxPooling 的问题是前向公式是一个“分段常数”的选择操作它不是一个处处可导的连续函数直接对它求导会碰到不可导点。更麻烦的是如果你不记住前向时到底选的是哪个位置反向时你根本不知道梯度该给谁。所以 MaxPooling 的反向必须依赖前向保存的索引信息这是一个非常强的前后向耦合。误解三觉得 AvgPooling 就是把梯度除以 2。有人想当然地以为窗口里有几个元素就把梯度分成几份结果遇到重叠池化、padding、或者不是整除的窗口时边界情况全错。窗口是 2x2 时确实是除以 4但一旦窗口和步长配置有重叠每个输入位置收到的梯度是多个窗口贡献的累加绝不是简单“平分”。这三个误区背后其实是一个规律池化层在反向传播里的角色完全由“前向运算对输入的依赖模式”决定。MaxPooling 的依赖模式是“只依赖最大值的位置”AvgPooling 的依赖模式是“均匀依赖窗口内所有位置”。后续的所有求导规则都是从这两个依赖关系推出来的。2. MaxPooling 求导前向留索引反向按图索骥2.1 前向传播到底要保存什么先定义一下前向过程。假设输入是 X输出是 Y对于一个池化窗口 WMaxPooling 做的事情是Y_j max_{i ∈ W_j} X_i这里 W_j 是第 j 个池化窗口对应的输入区域i 是窗口内的元素下标。如果输入是三维特征图通道、高度、宽度池化是逐通道做的所以每个通道独立执行上述运算窗口也只在空间维度滑动。关键问题来了反向传播时需要知道“max 这个操作把梯度递给谁”。max 函数在数学上几乎处处可微除了最大值点之外对其它输入的偏导都是 0在最大值点上的偏导是 1。但是这里有个工程问题当窗口里出现两个相等且同为最大的值时梯度给谁数学上这是一个不可微点次梯度可以是一组区间但工程实现必须有一个确定性的选择。主流框架的做法很一致前向计算时记录每个窗口内最大值所在的索引indices反向传播时只把梯度写回这些索引对应的位置。这就是 MaxPooling 前向必须额外保存索引的根本原因。你在一些框架源码里会看到一个专用数据结构来存这个 indices它本质上就是一个和输出形状相同的整数张量每个值表示对应输出元素在前向时选中的输入坐标。如果窗口大小是 k_h × k_w输入是 C × H × W输出是 C × H_out × W_out那么 indices 的形状就是 C × H_out × W_out。反向时你拿上游传来的梯度 δY逐元素读取 indices把 δY_j 累加到 δX_indices[j] 上。2.2 反向传播的掩膜mask操作具体到反向代码MaxPooling 求导可以这样看先初始化一个和输入 X 形状相同的全零梯度张量 δX然后对每个输出位置 j取出前向存储的索引 i*_j执行δX[i*_j] δY_j这就是标准的“掩膜”操作。每个输出梯度只被添加到那一个被选中的输入位置上窗口内其它位置的梯度保持为 0。这也是为什么很多人把 MaxPooling 反向叫作 scatter 操作——把输出梯度“散开”到输入坐标上的指定位置。这里用一段纯 NumPy 风格的伪代码来说明方便和公式对照import numpy as np def maxpool_backward(dY, indices, input_shape): # dY: C x H_out x W_out, indices: C x H_out x W_out dX np.zeros(input_shape) C dY.shape[0] for c in range(C): for j_h in range(dY.shape[1]): for j_w in range(dY.shape[2]): i_h, i_w indices[c, j_h, j_w] dX[c, i_h, i_w] dY[c, j_h, j_w] return dX注意用的是 而不是 。这个区别非常关键。如果池化窗口有重叠即 stride 小于窗口尺寸同一个输入位置可能属于多个输出窗口而且可能在多个窗口里都是最大值此时这个位置的梯度必须累加所有相关窗口传来的梯度。即使 strided 池化没有重叠用 也是一个更稳妥的通用写法。2.3 多通道、多维输入下的索引处理细节MaxPooling 反向最容易出错的地方其实是索引的空间坐标映射。以二维池化为例假设输入高 H、宽 W窗口 k_h × k_w步长 s_h × s_wpadding p_h × p_w那么输出形状是H_out floor((H 2 * p_h - k_h) / s_h) 1 W_out floor((W 2 * p_w - k_w) / s_w) 1前向计算某个输出位置 (j_h, j_w) 时对应的输入窗口区域是起始位置j_h * s_h - p_h 结束位置j_h * s_h - p_h k_h - 1最大值索引必须落在真实的输入范围内不能因为 padding 把越界元素也算进去。很多自定义实现前向时为了省事直接对填充后的张量做窗口滑动结果反向时把梯度写到了 padding 区域这一看就错了。padding 区域在反向中不接收任何梯度因为前向中它们只是被裁剪掉的无效值不参与任何输出计算。多维情况下同理通道维度不共享窗口每个通道独立记录索引。如果只是二维图像还有 batch 维那需要在 batch 维度循环。批量实现时更高效的做法是把空间索引展平成一维坐标然后用 np.add.at 或者等价操作一次性完成累加而不是三层循环逐元素处理。这里有一个我实际踩过的坑最初为了省内存我在前向时没有保存全局输入索引而是保存每个窗口内最大值的“局部偏移”。反向时用窗口起点加上局部偏移来还原全局坐标。听起来没问题但遇到偶数尺寸窗口加右对齐边界时局部偏移还原出来的坐标会因为取整误差偏了一格。后来我改成直接把全局索引存下来内存多花了一点但反向再也不出鬼问题了。在求稳的场景里索引就是一切千万不要在坐标还原上动脑筋节省。3. AvgPooling 求导梯度不是“分一半”这么简单3.1 从链式法则推出梯度分配系数AvgPooling 的前向就是一个均匀线性操作。对窗口 W_j 来说Y_j (1 / K) * sum_{i ∈ W_j} X_i其中 K k_h * k_w 是窗口内元素总数。这个公式对每个输入 X_i 都是线性的所以反向直接用链式法则就行∂Y_j / ∂X_i 1 / K只要 i 在窗口 W_j 内于是从上游拿到梯度 δY_j 后往回传递时窗口内每个输入位置都累加δX_i δY_j / K这是 AvgPooling 反向的核心。它不是把梯度“传给最大值”而是把梯度均匀分给所有参与前向的输入元素。注意如果你只是把梯度除以 2那是把 2x2 平均池化误当成了“二分梯度”。2x2 窗口的 K 是 4所以每个元素拿到的是 δY / 4不是 δY / 2。这个细节在代码 review 时经常被人忽略。3.2 重叠池化和边界填充时梯度要去重与累加和 MaxPooling 一样AvgPooling 也需要考虑窗口重叠的问题。前向时输出窗口在输入上滑动如果 stride 小于窗口尺寸同一个输入位置会落在多个窗口里。反向时这个位置会从多个输出位置各收到一份梯度必须累加。举例来说输入长度 5窗口大小 3stride 为 2不填充。前向会产生 2 个窗口第一个窗口覆盖元素 0、1、2第二个窗口覆盖元素 2、3、4。元素 2 同时出现在两个窗口里前向时它对两个输出都有贡献所以反向时它会收到两份梯度来自第一个输出的 δY_1 / 3加上来自第二个输出的 δY_2 / 3。这其实是任何“滑动窗口类算子”在反向时的通用规则某个输入位置对多个输出位置有贡献就要把梯度累加起来。这个规则和卷积层反向时的累加逻辑完全一致。AvgPooling 不存储任何前向额外信息因为它不需要记录选择结果只需要在反向时已知窗口配置K、stride、padding就能还原每个输入元素参与了哪些输出窗口。padding 对 AvgPooling 的坑藏在“除数 K 是否包含 padding 元素”这个问题上。有两种常见实现一种是前向先把输入 padding 好然后窗口在填充后张量上滑动分母 K 就是窗口面积无论窗口内有多少填充值另一种是前向时遇到 padding 区域就跳过实际有效的元素个数作为分母。这两种方式的前向输出都不同反向时跟随各自的前向公式求导即可。最怕的是前向按有效元素个数做平均反向却按完整窗口面积分梯度那数值就会对不上。如果用完整窗口除以 K 的方式反向时确实也要把梯度除以 K即使某些 K 元素来自 padding 区域。从数学上看padding 元素在该窗口的梯度也是 1 / K只是由于 padding 区域在输入梯度张量里不存在这部分梯度会被丢弃。你不需要特殊处理只需要保证不把梯度写到 padding 坐标上就行。3.3 全局平均池化的特殊情况全局平均池化Global Average PoolingGAP是 AvgPooling 的极端形态窗口大小等于整个特征图的空间尺寸输出只有一个值。设输入空间大小为 N H * W前向公式为Y (1 / N) * sum_i X_i那反向就是每个输入位置的梯度都等于δX_i δY / N这是个很漂亮的线性梯度流。因为 GAP 没有一个可学习的参数却能把每个位置的信息均匀聚合它在很多分类网络里被用来替代 Flatten 全连接层。反向梯度会均匀回传给特征图的每个像素不会产生稀疏性所以梯度流非常稳定。如果你看到某篇论文里说“用 GAP 减少参数量同时避免过拟合”它的反向计算本质上就是这套均匀分配规则。从这个角度再看 AvgPooling你会发现它与 MaxPooling 在反向传播中的地位完全不同MaxPooling 是一个不可微选择操作的工程近似AvgPooling 是一个货真价实的线性算子只是系数恰好是 1/K。4. 两种池化反向在工程实现上的差异与坑4.1 MaxPooling 的原子操作和稀疏性问题工程上实现 MaxPooling 反向时最大的麻烦是“同一位置可能接收多个梯度”。前面说过如果池化窗口有重叠且某个输入位置在多个窗口中都恰好是最大值那么多个上游梯度的写入目标会指向同一个坐标。在并行执行反向时这会引发数据竞争。主流框架的 CUDA 实现里会用到 atomicAdd 来做累加确保多个线程写同一个位置时不会互相覆盖。这就是为什么你在看一些框架的 backward kernel 源码时会看到对输出梯度张量做原子加操作的代码。如果你自己写 kernel忘了 atomicAdd遇到重叠池化时梯度就会随机丢失表现为训练过程忽好忽坏、loss 抖动异常。另一个工程特性是稀疏性。MaxPooling 反向产生的梯度张量中有大量零因为窗口内只有最大值位置有梯度。当 stride 等于 kernel 大小时每个输入元素只属于一个窗口此时梯度张量的非零元素比例最高也就是 1 / K。如果 K 等于 4那最多只有 25% 的位置有梯度。这种稀疏性会影响后续层的梯度信号如果你在 MaxPooling 后面接了 BatchNorm统计量计算时大量梯度为零可能让 BN 的方差估计发生偏移。4.2 AvgPooling 的数值稳定性AvgPooling 反向不涉及索引也不涉及原子操作理论上实现非常简单但有一个数值问题容易被忽视除以 K 之后的浮点误差。假设 K 是 4δY_j 是 1.0那每个元素分到 0.25。如果同样的输入位置被多个窗口覆盖多个 0.25 会累加成 0.5、0.75 之类的小数。在 float16 低精度训练里这种除以一个整数再反复累加的运算会累积浮点误差导致梯度值出现轻微漂移。虽然通常不会让训练崩掉但你要是做梯度检查gradient check时发现误差比其它层大一个量级很可能就是这个原因。另一个坑是窗口面积 K 的读取。如果你把 K 写死成 2x24之后换了 3x3 窗口却忘了改反向梯度就会整体大了一圈。建议实现时用配置文件里的 k_h 和 k_w 实时计算 K不要到处硬编码。4.3 手写自定义池化层时我踩过的坑我在自定义一个带池化逻辑的网络模块时有两种典型错误都是花了很长时间才定位的。先说第一个MaxPooling 的反向里我用的是一个预先清零的梯度数组然后对每个输出位置做“直接赋值”而不是 。前向是 stride2, kernel2 的无重叠池化所以一次也没碰到问题。后来实验需要把 stride 改成 1 做更密集的特征提取结果 loss 直接发散。原因就是重叠区域里第二个窗口的梯度把第一个窗口写好的梯度覆盖掉了。改成 atomicAdd 或者先检查目标位置是否已有值再累加问题立刻解决。第二个坑是 padding 的偏移。AvgPooling 前向如果用了 padding反向计算每个输出窗口对应的输入区间时需要把 padding 减掉。我在反向时忘记减去 padding导致窗口位置整体右移了一个 padding 宽度等于梯度被“平移”了。这种错误前向看不出来因为前向无论你理解成“先 padding 再滑动”还是“滑动后裁剪”结果通常一样但反向如果坐标算错就会把梯度写偏。一个非常有效的自检方法是用 torch.autograd.gradcheck 或者自己写数值梯度检查比较解析梯度和中心差分梯度。我第一次跑数值梯度检查时一眼就发现了坐标偏移问题省了至少半天的 debug 时间。5. 从求导过程看模型设计为什么 MaxPooling 不等于“免费的参数”5.1 稀疏梯度对网络训练的实际影响MaxPooling 的稀疏梯度并不只是实现细节它对整个网络的梯度流有真实影响。考虑一个三层网络Conv - MaxPool - Conv。前向时 MaxPool 只保留每个窗口的最大值反向时梯度只流回最大值对应的位置。这意味着那些在前向没有被选中的输入位置在反向时完全收不到梯度它们的误差信号被直接切断了。这带来两个问题。第一如果网络初始化不好某个区域的特征始终不是局部最大值那么它在训练早期就永远拿不到梯度可能一直学不好。有人会因此在 MaxPool 之前加一层带学习的激活或者多分支结构让梯度路径更充裕。第二MaxPool 会造成梯度的空间分布极不均匀特别是再叠加 Dropout 或者稀疏正则时梯度信号可能退化到只有少数几个 strong feature 在更新网络其余部分像是在被冻结一样。AvgPooling 则完全没有这个问题。它的梯度均匀分配到窗口内每个位置每个输入元素都能收到信号训练时所有位置的参数都能得到更新。在实际任务里如果你发现网络后半部分学得不好把 MaxPool 换成 AvgPool 甚至只是 stride2 的卷积往往就能缓解梯度稀疏带来的优化停滞。5.2 池化与步长卷积的梯度对比这里有个很自然的延伸问题既然 MaxPool 有稀疏梯度、AvgPool 有均匀梯度那直接用 stride2 的卷积替换池化梯度怎么变stride2 卷积在反向时的梯度计算方式和普通卷积一样输入梯度是输出梯度与卷积核的转置卷积。它没有“只选最大值”这种硬规则也没有“均匀分配”这种简单系数而是用旋转后的卷积核把梯度投影回输入空间。卷积的每个输入位置都会收到周围输出梯度按核权重加权后的贡献所以梯度密度和核大小有直接关系。它比 MaxPool 稠密比 AvgPool 更复杂。这也解释了为什么现代网络里越来越多的下采样直接用步进卷积完成。步进卷积有可学习参数能够自适应地决定聚合权重而不是像 MaxPool 那样只能被动接受最大值也不像 AvgPool 那样只能公平分发。从梯度视角看步进卷积反传的梯度同时携带了“哪些输入位置重要”的信息而这个信息是可学的。不过 MaxPool 仍有它的存在意义。它对激活值的平移变化更鲁棒而且前向计算极其便宜、不需要任何参数在早期轻量模型里是很划算的选择。工程上到底选哪个取决于你对内存、算力、梯度密度和任务类别的综合考量。5.3 实践建议与面试题级别的自测最后给几条实操建议。第一写任何自定义池化层时务必用数值梯度检查来验证反向实现。不需要多复杂中心差分误差在 1e-4 以下基本就没问题。第二MaxPool 反向一定要保存前向的全局索引不要在反向时重新计算最大值位置因为那既慢又容易因浮点比较出错。第三池化窗口如果和 padding、重叠配置同时上阵先在纸上把窗口起点、终点公式写清楚再写代码避免后续反复试错。下面这组自测题如果都能答出来说明你对这部分内容是真的吃透了输入 4x4kernel 2x2stride 2MaxPool 反向时一个输入位置最多收到几个上游梯度的累加输入 5x5kernel 3x3stride 2AvgPool 反向时中心位置收到的梯度系数是多少如果 MaxPool 的窗口内有两个相等最大值反向时梯度如何分配为什么 AvgPool 反向不需要保存索引而 MaxPool 必须保存如果 MaxPool 后面接一个全连接层梯度经过全连接层回到池化输出再经过池化反向哪些位置的梯度一定为 0前两个的答案分别是“一个”和“两到三个窗口的贡献累加”第三个的答案是框架会固定选其中一个第四个是因为 MaxPool 的前向操作不是线性均匀的第五个则是那些在前向中没被选为最大值的输入位置。我自己在带新人时最有效的训练方式就是让他在 NumPy 里手写这两个层的反向然后跑一轮数值梯度检查。这个过程比看十篇推导文章都管用。毕竟池化层的梯度反向真正难的不是公式本身而是把“前向依赖了什么”和“反向需要什么信息”这两件事严丝合缝地对起来。最后再分享一句我常对团队说的话看一个算子的反向实现本质上是看它前向到底“记住”了什么东西。MaxPool 记住了选择所以反向是 scatterAvgPool 什么都没记只靠配置还原依赖所以反向是 uniform 累加。把这一句话刻在脑子里池化层的求导就再也不会出错了。
阅读完成 · 觉得有帮助?
咨询建站