摘要:不必先搭建复杂的深度学习工程,也可以完整体验神经网络建模。本文以鸢尾花分类为主线,介绍多层感知机的基本原理、数据划分、特征标准化、模型训练与评价,再扩展到回归任务,重点解释数据泄漏、收敛警告和损失曲线等容易被忽略的问题。
关键词:Python、scikit-learn、神经网络、MLP、分类、回归
一、MLP 学习的究竟是什么?
多层感知机(Multi-Layer Perceptron,MLP)是一类前馈神经网络,由输入层、隐藏层和输出层组成。输入层接收特征,隐藏层逐层变换特征,输出层给出预测。
一个神经元的计算可以写成:
h = \\phi\\left(\\sum_{j=1}\^{p}w_jx_j+b\\right)
其中,x_j 是输入特征,w_j 是权重,b 是偏置,\\phi 是激活函数。以 ReLU 为例,它的表达式是 \\phi(z)=\\max(0,z)。
激活函数为什么重要?如果所有层都只进行线性变换,多层叠加仍然等价于一次线性变换。引入非线性后,网络才能表达更复杂的特征关系。
训练时,模型先计算预测及损失,再通过反向传播求梯度,由优化器更新参数。重复这一过程,就能逐步调整输入与输出之间的映射。
scikit-learn 提供了 MLPClassifier 和 MLPRegressor,分别处理分类与回归。它们适合本文这样的入门实验;该实现不提供 GPU 支持,不宜直接当作大规模深度学习训练框架使用。官方神经网络指南
二、准备数据:先划分,再标准化
下面的代码按顺序执行,可放在同一个脚本或多个 Notebook 单元格中。首先导入需要的工具:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris, make_regression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neural_network import MLPClassifier, MLPRegressor
from sklearn.metrics import (
accuracy_score, classification_report,
confusion_matrix, mean_squared_error
)
鸢尾花数据集包含 150 个样本、4 个数值特征,以及 3 个类别。四项特征分别是花萼长度、花萼宽度、花瓣长度和花瓣宽度,任务是根据这些测量值预测品种。
data = load_iris()
X, y = data.data, data.target
class_names = data.target_names
print("特征矩阵:", X.shape)
print("类别名称:", class_names)
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
random_state=42,
stratify=y
)
print("训练样本数:", len(X_train))
print("测试样本数:", len(X_test))
特征矩阵: (150, 4)
类别名称: ['setosa' 'versicolor' 'virginica']
训练样本数: 120
测试样本数: 30
划分后,训练集为 120 个样本,测试集为 30 个样本。stratify=y 按标签分层抽样,使两个集合的类别比例与原数据保持一致;本例测试集每个类别都有 10 个样本。
固定 random_state 有助于复现实验,但一个固定划分并不代表结论足够稳定。正式比较模型时,还需要交叉验证或多次重复划分。
接下来进行标准化:
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
标准化按特征进行,基本形式为:
z_j = \\frac{x_j-\\mu_j}{\\sigma_j}
这里的均值和标准差必须从训练集估计。测试集只能使用训练集得到的参数,不能单独 fit_transform,也不能先对完整数据标准化再划分。
原因在于,测试集用于模拟尚未见过的数据。如果预处理提前利用它的分布信息,就发生了数据泄漏。神经网络对输入尺度较敏感,因此标准化既关系到训练效果,也关系到评价是否规范。
三、构建并训练分类网络
本例设置两个隐藏层,神经元数量分别为 10 和 5:
mlp_model = MLPClassifier(
hidden_layer_sizes=(10, 5),
activation="relu",
solver="adam",
max_iter=500,
random_state=42
)
mlp_model.fit(X_train_scaled, y_train)
print("最终训练损失:", mlp_model.loss_)
print("实际训练轮数:", mlp_model.n_iter_)
最终训练损失: 0.08618280978332134
实际训练轮数: 500
网络的层宽依次是 4、10、5、3。注意,hidden_layer_sizes 只写隐藏层,输入和输出维度由数据及任务决定。
| 参数 | 本例取值 | 含义 |
|---|---|---|
hidden_layer_sizes |
(10, 5) |
两个隐藏层,分别包含 10、5 个神经元 |
activation |
"relu" |
隐藏层使用 ReLU 激活函数 |
solver |
"adam" |
使用 Adam 优化参数 |
max_iter |
500 |
此优化器下最多训练 500 个 epoch |
random_state |
42 |
控制初始化等环节的随机性 |
对 Adam 而言,一个 epoch 表示训练集被完整遍历一次,并不等于一次小批量参数更新。默认情况下,训练也可能因满足停止条件而提前结束。MLPClassifier 参数说明
原始实验的最终损失约为 0.0862,训练轮数为 500,同时出现 ConvergenceWarning。这表示达到设定上限时,训练尚未满足相应的收敛停止条件,不代表程序运行失败;模型仍可用于预测,但需要继续检查训练状态。
处理时应先检查尺度、异常值和损失走势,再考虑增加迭代次数或调整学习率。不要仅为了隐藏警告而放宽停止条件,也不要反复查看测试集分数来调参。参数选择应在训练数据内部完成。
四、评价模型:准确率之外还要看什么?
y_pred_train = mlp_model.predict(X_train_scaled)
y_pred_test = mlp_model.predict(X_test_scaled)
print(f"训练准确率:{accuracy_score(y_train, y_pred_train):.4f}")
print(f"测试准确率:{accuracy_score(y_test, y_pred_test):.4f}")
print(classification_report(
y_test, y_pred_test,
target_names=class_names,
zero_division=0
))
cm = confusion_matrix(y_test, y_pred_test)
print("混淆矩阵:\n", cm)
训练准确率:0.9750
测试准确率:0.9667
precision recall f1-score support
setosa 1.00 1.00 1.00 10
versicolor 1.00 0.90 0.95 10
virginica 0.91 1.00 0.95 10
accuracy 0.97 30
macro avg 0.97 0.97 0.97 30
weighted avg 0.97 0.97 0.97 30
混淆矩阵:
[[10 0 0]
[ 0 9 1]
[ 0 0 10]]
原始实验记录的训练准确率为 0.9750 ,测试准确率为 0.9667。以下数值用于解读该次实验,不保证不同依赖版本与运行环境逐位一致。
测试准确率约为 96.67%,也就是 30 个样本中预测正确 29 个。看起来成绩不错,但只错一个与错两个之间就相差约 3.33 个百分点,因此小测试集上的高分不能直接推广到真实应用。
分类报告进一步回答"哪类预测更可靠":
|-----------|--------------------------|
| 指标 | 解释 |
| Precision | 被预测成某类的样本中,真正属于该类的比例 |
| Recall | 真正属于某类的样本中,被正确识别的比例 |
| F1-score | Precision 与 Recall 的调和平均 |
| Support | 测试集中该类的真实样本数 |
原始混淆矩阵如下,行表示真实类别,列表示预测类别:
[[10 0 0]
[ 0 9 1]
[ 0 0 10]]
三个类别按 class_names 排列。唯一的错误是一个 versicolor 样本被预测为 virginica。因此,versicolor 的召回率为 9/10=0.90;virginica 的精确率为 10/11\\approx0.91。
这说明准确率只是总体概况,混淆矩阵才能定位错误方向。训练与测试分数接近,也只能说明本次划分未呈现明显的大幅性能落差,不能据此断言不存在过拟合。
五、查看损失曲线,理解训练过程
fig, ax = plt.subplots(figsize=(8, 4))
epochs = np.arange(1, len(mlp_model.loss_curve_) + 1)
ax.plot(epochs, mlp_model.loss_curve_, label="Training loss")
ax.set_xlabel("Epoch")
ax.set_ylabel("Loss")
ax.set_title("MLP Training Loss")
ax.grid(alpha=0.3)
ax.legend()
fig.tight_layout()
plt.show()

如果曲线持续下降,说明训练目标仍在改善;如果大幅震荡,可以检查学习率和输入数据;如果较早进入平台,则需结合任务表现判断模型是否已经学到有效规律。
特别注意:这条曲线记录的是训练损失,不是测试准确率。训练损失不断降低,并不意味着泛化能力不断提高。若需要判断过拟合,还应观察独立验证数据上的表现。
六、扩展到回归:预测连续数值
分类预测离散标签,回归预测连续数值。下面构造一个包含 300 个样本、5 个特征的模拟回归数据集,沿用相同的预处理流程:
X_reg, y_reg = make_regression(
n_samples=300,
n_features=5,
noise=10,
random_state=42
)
Xr_train, Xr_test, yr_train, yr_test = train_test_split(
X_reg, y_reg, test_size=0.2, random_state=42
)
scaler_r = StandardScaler()
Xr_train_scaled = scaler_r.fit_transform(Xr_train)
Xr_test_scaled = scaler_r.transform(Xr_test)
reg_mlp = MLPRegressor(
hidden_layer_sizes=(20, 10),
activation="relu",
solver="adam",
max_iter=600,
random_state=42
)
reg_mlp.fit(Xr_train_scaled, yr_train)
y_pred_r = reg_mlp.predict(Xr_test_scaled)
mse = mean_squared_error(yr_test, y_pred_r)
print(f"MSE:{mse:.4f}")
print(f"RMSE:{np.sqrt(mse):.4f}")
MSE:170.4409
RMSE:13.0553
均方误差的定义为:
\\mathrm{MSE}=\\frac{1}{n}\\sum_{i=1}\^{n}(y_i-\\hat y_i)\^2
原始实验得到 MSE 约 170.44,对应 RMSE 约 13.06。RMSE 与目标变量单位一致,往往更容易解释;MSE 则以误差平方为单位,对较大的预测偏差更敏感。
但不能孤立地判断"170.44 很大还是很小"。必须结合目标值的范围、噪声水平,以及简单基线模型的误差。本例由 make_regression 生成线性关系,用线性回归作对照很有必要,不能预设神经网络一定更好。
该次回归训练同样出现了收敛警告。除了检查迭代次数,还可考虑目标值尺度是否影响优化;如果对目标进行标准化,评价前必须将预测还原到原始尺度。
七、实际使用时,怎样选择模型参数?
1. 隐藏层并非越多越好
增加隐藏层和神经元,会提高模型的表达能力,也会增加需要估计的参数。对于只有少量样本的任务,较大的网络可能更容易记住训练集中的偶然波动。
以本文分类网络为例,第一层有 4\\times10+10=50 个参数,第二层有 10\\times5+5=55 个参数,输出层有 5\\times3+3=18 个参数,总共 123 个可训练参数。计算时不仅要统计连接权重,还要加上每一层的偏置。
这个数字不能单独用来判断是否过拟合,却能帮助理解:看似只改了一个元组,实际已经改变了模型复杂度。比较不同结构时,应保持数据划分与评价方法一致。
2. 收敛与泛化是两个问题
收敛讨论的是优化过程是否满足停止条件,泛化讨论的是模型对未见数据是否有效。一个充分收敛的模型仍然可能过拟合;一个达到训练上限的模型,也可能已经获得较高的测试准确率。
因此,不能把"没有警告"当作模型质量合格的证据。对于本文实验,更有信息量的做法是同时记录训练轮数、损失变化和验证指标,再决定是否修改训练设置。
若尝试早停,应明确验证样本从哪里来。小样本任务中,从训练集再划出一部分作验证,会减少真正参与权重学习的数据量,验证分数也可能较为波动。
3. 先设计评价流程,再寻找最高分
一个常见错误是反复修改网络,直到测试集分数满意。这样一来,测试集虽然没有进入 fit,却已经间接参与了模型选择,其分数也就不再是完全独立的评价。
更规范的流程是:先保留测试集,在其余数据上完成交叉验证和参数选择,再使用选定方案重新拟合训练数据,最后评价测试集。每个交叉验证折内,都必须重新拟合标准化器。
对于类别不平衡的数据,还需要事先确定关注的指标。例如,多数类占比很高时,仅预测多数类也可能得到漂亮的准确率,却几乎无法识别少数类。这时应结合各类召回率、宏平均 F1 和混淆矩阵判断。
回归任务同样需要基线。至少比较"始终预测训练集目标均值"的简单方案,才能知道神经网络是否学到了有效信息。比较多个模型时,应使用相同的测试样本和相同单位的误差指标。
八、从跑通代码到可靠建模
这组实验展示了完整的监督学习流程:准备数据、划分集合、训练集拟合预处理、训练模型、预测和评价。真正需要掌握的,是每一步承担什么职责,而不是记住某个网络结构。
迁移到自己的任务时,建议先建立简单基线,再用训练集内部的交叉验证选择网络规模和其他参数。将标准化与模型放入 Pipeline,可以降低交叉验证时误用预处理的风险。最终测试集应留到模型方案确定后再评价。
MLP 的价值在于提供灵活的非线性映射,但复杂模型并不自动带来更可靠的结果。相比把准确率提高一个小数点,避免数据泄漏、解释错误类型、认真处理训练警告,更能决定实验是否值得信任。