
MLP & Back Propagation —— 多层感知机与反向传播
系统讲解多层感知机(MLP)的前向传播矩阵运算与反向传播链式法则,以计算图为工具手动推导3层MLP的误差回传与参数梯度公式,提炼出误差回传、权重梯度、偏置梯度的核心公式。同时讨论通用近似定理(UAT)的砖块构造直觉,以及Batch Normalization训练时mini-batch统计与推理时滑动平均(EMA)机制。
阅读文章ZHY's Blog
A UNIVERSE OF IDEAS · BY ZHANG HAOYI
让好奇心 点亮知识宇宙
在代码、模型与思想之间自由漫游。这里持续记录人工智能、机器学习、软件工程与成长实践,让每次阅读都成为一次新的发现。
ARTICLE NOTE
在前五篇文章中,我们完整走过了从张量运算到MLP反向传播的全流程,并掌握了优化器如何驱动参数迈向最优。然而,当我们将MLP直接应用于图像时,一个根本性的矛盾浮现了:全连接层将二维图像展平为一维向量,彻底破坏了像素之间的空间邻接关系。一张224×224的图像展平后有5万多个输入维度,首层全连接参数量便高达千万级——这不仅带来了计算负担,更忽略了视觉世界最本质的规律:相邻像素相关,远处像素疏离。
本篇博客正是解决这一“空间建模”问题的关键章节。我们从卷积运算的数学本质出发,揭示深度学习框架中所谓的“卷积”实为互相关 —— 无需翻转,直接滑动,这一设计上的“偷懒”恰恰成就了计算效率。随后,我们将以感受野为线索,理解深层神经元如何逐层“看到”越来越大的输入区域,并推导其递推计算公式——这正是两个3×3卷积替代一个5×5卷积的底层数学依据。
在此基础上,我们将深入空洞卷积如何在不增加参数的前提下指数级扩大感受野,剖析Max Pooling与Average Pooling在反向传播中迥异的梯度分配机制 —— 前者“只传最优”,后者“雨露均沾”,以及1×1卷积作为通道维度上的“全能选手”,如何实现跨通道信息整合、降维升维与非线性增强三重功能。
值得注意的是,本篇对局部连接与权重共享的理解,将直接服务于后续ResNet残差网络 —— 当网络加深到数十层时,正是卷积结构的堆叠与残差连接的配合,才让千层网络成为可能。现在,请带着“如何让网络看见空间结构”的疑问进入正文——理解了CNN,您就掌握了现代计算机视觉的底层语法。
在信号处理和数学分析中,两个函数 f 和 g 的卷积定义为:
(f∗g)(t)=∫−∞∞f(τ)g(t−τ)dτ对于离散的二维信号(如图像),卷积定义为:
yij=u=1∑Uv=1∑Vwuv⋅xi−u+1,j−v+1注意下标中的 i−u+1 和 j−v+1 ——这意味着卷积核 w 在与输入 x 计算之前,要先进行水平和垂直方向的翻转(180°旋转) 。
然而,在深度学习框架(PyTorch、TensorFlow等)中,卷积层实际执行的是互相关(cross-correlation)运算:
yij=u=1∑Uv=1∑Vwuv⋅xi+u−1,j+v−1互相关与严格卷积的唯一区别在于:卷积核不需要翻转,直接与输入进行滑动点积。
💡 核心洞察:深度学习框架中所谓的“卷积层”,实际上是互相关运算。之所以这样称呼,是因为:
- 计算效率更高:省去了翻转操作,减少了不必要的计算开销
- 特征提取能力等价:卷积核的参数是通过训练学习的,如果网络需要翻转后的核,它可以直接学到翻转后的权重
- 历史惯例:早期研究沿用了“卷积”这一术语,并成为行业惯例
如果权重矩阵是对称的(很多情况下确实如此),那么卷积和互相关在数值上完全相同。
一个完整的卷积层(含多个卷积核)的计算可以表示为:
Zp=d=1∑DWp,d⊗Xd+bp,Yp=f(Zp)其中:
对于一个输入尺寸为 I×I、卷积核大小为 K×K、填充(padding)为 P、步长(stride)为 S 的卷积层,输出尺寸为:
O=SI−K+2P+11import numpy as np2
3def conv2d_naive(X, W, b, stride=1, padding=0):4 """5 二维互相关运算的朴素实现(教学用)6 X: (H_in, W_in) 单通道输入7 W: (K_h, K_w) 卷积核8 b: 标量偏置9 """10 h_in, w_in = X.shape11 k_h, k_w = W.shape12
13 # 填充14 if padding > 0:15 X_pad = np.pad(X, ((padding, padding), (padding, padding)), mode='constant')16 else:17 X_pad = X18
19 # 计算输出尺寸20 h_out = (h_in - k_h + 2 * padding) // stride + 121 w_out = (w_in - k_w + 2 * padding) // stride + 122
23 Y = np.zeros((h_out, w_out))24 for i in range(h_out):25 for j in range(w_out):26 # 提取感受野区域27 x_region = X_pad[i*stride:i*stride+k_h, j*stride:j*stride+k_w]28 # 逐元素乘积累加(这就是互相关)29 Y[i, j] = np.sum(x_region * W) + b30 return Y31
32# 示例33X = np.random.randn(5, 5) # 5x5 输入34W = np.random.randn(3, 3) # 3x3 卷积核35Y = conv2d_naive(X, W, b=0, stride=1, padding=0)36print(f"输入尺寸: {X.shape}, 输出尺寸: {Y.shape}") # (3, 3)感受野(Receptive Field)指的是卷积神经网络中某一层输出特征图上的一个元素,在原始输入图像上对应的区域大小。
直观理解:越深层的神经元,看到的输入区域越大。两个 3×3 卷积层堆叠(stride=1),第二层的一个神经元能看到第一层 3×3 的区域,而这个区域又对应原始输入的 5×5 区域。
从顶层向底层递推计算感受野:
RFi=RFi+1+(Ki−1)×Si+1其中:
从输出层开始:最顶层的感受野为 RFtop=1(输出层的一个像素对应自己),然后逐层向下计算。
VGG网络的一个重要设计原则是:用多个小卷积核堆叠替代大卷积核。
验证堆叠两个 3×3 卷积核(stride=1)的感受野:
结论:两个 3×3 卷积的感受野等于一个 5×5 卷积。
参数量的优势:假设输入和输出通道数均为 C:
参数量减少了 28%,同时还增加了非线性(两层激活函数)。
1def compute_receptive_field(layers):2 """3 计算感受野4 layers: list of (kernel_size, stride) 从输出层到输入层5 """6 rf = 17 for k, s in layers:8 rf = rf + (k - 1) * s9 return rf10
11# VGG风格:两个3x3 vs 一个5x512print(f"两个3x3 (stride=1) 感受野: {compute_receptive_field([(3,1), (3,1)])}") # 513print(f"一个5x5 (stride=1) 感受野: {compute_receptive_field([(5,1)])}") # 5在语义分割等密集预测任务中,我们需要:
传统方法通过下采样(池化或步长卷积) 扩大感受野,但这会降低特征图分辨率,丢失空间细节。
空洞卷积(又称扩张卷积,Atrous/Dilated Convolution)通过在卷积核元素之间插入空洞,在不增加参数数量的前提下扩大感受野。
空洞卷积引入了一个新参数——扩张率(dilation rate) r:
有效卷积核尺寸(感受野)为:
K′=K+(K−1)×(r−1)例如:3×3 卷积核,r=2 时,有效尺寸为 3+(3−1)×(2−1)=5,感受野从 3×3 扩大到 5×5。r=3 时,感受野扩大到 7×7。
核心优势:
典型应用:
空洞卷积也有缺点:
Gridding效应(网格效应) :由于卷积核是稀疏采样的,相邻的输出像素来自相互独立的输入子集,彼此之间缺乏相关性,导致局部信息丢失。
解决方案:
1import torch.nn as nn2
3class DilatedConvBlock(nn.Module):4 def __init__(self, in_channels, out_channels, dilation_rate):5 super().__init__()6 # dilation参数控制空洞间隔7 self.conv = nn.Conv2d(8 in_channels, out_channels,9 kernel_size=3,10 dilation=dilation_rate, # 扩张率11 padding=dilation_rate # 保持输出尺寸不变12 )13
14 def forward(self, x):15 return self.conv(x)16
17# 不同扩张率的空洞卷积18r1 = DilatedConvBlock(64, 64, dilation_rate=1) # 标准卷积,感受野3x319r2 = DilatedConvBlock(64, 64, dilation_rate=2) # 感受野5x520r3 = DilatedConvBlock(64, 64, dilation_rate=3) # 感受野7x7池化层(Pooling Layer)是CNN中一个特殊的存在:
池化层反向传播的核心原则是:
梯度总和守恒:池化前 N 个像素的梯度之和,必须等于池化后1个像素的梯度。
Max Pooling在前向传播中,只保留池化窗口中的最大值,丢弃其他值。
反向传播时:
为了在反向传播中知道哪个位置是最大值,前向传播时必须记录最大值的位置索引(max_id / argmax) 。
数学表达:
∂xij∂L={∂ypq∂L,0,如果 xij 是池化窗口中的最大值否则Average Pooling在前向传播中,计算池化窗口内所有值的平均值。
反向传播时:
⚠️ 常见错误:不能简单地把梯度复制 n 份传回去,否则梯度总和会变为原来的 n 倍,造成梯度爆炸。
数学表达(池化窗口大小为 k×k):
∂xij∂L=k21⋅∂ypq∂L,∀(i,j)∈窗口1import numpy as np2
3class MaxPool2D:4 def __init__(self, kernel_size=2, stride=2):5 self.kernel_size = kernel_size6 self.stride = stride7 self.argmax = None # 记录最大值位置8
9 def forward(self, X):10 """前向传播:记录最大值位置"""11 n, c, h, w = X.shape12 kh = kw = self.kernel_size13 sh = sw = self.stride14
15 h_out = (h - kh) // sh + 116 w_out = (w - kw) // sw + 117
18 Y = np.zeros((n, c, h_out, w_out))19 self.argmax = np.zeros((n, c, h_out, w_out, 2), dtype=int)20
21 for i in range(h_out):22 for j in range(w_out):23 # 提取池化窗口24 window = X[:, :, i*sh:i*sh+kh, j*sw:j*sw+kw]25 # 找到最大值26 Y[:, :, i, j] = np.max(window, axis=(2, 3))27 # 记录最大值的位置(用于反向传播)28 max_idx = np.argmax(window.reshape(n, c, -1), axis=2)29 self.argmax[:, :, i, j, 0] = max_idx // kw30 self.argmax[:, :, i, j, 1] = max_idx % kw31 return Y32
33 def backward(self, dY):34 """反向传播:梯度只传给最大值位置"""35 n, c, h_out, w_out = dY.shape36 kh = kw = self.kernel_size37 sh = sw = self.stride38
39 h_in = (h_out - 1) * sh + kh40 w_in = (w_out - 1) * sw + kw41 dX = np.zeros((n, c, h_in, w_in))42
43 for i in range(h_out):44 for j in range(w_out):45 for b in range(n):46 for ch in range(c):47 # 获取最大值的位置48 row = self.argmax[b, ch, i, j, 0]49 col = self.argmax[b, ch, i, j, 1]50 # 梯度只传给最大值位置51 dX[b, ch, i*sh+row, j*sw+col] = dY[b, ch, i, j]52 return dX53
54
55class AvgPool2D:56 def __init__(self, kernel_size=2, stride=2):57 self.kernel_size = kernel_size58 self.stride = stride59
60 def forward(self, X):61 """前向传播:计算平均值"""62 n, c, h, w = X.shape63 kh = kw = self.kernel_size64 sh = sw = self.stride65
66 h_out = (h - kh) // sh + 167 w_out = (w - kw) // sw + 168
69 Y = np.zeros((n, c, h_out, w_out))70 for i in range(h_out):71 for j in range(w_out):72 window = X[:, :, i*sh:i*sh+kh, j*sw:j*sw+kw]73 Y[:, :, i, j] = np.mean(window, axis=(2, 3))74 return Y75
76 def backward(self, dY):77 """反向传播:梯度平均分配"""78 n, c, h_out, w_out = dY.shape79 kh = kw = self.kernel_size80 sh = sw = self.stride81
82 h_in = (h_out - 1) * sh + kh83 w_in = (w_out - 1) * sw + kw84 dX = np.zeros((n, c, h_in, w_in))85
86 # 每个梯度平均分给 kh * kw 个位置87 for i in range(h_out):88 for j in range(w_out):89 dX[:, :, i*sh:i*sh+kh, j*sw:j*sw+kw] += \90 dY[:, :, i:i+1, j:j+1] / (kh * kw)91 return dX1×1 卷积与标准卷积完全一样,唯一的特殊点在于卷积核尺寸为 1×1。
它不关心空间维度上的局部关系(1×1 只覆盖一个像素),而是专注于通道维度上的信息整合。
数学上,1×1 卷积对每个像素位置,在所有通道上进行线性组合:
yijp=d=1∑Dwp,d⋅xijd+bp这本质上等价于在每个像素位置上执行一个全连接层。
作用一:跨通道的信息交互与整合
1×1 卷积将不同通道在同一位置的信息进行线性组合,实现跨通道的信息融合。这相当于对特征图的“深度”维度进行了一次特征重组合。
作用二:通道数的降维与升维
1×1 卷积不改变特征图的高度和宽度,只改变通道数:
作用三:增加非线性
在 1×1 卷积后接非线性激活函数(如ReLU),可以在不改变特征图空间尺寸的前提下,大幅增加网络的非线性表达能力。
GoogLeNet的Inception模块使用 1×1 卷积进行通道降维,大幅减少参数量。
以Inception模块的3a分支为例:
参数量减少到原来的三分之一!
ResNet-50及更深版本使用Bottleneck结构,通过 1×1 卷积先降维再升维:
这种设计使得 3×3 卷积的参数量从 3×3×256×256 减少到 3×3×64×64,参数量减少了16倍,让152层的ResNet得以在可接受的参数量下训练。
1import torch.nn as nn2
3class Bottleneck(nn.Module):4 """ResNet Bottleneck块(简化版)"""5 def __init__(self, in_channels, mid_channels, out_channels):6 super().__init__()7 # 1x1 降维8 self.conv1 = nn.Conv2d(in_channels, mid_channels, kernel_size=1)9 self.bn1 = nn.BatchNorm2d(mid_channels)10 # 3x3 空间特征提取11 self.conv2 = nn.Conv2d(mid_channels, mid_channels, kernel_size=3, padding=1)12 self.bn2 = nn.BatchNorm2d(mid_channels)13 # 1x1 升维14 self.conv3 = nn.Conv2d(mid_channels, out_channels, kernel_size=1)15 self.bn3 = nn.BatchNorm2d(out_channels)16 self.relu = nn.ReLU(inplace=True)17
18 def forward(self, x):19 identity = x20 out = self.relu(self.bn1(self.conv1(x)))21 out = self.relu(self.bn2(self.conv2(out)))22 out = self.bn3(self.conv3(out))23 out += identity # 残差连接24 return self.relu(out)25
26# 示例:Bottleneck将256通道压缩到64再恢复27block = Bottleneck(in_channels=256, mid_channels=64, out_channels=256)28print(f"参数量: {sum(p.numel() for p in block.parameters()):,}")29# 输出约为 70,000 参数量(相比直接用3x3的约1,500,000,减少了95%以上)本文系统梳理了CNN的四大核心操作:
| 操作 | 核心要点 | 关键公式 |
|---|---|---|
| 卷积(互相关) | 深度学习实现=互相关,无需翻转 | yij=∑u,vwuv⋅xi+u−1,j+v−1 |
| 感受野 | 深层神经元看到更大的输入区域 | RFi=RFi+1+(Ki−1)×Si+1 |
| 空洞卷积 | 插入空洞扩大感受野,不增参数 | K′=K+(K−1)(r−1) |
| 池化反向传播 | Max→只传最大值;Avg→平均分配 | Max: ∂L/∂xmax=∂L/∂y; Avg: ∂L/∂xij=(1/k2)∂L/∂y |
| 1×1卷积 | 跨通道信息整合,降维/升维,增非线性 | yijp=∑d=1Dwp,d⋅xijd+bp |
理解这些核心操作的数学原理,你就掌握了CNN的“底层语法”。无论是设计新的网络架构,还是调试现有模型,这些知识都是不可或缺的工具。
延伸阅读:
按顺序完成这组文章,循序渐进地掌握主题
发现错误、内容过时或有改进想法?欢迎告诉我
根据本文分类与标签,为你推荐可能感兴趣的内容

系统讲解多层感知机(MLP)的前向传播矩阵运算与反向传播链式法则,以计算图为工具手动推导3层MLP的误差回传与参数梯度公式,提炼出误差回传、权重梯度、偏置梯度的核心公式。同时讨论通用近似定理(UAT)的砖块构造直觉,以及Batch Normalization训练时mini-batch统计与推理时滑动平均(EMA)机制。
阅读文章
系统讲解LSTM与GRU的门控机制设计哲学与数学原理。从LSTM的“双轨”架构(细胞状态+隐藏状态)与三门结构(遗忘门、输入门、输出门)出发,推导细胞状态更新公式如何通过按元素加法路径缓解梯度消失,并深入剖析GRU的两大简化及其参数量优势。附带讨论双向LSTM与深层堆叠LSTM的应用场景。
阅读文章
系统讲解循环神经网络(RNN)的数学原理与随时间反向传播(BPTT)算法。从RNN的循环结构与共享参数出发,推导BPTT的梯度表达式,揭示梯度消失与梯度爆炸的数学根源,并介绍梯度裁剪作为应对梯度爆炸的工程解法。附带讨论LSTM如何通过门控机制缓解梯度消失。
阅读文章请使用微信扫描二维码分享
当前文章会保持在原页面