共计 2288 个字符,预计需要花费 6 分钟才能阅读完成。
1. 背景与痛点
分类任务是机器学习中的经典问题,但在实际应用中常遇到几个挑战:

- 数据不平衡 :某些类别样本量过少,导致模型偏向多数类
- 特征选择 :无关特征会干扰模型学习,有效特征组合能显著提升性能
- 模型过拟合 :在训练集表现良好但测试集差,泛化能力不足
鸢尾花数据集(含 setosa/versicolor/virginica 三类)是理想的入门案例,其特征明确(花萼 / 花瓣长宽)、样本均衡(每类 50 条),适合演示 BP 神经网络的基础实现。
2. 技术对比
不同分类算法各有优劣:
- SVM:适合小样本、高维数据,但对大规模数据计算成本高
- 决策树 :可解释性强,但容易过拟合
- BP 神经网络 :能自动学习特征组合,适合复杂非线性关系,但需要调参经验
3. 核心实现
3.1 数据预处理
import numpy as np
from sklearn.datasets import load_iris
# 加载数据
iris = load_iris()
X = iris.data # 特征 (150,4)
y = iris.target # 标签 (150,)
# 标准化(均值 0,标准差 1)X = (X - np.mean(X, axis=0)) / np.std(X, axis=0)
# one-hot 编码
def one_hot(y):
n_classes = len(np.unique(y))
return np.eye(n_classes)[y]
y_onehot = one_hot(y) # (150,3)
3.2 BP 神经网络数学原理
关键公式:
-
前向传播 :
$$z^l = W^l a^{l-1} + b^l$$
$$a^l = \sigma(z^l)$$ -
损失函数(交叉熵):
$$L = -\frac{1}{N}\sum_{i=1}^N \sum_{j=1}^C y_{ij}\log(a_{ij}^L)$$ -
反向传播 :
- 输出层误差:
$$\delta^L = a^L – y$$ - 隐藏层误差:
$$\delta^l = (W^{l+1})^T \delta^{l+1} \odot \sigma'(z^l)$$ - 参数梯度:
$$\frac{\partial L}{\partial W^l} = \delta^l (a^{l-1})^T$$
$$\frac{\partial L}{\partial b^l} = \delta^l$$
4. 完整代码实现
class BPNetwork:
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))
def sigmoid(self, x):
return 1 / (1 + np.exp(-x))
def forward(self, X):
self.z1 = np.dot(X, self.W1) + self.b1
self.a1 = self.sigmoid(self.z1)
self.z2 = np.dot(self.a1, self.W2) + self.b2
exp_z = np.exp(self.z2 - np.max(self.z2, axis=1, keepdims=True))
self.a2 = exp_z / np.sum(exp_z, axis=1, keepdims=True)
return self.a2
def backward(self, X, y, lr=0.01):
m = X.shape[0]
# 输出层误差
dz2 = self.a2 - y
dw2 = np.dot(self.a1.T, dz2) / m
db2 = np.sum(dz2, axis=0, keepdims=True) / m
# 隐藏层误差
dz1 = np.dot(dz2, self.W2.T) * self.a1 * (1 - self.a1)
dw1 = np.dot(X.T, dz1) / m
db1 = np.sum(dz1, axis=0, keepdims=True) / m
# 更新参数
self.W2 -= lr * dw2
self.b2 -= lr * db2
self.W1 -= lr * dw1
self.b1 -= lr * db1
def train(self, X, y, epochs=1000, lr=0.01):
losses = []
for i in range(epochs):
output = self.forward(X)
loss = -np.mean(y * np.log(output + 1e-8))
losses.append(loss)
self.backward(X, y, lr)
if i % 100 == 0:
print(f'Epoch {i}, Loss: {loss:.4f}')
return losses
5. 调优指南
关键超参数设置 :
- 学习率:建议从 0.01 开始尝试,过大导致震荡,过小收敛慢
- 隐藏层节点数:通常取输入特征的 2 - 3 倍(本例选择 8 个)
- 迭代次数:观察损失曲线,当下降平缓时停止
防梯度消失技巧 :
- 使用 Xavier 初始化权重
- 添加 Batch Normalization 层
- 改用 ReLU 激活函数
6. 常见错误排查
- 损失不下降 :检查学习率是否过小 / 数据未归一化
- 输出全零 :可能是初始化权重过大导致梯度消失
- 准确率波动大 :减小学习率或增加批量大小
7. 扩展思考
可以尝试:
1. 增加隐藏层数量(深度网络)
2. 更换激活函数(如 ReLU)
3. 应用于手写数字识别等其它数据集
通过这个案例,我们实现了从理论到实践的完整闭环。建议读者动手调整参数观察效果,这是理解神经网络行为的最佳方式。
正文完
