共计 3107 个字符,预计需要花费 8 分钟才能阅读完成。
背景:为什么选择 BP 神经网络
BP 神经网络(Backpropagation Neural Network)是解决分类问题的利器,尤其适合处理复杂的非线性边界。与 SVM、决策树相比,它的优势在于:

- 自动学习特征组合,无需手动设计特征
- 通过增加隐藏层可以逼近任意连续函数
- 对高维数据表现稳定(比如图像分类)
典型应用场景包括:
– 银行信用评分(二分类)
– 手写数字识别(多分类)
– 医疗影像分析
数学原理:反向传播到底在传播什么
核心是链式求导法则。假设网络有 1 个隐藏层,输出层使用 sigmoid 激活函数:
-
前向传播:
$$
\begin{aligned}
z^{[1]} &= W^{[1]}X + b^{[1]} \
a^{[1]} &= \text{ReLU}(z^{[1]}) \
z^{[2]} &= W^{[2]}a^{[1]} + b^{[2]} \
\hat{y} &= \sigma(z^{[2]})
\end{aligned}
$$ -
损失函数(交叉熵):
$$ L = -\frac{1}{m}\sum_{i=1}^m [y^{(i)}\log(\hat{y}^{(i)}) + (1-y^{(i)})\log(1-\hat{y}^{(i)})] $$ -
反向传播 关键导数:
$$
\begin{aligned}
dz^{[2]} &= \hat{y} – 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 \text{ReLU}'(z^{[1]})
\end{aligned}
$$
Python 实现:手把手搭建网络
import numpy as np
from sklearn.datasets import make_moons
# 数据准备
X, y = make_moons(n_samples=1000, noise=0.2)
y = y.reshape(-1, 1)
class BPNet:
def __init__(self, layer_dims):
self.params = {}
for l in range(1, len(layer_dims)):
# He 初始化应对 ReLU
self.params[f'W{l}'] = np.random.randn(layer_dims[l], layer_dims[l-1]) * np.sqrt(2/layer_dims[l-1])
self.params[f'b{l}'] = np.zeros((layer_dims[l], 1))
def forward(self, X):
cache = {'A0': X}
L = len(self.params) // 2
for l in range(1, L):
cache[f'Z{l}'] = np.dot(self.params[f'W{l}'], cache[f'A{l-1}']) + self.params[f'b{l}']
cache[f'A{l}'] = np.maximum(0, cache[f'Z{l}']) # ReLU
# 输出层用 sigmoid
cache[f'Z{L}'] = np.dot(self.params[f'W{L}'], cache[f'A{L-1}']) + self.params[f'b{L}']
cache[f'A{L}'] = 1/(1+np.exp(-cache[f'Z{L}']))
return cache
def compute_loss(self, AL, y, lambd=0.1):
m = y.shape[1]
# 交叉熵损失 + L2 正则化
cross_entropy = -np.mean(y*np.log(AL) + (1-y)*np.log(1-AL))
l2_penalty = sum(np.sum(np.square(W)) for W in self.params.values() if W.ndim > 1)
return cross_entropy + (lambd/(2*m)) * l2_penalty
def backward(self, cache, y, lambd=0.1):
grads = {}
m = y.shape[1]
L = len(self.params) // 2
# 输出层梯度
dZL = cache[f'A{L}'] - y
grads[f'dW{L}'] = np.dot(dZL, cache[f'A{L-1}'].T)/m + (lambd/m)*self.params[f'W{L}']
grads[f'db{L}'] = np.sum(dZL, axis=1, keepdims=True)/m
# 隐藏层梯度
for l in reversed(range(1, L)):
dAl = np.dot(self.params[f'W{l+1}'].T, dZL)
dZl = dAl * (cache[f'Z{l}'] > 0) # ReLU 导数
grads[f'dW{l}'] = np.dot(dZl, cache[f'A{l-1}'].T)/m + (lambd/m)*self.params[f'W{l}']
grads[f'db{l}'] = np.sum(dZl, axis=1, keepdims=True)/m
dZL = dZl
return grads
def update_params(self, grads, learning_rate):
for key in self.params:
self.params[key] -= learning_rate * grads[f'd{key}']
# 训练示例
net = BPNet([2, 4, 1]) # 输入层 2 维,隐藏层 4 神经元,输出 1 维
for epoch in range(1000):
cache = net.forward(X.T)
loss = net.compute_loss(cache['A2'], y.T)
grads = net.backward(cache, y.T)
net.update_params(grads, learning_rate=0.01*(1-epoch/1000)) # 学习率衰减
调优实战技巧
- 隐藏层设计:
- 通常从
[输入维度, 输出维度]之间的数值开始尝试 -
示例:对于 MNIST 分类(784 输入,10 输出),可先试
[784, 256, 10] -
激活函数对比:
# Sigmoid vs ReLU 实验 sigmoid_loss = [] relu_loss = [] for _ in range(10): # sigmoid 版本会梯度消失 sigmoid_loss.append(train_with_activation('sigmoid')) relu_loss.append(train_with_activation('relu')) -
早停法实现:
best_loss = float('inf') patience = 5 for epoch in range(100): # ... 训练代码... if loss < best_loss: best_loss = loss counter = 0 else: counter += 1 if counter >= patience: break
扩展思考
- 与传统算法对比:
- 决策树:训练更快,但难以处理细微特征差异
-
SVM:在小数据集上可能更优,但核函数选择困难
-
批归一化改进:
# 在前向传播中加入 Z = np.dot(W, A_prev) + b Z_norm = (Z - np.mean(Z))/np.std(Z) Z_tilde = gamma * Z_norm + beta # 可学习参数
结语
通过这次实践,我深刻体会到 BP 神经网络就像 ” 可编程的数学函数 ”——通过调整网络结构,它能自动学习到最适合当前任务的变换方式。建议初学者多尝试可视化权重矩阵(如plt.imshow(W1)),这能直观理解网络学到了什么特征。
