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

1次阅读
没有评论

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

image.webp

问题描述与网络结构

我们以一个简单的二分类问题为例:给定二维平面上的点,判断它们属于哪个类别(0 或 1)。为了演示 BP 神经网络的反向传播过程,我们设计一个包含 1 个隐藏层的网络结构:

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

  • 输入层:2 个神经元(对应 x / y 坐标)
  • 隐藏层:3 个神经元,使用 Sigmoid 激活函数
  • 输出层:1 个神经元,使用 Sigmoid 激活函数

数学符号定义:
– $W^{(1)}$: 输入层到隐藏层的权重矩阵(3×2)
– $b^{(1)}$: 隐藏层偏置向量(3×1)
– $W^{(2)}$: 隐藏层到输出层的权重矩阵(1×3)
– $b^{(2)}$: 输出层偏置(标量)

前向传播实现

前向传播分为三个步骤:

  1. 计算隐藏层输入:
    $$z^{(1)} = W^{(1)}X + b^{(1)}$$

  2. 计算隐藏层输出(应用 Sigmoid):
    $$a^{(1)} = \sigma(z^{(1)}) = \frac{1}{1+e^{-z^{(1)}}}$$

  3. 计算最终输出:
    $$z^{(2)} = W^{(2)}a^{(1)} + b^{(2)}$$
    $$a^{(2)} = \sigma(z^{(2)})$$

损失函数与反向传播推导

采用交叉熵损失函数:
$$L = -[y\log(a^{(2)}) + (1-y)\log(1-a^{(2)})]$$

反向传播使用链式法则计算梯度:

  1. 输出层梯度:
    $$\frac{\partial L}{\partial z^{(2)}} = a^{(2)} – y$$

  2. 隐藏层梯度:
    $$\frac{\partial L}{\partial z^{(1)}} = (W^{(2)})^T \cdot \frac{\partial L}{\partial z^{(2)}} \odot \sigma'(z^{(1)})$$

  3. 权重更新公式:
    $$W^{(2)} := W^{(2)} – \eta \frac{\partial L}{\partial W^{(2)}}$$
    $$\frac{\partial L}{\partial W^{(2)}} = \frac{\partial L}{\partial z^{(2)}} \cdot (a^{(1)})^T$$

同理可推导其他参数的更新公式。

Python 实现与解释

import numpy as np
import matplotlib.pyplot as plt

# Sigmoid 激活函数及其导数
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(x):
    return x * (1 - x)

# 网络参数初始化
input_size = 2
hidden_size = 3
output_size = 1

# 随机初始化权重
W1 = np.random.randn(hidden_size, input_size) * 0.01
b1 = np.zeros((hidden_size, 1))
W2 = np.random.randn(output_size, hidden_size) * 0.01
b2 = np.zeros((output_size, 1))

# 训练数据
X = np.array([[0,0], [0,1], [1,0], [1,1]]).T  # 4 个样本
y = np.array([[0, 1, 1, 0]])

# 训练参数
learning_rate = 0.1
epochs = 10000

# 训练过程
for i in range(epochs):
    # 前向传播
    Z1 = np.dot(W1, X) + b1
    A1 = sigmoid(Z1)
    Z2 = np.dot(W2, A1) + b2
    A2 = sigmoid(Z2)

    # 计算损失
    loss = -np.mean(y * np.log(A2) + (1-y) * np.log(1-A2))

    # 反向传播
    dZ2 = A2 - y
    dW2 = np.dot(dZ2, A1.T) / X.shape[1]
    db2 = np.sum(dZ2, axis=1, keepdims=True) / X.shape[1]

    dZ1 = np.dot(W2.T, dZ2) * sigmoid_derivative(A1)
    dW1 = np.dot(dZ1, X.T) / X.shape[1]
    db1 = np.sum(dZ1, axis=1, keepdims=True) / X.shape[1]

    # 更新参数
    W2 -= learning_rate * dW2
    b2 -= learning_rate * db2
    W1 -= learning_rate * dW1
    b1 -= learning_rate * db1

    # 每 1000 次打印损失
    if i % 1000 == 0:
        print(f'Epoch {i}, Loss: {loss}')

# 测试
print("Final predictions:", A2)

实验结果分析

  1. 学习率选择:
  2. 过大(如 >0.5)会导致震荡甚至发散
  3. 过小(如 <0.01)会使训练速度极慢
  4. 建议使用学习率衰减策略

  5. 梯度消失问题:

  6. 深层网络中 Sigmoid 导数最大为 0.25,多次连乘导致梯度指数级减小
  7. 解决方案:改用 ReLU 激活函数、残差连接、批归一化等

  8. 激活函数比较:

  9. Sigmoid:输出范围 (0,1),适合二分类但易饱和
  10. Tanh:输出范围 (-1,1),中心对称但仍有饱和问题
  11. ReLU:计算简单,缓解梯度消失但可能导致神经元死亡

避坑指南

  1. 权重初始化:
  2. 避免全零初始化(导致对称性问题)
  3. 推荐 Xavier/Glorot 初始化

  4. 数值稳定性:

  5. 计算交叉熵时添加微小值防止 log(0)
  6. 使用稳定的 Softmax 实现

  7. 常见错误:

  8. 忘记转置矩阵(维度不匹配)
  9. 梯度计算符号错误
  10. 批量训练时未正确平均梯度

延伸思考

  1. 如何扩展到多分类问题?
  2. 如何实现 Mini-batch 训练?
  3. 尝试不同的优化器(Adam、RMSprop 等)
  4. 添加 L2 正则化防止过拟合
正文完
 0
评论(没有评论)