共计 2252 个字符,预计需要花费 6 分钟才能阅读完成。
问题描述与网络结构
我们以一个简单的二分类问题为例:给定二维平面上的点,判断它们属于哪个类别(0 或 1)。为了演示 BP 神经网络的反向传播过程,我们设计一个包含 1 个隐藏层的网络结构:

- 输入层:2 个神经元(对应 x / y 坐标)
- 隐藏层:3 个神经元,使用 Sigmoid 激活函数
- 输出层:1 个神经元,使用 Sigmoid 激活函数
数学符号定义:
– $W^{(1)}$: 输入层到隐藏层的权重矩阵(3×2)
– $b^{(1)}$: 隐藏层偏置向量(3×1)
– $W^{(2)}$: 隐藏层到输出层的权重矩阵(1×3)
– $b^{(2)}$: 输出层偏置(标量)
前向传播实现
前向传播分为三个步骤:
-
计算隐藏层输入:
$$z^{(1)} = W^{(1)}X + b^{(1)}$$ -
计算隐藏层输出(应用 Sigmoid):
$$a^{(1)} = \sigma(z^{(1)}) = \frac{1}{1+e^{-z^{(1)}}}$$ -
计算最终输出:
$$z^{(2)} = W^{(2)}a^{(1)} + b^{(2)}$$
$$a^{(2)} = \sigma(z^{(2)})$$
损失函数与反向传播推导
采用交叉熵损失函数:
$$L = -[y\log(a^{(2)}) + (1-y)\log(1-a^{(2)})]$$
反向传播使用链式法则计算梯度:
-
输出层梯度:
$$\frac{\partial L}{\partial z^{(2)}} = a^{(2)} – y$$ -
隐藏层梯度:
$$\frac{\partial L}{\partial z^{(1)}} = (W^{(2)})^T \cdot \frac{\partial L}{\partial z^{(2)}} \odot \sigma'(z^{(1)})$$ -
权重更新公式:
$$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)
实验结果分析
- 学习率选择:
- 过大(如 >0.5)会导致震荡甚至发散
- 过小(如 <0.01)会使训练速度极慢
-
建议使用学习率衰减策略
-
梯度消失问题:
- 深层网络中 Sigmoid 导数最大为 0.25,多次连乘导致梯度指数级减小
-
解决方案:改用 ReLU 激活函数、残差连接、批归一化等
-
激活函数比较:
- Sigmoid:输出范围 (0,1),适合二分类但易饱和
- Tanh:输出范围 (-1,1),中心对称但仍有饱和问题
- ReLU:计算简单,缓解梯度消失但可能导致神经元死亡
避坑指南
- 权重初始化:
- 避免全零初始化(导致对称性问题)
-
推荐 Xavier/Glorot 初始化
-
数值稳定性:
- 计算交叉熵时添加微小值防止 log(0)
-
使用稳定的 Softmax 实现
-
常见错误:
- 忘记转置矩阵(维度不匹配)
- 梯度计算符号错误
- 批量训练时未正确平均梯度
延伸思考
- 如何扩展到多分类问题?
- 如何实现 Mini-batch 训练?
- 尝试不同的优化器(Adam、RMSprop 等)
- 添加 L2 正则化防止过拟合
