共计 2783 个字符,预计需要花费 7 分钟才能阅读完成。
问题场景:为什么选择异或问题
异或(XOR)是一个经典的线性不可分问题,简单但能充分展示神经网络的威力。假设我们有以下四个数据点:

- (0, 0) → 类别 0
- (0, 1) → 类别 1
- (1, 0) → 类别 1
- (1, 1) → 类别 0
用单层感知机无法正确分类,但带有一个隐藏层的 BP 神经网络可以解决这个问题。接下来我们会用这个例子贯穿全文。
数学推导:前向与反向传播
网络结构定义
我们构建一个 2 -2- 1 的三层网络:
– 输入层:2 个神经元(对应 x1,x2)
– 隐藏层:2 个神经元
– 输出层:1 个神经元
前向传播公式
-
输入层到隐藏层:
$$\mathbf{z}^{(1)} = W^{(1)}\mathbf{x} + \mathbf{b}^{(1)}$$
$$\mathbf{a}^{(1)} = \sigma(\mathbf{z}^{(1)})$$ -
隐藏层到输出层:
$$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$
-
输出层误差:
$$\delta^{(2)} = (a^{(2)} – y) \cdot \sigma'(z^{(2)})$$ -
隐藏层误差:
$$\delta^{(1)} = (W^{(2)})^T \delta^{(2)} \odot \sigma'(\mathbf{z}^{(1)})$$ -
参数梯度:
$$\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):可能导致震荡无法收敛
- 太小(如 <0.01):训练速度过慢
-
推荐范围:0.1-0.5,可以通过观察损失曲线调整
-
隐藏层神经元数量 :
- 异或问题:2- 4 个足够
- 复杂问题:可以从输入层大小的 1 / 2 到 2 倍开始尝试
-
太多会导致过拟合,表现为训练误差很小但测试误差大
-
梯度检查 :
- 用数值方法近似计算梯度,与反向传播结果对比
- 实现示例:
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 对比
扩展思考
- 与其他算法对比 :
- SVM:需要合适的核函数才能解决非线性问题
- 决策树:可以处理但可能产生复杂的决策边界
-
BP 网络:天然适合非线性问题,但需要调参
-
PyTorch 重构 :
- 自动微分省去手动推导
- 优化器内置多种学习率策略
- 示例代码片段:
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 神经网络如何处理非线性分类。虽然现代深度学习框架已经帮我们封装了大部分细节,但理解底层的数学原理对于调试网络和解决实际问题非常重要。建议初学者:
- 先在小数据集上手动实现,再过渡到框架
- 可视化是理解网络行为的有力工具
- 梯度检查能确保反向传播的正确性
- 调参时保持耐心,记录每次实验的超参数和结果
