共计 2984 个字符,预计需要花费 8 分钟才能阅读完成。
核心概念:BP 神经网络如何工作
BP 神经网络的核心是前向传播和反向传播两个过程。前向传播计算预测值,反向传播根据误差调整参数。以下是关键公式:

前向传播 (以单隐层为例):
$$
\begin{aligned}
z^{[1]} &= W^{[1]}X + b^{[1]} \
a^{[1]} &= g(z^{[1]}) \
z^{[2]} &= W^{[2]}a^{[1]} + b^{[2]} \
a^{[2]} &= \sigma(z^{[2]})
\end{aligned}
$$
反向传播 (链式求导):
$$
\begin{aligned}
dz^{[2]} &= a^{[2]} – y \
dW^{[2]} &= \frac{1}{m} dz^{[2]} a^{[1]T} \
db^{[2]} &= \frac{1}{m} \sum dz^{[2]} \
dz^{[1]} &= W^{[2]T} dz^{[2]} \odot g'(z^{[1]}) \
dW^{[1]} &= \frac{1}{m} dz^{[1]} X^T \
db^{[1]} &= \frac{1}{m} \sum dz^{[1]}
\end{aligned}
$$
激活函数的选择直接影响梯度传播:
- Sigmoid 容易导致梯度消失(导数最大仅 0.25)
- ReLU($\max(0,x)$)能缓解梯度消失,但可能出现神经元死亡
新手常见痛点分析
以下是初学者常踩的坑:
-
参数初始化 :全零初始化会导致对称性问题,推荐使用 Xavier 初始化:
W = np.random.randn(fan_in, fan_out) * np.sqrt(1/fan_in) -
学习率设置 :
- 过大(>0.1):损失震荡不收敛
-
过小(<1e-5):训练速度极慢
-
梯度消失 :使用 Sigmoid 的深层网络,前几层参数几乎不更新
代码实现:从零搭建 BP 网络
# Python 3.8+, 需要 numpy>=1.20 和 matplotlib
import numpy as np
class NeuralNetwork:
def __init__(self, input_size, hidden_size, output_size):
# Xavier 初始化
self.W1 = np.random.randn(input_size, hidden_size) * np.sqrt(1/input_size)
self.b1 = np.zeros(hidden_size)
self.W2 = np.random.randn(hidden_size, output_size) * np.sqrt(1/hidden_size)
self.b2 = np.zeros(output_size)
def relu(self, x):
return np.maximum(0, x)
def softmax(self, x):
exps = np.exp(x - np.max(x))
return exps / np.sum(exps, axis=1, keepdims=True)
def forward(self, X):
self.z1 = np.dot(X, self.W1) + self.b1
self.a1 = self.relu(self.z1)
self.z2 = np.dot(self.a1, self.W2) + self.b2
return self.softmax(self.z2)
def backward(self, X, y, output, l2_lambda=0.01):
m = X.shape[0]
# 输出层误差
dz2 = output - y
dW2 = (1/m) * np.dot(self.a1.T, dz2) + (l2_lambda/m)*self.W2
db2 = (1/m) * np.sum(dz2, axis=0)
# 隐层误差
dz1 = np.dot(dz2, self.W2.T) * (self.z1 > 0) # ReLU 导数
dW1 = (1/m) * np.dot(X.T, dz1) + (l2_lambda/m)*self.W1
db1 = (1/m) * np.sum(dz1, axis=0)
return dW1, db1, dW2, db2
MNIST 实战:手写数字识别
数据预处理
from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', version=1)
# 归一化到 [0,1]
X = mnist.data / 255.0
y = mnist.target.astype(np.int8)
# One-hot 编码
y_onehot = np.eye(10)[y]
# 划分训练测试集
X_train, X_test = X[:60000], X[60000:]
y_train, y_test = y_onehot[:60000], y_onehot[60000:]
训练过程可视化
import matplotlib.pyplot as plt
# 训练循环
losses = []
accuracies = []
for epoch in range(100):
# 前向传播
output = model.forward(X_train)
# 计算损失(带 L2 正则化)data_loss = -np.mean(y_train * np.log(output + 1e-10))
reg_loss = 0.5 * l2_lambda * (np.sum(model.W1**2) + np.sum(model.W2**2)) / len(y_train)
loss = data_loss + reg_loss
losses.append(loss)
# 计算准确率
preds = np.argmax(output, axis=1)
truth = np.argmax(y_train, axis=1)
acc = np.mean(preds == truth)
accuracies.append(acc)
# 反向传播
dW1, db1, dW2, db2 = model.backward(X_train, y_train, output)
# 参数更新(带动量)beta = 0.9
model.vW1 = beta * model.vW1 + (1-beta) * dW1
model.W1 -= learning_rate * model.vW1
# 其他参数更新类似...
# 绘制曲线
plt.figure(figsize=(12,4))
plt.subplot(121)
plt.plot(losses)
plt.title('Training Loss')
plt.subplot(122)
plt.plot(accuracies)
plt.title('Training Accuracy')
生产环境调优建议
- 超参数搜索 :
- 使用网格搜索或随机搜索确定最佳学习率(建议范围 1e- 4 到 1e-2)
-
隐藏层节点数通常在 50-500 之间
-
Early Stopping:
if len(val_loss) > 10 and np.mean(val_loss[-10:]) >= np.mean(val_loss[-20:-10]): print('Early stopping triggered') break -
GPU 加速 :
- 将 NumPy 数组转换为 CuPy 数组
- 批处理大小设为 2 的幂次(如 64,128,256)
延伸思考
读者可以尝试以下改进:
- 添加 Dropout 层防止过拟合
- 使用交叉熵损失替代 MSE
- 实现 Adam 优化器替代 SGD
- 添加 Batch Normalization 层
完整代码已上传 GitHub(虚构链接):github.com/username/bpnn-mnist
