深度学习的数学原理(十二)—— CNN的反向传播

在上一篇文章中,我们系统讲解了二维卷积的数学定义、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 关键前置知识点回顾

  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),计算简单且能缓解梯度消失。

  3. 梯度回传的本质:损失的梯度从后往前回传,每一层的梯度计算都依赖于下一层(后续层)的梯度,卷积层的梯度回传需适配"滑动窗口"的特性,这也是与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为例):

  1. 设池化层输入为 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))

  2. 链式求导:损失对 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)

  3. 计算偏导:若 ( 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为例):

  1. 链式求导拆分,与最大值池化一致:
    ∂ 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)

  2. 计算偏导:由前向传播公式,每个 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

  3. 化简梯度公式:
    ∂ 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):

  1. 卷积层梯度:偏置、权重、输入的手动计算结果与PyTorch自动求导结果完全一致(误差接近0),证明前文的卷积层梯度推导完全正确;

  2. 最大值池化层梯度:手动分配的梯度与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自动求导验证了推导的正确性,核心要点总结如下:

  1. CNN反向传播的核心仍是链式求导,与MLP一致,仅需适配卷积层"滑动窗口"和池化层"无参数"的特性,无需引入新的数学基础;

  2. 卷积层三大梯度:

  • 偏置梯度:损失对Z的梯度之和,计算最简洁;

  • 权重梯度:损失对Z的梯度图与输入X的互相关操作,本质是累加所有滑动窗口的梯度贡献;

  • 输入梯度:损失对Z的梯度图补零后与卷积核K的互相关操作,用于梯度回传至上一层。

  1. 池化层梯度:无参数,仅分配梯度------最大值池化梯度回传给窗口内最大值元素,平均值池化梯度均分至窗口内所有元素;

  2. PyTorch自动求导与手动推导完全一致,框架封装了繁琐的计算过程,但手动推导是理解CNN参数更新本质、排查训练问题的关键。

相关推荐
冰西瓜6002 小时前
深度学习的数学原理(十一)—— CNN:二维卷积的数学本质与图像特征提取
人工智能·深度学习·cnn
飞哥数智坊2 小时前
春节没顾上追新模型?17款新品一文速览
人工智能·llm
陈天伟教授2 小时前
人工智能应用- 人工智能交叉:04. 安芬森理论
人工智能
光的方向_2 小时前
ChatGPT提示工程入门 Prompt 03-迭代式提示词开发
人工智能·chatgpt·prompt·aigc
盼小辉丶2 小时前
PyTorch实战(29)——使用TorchServe部署PyTorch模型
人工智能·pytorch·深度学习·模型部署
郝学胜-神的一滴2 小时前
在Vibe Coding时代,学习设计模式与软件架构
人工智能·学习·设计模式·架构·软件工程
AI英德西牛仔2 小时前
AI输出无乱码
人工智能
艾醒(AiXing-w)2 小时前
技术速递——通义千问 3.5 深度横评:纸面超越 GPT‑5.2,实测差距在哪?
人工智能·python·语言模型
xiangzhihong82 小时前
Gemini 3.1 Pro血洗Claude与GPT,12项基准测试第一!
人工智能