3、生成实验数据集:使用 sklearn 生成非线性二分类数据集

好,咱们进入第三个环节——造数据。

说实话,做深度学习实验,很多时候数据比模型还重要。你模型再花哨,数据不行,结果就是白搭。我个人习惯是,在动手调网络之前,先把数据集搞得明明白白。这样后面出了问题,至少能排除数据这个变量。

3.1 为什么选非线性二分类?

咱们这个实验的核心,是观察 MLP 中间层宽度对泛化能力的影响。说白了,就是要看网络能不能学会一个复杂的决策边界。

线性可分的数据,用单层感知器就能搞定,体现不出 MLP 的优势。所以我选了非线性二分类——数据在二维平面上,类别边界是弯弯曲曲的。这样 MLP 的隐藏层才有用武之地。

你想想看,如果数据是线性可分的,那中间层宽度设成 1 和设成 100,效果可能差不多。但换成非线性数据,差别就出来了。窄的网络学不到复杂边界,宽的网络可能过拟合。这正是我们想观察的现象。

核心思路:用非线性数据来放大 MLP 宽度对模型容量的影响,从而观察泛化能力的变化趋势。

3.2 用 make_moons 生成数据

sklearn 里生成非线性二分类数据,最常用的就是 make_moonsmake_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 模型了。我个人觉得,数据准备这一步花点时间是值得的——数据质量决定了实验的天花板。