共计 4325 个字符,预计需要花费 11 分钟才能阅读完成。
核心概念
卷积神经网络(CNN)的反向传播算法是深度学习中的核心内容之一。理解这一过程对于掌握 CNN 的工作原理至关重要。首先,我们需要明确几个关键层的梯度传播规律。

卷积层的梯度传播
在卷积层中,输入 $X$ 和卷积核 $W$ 进行卷积操作得到输出 $Y$。反向传播时,我们需要计算损失函数 $L$ 对 $W$ 和 $X$ 的梯度,即 $\frac{\partial L}{\partial W}$ 和 $\frac{\partial L}{\partial X}$。
- 梯度计算:
- $\frac{\partial L}{\partial W}$ 可以通过输入 $X$ 和上一层的梯度 $\frac{\partial L}{\partial Y}$ 的卷积得到。
-
$\frac{\partial L}{\partial X}$ 则是通过将 $\frac{\partial L}{\partial Y}$ 与翻转后的卷积核 $W$ 进行全卷积(full convolution)得到。
-
数学推导:
- 对于 $\frac{\partial L}{\partial W}$,公式为:
$$\frac{\partial L}{\partial W} = X \ast \frac{\partial L}{\partial Y}$$ - 对于 $\frac{\partial L}{\partial X}$,公式为:
$$\frac{\partial L}{\partial X} = \frac{\partial L}{\partial Y} \ast_{\text{full}} \text{flip}(W)$$
激活层的梯度传播
激活函数(如 ReLU、Sigmoid)的反向传播相对简单,只需根据激活函数的导数进行计算。例如,对于 ReLU 激活函数:
- 如果输入大于 0,梯度为 1;否则为 0。
池化层的梯度传播
池化层(如 Max Pooling)的反向传播需要记录前向传播时的最大值位置,梯度只回传到这些位置。对于平均池化(Average Pooling),梯度均匀分布到所有输入位置。
痛点分析
与传统全连接网络相比,CNN 的反向传播面临以下挑战:
- 参数共享:卷积核在输入的不同位置共享参数,这使得梯度计算更加复杂。
- 局部连接:每个输出只与输入的局部区域相关,导致梯度传播路径变短,容易出现梯度消失或爆炸。
- 计算复杂度:由于卷积操作涉及大量重复计算,直接实现反向传播可能导致效率低下。
代码实现
卷积层的前向与反向传播
import numpy as np
def conv_forward(X, W, b, stride=1, padding=0):
"""
卷积层的前向传播
:param X: 输入数据,形状为 (N, C, H, W)
:param W: 卷积核,形状为 (F, C, HH, WW)
:param b: 偏置,形状为 (F,)
:param stride: 步长
:param padding: 填充
:return: 输出数据
"""
N, C, H, W = X.shape
F, _, HH, WW = W.shape
# 计算输出尺寸
out_h = (H + 2 * padding - HH) // stride + 1
out_w = (W + 2 * padding - WW) // stride + 1
# 初始化输出
out = np.zeros((N, F, out_h, out_w))
# 填充输入
if padding > 0:
X_padded = np.pad(X, ((0, 0), (0, 0), (padding, padding), (padding, padding)), mode='constant')
else:
X_padded = X
# 卷积操作
for n in range(N):
for f in range(F):
for i in range(out_h):
for j in range(out_w):
h_start = i * stride
h_end = h_start + HH
w_start = j * stride
w_end = w_start + WW
window = X_padded[n, :, h_start:h_end, w_start:w_end]
out[n, f, i, j] = np.sum(window * W[f]) + b[f]
return out
def conv_backward(dout, X, W, b, stride=1, padding=0):
"""
卷积层的反向传播
:param dout: 上游梯度,形状为 (N, F, out_h, out_w)
:param X: 输入数据,形状为 (N, C, H, W)
:param W: 卷积核,形状为 (F, C, HH, WW)
:param b: 偏置,形状为 (F,)
:param stride: 步长
:param padding: 填充
:return: dX, dW, db
"""
N, C, H, W = X.shape
F, _, HH, WW = W.shape
_, _, out_h, out_w = dout.shape
# 初始化梯度
dX = np.zeros_like(X)
dW = np.zeros_like(W)
db = np.zeros_like(b)
# 填充输入
if padding > 0:
X_padded = np.pad(X, ((0, 0), (0, 0), (padding, padding), (padding, padding)), mode='constant')
dX_padded = np.pad(dX, ((0, 0), (0, 0), (padding, padding), (padding, padding)), mode='constant')
else:
X_padded = X
dX_padded = dX
# 反向传播
for n in range(N):
for f in range(F):
for i in range(out_h):
for j in range(out_w):
h_start = i * stride
h_end = h_start + HH
w_start = j * stride
w_end = w_start + WW
window = X_padded[n, :, h_start:h_end, w_start:w_end]
dX_padded[n, :, h_start:h_end, w_start:w_end] += W[f] * dout[n, f, i, j]
dW[f] += window * dout[n, f, i, j]
db[f] += dout[n, f, i, j]
# 去除填充
if padding > 0:
dX = dX_padded[:, :, padding:-padding, padding:-padding]
else:
dX = dX_padded
return dX, dW, db
Max Pooling 的梯度回传
def max_pool_forward(X, pool_size=2, stride=2):
"""
Max Pooling 的前向传播
:param X: 输入数据,形状为 (N, C, H, W)
:param pool_size: 池化窗口大小
:param stride: 步长
:return: 输出数据和最大值索引
"""
N, C, H, W = X.shape
out_h = (H - pool_size) // stride + 1
out_w = (W - pool_size) // stride + 1
out = np.zeros((N, C, out_h, out_w))
max_idx = np.zeros((N, C, out_h, out_w, 2), dtype=int)
for n in range(N):
for c in range(C):
for i in range(out_h):
for j in range(out_w):
h_start = i * stride
h_end = h_start + pool_size
w_start = j * stride
w_end = w_start + pool_size
window = X[n, c, h_start:h_end, w_start:w_end]
out[n, c, i, j] = np.max(window)
max_idx[n, c, i, j] = np.unravel_index(np.argmax(window), (pool_size, pool_size))
return out, max_idx
def max_pool_backward(dout, X, pool_size=2, stride=2, max_idx=None):
"""
Max Pooling 的反向传播
:param dout: 上游梯度,形状为 (N, C, out_h, out_w)
:param X: 输入数据,形状为 (N, C, H, W)
:param pool_size: 池化窗口大小
:param stride: 步长
:param max_idx: 最大值索引
:return: dX
"""
N, C, H, W = X.shape
dX = np.zeros_like(X)
if max_idx is None:
_, max_idx = max_pool_forward(X, pool_size, stride)
for n in range(N):
for c in range(C):
for i in range(dout.shape[2]):
for j in range(dout.shape[3]):
h_start = i * stride
w_start = j * stride
h_max, w_max = max_idx[n, c, i, j]
dX[n, c, h_start + h_max, w_start + w_max] = dout[n, c, i, j]
return dX
性能优化
为了提高卷积操作的效率,可以使用 im2col 方法将输入数据转换为矩阵形式,然后利用矩阵乘法加速计算。这种方法的核心思想是将卷积操作转换为矩阵乘法,从而利用 GPU 或 CPU 的并行计算能力。
- im2col 原理:
- 将输入数据的局部窗口展开为列向量,形成一个大的矩阵。
- 卷积核也展开为行向量,形成另一个矩阵。
-
通过矩阵乘法计算输出。
-
FLOPS 对比:
- 普通实现的 FLOPS 与输入尺寸和卷积核尺寸直接相关,计算复杂度较高。
im2col方法通过矩阵乘法优化,计算复杂度显著降低。
避坑指南
在实际实现 CNN 反向传播时,初学者常遇到以下问题:
- 忘记翻转卷积核:在计算 $\frac{\partial L}{\partial X}$ 时,必须将卷积核翻转 180 度。
- 池化层索引保存不当:Max Pooling 的反向传播需要保存最大值的位置索引,否则无法正确回传梯度。
- 步长和填充设置错误:步长和填充的设置会直接影响输出尺寸,错误设置可能导致尺寸不匹配。
延伸思考
如何将算法扩展到可变输入尺寸的场景?
在实际应用中,输入图像的尺寸可能不一致。为了处理这种情况,可以考虑以下方法:
- 动态调整网络结构:根据输入尺寸动态调整网络的层数和参数。
- 使用全局池化:在网络的最后使用全局平均池化(Global Average Pooling),将不同尺寸的输入转换为固定长度的特征向量。
- 图像预处理:将所有输入图像调整为统一尺寸,但这可能会丢失部分信息。
通过以上方法,可以灵活应对不同尺寸的输入,提升模型的泛化能力。
