BP神经网络反向传播算法公式推导:从数学原理到Python实现

1次阅读
没有评论

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

image.webp

反向传播算法是神经网络训练的核心引擎,它像一位精准的导航员,通过计算损失函数对每个权重的梯度,指导网络参数朝着误差减小的方向调整。下面我们拆解这个看似复杂的数学过程,用最直白的方式呈现从理论到代码的全路径。

BP 神经网络反向传播算法公式推导:从数学原理到 Python 实现

原理推导

1. 网络结构定义

设单隐藏层网络结构如下(符号定义遵循通用惯例):

  • 输入层:$\mathbf{x} = [x_1, x_2, …, x_n]^T$
  • 隐藏层:$\mathbf{h} = \sigma(\mathbf{W}_1 \mathbf{x} + \mathbf{b}_1)$,其中 $\sigma$ 为 sigmoid 函数
  • 输出层:$\mathbf{y} = \text{softmax}(\mathbf{W}_2 \mathbf{h} + \mathbf{b}_2)$

2. 损失函数选择

使用交叉熵损失(适合分类任务):
$$ L = -\sum_k t_k \log y_k $$
其中 $t_k$ 为 one-hot 编码的真实标签。

3. 关键梯度推导(链式法则实战)

输出层权重梯度 :
$$
\frac{\partial L}{\partial \mathbf{W}2} = \underbrace{\frac{\partial L}{\partial \mathbf{y}}}} – \mathbf{t})} \cdot \underbrace{\frac{\partial \mathbf{y}}{\partial (\mathbf{W2 \mathbf{h})}}}} \cdot \underbrace{\frac{\partial (\mathbf{W2 \mathbf{h})}{\partial \mathbf{W}_2}}
$$}^T

隐藏层权重梯度 (体现反向传播特性):
$$
\frac{\partial L}{\partial \mathbf{W}1} = \underbrace{\frac{\partial L}{\partial \mathbf{h}}}2^T (\mathbf{y}-\mathbf{t})} \cdot \underbrace{\frac{\partial \mathbf{h}}{\partial (\mathbf{W}_1 \mathbf{x})}}} \cdot \underbrace{\frac{\partial (\mathbf{W1 \mathbf{x})}{\partial \mathbf{W}_1}}
$$}^T

注意到 sigmoid 导数 $\sigma’ = \sigma(1-\sigma)$ 的特性,这正是梯度消失问题的根源。

代码实现

import numpy as np

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def bp_nn(X, y, hidden_size=64, lr=0.1, epochs=100):
    # 初始化权重(Xavier 初始化)W1 = np.random.randn(X.shape[1], hidden_size) * np.sqrt(2/X.shape[1])
    b1 = np.zeros(hidden_size)
    W2 = np.random.randn(hidden_size, y.shape[1]) * np.sqrt(2/hidden_size)
    b2 = np.zeros(y.shape[1])

    for _ in range(epochs):
        # 前向传播
        h = sigmoid(X.dot(W1) + b1)
        y_pred = softmax(h.dot(W2) + b2)

        # 反向传播
        dy = y_pred - y  # 输出层误差
        grad_W2 = h.T.dot(dy)
        grad_b2 = dy.sum(axis=0)

        dh = dy.dot(W2.T) * h * (1 - h)  # 隐藏层误差(注意 sigmoid 导数)grad_W1 = X.T.dot(dh)
        grad_b1 = dh.sum(axis=0)

        # 参数更新(加入梯度裁剪)for grad in [grad_W1, grad_W2]:
            np.clip(grad, -1, 1, out=grad)

        W1 -= lr * grad_W1
        b1 -= lr * grad_b1
        W2 -= lr * grad_W2
        b2 -= lr * grad_b2

    return W1, b1, W2, b2

代码关键点说明:

  • 向量化计算:整个 batch 的矩阵运算取代逐样本 for 循环
  • 梯度裁剪:防止梯度爆炸的实用技巧
  • Xavier 初始化:缓解梯度消失的初始化策略

调试技巧

1. 学习率选择

经验公式:初始学习率可设为 $\frac{1}{\sqrt{\text{batch_size}}}$。当发现损失震荡时减小 10 倍,收敛过慢时增大 2 倍。

2. 梯度检验

用数值梯度验证解析梯度的正确性:

def check_grad(func, param, eps=1e-5):
    grad = np.zeros_like(param)
    it = np.nditer(param, flags=['multi_index'])
    while not it.finished:
        idx = it.multi_index
        tmp = param[idx]

        param[idx] = tmp + eps
        f1 = func()

        param[idx] = tmp - eps
        f2 = func()

        grad[idx] = (f1 - f2) / (2 * eps)
        param[idx] = tmp
        it.iternext()
    return grad

3. 批量训练技巧

  • 累积小批量梯度时,注意除以 batch_size 保持梯度尺度一致
  • 使用随机 shuffle 避免样本顺序影响

思考题实践

要实现带动量的梯度下降,只需在参数更新阶段加入速度项:

# 初始化速度
v_W1, v_b1 = 0, 0
v_W2, v_b2 = 0, 0
momentum = 0.9

# 更新规则改为
v_W1 = momentum * v_W1 + lr * grad_W1
W1 -= v_W1

反向传播就像拆解一台精密钟表——当我们理解每个齿轮(公式)的咬合关系后,就能自己动手组装(实现)出完整的系统。现在你可以尝试调整隐藏层数,观察梯度传播的变化规律了。

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