BP神经网络基本原理:从数学推导到Python实战

1次阅读
没有评论

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

image.webp

从 MNIST 分类看 BP 网络的价值

手写数字识别(MNIST)是理解 BP 网络的经典场景。每个 28×28 像素的图像被展平成 784 维输入向量,我们需要一个能自动学习特征并分类的网络。传统线性模型无法处理像素间的复杂关系,而具有隐藏层的 BP 网络通过非线性变换实现了这一点。

BP 神经网络基本原理:从数学推导到 Python 实战

数学推导:链式法则与权重更新

计算图视角下的反向传播

假设三层网络结构为:输入层 $\mathbf{x}$、隐藏层 $\mathbf{h} = \sigma(\mathbf{W}_1\mathbf{x}+\mathbf{b}_1)$、输出层 $\mathbf{y} = \text{softmax}(\mathbf{W}_2\mathbf{h}+\mathbf{b}_2)$,损失函数为交叉熵 $E$。关键推导步骤如下:

  1. 输出层梯度:
    $$\frac{\partial E}{\partial \mathbf{W}_2} = \frac{\partial E}{\partial \mathbf{y}} \cdot \frac{\partial \mathbf{y}}{\partial (\mathbf{W}_2\mathbf{h})} \cdot \frac{\partial (\mathbf{W}_2\mathbf{h})}{\partial \mathbf{W}_2} = (\mathbf{y} – \mathbf{t}) \otimes \mathbf{h}^T$$
    其中 $\mathbf{t}$ 为 one-hot 标签

  2. 隐藏层梯度:
    $$\frac{\partial E}{\partial \mathbf{W}_1} = \frac{\partial E}{\partial \mathbf{h}} \cdot \frac{\partial \mathbf{h}}{\partial (\mathbf{W}_1\mathbf{x})} \cdot \frac{\partial (\mathbf{W}_1\mathbf{x})}{\partial \mathbf{W}_1}$$
    展开后为:
    $$\mathbf{W}_2^T(\mathbf{y} – \mathbf{t}) \odot \sigma'(\mathbf{W}_1\mathbf{x}) \otimes \mathbf{x}^T$$

NumPy 实现关键代码

import numpy as np

class ThreeLayerNet:
    def __init__(self, input_size, hidden_size, output_size):
        # 初始化权重(He 初始化)self.W1 = np.random.randn(input_size, hidden_size) * np.sqrt(2/input_size)
        self.b1 = np.zeros(hidden_size)
        self.W2 = np.random.randn(hidden_size, output_size) * np.sqrt(2/hidden_size)
        self.b2 = np.zeros(output_size)

    def forward(self, x):
        # 前向传播维度变化: (batch, input) -> (batch, hidden) -> (batch, output)
        self.z1 = x @ self.W1 + self.b1  # 注意广播机制
        self.h = np.maximum(0, self.z1)  # ReLU 激活
        self.z2 = self.h @ self.W2 + self.b2
        self.y = self._softmax(self.z2)
        return self.y

    def backward(self, x, t, lr):
        batch_size = x.shape[0]

        # 输出层梯度
        dy = (self.y - t) / batch_size  # 交叉熵 +softmax 的联合梯度
        dW2 = self.h.T @ dy
        db2 = np.sum(dy, axis=0)

        # 隐藏层梯度(利用前向传播缓存的 z1)dh = dy @ self.W2.T
        dz1 = dh * (self.z1 > 0)  # ReLU 导数
        dW1 = x.T @ dz1
        db1 = np.sum(dz1, axis=0)

        # 参数更新(带学习率衰减)self.W2 -= lr * dW2
        self.b2 -= lr * db2
        self.W1 -= lr * dW1
        self.b1 -= lr * db1

优化技巧与避坑指南

激活函数选择

  • Sigmoid 问题 :当输入绝对值较大时梯度接近 0(梯度消失),且计算含指数运算较慢
    $$\sigma'(z) = \sigma(z)(1-\sigma(z))$$

  • ReLU 优势 :正向区间梯度为 1,避免梯度消失,计算速度快
    $$\text{ReLU}'(z) = \begin{cases} 1 & z > 0 \ 0 & \text{otherwise} \end{cases}$$

批量归一化实现

class BatchNorm:
    def __init__(self, dim, eps=1e-5):
        self.gamma = np.ones(dim)
        self.beta = np.zeros(dim)
        self.eps = eps

    def forward(self, x, train=True):
        if train:
            self.mu = np.mean(x, axis=0)
            self.sigma2 = np.var(x, axis=0)
            x_hat = (x - self.mu) / np.sqrt(self.sigma2 + self.eps)
            self.cache = (x, x_hat)
            return self.gamma * x_hat + self.beta
        else:
            # 测试时使用移动平均
            return self.gamma * (x - self.moving_mu) / np.sqrt(self.moving_sigma2 + self.eps) + self.beta

超参数设置经验

  1. 隐藏层神经元数量:通常取输入输出层大小的几何平均数,例如 MNIST 中可设为 $\sqrt{784 \times 10} \approx 88$

  2. 梯度检查实现(数值梯度 vs 解析梯度):

    def gradient_check(x, t, net, param_name, epsilon=1e-7):
        param = getattr(net, param_name)
        grad = getattr(net, 'd'+param_name)  # 反向传播得到的梯度
    
        for i in range(min(10, param.size)):  # 随机检查部分维度
            idx = np.unravel_index(i, param.shape)
            orig = param[idx]
    
            param[idx] = orig + epsilon
            loss_plus = net.loss(x, t)
    
            param[idx] = orig - epsilon
            loss_minus = net.loss(x, t)
    
            numeric_grad = (loss_plus - loss_minus) / (2*epsilon)
            param[idx] = orig  # 恢复原值
    
            diff = np.abs(numeric_grad - grad[idx])
            if diff > 1e-5:
                print(f'梯度检查失败:{param_name} 维度 {idx},差异 {diff:.2e}')

  3. 学习率与 batch size:大 batch 需要更大学习率,推荐线性缩放规则(batch 扩大 k 倍时 lr 乘以 k)

思考题解析

当输出层采用交叉熵损失 +softmax 时,求导会出现神奇简化:
$$\frac{\partial E}{\partial z_i} = \sum_j \frac{\partial E}{\partial y_j} \frac{\partial y_j}{\partial z_i} = y_i – t_i$$
这是因为 $\frac{\partial E}{\partial y_j} = -\frac{t_j}{y_j}$,而 softmax 导数为 $\frac{\partial y_j}{\partial z_i} = y_j(\delta_{ij} – y_i)$,二者相乘后所有项相互抵消,最终得到简洁形式。

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