3、生成实验数据集:使用 sklearn 生成非线性二分类数据集
好,咱们进入第三个环节——造数据。
说实话,做深度学习实验,很多时候数据比模型还重要。你模型再花哨,数据不行,结果就是白搭。我个人习惯是,在动手调网络之前,先把数据集搞得明明白白。这样后面出了问题,至少能排除数据这个变量。
3.1 为什么选非线性二分类?
咱们这个实验的核心,是观察 MLP 中间层宽度对泛化能力的影响。说白了,就是要看网络能不能学会一个复杂的决策边界。
线性可分的数据,用单层感知器就能搞定,体现不出 MLP 的优势。所以我选了非线性二分类——数据在二维平面上,类别边界是弯弯曲曲的。这样 MLP 的隐藏层才有用武之地。
你想想看,如果数据是线性可分的,那中间层宽度设成 1 和设成 100,效果可能差不多。但换成非线性数据,差别就出来了。窄的网络学不到复杂边界,宽的网络可能过拟合。这正是我们想观察的现象。
核心思路:用非线性数据来放大 MLP 宽度对模型容量的影响,从而观察泛化能力的变化趋势。
3.2 用 make_moons 生成数据
sklearn 里生成非线性二分类数据,最常用的就是 make_moons 和 make_circles。我这次选的是 make_moons,因为它生成的数据形状像两个月牙交错,决策边界是弧形的,比较典型。
我在项目中遇到过类似场景——客户给的数据就是这种月牙形分布,当时用逻辑回归死活分不开,换成 MLP 一下就解决了。嗯,从那以后我就记住了这个数据集。
from sklearn.datasets import make_moons
import matplotlib.pyplot as plt
# 生成 1000 个样本,噪声 0.2
X, y = make_moons(n_samples=1000, noise=0.2, random_state=42)
# 可视化一下
plt.figure(figsize=(6, 5))
plt.scatter(X[y==0, 0], X[y==0, 1], label='Class 0', alpha=0.6)
plt.scatter(X[y==1, 0], X[y==1, 1], label='Class 1', alpha=0.6)
plt.legend()
plt.title('Moons Dataset (Nonlinear Binary Classification)')
plt.show()
这里有几个参数我解释一下:
- n_samples=1000:样本总数。我试过 500 和 2000,1000 是个比较平衡的数字,训练快,结果也稳定。
- noise=0.2:噪声水平。值越大,两个月牙重叠越严重。0.2 是我觉得比较合适的——有点挑战性,但又不至于太难分。
- random_state=42:固定随机种子。这样每次生成的数据都一样,实验结果可复现。
小技巧:如果你想让数据更难一点,可以把 noise 调到 0.3 或 0.4。但咱们这个实验主要看宽度的影响,数据难度适中就好,太简单或太难都会掩盖宽度带来的差异。
3.3 数据预处理:训练集和测试集
数据生成完了,接下来要分成训练集和测试集。这个步骤看似简单,但有个坑——我刚开始做实验时,经常忘记做特征缩放,结果 MLP 训练半天不收敛。
为什么会这样?因为 MLP 对输入特征的尺度很敏感。如果特征范围差太多,梯度更新就会不稳定。所以,标准化是必须的。
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
# 划分训练集和测试集(80% 训练,20% 测试)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 标准化:均值为0,标准差为1
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
print(f"训练集大小: {X_train.shape[0]}")
print(f"测试集大小: {X_test.shape[0]}")
注意这里:fit_transform 只用在训练集上,测试集用 transform 就行。千万别在测试集上重新 fit,否则数据泄露,测试结果就不可信了。
避坑指南:我曾经犯过一个错误——把整个数据集标准化后再划分训练集和测试集。结果测试集的信息提前泄露到了标准化参数里,导致模型评估结果虚高。正确的做法是先划分,再标准化。
3.4 数据可视化:看看长什么样
数据准备好了,咱们画个图看看分布。我习惯把训练集和测试集分开画,这样能直观感受数据是否分布一致。
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# 训练集
axes[0].scatter(X_train[y_train==0, 0], X_train[y_train==0, 1],
label='Class 0', alpha=0.6)
axes[0].scatter(X_train[y_train==1, 0], X_train[y_train==1, 1],
label='Class 1', alpha=0.6)
axes[0].set_title('Training Set')
axes[0].legend()
# 测试集
axes[1].scatter(X_test[y_test==0, 0], X_test[y_test==0, 1],
label='Class 0', alpha=0.6)
axes[1].scatter(X_test[y_test==1, 0], X_test[y_test==1, 1],
label='Class 1', alpha=0.6)
axes[1].set_title('Test Set')
axes[1].legend()
plt.show()
从图上可以看到,两个类别的分布是交错的,没有一条直线能完全分开它们。这就是非线性数据的典型特征。
3.5 数据集的参数总结
为了方便后续实验,我把数据集的参数整理成表格:
| 参数 | 值 | 说明 |
|---|---|---|
| 样本总数 | 1000 | 训练 800,测试 200 |
| 特征维度 | 2 | 二维平面坐标 |
| 类别数 | 2 | 二分类 |
| 噪声水平 | 0.2 | 控制重叠程度 |
| 数据分布 | 月牙形 | 非线性决策边界 |
这个数据集会用在后续所有 MLP 宽度实验中。也就是说,后面咱们调整隐藏层宽度时,数据是固定的。这样对比才有意义——变量只有宽度,其他条件不变。
总结一下:这一节我们生成了一个非线性二分类数据集,做了标准化和划分。数据准备好了,下一节就可以开始搭建 MLP 模型了。我个人觉得,数据准备这一步花点时间是值得的——数据质量决定了实验的天花板。