在这里插入图片描述

从链式法则到 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 定理的局限

虽然定理说「可以逼近」,但没有告诉我们:

  1. 需要多少神经元:可能需要天文数字
  2. 怎么训练:梯度下降不一定能找到最优解
  3. 泛化能力:训练集上逼近,不代表测试集上也好

实践中,我们通常用多层网络,而不是一个巨大的单隐层。


五、工业应用: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₂ → ... → 输出 → 损失
    (从输入到输出)

反向传播:
    损失 → 输出层梯度 → 隐藏层梯度 → ... → 输入层梯度
    (从输出到输入,方向相反)

所以叫「反向」传播

八、本篇总结

核心要点回顾

  1. 多层感知器(MLP):输入层 → 隐藏层 → 输出层,全连接
  2. 前向传播:z = Wx + b, a = f(z)
  3. 反向传播:用链式法则计算梯度,从输出层传回输入层
  4. 链式法则:∂L/∂w = ∂L/∂y · ∂y/∂z · ∂z/∂w
  5. 万能逼近定理:单隐层网络可以逼近任意连续函数
  6. 权重初始化:Xavier 或 He 初始化,不能全零初始化

下篇预告

下一篇我们学习核方法。

MLP 可以解决非线性问题,但有一个问题:需要很多数据和计算资源。核方法用「升维」的技巧,不需要显式计算高维映射,就能处理非线性问题。

下一篇你将学到:

  • 核技巧的直觉
  • 常用核函数(线性核、多项式核、RBF 核)
  • RBF 网络的实现
  • 与 MLP 的对比

本期互动

你对反向传播有什么看法?

  • 你手写过 BP 算法吗?
  • 你觉得 BP 和 PyTorch 的 autograd 有什么区别?
  • 你遇到过梯度消失/爆炸的问题吗?怎么解决的?

欢迎在评论区留言。


系列目录

篇标题状态
01Haykin 精讲开篇:从「只会调参」到「理解神经网络的灵魂」✅ 完成
02感知器:神经网络的「鼻祖」,为什么它能「学会」分类?✅ 完成
03LMS 算法:从最小二乘到随机梯度下降,工业自适应滤波的核心✅ 完成
04反向传播:神经网络为什么能「学习」?用 NumPy 手写 BP✅ 当前
05核方法:为什么 SVM 能处理非线性问题?理解「升维」的本质⏳ 下一篇
06支持向量机:最大间隔的「艺术」,为什么它是「小数据之王」?⏳ 待写
07正则化:为什么模型越复杂越容易过拟合?L1/L2/Dropout⏳ 待写
08PCA:为什么降维能「去噪」?从特征值分解到核 PCA⏳ 待写
09SOM:无监督学习的「聚类之王」,为什么它能「自组织」?⏳ 待写
10信息论:为什么「信息最大化」能学特征?从熵到 ICA⏳ 待写
11玻尔兹曼机:深度学习的「前世」,从统计力学到 RBM⏳ 待写
12动态规划:强化学习的「数学基础」,从 MDP 到值迭代⏳ 待写
13Hopfield 网络:联想记忆的「鼻祖」,为什么它能「回忆」?⏳ 待写
14卡尔曼滤波:为什么它能「预测」?从贝叶斯推断到粒子滤波⏳ 待写
15Haykin 精讲终篇:从感知器到深度学习——一部神经网络的「进化史」⏳ 待写

点赞收藏转发,是我持续更新的动力!

更多推荐