BP神经网络例题实战:从数学推导到Python实现

1次阅读
没有评论

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

image.webp

问题场景:为什么选择异或问题

异或(XOR)是一个经典的线性不可分问题,简单但能充分展示神经网络的威力。假设我们有以下四个数据点:

BP 神经网络例题实战:从数学推导到 Python 实现

  • (0, 0) → 类别 0
  • (0, 1) → 类别 1
  • (1, 0) → 类别 1
  • (1, 1) → 类别 0

用单层感知机无法正确分类,但带有一个隐藏层的 BP 神经网络可以解决这个问题。接下来我们会用这个例子贯穿全文。

数学推导:前向与反向传播

网络结构定义

我们构建一个 2 -2- 1 的三层网络:
– 输入层:2 个神经元(对应 x1,x2)
– 隐藏层:2 个神经元
– 输出层:1 个神经元

前向传播公式

  1. 输入层到隐藏层:
    $$\mathbf{z}^{(1)} = W^{(1)}\mathbf{x} + \mathbf{b}^{(1)}$$
    $$\mathbf{a}^{(1)} = \sigma(\mathbf{z}^{(1)})$$

  2. 隐藏层到输出层:
    $$z^{(2)} = W^{(2)}\mathbf{a}^{(1)} + b^{(2)}$$
    $$a^{(2)} = \sigma(z^{(2)})$$

其中 $\sigma$ 是 sigmoid 函数:$\sigma(x) = \frac{1}{1+e^{-x}}$

反向传播推导

使用平方误差损失函数:$J = \frac{1}{2}(y – a^{(2)})^2$

  1. 输出层误差:
    $$\delta^{(2)} = (a^{(2)} – y) \cdot \sigma'(z^{(2)})$$

  2. 隐藏层误差:
    $$\delta^{(1)} = (W^{(2)})^T \delta^{(2)} \odot \sigma'(\mathbf{z}^{(1)})$$

  3. 参数梯度:
    $$\frac{\partial J}{\partial W^{(2)}} = \delta^{(2)} (\mathbf{a}^{(1)})^T$$
    $$\frac{\partial J}{\partial b^{(2)}} = \delta^{(2)}$$
    $$\frac{\partial J}{\partial W^{(1)}} = \delta^{(1)} \mathbf{x}^T$$
    $$\frac{\partial J}{\partial \mathbf{b}^{(1)}} = \delta^{(1)}$$

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 = 2
output_size = 1
learning_rate = 0.5

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

# 训练数据
X = np.array([[0,0], [0,1], [1,0], [1,1]])
y = np.array([[0], [1], [1], [0]])

# 训练循环
loss_history = []
for epoch in range(10000):
    # 前向传播
    z1 = np.dot(X, W1) + b1
    a1 = sigmoid(z1)
    z2 = np.dot(a1, W2) + b2
    a2 = sigmoid(z2)

    # 计算损失
    loss = 0.5 * np.mean((y - a2)**2)
    loss_history.append(loss)

    # 反向传播
    delta2 = (a2 - y) * sigmoid_derivative(a2)
    dW2 = np.dot(a1.T, delta2)
    db2 = np.sum(delta2, axis=0, keepdims=True)

    delta1 = np.dot(delta2, W2.T) * sigmoid_derivative(a1)
    dW1 = np.dot(X.T, delta1)
    db1 = np.sum(delta1, axis=0)

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

    if epoch % 1000 == 0:
        print(f'Epoch {epoch}, Loss: {loss}')

# 可视化损失曲线
plt.plot(loss_history)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss Curve')
plt.show()

避坑指南

  1. 学习率选择
  2. 太大(如 >1):可能导致震荡无法收敛
  3. 太小(如 <0.01):训练速度过慢
  4. 推荐范围:0.1-0.5,可以通过观察损失曲线调整

  5. 隐藏层神经元数量

  6. 异或问题:2- 4 个足够
  7. 复杂问题:可以从输入层大小的 1 / 2 到 2 倍开始尝试
  8. 太多会导致过拟合,表现为训练误差很小但测试误差大

  9. 梯度检查

  10. 用数值方法近似计算梯度,与反向传播结果对比
  11. 实现示例:
    epsilon = 1e-7
    # 对 W1[i,j] 的梯度检查
    W1_plus = W1.copy()
    W1_plus[i,j] += epsilon
    # 计算损失 J_plus...
    W1_minus = W1.copy()
    W1_minus[i,j] -= epsilon
    # 计算损失 J_minus...
    numerical_grad = (J_plus - J_minus) / (2*epsilon)
    # 与反向传播得到的 grad 对比 

扩展思考

  1. 与其他算法对比
  2. SVM:需要合适的核函数才能解决非线性问题
  3. 决策树:可以处理但可能产生复杂的决策边界
  4. BP 网络:天然适合非线性问题,但需要调参

  5. PyTorch 重构

  6. 自动微分省去手动推导
  7. 优化器内置多种学习率策略
  8. 示例代码片段:
    import torch
    import torch.nn as nn
    
    class XORNet(nn.Module):
        def __init__(self):
            super().__init__()
            self.fc1 = nn.Linear(2, 2)
            self.fc2 = nn.Linear(2, 1)
    
        def forward(self, x):
            x = torch.sigmoid(self.fc1(x))
            x = torch.sigmoid(self.fc2(x))
            return x

实践心得

通过这个简单的异或问题,我们可以清晰地看到 BP 神经网络如何处理非线性分类。虽然现代深度学习框架已经帮我们封装了大部分细节,但理解底层的数学原理对于调试网络和解决实际问题非常重要。建议初学者:

  1. 先在小数据集上手动实现,再过渡到框架
  2. 可视化是理解网络行为的有力工具
  3. 梯度检查能确保反向传播的正确性
  4. 调参时保持耐心,记录每次实验的超参数和结果
正文完
 0
评论(没有评论)