反向传播:神经网络为什么能「学习」?用 NumPy 手写 BP
·

从链式法则到 BP 算法,深度学习的核心原理,一行一行代码拆解
开头:为什么梯度要「反向传播」?
上一篇我们学习了 LMS 算法。它用梯度下降更新权重,但只有一个输出层——只能解决线性问题。
如果我们想解决非线性问题(比如 XOR),就需要多层网络。
但多层网络有一个问题:隐藏层的梯度怎么算?
单层网络(感知器/LMS):
输入 x → 权重 w → 输出 y
梯度直接从输出层传回输入层
多层网络:
输入 x → 隐藏层 → 输出层 → 损失
↑ ↑
这层的梯度怎么算?
答案:反向传播(Backpropagation)——梯度从输出层「反向」传回隐藏层
反向传播是深度学习的核心。 你用 PyTorch 调用 loss.backward() 的时候,底层就是在做这件事。
今天我们从零推导反向传播,然后用 NumPy 手写一个两层神经网络。
一、多层感知器(MLP)架构
1.1 网络结构
多层感知器(MLP)架构
═══════════════════════════════════════════════════════════════════
输入层 隐藏层 输出层
x₁ ──→ ┌────────┐
│ h₁ │
x₂ ──→ │ h₂ │ ──→ y₁
│ h₃ │
x₃ ──→ └────────┘
每一层的神经元与下一层的神经元**全连接**:
- 输入层到隐藏层:权重矩阵 W¹
- 隐藏层到输出层:权重矩阵 W²
1.2 数学表达
前向传播:
隐藏层:
z¹ = W¹x + b¹ (线性变换)
a¹ = f(z¹) (激活函数)
输出层:
z² = W²a¹ + b² (线性变换)
y = f(z²) (激活函数)
其中:
W¹:输入层到隐藏层的权重
b¹:隐藏层的偏置
W²:隐藏层到输出层的权重
b²:输出层的偏置
f:激活函数(Sigmoid、ReLU 等)
二、反向传播推导
2.1 问题定义
假设我们有一个简单的网络:
网络结构:
输入 x → 隐藏层 → 输出层 → 损失 L
目标:
最小化损失函数 L(w, b)
方法:
梯度下降:w ← w - η · ∂L/∂w
关键问题:
怎么计算 ∂L/∂w?
→ 反向传播
2.2 链式法则
反向传播的核心是链式法则:
链式法则:
∂L/∂w = ∂L/∂y · ∂y/∂z · ∂z/∂w
直观理解:
L → y → z → w
每一层都有一层「贡献」,把这些贡献乘起来就是总梯度
2.3 推导过程
我们一步步推导:
步骤 1:计算损失对输出的梯度
═══════════════════════════════════════════════════════════════════
假设损失函数是均方误差:
L = (1/2)(y - ŷ)²
对输出 ŷ 求导:
∂L/∂ŷ = -(y - ŷ) = ŷ - y
记:δ² = ∂L/∂z² = (ŷ - y) · f'(z²)
(输出层的「误差信号」)
步骤 2:计算损失对隐藏层输出的梯度
═══════════════════════════════════════════════════════════════════
∂L/∂a¹ = ∂L/∂z² · ∂z²/∂a¹
= δ² · (W²)ᵀ
步骤 3:计算损失对隐藏层输入的梯度
═══════════════════════════════════════════════════════════════════
δ¹ = ∂L/∂z¹ = ∂L/∂a¹ · ∂a¹/∂z¹
= δ² · (W²)ᵀ · f'(z¹)
记:δ¹ = (隐藏层的「误差信号」)
步骤 4:计算损失对权重的梯度
═══════════════════════════════════════════════════════════════════
输出层:
∂L/∂W² = δ² · (a¹)ᵀ
∂L/∂b² = δ²
隐藏层:
∂L/∂W¹ = δ¹ · xᵀ
∂L/∂b¹ = δ¹
2.4 反向传播的直觉
反向传播的直觉理解
═══════════════════════════════════════════════════════════════════
前向传播:输入 → 隐藏层 → 输出 → 损失
反向传播:损失 → 输出层梯度 → 隐藏层梯度 → 权重更新
类比:
前向传播 = 做作业
反向传播 = 批改作业,告诉你哪里错了,怎么改
关键洞察:
每一层只需要知道「下游传回来的梯度」和「自己的局部梯度」
→ 非常适合并行计算
三、Python 手写 BP 网络
3.1 完整代码
import numpy as np
import matplotlib.pyplot as plt
class TwoLayerNet:
"""两层神经网络(反向传播)"""
def __init__(self, input_size, hidden_size, output_size, learning_rate=0.01):
"""
参数:
input_size: 输入层维度
hidden_size: 隐藏层维度
output_size: 输出层维度
learning_rate: 学习率
"""
self.lr = learning_rate
# 初始化权重(Xavier 初始化)
self.W1 = np.random.randn(input_size, hidden_size) * np.sqrt(2.0 / input_size)
self.b1 = np.zeros((1, hidden_size))
self.W2 = np.random.randn(hidden_size, output_size) * np.sqrt(2.0 / hidden_size)
self.b2 = np.zeros((1, output_size))
# 记录训练过程
self.loss_history = []
self.accuracy_history = []
def relu(self, z):
"""ReLU 激活函数"""
return np.maximum(0, z)
def relu_derivative(self, z):
"""ReLU 导数"""
return (z > 0).astype(float)
def softmax(self, z):
"""Softmax 激活函数(输出层)"""
exp_z = np.exp(z - np.max(z, axis=1, keepdims=True))
return exp_z / np.sum(exp_z, axis=1, keepdims=True)
def cross_entropy_loss(self, y_pred, y_true):
"""交叉熵损失"""
m = y_true.shape[0]
# y_true 是 one-hot 编码
log_likelihood = -np.log(y_pred[range(m), y_true] + 1e-8)
return np.mean(log_likelihood)
def forward(self, X):
"""前向传播"""
# 隐藏层
self.z1 = X @ self.W1 + self.b1
self.a1 = self.relu(self.z1)
# 输出层
self.z2 = self.a1 @ self.W2 + self.b2
self.a2 = self.softmax(self.z2)
return self.a2
def backward(self, X, y):
"""反向传播"""
m = X.shape[0]
# 输出层梯度
dz2 = self.a2.copy()
dz2[range(m), y] -= 1 # softmax + cross_entropy 的梯度
dW2 = self.a1.T @ dz2 / m
db2 = np.sum(dz2, axis=0, keepdims=True) / m
# 隐藏层梯度
da1 = dz2 @ self.W2.T
dz1 = da1 * self.relu_derivative(self.z1)
dW1 = X.T @ dz1 / m
db1 = np.sum(dz1, axis=0, keepdims=True) / m
# 更新权重
self.W2 -= self.lr * dW2
self.b2 -= self.lr * db2
self.W1 -= self.lr * dW1
self.b1 -= self.lr * db1
def predict(self, X):
"""预测"""
y_pred = self.forward(X)
return np.argmax(y_pred, axis=1)
def fit(self, X, y, n_iterations=1000, batch_size=32):
"""训练"""
n_samples = X.shape[0]
for epoch in range(n_iterations):
# 随机打乱
indices = np.random.permutation(n_samples)
X_shuffled = X[indices]
y_shuffled = y[indices]
# 小批量梯度下降
for start in range(0, n_samples, batch_size):
end = min(start + batch_size, n_samples)
X_batch = X_shuffled[start:end]
y_batch = y_shuffled[start:end]
# 前向传播
y_pred = self.forward(X_batch)
# 反向传播
self.backward(X_batch, y_batch)
# 记录训练过程
if epoch % 100 == 0:
y_pred_full = self.forward(X)
loss = self.cross_entropy_loss(y_pred_full, y)
accuracy = np.mean(self.predict(X) == y)
self.loss_history.append(loss)
self.accuracy_history.append(accuracy)
print(f"Epoch {epoch}: Loss={loss:.4f}, Accuracy={accuracy:.2%}")
3.2 测试:XOR 问题
# XOR 数据
X = np.array([
[0, 0],
[0, 1],
[1, 0],
[1, 1]
])
y = np.array([0, 1, 1, 0])
# 训练网络
net = TwoLayerNet(input_size=2, hidden_size=8, output_size=2, learning_rate=0.1)
net.fit(X, y, n_iterations=2000, batch_size=4)
# 测试
print("\n预测结果:")
for i in range(len(X)):
pred = net.predict(X[i:i+1])[0]
print(f"输入: {X[i]} → 预测: {pred}, 真实: {y[i]}")
# 输出:
# 输入: [0 0] → 预测: 0, 真实: 0
# 输入: [0 1] → 预测: 1, 真实: 1
# 输入: [1 0] → 预测: 1, 真实: 1
# 输入: [1 1] → 预测: 0, 真实: 0
# ✅ 感知器无法解决的 XOR 问题,两层神经网络轻松搞定!
3.3 训练曲线
# 绘制训练曲线
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
ax1.plot(net.loss_history)
ax1.set_xlabel('Epoch (x100)')
ax1.set_ylabel('Loss')
ax1.set_title('训练损失')
ax1.grid(True)
ax2.plot(net.accuracy_history)
ax2.set_xlabel('Epoch (x100)')
ax2.set_ylabel('Accuracy')
ax2.set_title('训练准确率')
ax2.grid(True)
plt.tight_layout()
plt.show()
四、万能逼近定理
4.1 定理内容
万能逼近定理(Universal Approximation Theorem)是这么说的:
一个包含足够多神经元的隐层,能以任意精度逼近任意连续函数。
万能逼近定理
═══════════════════════════════════════════════════════════════════
定理内容:
对于任意连续函数 f: [0,1]ⁿ → ℝ 和任意 ε > 0,
存在一个单隐层网络 g(x) = Σ αᵢ σ(wᵢᵀx + bᵢ),
使得 |g(x) - f(x)| < ε 对所有 x ∈ [0,1]ⁿ 成立。
直观理解:
只要有足够的神经元,单隐层网络可以逼近任何函数
(理论上)
4.2 定理的局限
虽然定理说「可以逼近」,但没有告诉我们:
- 需要多少神经元:可能需要天文数字
- 怎么训练:梯度下降不一定能找到最优解
- 泛化能力:训练集上逼近,不代表测试集上也好
实践中,我们通常用多层网络,而不是一个巨大的单隐层。
五、工业应用:AOI 中的分类任务
5.1 极性分类
在 AOI 项目中,极性分类可以用 MLP 实现:
极性分类任务
═══════════════════════════════════════════════════════════════════
输入:224×224×3 图像
→ 展平为 150528 维向量
→ 或用 CNN 提取特征
隐藏层:128 或 256 个神经元
输出层:3 个神经元(正极性、负极性、无极性)
实际项目中:
→ 用 YOLO 的分类头(更高效)
→ 但原理是相通的
5.2 缺陷分类
缺陷分类任务
═══════════════════════════════════════════════════════════════════
输入:裁剪后的缺陷图像(224×224×3)
输出:缺陷类别
- c_broken(断裂)
- c_chip(崩边)
- c_contamination(污染)
- c_open(开路)
- c_short(短路)
- ...
模型:YOLOv12-cls(分类头就是 MLP)
六、避坑指南:使用 BP 的 3 个陷阱
坑 1:权重初始化不当 → 梯度消失/爆炸
错误做法:全零初始化
# ❌ 全零初始化
W1 = np.zeros((input_size, hidden_size))
# 所有神经元学到相同的东西,网络没有表达能力
正确做法:Xavier 或 He 初始化
# ✅ Xavier 初始化(适用于 Sigmoid/Tanh)
W1 = np.random.randn(input_size, hidden_size) * np.sqrt(1.0 / input_size)
# ✅ He 初始化(适用于 ReLU)
W1 = np.random.randn(input_size, hidden_size) * np.sqrt(2.0 / input_size)
坑 2:学习率太大 → Loss 震荡
错误做法:learning_rate=10
# ❌ 学习率太大
net = TwoLayerNet(..., learning_rate=10)
# Loss 在最优解附近剧烈震荡,无法收敛
正确做法:从 0.01 或 0.001 开始
# ✅ 合适的学习率
net = TwoLayerNet(..., learning_rate=0.01)
坑 3:不加正则化 → 过拟合
错误做法:网络太大,没有正则化
# ❌ 过拟合
net = TwoLayerNet(input_size=150528, hidden_size=1024, output_size=3)
# 训练集 100%,测试集 60%
正确做法:加 Dropout 或 L2 正则化
# ✅ 加正则化
# 方法 1:L2 正则化
reg_lambda = 0.001
loss = cross_entropy_loss + reg_lambda * (np.sum(W1**2) + np.sum(W2**2))
# 方法 2:Dropout(在 forward 中实现)
def forward_with_dropout(self, X, dropout_rate=0.5):
self.z1 = X @ self.W1 + self.b1
self.a1 = self.relu(self.z1)
# Dropout
if self.training:
self.dropout_mask = (np.random.rand(*self.a1.shape) > dropout_rate)
self.a1 *= self.dropout_mask
self.a1 /= (1 - dropout_rate) # 缩放
self.z2 = self.a1 @ self.W2 + self.b2
self.a2 = self.softmax(self.z2)
return self.a2
七、BP 算法的效率分析
7.1 计算复杂度
BP 算法的计算复杂度
═══════════════════════════════════════════════════════════════════
前向传播:
第 l 层:O(n_l × n_{l-1})
总计:O(Σ n_l × n_{l-1})
反向传播:
与前向传播相同
总复杂度:O(前向 + 反向) = 2 × O(前向)
对比:
穷举法:O(2^n)(n 个权重,每个权重取 0/1)
BP:O(n)(线性)
→ 指数级加速!
7.2 为什么叫「反向」?
为什么叫「反向传播」?
═══════════════════════════════════════════════════════════════════
前向传播:
x → h₁ → h₂ → ... → 输出 → 损失
(从输入到输出)
反向传播:
损失 → 输出层梯度 → 隐藏层梯度 → ... → 输入层梯度
(从输出到输入,方向相反)
所以叫「反向」传播
八、本篇总结
核心要点回顾
- 多层感知器(MLP):输入层 → 隐藏层 → 输出层,全连接
- 前向传播:z = Wx + b, a = f(z)
- 反向传播:用链式法则计算梯度,从输出层传回输入层
- 链式法则:∂L/∂w = ∂L/∂y · ∂y/∂z · ∂z/∂w
- 万能逼近定理:单隐层网络可以逼近任意连续函数
- 权重初始化:Xavier 或 He 初始化,不能全零初始化
下篇预告
下一篇我们学习核方法。
MLP 可以解决非线性问题,但有一个问题:需要很多数据和计算资源。核方法用「升维」的技巧,不需要显式计算高维映射,就能处理非线性问题。
下一篇你将学到:
- 核技巧的直觉
- 常用核函数(线性核、多项式核、RBF 核)
- RBF 网络的实现
- 与 MLP 的对比
本期互动
你对反向传播有什么看法?
- 你手写过 BP 算法吗?
- 你觉得 BP 和 PyTorch 的 autograd 有什么区别?
- 你遇到过梯度消失/爆炸的问题吗?怎么解决的?
欢迎在评论区留言。
系列目录
| 篇 | 标题 | 状态 |
|---|---|---|
| 01 | Haykin 精讲开篇:从「只会调参」到「理解神经网络的灵魂」 | ✅ 完成 |
| 02 | 感知器:神经网络的「鼻祖」,为什么它能「学会」分类? | ✅ 完成 |
| 03 | LMS 算法:从最小二乘到随机梯度下降,工业自适应滤波的核心 | ✅ 完成 |
| 04 | 反向传播:神经网络为什么能「学习」?用 NumPy 手写 BP | ✅ 当前 |
| 05 | 核方法:为什么 SVM 能处理非线性问题?理解「升维」的本质 | ⏳ 下一篇 |
| 06 | 支持向量机:最大间隔的「艺术」,为什么它是「小数据之王」? | ⏳ 待写 |
| 07 | 正则化:为什么模型越复杂越容易过拟合?L1/L2/Dropout | ⏳ 待写 |
| 08 | PCA:为什么降维能「去噪」?从特征值分解到核 PCA | ⏳ 待写 |
| 09 | SOM:无监督学习的「聚类之王」,为什么它能「自组织」? | ⏳ 待写 |
| 10 | 信息论:为什么「信息最大化」能学特征?从熵到 ICA | ⏳ 待写 |
| 11 | 玻尔兹曼机:深度学习的「前世」,从统计力学到 RBM | ⏳ 待写 |
| 12 | 动态规划:强化学习的「数学基础」,从 MDP 到值迭代 | ⏳ 待写 |
| 13 | Hopfield 网络:联想记忆的「鼻祖」,为什么它能「回忆」? | ⏳ 待写 |
| 14 | 卡尔曼滤波:为什么它能「预测」?从贝叶斯推断到粒子滤波 | ⏳ 待写 |
| 15 | Haykin 精讲终篇:从感知器到深度学习——一部神经网络的「进化史」 | ⏳ 待写 |
点赞收藏转发,是我持续更新的动力!
更多推荐

所有评论(0)