从零实现bp反向传播神经网络:训练与推理的完整指南

1次阅读
没有评论

共计 3228 个字符,预计需要花费 9 分钟才能阅读完成。

image.webp

为什么需要神经网络?

传统机器学习方法(如 SVM、随机森林)依赖手工设计特征,但面临两大瓶颈:

从零实现 bp 反向传播神经网络:训练与推理的完整指南

  • 特征工程成本高:图像、语音等原始数据维度高,人工提取有效特征需要领域专业知识
  • 表达能力有限:线性模型难以捕捉数据中的非线性关系,例如图像中的边缘、纹理组合

神经网络通过多层非线性变换自动学习特征表示。以 MNIST 手写数字识别为例:

  1. 原始输入是 784 维像素值(28×28 图像展平)
  2. 第一层隐层可学习到边缘、角点等低级特征
  3. 后续层逐步组合出数字部件(如弧线、横笔)
  4. 输出层对应数字类别的概率分布

前向传播与损失函数

定义 3 层网络结构(输入层 $h_0$,隐层 $h_1$, $h_2$,输出层 $h_3$):

$$
\begin{aligned}
h_1 &= \sigma(W_1 h_0 + b_1) \
h_2 &= \sigma(W_2 h_1 + b_2) \
h_3 &= \text{softmax}(W_3 h_2 + b_3)
\end{aligned}
$$

其中 $\sigma$ 为 sigmoid 激活函数:

$$
\sigma(z) = \frac{1}{1+e^{-z}}
$$

使用交叉熵损失函数衡量预测 $h_3$ 与真实标签 $y$ 的差异:

$$
L = -\sum_{k=1}^{10} y_k \log h_3^{(k)}
$$

反向传播推导

核心是通过链式法则计算损失对参数的偏导。以输出层权重 $W_3$ 为例:

$$
\frac{\partial L}{\partial W_3} = \frac{\partial L}{\partial h_3} \frac{\partial h_3}{\partial z_3} \frac{\partial z_3}{\partial W_3}
$$

具体分步计算:

  1. $\frac{\partial L}{\partial h_3} = -\frac{y}{h_3}$(交叉熵梯度)
  2. $\frac{\partial h_3}{\partial z_3} = h_3 \circ (1-h_3)$(softmax 梯度)
  3. $\frac{\partial z_3}{\partial W_3} = h_2^T$(线性变换梯度)

最终得到:

$$
\frac{\partial L}{\partial W_3} = (h_3 – y) h_2^T
$$

隐层参数梯度需继续反向传播。以 $W_2$ 为例:

$$
\frac{\partial L}{\partial W_2} = \left(\frac{\partial L}{\partial h_2} \circ \sigma'(z_2) \right) h_1^T
$$

其中 $\frac{\partial L}{\partial h_2} = W_3^T (h_3 – y)$,$\sigma'(z_2)$ 为 sigmoid 导数。

Python 实现

import numpy as np
from sklearn.datasets import fetch_openml
from sklearn.preprocessing import OneHotEncoder

# 网络初始化
class NeuralNetwork:
    def __init__(self, input_size, hidden_sizes, output_size):
        self.weights = [np.random.randn(input_size, hidden_sizes[0]) * np.sqrt(2./input_size),  # Xavier 初始化
            np.random.randn(hidden_sizes[0], hidden_sizes[1]) * np.sqrt(2./hidden_sizes[0]),
            np.random.randn(hidden_sizes[1], output_size) * np.sqrt(2./hidden_sizes[1])
        ]
        self.biases = [np.zeros((1, size)) for size in hidden_sizes + [output_size]]

    def forward(self, x):
        self.z1 = x @ self.weights[0] + self.biases[0]
        self.h1 = 1 / (1 + np.exp(-self.z1))  # sigmoid

        self.z2 = self.h1 @ self.weights[1] + self.biases[1]
        self.h2 = 1 / (1 + np.exp(-self.z2))

        self.z3 = self.h2 @ self.weights[2] + self.biases[2]
        exp_scores = np.exp(self.z3 - np.max(self.z3, axis=1, keepdims=True))
        self.h3 = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)  # softmax
        return self.h3

    def backward(self, x, y, l2_lambda=0.01):
        m = x.shape[0]

        # 输出层梯度
        dz3 = self.h3 - y
        dw3 = (self.h2.T @ dz3) / m + l2_lambda * self.weights[2]  # L2 正则化
        db3 = np.sum(dz3, axis=0, keepdims=True) / m

        # 第二隐层梯度
        dh2 = dz3 @ self.weights[2].T
        dz2 = dh2 * (self.h2 * (1 - self.h2))  # sigmoid 导数
        dw2 = (self.h1.T @ dz2) / m + l2_lambda * self.weights[1]
        db2 = np.sum(dz2, axis=0, keepdims=True) / m

        # 第一隐层梯度
        dh1 = dz2 @ self.weights[1].T
        dz1 = dh1 * (self.h1 * (1 - self.h1))
        dw1 = (x.T @ dz1) / m + l2_lambda * self.weights[0]
        db1 = np.sum(dz1, axis=0, keepdims=True) / m

        return dw1, db1, dw2, db2, dw3, db3

训练技巧与避坑指南

梯度消失 / 爆炸

  • 现象 :深层网络在反向传播时,梯度呈指数级减小(消失)或增大(爆炸)
  • 解决方案
  • 使用 Xavier/Glorot 初始化:权重方差 $\text{Var}(W) = \frac{2}{n_{in} + n_{out}}$
  • 改用 ReLU 激活函数:$\text{ReLU}(z) = \max(0,z)$,导数为 0 或 1,缓解梯度消失

学习率设置

  • 初始学习率通常取 0.01~0.1
  • 采用指数衰减策略:
    learning_rate = initial_lr * (0.95 ** epoch)

批量归一化(BatchNorm)

在激活函数前插入:

# 前向传播
self.z1 = x @ self.weights[0] + self.biases[0]
self.z1 = (self.z1 - np.mean(self.z1, axis=0)) / np.std(self.z1, axis=0)  # BN
self.h1 = np.maximum(0, self.z1)  # ReLU

MNIST 实验对比

设置相同超参数(隐层 256 单元,学习率 0.05,迭代 50 轮):

激活函数 测试准确率 训练时间
Sigmoid 92.3% 38s
ReLU 97.6% 29s

ReLU 优势明显:

  • 计算更快(无需指数运算)
  • 缓解梯度消失(正区间导数为 1)
  • 带来稀疏激活(约 50% 神经元输出为 0)

下一步挑战

尝试实现卷积层的反向传播:

  1. 将全连接层的矩阵乘法替换为卷积操作
  2. 计算卷积核权重的梯度时需进行 im2col 转换
  3. 池化层反向传播需记录最大值位置(最大池化)

关键公式:

$$
\frac{\partial L}{\partial W_{conv}} = \text{rot180}(\frac{\partial L}{\partial Z} * X)
$$

其中 $*$ 表示卷积操作,rot180 表示旋转 180 度。

通过本文的实现,你已经掌握了 bp 算法的核心思想。建议尝试在 CIFAR-10 数据集上测试卷积网络,观察特征学习的效果提升。

正文完
 0
评论(没有评论)