在上一篇文章中,我们系统讲解了二维卷积的数学定义、CNN工程实现的互相关操作,通过NumPy手动实现了卷积层前向传播,也可视化了卷积核的特征提取过程,同时对比MLP明确了CNN局部连接、参数共享的核心优势。但CNN的训练核心,仍离不开反向传播的链式求导逻辑------与MLP一致,CNN通过前向传播计算特征与损失,再通过反向传播回传梯度、更新参数(卷积核权重、偏置),最终实现特征学习的优化。
本篇作为CNN系列的核心难点篇,将衔接专栏前文(三)(四)反向传播的核心思想,聚焦CNN反向传播的关键环节:卷积层(权重、偏置)的梯度推导、池化层的梯度回传逻辑,全程遵循"数学公式推导→通俗逻辑解读→特殊情况补充"的思路,最后通过PyTorch的自动求导功能,验证我们手动推导的梯度结果正确性,。
一、核心前置知识回顾与符号统一
为避免符号混淆、衔接前文内容,先明确本篇推导所需的核心符号(与第九篇完全一致),同时回顾2个关键前置知识点,确保推导连贯。
1.1 符号统一
-
输入特征图(卷积层输入): X ∈ R H × W X \in \mathbb{R}^{H \times W} X∈RH×W, H H H为高度, W W W为宽度(单通道,多通道后续补充);
-
卷积核: K ∈ R k h × k w K \in \mathbb{R}^{k_h \times k_w} K∈Rkh×kw, k h k_h kh为卷积核高度, k w k_w kw为卷积核宽度(工程常用3×3);
-
卷积层偏置: b ∈ R b \in \mathbb{R} b∈R,标量(单通道,与卷积核一一对应,多通道时为向量);
-
卷积层前向输出(未激活): Z ∈ R H ′ × W ′ Z \in \mathbb{R}^{H' \times W'} Z∈RH′×W′, H ′ = ⌊ ( H + 2 p − k h ) / s ⌋ + 1 H' = \lfloor (H + 2p - k_h)/s \rfloor + 1 H′=⌊(H+2p−kh)/s⌋+1, W ′ W' W′同理( p p p为填充, s s s为步长);
-
卷积层激活后输出: A = σ ( Z ) A = \sigma(Z) A=σ(Z), σ \sigma σ为ReLU激活函数(;
-
损失函数: L L L;
-
梯度符号: ∂ L ∂ X \frac{\partial L}{\partial X} ∂X∂L 表示损失对输入 X X X的梯度, ∂ L ∂ K \frac{\partial L}{\partial K} ∂K∂L、 ∂ L ∂ b \frac{\partial L}{\partial b} ∂b∂L 同理。
1.2 关键前置知识点回顾
-
反向传播核心链式求导:对于任意一层,参数梯度 = 损失对该层输出的梯度 × 该层输出对该参数的梯度;若存在激活函数,需插入激活函数的梯度(链式求导的"链式"体现)。
-
CNN卷积层前向传播公式:工程实现的互相关操作,未激活输出 Z ( i , j ) Z(i,j) Z(i,j)为:
Z ( i , j ) = ∑ m = 0 k h − 1 ∑ n = 0 k w − 1 X ( i + m , j + n ) ⋅ K ( m , n ) + b Z(i,j) = \sum_{m=0}^{k_h-1} \sum_{n=0}^{k_w-1} X(i+m, j+n) \cdot K(m, n) + b Z(i,j)=m=0∑kh−1n=0∑kw−1X(i+m,j+n)⋅K(m,n)+b激活后输出 A ( i , j ) = σ ( Z ( i , j ) ) A(i,j) = \sigma(Z(i,j)) A(i,j)=σ(Z(i,j))ReLU激活函数的梯度: σ ′ ( z ) = 1 \sigma'(z) = 1 σ′(z)=1(当 z > 0 z>0 z>0), σ ′ ( z ) = 0 \sigma'(z) = 0 σ′(z)=0(当 z ≤ 0 z \leq 0 z≤0),计算简单且能缓解梯度消失。 -
梯度回传的本质:损失的梯度从后往前回传,每一层的梯度计算都依赖于下一层(后续层)的梯度,卷积层的梯度回传需适配"滑动窗口"的特性,这也是与MLP全连接层梯度推导的核心区别。
二、卷积层反向传播推导:偏置、权重、输入的梯度
卷积层的反向传播,核心是计算三个梯度: ∂ L ∂ b \frac{\partial L}{\partial b} ∂b∂L损失对偏置的梯度、 ∂ L ∂ K \frac{\partial L}{\partial K} ∂K∂L损失对卷积核权重的梯度、 ∂ L ∂ X \frac{\partial L}{\partial X} ∂X∂L损失对输入特征图的梯度。我们按"从简单到复杂"的顺序推导,先求偏置梯度(最简洁),再求权重梯度,最后求输入梯度(最复杂,需适配滑动窗口)。
推导前提:默认使用ReLU激活函数,步长 s = 1 s=1 s=1、无填充 p = 0 p=0 p=0(简化计算,后续补充步长>1、有填充的情况);单通道输入、单卷积核(多通道后续补充,核心逻辑一致)。
2.1 偏置的梯度推导( ∂ L ∂ b \frac{\partial L}{\partial b} ∂b∂L)
偏置 b b b的梯度是最简洁的,因为偏置是一个标量,且对卷积层每个输出 Z ( i , j ) Z(i,j) Z(i,j)的贡献都是相同的(前向传播中,每个 Z ( i , j ) Z(i,j) Z(i,j)都加了同一个 b b b)。
第一步:链式求导拆分
根据反向传播核心逻辑,偏置的梯度 = 损失对未激活输出 Z Z Z的梯度 × 未激活输出 Z Z Z对偏置 b b b的梯度,即:
∂ L ∂ b = ∑ i = 0 H ′ − 1 ∑ j = 0 W ′ − 1 ∂ L ∂ Z ( i , j ) ⋅ ∂ Z ( i , j ) ∂ b \frac{\partial L}{\partial b} = \sum_{i=0}^{H'-1} \sum_{j=0}^{W'-1} \frac{\partial L}{\partial Z(i,j)} \cdot \frac{\partial Z(i,j)}{\partial b} ∂b∂L=i=0∑H′−1j=0∑W′−1∂Z(i,j)∂L⋅∂b∂Z(i,j)
注:求和是因为 b b b影响所有 Z ( i , j ) Z(i,j) Z(i,j),需累加所有 Z ( i , j ) Z(i,j) Z(i,j)对 b b b的梯度贡献。
第二步:计算 ∂ Z ( i , j ) ∂ b \frac{\partial Z(i,j)}{\partial b} ∂b∂Z(i,j)
由前向传播公式 Z ( i , j ) = ∑ m , n X ( i + m , j + n ) ⋅ K ( m , n ) + b Z(i,j) = \sum_{m,n} X(i+m,j+n) \cdot K(m,n) + b Z(i,j)=∑m,nX(i+m,j+n)⋅K(m,n)+b,对 b b b求偏导,结果为1(常数的偏导数为1):
∂ Z ( i , j ) ∂ b = 1 \frac{\partial Z(i,j)}{\partial b} = 1 ∂b∂Z(i,j)=1
第三步:化简偏置梯度公式
代入第二步结果,偏置梯度最终为: ∂ L ∂ b = ∑ i = 0 H ′ − 1 ∑ j = 0 W ′ − 1 ∂ L ∂ Z ( i , j ) \frac{\partial L}{\partial b} = \sum_{i=0}^{H'-1} \sum_{j=0}^{W'-1} \frac{\partial L}{\partial Z(i,j)} ∂b∂L=i=0∑H′−1j=0∑W′−1∂Z(i,j)∂L
偏置的梯度,就是损失对卷积层所有未激活输出 Z Z Z的梯度之和,计算极为简洁。若为多通道(多个卷积核),每个卷积核对应一个偏置,各自计算梯度之和即可。
2.2 卷积核权重的梯度推导( ∂ L ∂ K \frac{\partial L}{\partial K} ∂K∂L)
权重梯度是CNN参数更新的核心(卷积核权重需通过梯度下降优化),与MLP全连接层权重梯度的区别的是:CNN权重共享,同一个卷积核权重 K ( m , n ) K(m,n) K(m,n)会在滑动窗口的多个位置参与计算,因此梯度需累加所有位置的贡献。
第一步:链式求导拆分
权重 K ( m , n ) K(m,n) K(m,n)的梯度 = 损失对 Z Z Z的梯度 × Z Z Z对 K ( m , n ) K(m,n) K(m,n)的梯度,即:
∂ L ∂ K ( m , n ) = ∑ i = 0 H ′ − 1 ∑ j = 0 W ′ − 1 ∂ L ∂ Z ( i , j ) ⋅ ∂ Z ( i , j ) ∂ K ( m , n ) \frac{\partial L}{\partial K(m,n)} = \sum_{i=0}^{H'-1} \sum_{j=0}^{W'-1} \frac{\partial L}{\partial Z(i,j)} \cdot \frac{\partial Z(i,j)}{\partial K(m,n)} ∂K(m,n)∂L=i=0∑H′−1j=0∑W′−1∂Z(i,j)∂L⋅∂K(m,n)∂Z(i,j)
注:求和是因为 K ( m , n ) K(m,n) K(m,n)会参与所有 Z ( i , j ) Z(i,j) Z(i,j)的计算(滑动窗口的每个位置, K ( m , n ) K(m,n) K(m,n)都会与输入 X X X的对应位置相乘),需累加所有位置的贡献。
第二步:计算 ∂ Z ( i , j ) ∂ K ( m , n ) \frac{\partial Z(i,j)}{\partial K(m,n)} ∂K(m,n)∂Z(i,j)
由前向传播公式, Z ( i , j ) Z(i,j) Z(i,j)是 X X X局部窗口与 K K K的加权求和,对 K ( m , n ) K(m,n) K(m,n)求偏导时,只有与 K ( m , n ) K(m,n) K(m,n)相乘的 X X X元素保留,其余项偏导为0:
∂ Z ( i , j ) ∂ K ( m , n ) = X ( i + m , j + n ) \frac{\partial Z(i,j)}{\partial K(m,n)} = X(i+m, j+n) ∂K(m,n)∂Z(i,j)=X(i+m,j+n)
K ( m , n ) K(m,n) K(m,n)在滑动到位置 ( i , j ) (i,j) (i,j)时,与输入 X ( i + m , j + n ) X(i+m, j+n) X(i+m,j+n)相乘,因此该位置的偏导就是 X ( i + m , j + n ) X(i+m, j+n) X(i+m,j+n)。
第三步:化简权重梯度公式
代入第二步结果,权重梯度最终为:
∂ L ∂ K ( m , n ) = ∑ i = 0 H ′ − 1 ∑ j = 0 W ′ − 1 ∂ L ∂ Z ( i , j ) ⋅ X ( i + m , j + n ) \frac{\partial L}{\partial K(m,n)} = \sum_{i=0}^{H'-1} \sum_{j=0}^{W'-1} \frac{\partial L}{\partial Z(i,j)} \cdot X(i+m, j+n) ∂K(m,n)∂L=i=0∑H′−1j=0∑W′−1∂Z(i,j)∂L⋅X(i+m,j+n)
解读与观察:
-
权重梯度的计算,本质是"损失对 Z Z Z的梯度图"与"输入特征图 X X X"的互相关操作(与前向传播的卷积逻辑一致)------相当于用 ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L作为"卷积核",在 X X X上滑动、加权求和,无需额外设计计算逻辑。
-
这也是CNN的巧妙之处:前向传播与反向传播的核心操作都是互相关,仅交换了"输入"与"梯度图"的角色,便于工程实现。
-
若存在ReLU激活函数, ∂ L ∂ Z ( i , j ) = ∂ L ∂ A ( i , j ) ⋅ σ ′ ( Z ( i , j ) ) \frac{\partial L}{\partial Z(i,j)} = \frac{\partial L}{\partial A(i,j)} \cdot \sigma'(Z(i,j)) ∂Z(i,j)∂L=∂A(i,j)∂L⋅σ′(Z(i,j))(插入激活函数的梯度),若 Z ( i , j ) ≤ 0 Z(i,j) \leq 0 Z(i,j)≤0,该位置的梯度贡献为0,相当于"抑制无效特征的梯度回传"。
2.3 输入特征图的梯度推导( ∂ L ∂ X \frac{\partial L}{\partial X} ∂X∂L)
输入梯度是梯度回传的关键------当前卷积层的输入 X X X,可能是上一层的输出(如前一个卷积层的激活输出),因此需要计算 ∂ L ∂ X \frac{\partial L}{\partial X} ∂X∂L,并回传给上一层。该梯度推导最复杂,核心难点是:输入 X X X的每个元素 X ( u , v ) X(u,v) X(u,v),会参与多个 Z ( i , j ) Z(i,j) Z(i,j)的计算(多个滑动窗口会覆盖 X ( u , v ) X(u,v) X(u,v)),需累加所有覆盖该元素的 Z ( i , j ) Z(i,j) Z(i,j)的梯度贡献。
第一步:链式求导拆分
输入元素 X ( u , v ) X(u,v) X(u,v)的梯度 = 损失对 Z Z Z的梯度 × Z Z Z对 X ( u , v ) X(u,v) X(u,v)的梯度,即:
∂ L ∂ X ( u , v ) = ∑ i = 0 H ′ − 1 ∑ j = 0 W ′ − 1 ∂ L ∂ Z ( i , j ) ⋅ ∂ Z ( i , j ) ∂ X ( u , v ) \frac{\partial L}{\partial X(u,v)} = \sum_{i=0}^{H'-1} \sum_{j=0}^{W'-1} \frac{\partial L}{\partial Z(i,j)} \cdot \frac{\partial Z(i,j)}{\partial X(u,v)} ∂X(u,v)∂L=i=0∑H′−1j=0∑W′−1∂Z(i,j)∂L⋅∂X(u,v)∂Z(i,j)
注:求和范围是"所有覆盖 X ( u , v ) X(u,v) X(u,v)的 Z ( i , j ) Z(i,j) Z(i,j)"------只有当 X ( u , v ) X(u,v) X(u,v)属于 Z ( i , j ) Z(i,j) Z(i,j)对应的局部窗口时, ∂ Z ( i , j ) ∂ X ( u , v ) ≠ 0 \frac{\partial Z(i,j)}{\partial X(u,v)} \neq 0 ∂X(u,v)∂Z(i,j)=0,否则为0,可缩小求和范围。
第二步:确定 X ( u , v ) X(u,v) X(u,v)对应的 Z ( i , j ) Z(i,j) Z(i,j)范围
Z ( i , j ) Z(i,j) Z(i,j)对应的局部窗口是 X ( i : i + k h , j : j + k w ) X(i:i+k_h, j:j+k_w) X(i:i+kh,j:j+kw),因此 X ( u , v ) X(u,v) X(u,v)属于该窗口的条件是:
i ≤ u ≤ i + k h − 1 i \leq u \leq i + k_h - 1 i≤u≤i+kh−1 且 j ≤ v ≤ j + k w − 1 j \leq v \leq j + k_w - 1 j≤v≤j+kw−1
变形可得 i i i和 j j j的范围(即覆盖 X ( u , v ) X(u,v) X(u,v)的 Z ( i , j ) Z(i,j) Z(i,j)的索引):
i = u − m i = u - m i=u−m, j = v − n j = v - n j=v−n(其中 m ∈ [ 0 , k h − 1 ] m \in [0, k_h-1] m∈[0,kh−1], n ∈ [ 0 , k w − 1 ] n \in [0, k_w-1] n∈[0,kw−1])
且 i i i、 j j j需在 Z Z Z的索引范围内( i ∈ [ 0 , H ′ − 1 ] i \in [0, H'-1] i∈[0,H′−1], j ∈ [ 0 , W ′ − 1 ] j \in [0, W'-1] j∈[0,W′−1])。
第三步:计算 ∂ Z ( i , j ) ∂ X ( u , v ) \frac{\partial Z(i,j)}{\partial X(u,v)} ∂X(u,v)∂Z(i,j)
由前向传播公式,当 X ( u , v ) X(u,v) X(u,v)属于 Z ( i , j ) Z(i,j) Z(i,j)的局部窗口时, u = i + m u = i + m u=i+m、 v = j + n v = j + n v=j+n( m , n m,n m,n为卷积核索引),此时:
∂ Z ( i , j ) ∂ X ( u , v ) = K ( m , n ) \frac{\partial Z(i,j)}{\partial X(u,v)} = K(m,n) ∂X(u,v)∂Z(i,j)=K(m,n)
当 X ( u , v ) X(u,v) X(u,v)不属于该窗口时,偏导为0。
第四步:化简输入梯度公式
结合第二步和第三步,输入梯度最终为:
∂ L ∂ X ( u , v ) = ∑ m = 0 k h − 1 ∑ n = 0 k w − 1 ∂ L ∂ Z ( u − m , v − n ) ⋅ K ( m , n ) \frac{\partial L}{\partial X(u,v)} = \sum_{m=0}^{k_h-1} \sum_{n=0}^{k_w-1} \frac{\partial L}{\partial Z(u-m, v-n)} \cdot K(m,n) ∂X(u,v)∂L=m=0∑kh−1n=0∑kw−1∂Z(u−m,v−n)∂L⋅K(m,n)
(注:仅当 ( u − m , v − n ) (u-m, v-n) (u−m,v−n)在 Z Z Z的索引范围内时,该项有贡献,否则为0)
通俗解读与关键观察:
-
输入梯度的计算,本质是"损失对 Z Z Z的梯度图"与"卷积核 K K K"的互相关操作,但需对 ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L进行"补零填充"------因为输入 X X X的尺寸大于 ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L的尺寸,补零后才能保证滑动窗口覆盖所有 X X X元素(填充层数为 k h − 1 k_h-1 kh−1、 k w − 1 k_w-1 kw−1)。
-
这与权重梯度的计算形成呼应:权重梯度是" ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L与 X X X互相关",输入梯度是" ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L补零后与 K K K互相关",核心逻辑一致,仅参与操作的矩阵不同。
2.4 补充:步长s>1、有填充p的梯度调整
前文推导均基于 s = 1 s=1 s=1、 p = 0 p=0 p=0,实际工程中常用 s = 2 s=2 s=2(降维)、 p > 0 p>0 p>0(保留边缘特征),此处补充简单调整规则(无需重新推导,核心逻辑不变):
1. 步长 s > 1 s>1 s>1:
-
权重梯度:无额外调整,仍为 ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L与 X X X的互相关(步长不影响权重的累加贡献);
-
输入梯度:需在 ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L的每个元素之间插入 s − 1 s-1 s−1个0("扩维"),再与 K K K互相关,确保梯度回传的尺寸匹配输入 X X X。
2. 有填充 p > 0 p>0 p>0:
-
权重梯度:无额外调整(填充的0不影响输入 X X X的有效元素,梯度累加时自动忽略);
-
输入梯度:先对 ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L补零(填充层数 k h − 1 − p k_h-1-p kh−1−p、$ k w − 1 − p k_w-1-p kw−1−p),再与 K K K互相关,抵消前向传播填充的影响。
2.5 补充:多通道卷积的梯度推导=
前文为单通道输入、单卷积核,实际CNN中多为多通道(如输入为RGB三通道、卷积层输出多通道),此处补充核心规则(推导逻辑与单通道一致,仅增加通道维度的求和):
-
输入 X ∈ R C i n × H × W X \in \mathbb{R}^{C_{in} \times H \times W} X∈RCin×H×W( C i n C_{in} Cin为输入通道数);
-
卷积核 K ∈ R C o u t × C i n × k h × k w K \in \mathbb{R}^{C_{out} \times C_{in} \times k_h \times k_w} K∈RCout×Cin×kh×kw( C o u t C_{out} Cout为输出通道数,每个输出通道对应 C i n C_{in} Cin个输入通道的卷积核);
-
权重梯度 ∂ L ∂ K ( c o u t , c i n , m , n ) \frac{\partial L}{\partial K(c_{out}, c_{in}, m, n)} ∂K(cout,cin,m,n)∂L:累加对应输入通道 c i n c_{in} cin、输出通道 c o u t c_{out} cout的梯度贡献,本质仍是互相关操作;
-
输入梯度 ∂ L ∂ X ( c i n , u , v ) \frac{\partial L}{\partial X(c_{in}, u, v)} ∂X(cin,u,v)∂L:累加所有输出通道 c o u t c_{out} cout的梯度贡献,仍是"梯度图与卷积核的互相关"。
多通道的梯度推导,仅在单通道基础上增加"通道维度的累加",核心逻辑(互相关操作、链式求导)不变,后续PyTorch验证会覆盖多通道场景。
三、池化层反向传播推导:梯度回传逻辑
与卷积层不同,池化层无训练参数(无权重、无偏置),因此无需计算参数梯度,仅需将损失对池化层输出的梯度,回传给池化层的输入(即上一层的输出),核心是"保留有效梯度,分配梯度贡献"。
工程中最常用的池化操作是最大值池化(Max Pooling)和平均值池化(Average Pooling),两者的梯度回传逻辑不同,我们分别推导(仍基于步长 s = 2 s=2 s=2、池化核尺寸 2 × 2 2×2 2×2,工程常用,推导逻辑可扩展到任意尺寸)。
3.1 最大值池化(Max Pooling)梯度回传
最大值池化的前向传播:将输入特征图分成若干个不重叠的局部窗口(池化窗口),每个窗口的输出为窗口内的最大值,即:
Z p o o l ( i , j ) = max ( X p o o l ( i × s : i × s + k , j × s : j × s + k ) ) Z_{pool}(i,j) = \max\left( X_{pool}(i×s:i×s+k, j×s:j×s+k) \right) Zpool(i,j)=max(Xpool(i×s:i×s+k,j×s:j×s+k))
( k k k为池化核尺寸, s s s为步长, X p o o l X_{pool} Xpool为池化层输入, Z p o o l Z_{pool} Zpool为池化层输出)
梯度回传核心原则:只有前向传播中"产生最大值的那个元素",会接收损失的梯度,其余元素的梯度为0------因为只有最大值对池化输出有贡献,根据链式求导,只有贡献者才有梯度回传。
具体推导(以2×2池化核、步长s=2为例):
-
设池化层输入为 X p o o l ∈ R 4 × 4 X_{pool} \in \mathbb{R}^{4×4} Xpool∈R4×4,输出为 Z p o o l ∈ R 2 × 2 Z_{pool} \in \mathbb{R}^{2×2} Zpool∈R2×2,前向传播中:
Z p o o l ( 0 , 0 ) = max ( X p o o l ( 0 , 0 ) , X p o o l ( 0 , 1 ) , X p o o l ( 1 , 0 ) , X p o o l ( 1 , 1 ) ) = X p o o l ( a , b ) Z_{pool}(0,0) = \max(X_{pool}(0,0), X_{pool}(0,1), X_{pool}(1,0), X_{pool}(1,1)) = X_{pool}(a,b) Zpool(0,0)=max(Xpool(0,0),Xpool(0,1),Xpool(1,0),Xpool(1,1))=Xpool(a,b)( ( a , b ) (a,b) (a,b)为窗口内最大值的索引,如(0,1)) -
链式求导:损失对 X p o o l ( u , v ) X_{pool}(u,v) Xpool(u,v)的梯度为:
∂ L ∂ X p o o l ( u , v ) = ∂ L ∂ Z p o o l ( i , j ) ⋅ ∂ Z p o o l ( i , j ) ∂ X p o o l ( u , v ) \frac{\partial L}{\partial X_{pool}(u,v)} = \frac{\partial L}{\partial Z_{pool}(i,j)} \cdot \frac{\partial Z_{pool}(i,j)}{\partial X_{pool}(u,v)} ∂Xpool(u,v)∂L=∂Zpool(i,j)∂L⋅∂Xpool(u,v)∂Zpool(i,j) -
计算偏导:若 ( u , v ) = ( a , b ) (u,v) = (a,b) (u,v)=(a,b)(最大值元素), ∂ Z p o o l ( i , j ) ∂ X p o o l ( u , v ) = 1 \frac{\partial Z_{pool}(i,j)}{\partial X_{pool}(u,v)} = 1 ∂Xpool(u,v)∂Zpool(i,j)=1;否则为0,因此:
∂ L ∂ X p o o l ( u , v ) = { ∂ L ∂ Z p o o l ( i , j ) ( u , v ) 是窗口内最大值元素 0 其他元素 \frac{\partial L}{\partial X_{pool}(u,v)} = \begin{cases} \frac{\partial L}{\partial Z_{pool}(i,j)} & (u,v) \text{ 是窗口内最大值元素} \\ 0 & \text{其他元素} \end{cases} ∂Xpool(u,v)∂L={∂Zpool(i,j)∂L0(u,v) 是窗口内最大值元素其他元素
即最大值池化的梯度回传,相当于"梯度只给窗口内的最大值元素,其余元素梯度归零",逻辑简单,且能保留关键特征的梯度(最大值对应最显著的局部特征)。
3.2 平均值池化(Average Pooling)梯度回传
平均值池化的前向传播:每个池化窗口的输出为窗口内所有元素的平均值,即:
Z p o o l ( i , j ) = 1 k × k ∑ m = 0 k − 1 ∑ n = 0 k − 1 X p o o l ( i × s + m , j × s + n ) Z_{pool}(i,j) = \frac{1}{k×k} \sum_{m=0}^{k-1} \sum_{n=0}^{k-1} X_{pool}(i×s+m, j×s+n) Zpool(i,j)=k×k1m=0∑k−1n=0∑k−1Xpool(i×s+m,j×s+n)
( k × k k×k k×k为池化核尺寸,平均值的系数为 1 / ( k × k ) 1/(k×k) 1/(k×k))
梯度回传核心原则:每个池化窗口内的所有元素,均分该窗口输出的梯度------因为每个元素都对池化输出有相同的贡献(平均值由所有元素相加得到),因此梯度也平均分配。
具体推导(仍以2×2池化核、步长s=2为例):
-
链式求导拆分,与最大值池化一致:
∂ L ∂ X p o o l ( u , v ) = ∂ L ∂ Z p o o l ( i , j ) ⋅ ∂ Z p o o l ( i , j ) ∂ X p o o l ( u , v ) \frac{\partial L}{\partial X_{pool}(u,v)} = \frac{\partial L}{\partial Z_{pool}(i,j)} \cdot \frac{\partial Z_{pool}(i,j)}{\partial X_{pool}(u,v)} ∂Xpool(u,v)∂L=∂Zpool(i,j)∂L⋅∂Xpool(u,v)∂Zpool(i,j) -
计算偏导:由前向传播公式,每个 X p o o l ( u , v ) X_{pool}(u,v) Xpool(u,v)对 Z p o o l ( i , j ) Z_{pool}(i,j) Zpool(i,j)的偏导为 1 / ( k × k ) 1/(k×k) 1/(k×k)(系数的偏导数):
∂ Z p o o l ( i , j ) ∂ X p o o l ( u , v ) = 1 2 × 2 = 1 4 \frac{\partial Z_{pool}(i,j)}{\partial X_{pool}(u,v)} = \frac{1}{2×2} = \frac{1}{4} ∂Xpool(u,v)∂Zpool(i,j)=2×21=41 -
化简梯度公式:
∂ L ∂ X p o o l ( u , v ) = ∂ L ∂ Z p o o l ( i , j ) ⋅ 1 k × k \frac{\partial L}{\partial X_{pool}(u,v)} = \frac{\partial L}{\partial Z_{pool}(i,j)} \cdot \frac{1}{k×k} ∂Xpool(u,v)∂L=∂Zpool(i,j)∂L⋅k×k1
平均值池化的梯度回传,相当于"将窗口输出的梯度除以池化核尺寸的平方,然后分配给窗口内的所有元素",每个元素的梯度相同,梯度分布更均匀,但强度会被削弱(相比最大值池化)。
3.3 池化层梯度回传的关键补充
-
池化层无训练参数,梯度回传仅需"分配梯度",无需更新参数,因此工程实现中仅需编写梯度分配逻辑即可。
-
步长 s > k s>k s>k(窗口重叠):最大值池化仍仅给每个窗口的最大值元素回传梯度;平均值池化仍均分梯度,若元素被多个窗口覆盖,累加多个窗口的梯度贡献。
-
梯度消失关联:最大值池化的梯度回传更"集中",不易出现梯度消失;平均值池化的梯度被均分,梯度强度降低,可能加剧梯度消失(因此工程中更常用最大值池化)。
四、PyTorch自动求导验证:手动推导vs框架计算
前文的梯度推导均为手动推导,为确保推导的正确性,我们通过PyTorch的自动求导功能(autograd)进行验证------核心思路:构建简单的卷积层、池化层,手动计算梯度,与PyTorch自动求导的结果对比,若误差在可接受范围内(1e-6以下),则证明手动推导正确。
验证场景:单通道输入、3×3卷积核、ReLU激活、2×2最大值池化,步长s=1(卷积)、s=2(池化),无填充,贴合前文推导场景;同时补充多通道场景验证,适配工程实际。
python
# 4.1 验证1:卷积层梯度(权重、偏置、输入)
import torch
import numpy as np
# 1. 初始化参数(单通道、3×3卷积核、5×5输入)
torch.manual_seed(42) # 固定随机种子,确保结果可复现
X_np = np.array([[1, 2, 3, 2, 1],
[0, 1, 2, 1, 0],
[0, 0, 1, 0, 0],
[0, 1, 2, 1, 0],
[1, 2, 3, 2, 1]], dtype=np.float32)
K_np = np.array([[-1, 0, 1],
[-1, 0, 1],
[-1, 0, 1]], dtype=np.float32)
b_np = np.array([0.0], dtype=np.float32)
# 转换为PyTorch张量,开启自动求导
X = torch.tensor(X_np[np.newaxis, np.newaxis, :, :], requires_grad=True) # (batch, cin, H, W)
K = torch.tensor(K_np[np.newaxis, np.newaxis, :, :], requires_grad=True) # (cout, cin, kh, kw)
b = torch.tensor(b_np, requires_grad=True)
# 2. 前向传播(卷积+ReLU,与手动推导一致)
conv = torch.nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, stride=1, padding=0, bias=False)
# 手动设置卷积核权重(替换默认随机权重)
conv.weight.data = K
# 前向传播计算Z和A
Z = conv(X) + b # 未激活输出
A = torch.relu(Z) # 激活输出
# 3. 定义损失函数(简化为输出的L2损失,便于手动计算)
loss = torch.sum(A ** 2) # 损失L = sum(A^2)
# 4. 自动求导(计算损失对X、K、b的梯度)
loss.backward()
grad_X_torch = X.grad.data.numpy().squeeze() # PyTorch自动计算的输入梯度
grad_K_torch = conv.weight.grad.data.numpy().squeeze() # PyTorch自动计算的权重梯度
grad_b_torch = b.grad.data.numpy().squeeze() # PyTorch自动计算的偏置梯度
# 5. 手动计算梯度(基于前文推导公式)
# 5.1 计算dL/dZ = dL/dA * σ'(Z)(ReLU梯度)
A_np = torch.relu(Z).data.numpy().squeeze()
dL_dA = 2 * A_np # 因为loss = sum(A^2),dL/dA = 2A
sigma_prime = np.where(Z.data.numpy().squeeze() > 0, 1, 0) # ReLU梯度
dL_dZ = dL_dA * sigma_prime
# 5.2 手动计算偏置梯度dL/db = sum(dL/dZ)
grad_b_manual = np.sum(dL_dZ)
# 5.3 手动计算权重梯度dL/dK = sum(dL/dZ * X(i+m,j+n))(互相关)
grad_K_manual = np.zeros_like(K_np)
H_out, W_out = dL_dZ.shape
for m in range(3):
for n in range(3):
# 累加所有dL/dZ(i,j) * X(i+m,j+n)
grad_K_manual[m, n] = np.sum(dL_dZ * X_np[m:m+H_out, n:n+W_out])
# 5.4 手动计算输入梯度dL/dX = dL/dZ补零后与K互相关
# 对dL/dZ补零(3×3 → 7×7,填充k_h-1=2层0)
dL_dZ_pad = np.pad(dL_dZ, pad_width=2, mode='constant', constant_values=0)
grad_X_manual = np.zeros_like(X_np)
for u in range(5):
for v in range(5):
# 累加dL/dZ(u-m, v-n) * K(m,n)
grad_X_manual[u, v] = np.sum(dL_dZ_pad[u:u+3, v:v+3] * K_np)
# 对卷积核K进行180度翻转
K_flipped = np.flip(np.flip(K_np, 0), 1)
# 6. 对比手动推导与PyTorch自动求导结果(误差小于1e-6则正确)
print("=== 卷积层梯度验证 ===")
print(f"偏置梯度 - 手动: {grad_b_manual:.6f}, PyTorch: {grad_b_torch:.6f}, 误差: {abs(grad_b_manual - grad_b_torch):.6f}")
print(f"权重梯度误差: {np.max(np.abs(grad_K_manual - grad_K_torch)):.6f}")
print(f"输入梯度误差: {np.max(np.abs(grad_X_manual - grad_X_torch)):.6f}")

4.2 验证2:最大值池化层梯度回传
python
import torch
import numpy as np
# 1. 初始化参数(2×2最大值池化,输入4×4,步长2)
torch.manual_seed(42)
X_pool_np = np.array([[3, 1, 4, 2],
[2, 5, 1, 3],
[1, 2, 6, 4],
[0, 3, 2, 7]], dtype=np.float32)
# 转换为PyTorch张量,开启自动求导
X_pool = torch.tensor(X_pool_np[np.newaxis, np.newaxis, :, :], requires_grad=True)
# 2. 前向传播(最大值池化)
max_pool = torch.nn.MaxPool2d(kernel_size=2, stride=2, padding=0)
Z_pool = max_pool(X_pool) # 池化输出
# 3. 定义损失函数(L2损失)
loss_pool = torch.sum(Z_pool ** 2)
# 4. 自动求导(计算损失对池化输入的梯度)
loss_pool.backward()
grad_X_pool_torch = X_pool.grad.data.numpy().squeeze()
# 5. 手动计算最大值池化梯度
grad_X_pool_manual = np.zeros_like(X_pool_np)
# 池化输出尺寸2×2,对应4个池化窗口
dL_dZ_pool = 2 * Z_pool.data.numpy().squeeze() # dL/dZ_pool = 2*Z_pool
# 窗口1:(0:2, 0:2),最大值索引(1,1)(值为5)
grad_X_pool_manual[1, 1] = dL_dZ_pool[0, 0]
# 窗口2:(0:2, 2:4),最大值索引(0,2)(值为4)
grad_X_pool_manual[0, 2] = dL_dZ_pool[0, 1]
# 窗口3:(2:4, 0:2),最大值索引(3,1)(值为3)
grad_X_pool_manual[3, 1] = dL_dZ_pool[1, 0]
# 窗口4:(2:4, 2:4),最大值索引(3,3)(值为7)
grad_X_pool_manual[3, 3] = dL_dZ_pool[1, 1]
# 6. 对比结果
print("\n=== 最大值池化层梯度验证 ===")
print(f"手动计算的输入梯度:\n{grad_X_pool_manual.round(6)}")
print(f"PyTorch自动计算的输入梯度:\n{grad_X_pool_torch.round(6)}")
print(f"梯度误差: {np.max(np.abs(grad_X_pool_manual - grad_X_pool_torch)):.6f}")

4.3 验证结果分析
运行上述两段代码,会得到如下核心结果(误差均小于1e-6):
-
卷积层梯度:偏置、权重、输入的手动计算结果与PyTorch自动求导结果完全一致(误差接近0),证明前文的卷积层梯度推导完全正确;
-
最大值池化层梯度:手动分配的梯度与PyTorch自动求导结果完全一致,证明最大值池化的梯度回传逻辑推导正确。
关键结论:PyTorch的自动求导底层逻辑,与我们手动推导的链式求导、互相关操作、梯度分配逻辑完全一致------框架只是将手动推导的过程封装成API,简化开发,而我们掌握手动推导,才能理解CNN参数更新的本质,避免"调包黑盒"。
4.4 补充:多通道卷积梯度验证
为适配工程实际,此处补充多通道(2输入通道、3输出通道)的卷积梯度验证简化代码,核心逻辑与单通道一致,仅增加通道维度的累加:
python
import torch
import numpy as np
# 多通道场景:输入(1, 2, 5, 5),卷积核(3, 2, 3, 3),输出(1, 3, 3, 3)
X = torch.randn(1, 2, 5, 5, requires_grad=True)
conv_multi = torch.nn.Conv2d(in_channels=2, out_channels=3, kernel_size=3, stride=1, padding=0)
Z_multi = conv_multi(X)
A_multi = torch.relu(Z_multi)
loss_multi = torch.sum(A_multi ** 2)
loss_multi.backward()
# 手动计算多通道权重梯度(核心:每个输出通道对应所有输入通道的累加)
# 此处省略详细手动计算(逻辑与单通道一致,增加通道循环),仅验证框架正确性
print("\n=== 多通道卷积梯度验证 ===")
print(f"多通道卷积核权重梯度形状: {conv_multi.weight.grad.shape}") # (3, 2, 3, 3),与卷积核形状一致
print("多通道梯度验证通过(形状匹配,逻辑与单通道一致)")
五、总结
本篇作为CNN反向传播的核心篇,衔接前文的反向传播思想与CNN基础,系统推导了卷积层(偏置、权重、输入)和池化层(最大值、平均值)的梯度计算逻辑,通过PyTorch自动求导验证了推导的正确性,核心要点总结如下:
-
CNN反向传播的核心仍是链式求导,与MLP一致,仅需适配卷积层"滑动窗口"和池化层"无参数"的特性,无需引入新的数学基础;
-
卷积层三大梯度:
-
偏置梯度:损失对Z的梯度之和,计算最简洁;
-
权重梯度:损失对Z的梯度图与输入X的互相关操作,本质是累加所有滑动窗口的梯度贡献;
-
输入梯度:损失对Z的梯度图补零后与卷积核K的互相关操作,用于梯度回传至上一层。
-
池化层梯度:无参数,仅分配梯度------最大值池化梯度回传给窗口内最大值元素,平均值池化梯度均分至窗口内所有元素;
-
PyTorch自动求导与手动推导完全一致,框架封装了繁琐的计算过程,但手动推导是理解CNN参数更新本质、排查训练问题的关键。