共计 2951 个字符,预计需要花费 8 分钟才能阅读完成。
问题背景
异或(XOR)问题是神经网络发展史上的一个经典案例。它是一个简单的二元分类问题,输入是两个二进制值,输出是它们的异或结果。具体来说:

- 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 函数作为激活函数,原因如下:
- 它的输出范围在 (0,1) 之间,适合作为概率输出
- 它是可微的,这对反向传播至关重要
- 它引入了非线性,使网络能够学习复杂的模式
Sigmoid 函数的表达式为:
$$
\sigma(z) = \frac{1}{1 + e^{-z}}
$$
其导数为:
$$
\sigma'(z) = \sigma(z)(1 – \sigma(z))
$$
数学推导
前向传播
对于输入 x =(x₁, x₂),前向传播过程如下:
-
隐藏层输入:
$$
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)}
$$ -
隐藏层输出(应用 Sigmoid 激活):
$$
a_1^{(2)} = \sigma(z_1^{(2)})
$$
$$
a_2^{(2)} = \sigma(z_2^{(2)})
$$ -
输出层输入:
$$
z_1^{(3)} = w_{11}^{(2)}a_1^{(2)} + w_{21}^{(2)}a_2^{(2)} + b_1^{(2)}
$$ -
最终输出:
$$
\hat{y} = a_1^{(3)} = \sigma(z_1^{(3)})
$$
反向传播
我们使用均方误差作为损失函数:
$$
J = \frac{1}{2}(y – \hat{y})^2
$$
反向传播的梯度计算如下:
-
输出层误差:
$$
\delta_1^{(3)} = (\hat{y} – y) \cdot \sigma'(z_1^{(3)})
$$ -
隐藏层误差:
$$
\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)})
$$ -
权重更新(η 为学习率):
$$
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)
调参技巧
- 学习率:太大会导致震荡,太小会收敛缓慢。建议从 0.1 开始尝试
- 迭代次数:观察损失曲线,当损失不再明显下降时即可停止
- 隐藏层大小:对于简单问题如异或,2- 3 个神经元足够
- 权重初始化:使用小随机数打破对称性(如从标准正态分布采样)
扩展讨论
这个方法可以推广到其他非线性分类问题:
- 对于更复杂的问题,可以增加隐藏层数量(深度网络)
- 对于多分类问题,输出层可以使用 Softmax 激活函数
- 可以使用不同的激活函数(如 ReLU)提高训练效率
- 引入正则化技术(如 L2 正则化)防止过拟合
思考题
如何改进网络结构以提高训练效率?一些可能的思路:
- 尝试不同的激活函数(如 ReLU)
- 引入批量归一化(Batch Normalization)
- 使用更先进的优化器(如 Adam)
- 调整网络深度和宽度,找到最佳平衡点
通过这个简单的异或问题,我们深入理解了 BP 神经网络的工作原理。虽然问题本身很简单,但涉及的原理和技术可以扩展到更复杂的现实世界问题中。
