BP神经网络解决异或问题:从理论推导到Python实现

1次阅读
没有评论

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

image.webp

问题背景

异或(XOR)问题是神经网络发展史上的一个经典案例。它是一个简单的二元分类问题,输入是两个二进制值,输出是它们的异或结果。具体来说:

BP 神经网络解决异或问题:从理论推导到 Python 实现

  • 0 XOR 0 = 0
  • 0 XOR 1 = 1
  • 1 XOR 0 = 1
  • 1 XOR 1 = 0

这个看似简单的问题却难倒了早期的单层感知机。单层感知机本质上是一个线性分类器,而异或问题是一个非线性可分问题——你无法用一条直线将 (0,1) 和(1,0)归为一类,(0,0)和 (1,1) 归为另一类。这就是为什么我们需要多层感知机(MLP)和反向传播算法(BP)来解决这类问题。

技术方案

网络结构设计

对于异或问题,一个 2 -2- 1 的三层网络结构就足够了:

  • 输入层:2 个神经元(对应两个输入)
  • 隐藏层:2 个神经元(这是关键,使网络具备非线性能力)
  • 输出层:1 个神经元(输出 0 或 1)

激活函数选择

我们选择 Sigmoid 函数作为激活函数,原因如下:

  1. 它的输出范围在 (0,1) 之间,适合作为概率输出
  2. 它是可微的,这对反向传播至关重要
  3. 它引入了非线性,使网络能够学习复杂的模式

Sigmoid 函数的表达式为:

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

其导数为:

$$
\sigma'(z) = \sigma(z)(1 – \sigma(z))
$$

数学推导

前向传播

对于输入 x =(x₁, x₂),前向传播过程如下:

  1. 隐藏层输入:
    $$
    z_1^{(2)} = w_{11}^{(1)}x_1 + w_{21}^{(1)}x_2 + b_1^{(1)}
    $$
    $$
    z_2^{(2)} = w_{12}^{(1)}x_1 + w_{22}^{(1)}x_2 + b_2^{(1)}
    $$

  2. 隐藏层输出(应用 Sigmoid 激活):
    $$
    a_1^{(2)} = \sigma(z_1^{(2)})
    $$
    $$
    a_2^{(2)} = \sigma(z_2^{(2)})
    $$

  3. 输出层输入:
    $$
    z_1^{(3)} = w_{11}^{(2)}a_1^{(2)} + w_{21}^{(2)}a_2^{(2)} + b_1^{(2)}
    $$

  4. 最终输出:
    $$
    \hat{y} = a_1^{(3)} = \sigma(z_1^{(3)})
    $$

反向传播

我们使用均方误差作为损失函数:

$$
J = \frac{1}{2}(y – \hat{y})^2
$$

反向传播的梯度计算如下:

  1. 输出层误差:
    $$
    \delta_1^{(3)} = (\hat{y} – y) \cdot \sigma'(z_1^{(3)})
    $$

  2. 隐藏层误差:
    $$
    \delta_1^{(2)} = w_{11}^{(2)}\delta_1^{(3)} \cdot \sigma'(z_1^{(2)})
    $$
    $$
    \delta_2^{(2)} = w_{21}^{(2)}\delta_1^{(3)} \cdot \sigma'(z_2^{(2)})
    $$

  3. 权重更新(η 为学习率):
    $$
    w_{ij}^{(l)} = w_{ij}^{(l)} – \eta \cdot \delta_j^{(l+1)} \cdot a_i^{(l)}
    $$
    $$
    b_j^{(l)} = b_j^{(l)} – \eta \cdot \delta_j^{(l+1)}
    $$

Python 实现

以下是使用 NumPy 实现的完整代码:

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)

# 输入数据 (X) 和标签 (y)
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([[0], [1], [1], [0]])

# 网络参数
input_size = 2
hidden_size = 2
output_size = 1

# 随机初始化权重
np.random.seed(42)
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))

# 训练参数
learning_rate = 0.5
epochs = 10000
loss_history = []

# 训练过程
for epoch in range(epochs):
    # 前向传播
    hidden_input = np.dot(X, W1) + b1
    hidden_output = sigmoid(hidden_input)
    output_input = np.dot(hidden_output, W2) + b2
    predicted_output = sigmoid(output_input)

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

    # 反向传播
    output_error = predicted_output - y
    output_delta = output_error * sigmoid_derivative(predicted_output)

    hidden_error = output_delta.dot(W2.T)
    hidden_delta = hidden_error * sigmoid_derivative(hidden_output)

    # 更新权重
    W2 -= learning_rate * hidden_output.T.dot(output_delta)
    b2 -= learning_rate * np.sum(output_delta, axis=0, keepdims=True)
    W1 -= learning_rate * X.T.dot(hidden_delta)
    b1 -= learning_rate * np.sum(hidden_delta, axis=0, keepdims=True)

# 绘制损失曲线
plt.plot(loss_history)
plt.title('Training Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.show()

# 测试预测
print("Final predictions:")
print(predicted_output)

调参技巧

  1. 学习率:太大会导致震荡,太小会收敛缓慢。建议从 0.1 开始尝试
  2. 迭代次数:观察损失曲线,当损失不再明显下降时即可停止
  3. 隐藏层大小:对于简单问题如异或,2- 3 个神经元足够
  4. 权重初始化:使用小随机数打破对称性(如从标准正态分布采样)

扩展讨论

这个方法可以推广到其他非线性分类问题:

  1. 对于更复杂的问题,可以增加隐藏层数量(深度网络)
  2. 对于多分类问题,输出层可以使用 Softmax 激活函数
  3. 可以使用不同的激活函数(如 ReLU)提高训练效率
  4. 引入正则化技术(如 L2 正则化)防止过拟合

思考题

如何改进网络结构以提高训练效率?一些可能的思路:

  1. 尝试不同的激活函数(如 ReLU)
  2. 引入批量归一化(Batch Normalization)
  3. 使用更先进的优化器(如 Adam)
  4. 调整网络深度和宽度,找到最佳平衡点

通过这个简单的异或问题,我们深入理解了 BP 神经网络的工作原理。虽然问题本身很简单,但涉及的原理和技术可以扩展到更复杂的现实世界问题中。

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