共计 2615 个字符,预计需要花费 7 分钟才能阅读完成。
为什么选择 BP 神经网络?
相比逻辑回归等线性模型,BP 神经网络通过隐藏层和非线性激活函数能够自动学习特征间的高阶交互关系。对于 Iris 数据集这类特征边界复杂的分类任务,三层网络即可达到 98%+ 准确率(而逻辑回归通常卡在 92% 左右)。其核心优势在于:

- 自动特征组合:无需人工设计多项式特征
- 非线性决策边界:通过 Sigmoid/ReLU 激活函数实现
- 梯度驱动优化:反向传播自动调整权重
数据预处理实战
标准化处理
Iris 数据集包含萼片长度等 4 个量纲不同的特征,必须进行 Z -score 标准化:
from sklearn.datasets import load_iris
import numpy as np
iris = load_iris()
X, y = iris.data, iris.target
# 均值方差标准化
def standardize(X):
mean = np.mean(X, axis=0)
std = np.std(X, axis=0)
return (X - mean) / (std + 1e-8) # 防止除零
X_norm = standardize(X)
标签 One-hot 编码
将类别标签转为向量形式,便于计算交叉熵损失:
def one_hot(y, classes):
return np.eye(classes)[y]
y_onehot = one_hot(y, 3) # Iris 有 3 个类别
网络架构设计
层节点数选择
- 输入层 :4 个节点(对应 4 个特征)
- 隐藏层 :经验公式 $(输入 + 输出)/2$ 得出 6 节点(实际测试 8 节点效果更佳)
- 输出层 :3 个节点(Softmax 输出概率分布)
核心公式推导
前向传播
隐藏层输出:
$$h = \sigma(W_1 X + b_1)$$
输出层结果:
$$\hat{y} = \text{Softmax}(W_2 h + b_2)$$
损失函数(交叉熵)
$$L = -\frac{1}{N}\sum_{i=1}^N \sum_{j=1}^3 y_{ij}\log(\hat{y}_{ij})$$
反向传播(关键步骤)
输出层梯度:
$$\frac{\partial L}{\partial W_2} = (\hat{y} – y) \cdot h^T$$
隐藏层梯度:
$$\frac{\partial L}{\partial W_1} = (W_2^T(\hat{y} – y)) \odot \sigma'(z) \cdot X^T$$
其中 $\sigma’$ 为 Sigmoid 导数:
$$\sigma'(z) = \sigma(z)(1-\sigma(z))$$
Python 完整实现
class BPNet:
def __init__(self, input_size, hidden_size, output_size):
self.W1 = np.random.randn(input_size, hidden_size) * 0.01
self.b1 = np.zeros(hidden_size)
self.W2 = np.random.randn(hidden_size, output_size) * 0.01
self.b2 = np.zeros(output_size)
def sigmoid(self, z):
return 1 / (1 + np.exp(-z))
def forward(self, X):
self.z1 = np.dot(X, self.W1) + self.b1
self.h = self.sigmoid(self.z1)
self.z2 = np.dot(self.h, self.W2) + self.b2
# Softmax 输出
exp = np.exp(self.z2 - np.max(self.z2, axis=1, keepdims=True))
self.y_hat = exp / np.sum(exp, axis=1, keepdims=True)
return self.y_hat
def backward(self, X, y, lr=0.01):
m = X.shape[0]
# 输出层梯度
dy = self.y_hat - y
dW2 = np.dot(self.h.T, dy) / m
db2 = np.sum(dy, axis=0) / m
# 隐藏层梯度
dh = np.dot(dy, self.W2.T)
dz1 = dh * self.h * (1 - self.h) # Sigmoid 导数
dW1 = np.dot(X.T, dz1) / m
db1 = np.sum(dz1, axis=0) / m
# 参数更新
self.W2 -= lr * dW2
self.b2 -= lr * db2
self.W1 -= lr * dW1
self.b1 -= lr * db1
调优技巧详解
学习率对比实验
当学习率设为 0.1 时损失值震荡剧烈(橙色线),0.01 时稳定下降(蓝色线):
lr_list = [0.1, 0.01]
for lr in lr_list:
net = BPNet(4, 8, 3)
losses = []
for epoch in range(1000):
y_pred = net.forward(X_norm)
loss = -np.mean(y_onehot * np.log(y_pred + 1e-8))
net.backward(X_norm, y_onehot, lr)
losses.append(loss)
plt.plot(losses, label=f'lr={lr}')
激活函数对比
- ReLU:收敛更快但可能造成神经元死亡
- Sigmoid:梯度平滑但容易出现梯度消失
实验显示 ReLU 最终准确率高出约 2%,但需要更小的学习率(0.001)。
常见问题解决方案
梯度爆炸
现象:损失值突然变为 NaN
解决方法:
-
梯度裁剪
grad = np.clip(grad, -1, 1) -
权重初始化改用 Xavier 方法
self.W1 = np.random.randn(input_size, hidden_size) / np.sqrt(input_size)
类别不平衡
当某一类别样本极少时:
- 改用 F1-score 替代准确率
- 对损失函数添加类别权重
class_weight = [1.0, 2.0, 1.0] # 假设第二类样本少 loss = -np.mean(class_weight * y_onehot * np.log(y_pred))
延伸思考
- 多分类扩展:输出层节点数改为类别数,Softmax 改为 LogSoftmax+NLLLoss 组合
- BatchNorm 适用性:对于小批量数据(Iris 仅 150 条),BN 层可能引入噪声,建议隐藏层节点超过 20 时再考虑
通过本实践可掌握 BP 网络的核心实现逻辑,建议尝试调整隐藏层深度观察性能变化,这是理解神经网络表达能力的关键一步。
