共计 2778 个字符,预计需要花费 7 分钟才能阅读完成。
为什么需要 BP 神经网络?
在开始理论推导前,先看两个典型场景:

- 手写数字识别 :银行支票上的手写金额识别,本质是将 28×28 像素的图片分类为 0 - 9 的数字。传统算法难以处理这种非线性关系
- 商品销量预测 :基于历史销量、季节、促销活动等数十个因素预测下月销量,变量间存在复杂交互作用
这类问题的共性是:输入与输出间存在多层非线性关系,这正是 BP(Backpropagation)神经网络的用武之地。
数学原理三步走
1. 前向传播的矩阵表示
假设三层网络结构(输入层 2 节点,隐藏层 3 节点,输出层 1 节点):
# 权重矩阵维度说明
W1 = np.random.randn(2, 3) # 输入层→隐藏层
W2 = np.random.randn(3, 1) # 隐藏层→输出层
前向传播公式:
- 隐藏层输入:$Z_1 = X W_1 + b_1$
- 隐藏层输出:$A_1 = \sigma(Z_1)$(σ 为激活函数)
- 输出层结果:$\hat{Y} = \sigma(Z_2) = \sigma(A_1 W_2 + b_2)$
2. 损失函数选择
分类问题常用交叉熵损失(Cross-Entropy):
$$ L = -\frac{1}{N}\sum_{i=1}^N [y_i\ln(\hat{y}_i) + (1-y_i)\ln(1-\hat{y}_i)] $$
回归问题可用均方误差(MSE)。前者能更好处理概率输出。
3. 反向传播推导(核心)
关键是通过链式法则计算权重梯度:
-
输出层梯度:
$$ \frac{\partial L}{\partial W_2} = \frac{\partial L}{\partial \hat{Y}} \cdot \frac{\partial \hat{Y}}{\partial Z_2} \cdot \frac{\partial Z_2}{\partial W_2} = (\hat{Y}-Y) \cdot A_1^T $$ -
隐藏层梯度:
$$ \frac{\partial L}{\partial W_1} = (\hat{Y}-Y) W_2^T \odot \sigma'(Z_1) \cdot X^T $$
(⊙表示逐元素乘法)
最终权重更新公式:
$$ W \leftarrow W – \eta \cdot \frac{\partial L}{\partial W} $$
(η 为学习率)
Python 实战实现
网络结构定义
import numpy as np
class NeuralNetwork:
def __init__(self, input_size, hidden_size, output_size):
self.W1 = np.random.randn(input_size, hidden_size) * 0.01
self.b1 = np.zeros((1, hidden_size))
self.W2 = np.random.randn(hidden_size, output_size) * 0.01
self.b2 = np.zeros((1, output_size))
Sigmoid 激活函数
def sigmoid(self, x):
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(self, x):
return x * (1 - x) # 注意此处 x 应为 sigmoid 输出值
训练过程
def train(self, X, y, epochs=1000, lr=0.1):
for epoch in range(epochs):
# 前向传播
Z1 = np.dot(X, self.W1) + self.b1
A1 = self.sigmoid(Z1)
Z2 = np.dot(A1, self.W2) + self.b2
y_pred = self.sigmoid(Z2)
# 计算损失
loss = -np.mean(y * np.log(y_pred) + (1-y) * np.log(1-y_pred))
# 反向传播
dy_pred = (y_pred - y) / (y_pred * (1 - y_pred + 1e-9)) # 防止除零
dW2 = np.dot(A1.T, dy_pred * self.sigmoid_derivative(y_pred))
db2 = np.sum(dy_pred * self.sigmoid_derivative(y_pred), axis=0)
dhidden = np.dot(dy_pred * self.sigmoid_derivative(y_pred), self.W2.T)
dW1 = np.dot(X.T, dhidden * self.sigmoid_derivative(A1))
db1 = np.sum(dhidden * self.sigmoid_derivative(A1), axis=0)
# 更新权重
self.W1 -= lr * dW1
self.b1 -= lr * db1
self.W2 -= lr * dW2
self.b2 -= lr * db2
MNIST 实战测试
加载数据并训练:
from sklearn.datasets import fetch_openml
from sklearn.preprocessing import MinMaxScaler
mnist = fetch_openml('mnist_784')
X = mnist.data.astype('float32') / 255.0
y = pd.get_dummies(mnist.target).values # one-hot 编码
# 划分训练测试集
X_train, X_test = X[:60000], X[60000:]
y_train, y_test = y[:60000], y[60000:]
# 创建网络
nn = NeuralNetwork(784, 128, 10) # 输入 784 像素,隐藏层 128 节点,输出 10 类
nn.train(X_train, y_train, epochs=50, lr=0.1)
常见问题及解决方案
梯度消失问题
现象:深层网络训练时,前面层的权重更新幅度极小
解决方案:
- 使用 ReLU 激活函数替代 Sigmoid
- 采用 Batch Normalization
- 残差连接(ResNet 思路)
过拟合应对
识别方法:训练集准确率持续上升但测试集开始下降
应对策略:
- 添加 L2 正则化项
- 使用 Dropout 随机屏蔽神经元
- 早停法(Early Stopping)
延伸思考
- 与 CNN/RNN 对比 :
- CNN 通过卷积核提取空间特征,适合图像
- RNN 具有记忆单元,适合序列数据
-
全连接 BP 网络更适合结构化数据
-
Mini-Batch 改造 :
将训练数据分批次(如 batch_size=64):for epoch in range(epochs): for i in range(0, len(X), batch_size): X_batch = X[i:i+batch_size] y_batch = y[i:i+batch_size] # 在该 batch 上执行前向 / 反向传播
通过这个实现,你会发现神经网络不再神秘。建议尝试调整隐藏层节点数、更换激活函数,观察对模型性能的影响,这是理解深度学习的最佳途径。
