☰
四个经典数据集带你快速入门机器学习:MNIST、Sonar、SMS Spam与Iris实战
2026/10/2 17:43:31 网站建设 项目流程

简介:这份资源面向零基础或刚接触机器学习的小白学习者,围绕四个经典入门项目展开:手写数字识别、声呐目标分类、垃圾短信识别与鸢尾花分类,帮助读者在动手实践中理解分类任务的基本流程与常见算法思路。压缩包共13个文件,约630KB,包含4个csv数据集、3个ipynb交互式笔记本、3个py脚本、2个txt说明及1个md文档,覆盖数据读取、模型训练与结果验证等环节,便于边看边跑、逐步调试。目前已有142人学习下载,适合作为课程实验、自学练手或项目作业的参考素材。读者可借助源码与数据集快速复现四个案例,对比不同模型在图像、文本与数值特征上的表现,并在此基础上尝试调参与改进,形成对机器学习入门路径的直观认识。

1. 四个数据集串起机器学习入门:为什么它们比啃教材快十倍

很多人入门机器学习的第一反应是买一本《机器学习》从头啃公式,结果卡在第三章的梯度推导上就再也没翻过书。我带过不少新人,最有效的方式反而是反过来的:先拿四个体量小、语义清晰、结果可验证的数据集把整条链路跑通,再回头补理论。这四个数据集就是手写数字识别(MNIST)、声呐分类(Sonar)、垃圾短信识别(SMS Spam)和鸢尾花分类(Iris)。它们分别对应图像、信号、文本、表格四类数据形态,覆盖了机器学习入门阶段最核心的几种任务类型:多分类、二分类、高维小样本、类别不平衡。

你可能会问,为什么偏偏是这四个?因为它们有一个共同特征——数据量小到能在笔记本上几分钟跑完一轮,但每个都保留了真实项目里的关键难点。MNIST 让你理解图像怎么变成向量,Sonar 让你体会高维特征下模型容易过拟合,垃圾短信让你第一次面对文本清洗和类别不平衡,Iris 则是你验证任何新算法时最省事的试验田。把这四个跑通,你就有了一个属于自己的“算法试炼场”,以后学任何新模型都可以先在这四个数据集上验证一遍。

这篇文章面向的是想动手但不知道从哪下手的人。我会按“先跑通再优化”的思路,把每个数据集的加载、预处理、模型训练、评估、调参都拆开讲,代码可以直接抄,参数会解释为什么这么设。中间会专门用一章讲我踩过的坑,最后给一套把这四个项目串起来的进阶玩法。不废话,直接开始。

2. 四个数据集的加载与预处理:从原始文件到模型能吃的张量

2.1 手写数字识别:MNIST 的三种加载方式与归一化参数

MNIST 是 70000 张 28×28 的灰度图,60000 张训练、10000 张测试。最常见的加载方式是用torchvision.datasets.MNIST或keras.datasets.mnist,但如果你拿到的是一堆 png 文件,就得自己写 Dataset。我一般先用 torchvision 跑通,再换成自定义 Dataset 验证。

import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader # 关键参数:ToTensor 把像素从 0-255 转到 0-1,Normalize 再减均值除标准差 transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) # MNIST 全局均值和标准差 ]) train_set = datasets.MNIST(root='./data', train=True, download=True, transform=transform) test_set = datasets.MNIST(root='./data', train=False, download=True, transform=transform) train_loader = DataLoader(train_set, batch_size=64, shuffle=True, num_workers=2) test_loader = DataLoader(test_set, batch_size=1000, shuffle=False, num_workers=2)

Normalize里的 0.1307 和 0.3081 是 MNIST 训练集的全局均值和标准差,不是随便写的。如果你不做归一化,像素值集中在 0 和 1 两端,模型收敛会慢很多。batch_size=64是入门阶段的稳妥选择,显存不够就降到 32,但不要低于 16,否则 BatchNorm 会不稳定。num_workers在 Windows 上设 0,Linux 上设 2 到 4,设太大反而会因为进程切换拖慢速度。

2.2 声呐分类:Sonar 数据的列名陷阱与分层抽样

Sonar 数据集是 208 个样本,每个样本 60 个特征,代表声呐回波在不同频段的能量。标签是 R(岩石)和 M(金属)。这个数据集最坑的地方是原始文件没有表头,第一列是标签,后面 60 列是特征。很多人直接pd.read_csv会把第一列当成索引或者特征。

import pandas as pd from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # 原始 sonar.csv 没有表头,第一列是标签 df = pd.read_csv('sonar.csv', header=None) X = df.iloc[:, :-1].values # 前 60 列是特征 y = df.iloc[:, -1].values # 最后一列是标签 # 标签转成 0/1 y = (y == 'M').astype(int) # 分层抽样保证训练集和测试集里 R/M 比例一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 标准化:Sonar 特征量纲差异大,必须做 scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test)

stratify=y是必须加的。Sonar 只有 208 个样本,如果不分层,测试集里可能全是 R 或者全是 M,准确率就没意义了。StandardScaler的fit只能在训练集上做,然后transform测试集,这个顺序反了就是数据泄露。我见过有人把整个数据集一起 fit,测试集准确率虚高十几个点,上线就翻车。

2.3 垃圾短信识别:文本清洗的四个步骤与 TF-IDF 参数

SMS Spam 数据集是 5572 条短信,标签是 ham 和 spam,比例大约是 87:13,属于典型的类别不平衡。文本数据不能直接喂给模型,必须先转成数值向量。常见做法是 TF-IDF,但清洗步骤决定了上限。

import re import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer df = pd.read_csv('spam.csv', encoding='latin-1') df = df[['v1', 'v2']].rename(columns={'v1': 'label', 'v2': 'text'}) df['label'] = (df['label'] == 'spam').astype(int) def clean_text(s): s = s.lower() # 转小写 s = re.sub(r'[^a-z0-9\s]', '', s) # 去标点 s = re.sub(r'\s+', ' ', s).strip() # 合并空格 return s df['text'] = df['text'].apply(clean_text) # TF-IDF:max_features 控制词表大小,ngram_range 捕捉短语 tfidf = TfidfVectorizer(max_features=3000, ngram_range=(1, 2), min_df=2) X = tfidf.fit_transform(df['text']) y = df['label'].values

max_features=3000是我在 5572 条数据上的经验值,词表再大只会增加噪声。ngram_range=(1,2)让模型能捕捉 “free entry” 这种短语,对垃圾短信识别很关键。min_df=2过滤掉只出现一次的词,减少过拟合。注意fit_transform只能在训练集上做,测试集要用transform,否则词表不一致。

2.4 鸢尾花分类:Iris 的标准化与决策树可视化

Iris 只有 150 个样本,4 个特征,3 个类别。它太简单了,简单到很多人不屑于用它。但正是因为它简单,你才能把注意力放在模型本身而不是数据清洗上。我一般用它来验证新算法的基本正确性。

from sklearn.datasets import load_iris from sklearn.tree import DecisionTreeClassifier, export_text from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score iris = load_iris() X, y = iris.data, iris.target X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) clf = DecisionTreeClassifier(max_depth=3, random_state=42) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) print(f"准确率: {accuracy_score(y_test, y_pred):.4f}") print(export_text(clf, feature_names=iris.feature_names))

max_depth=3是防止过拟合的关键。Iris 只有 150 个样本,树太深会记住训练集,测试集准确率反而下降。export_text能把决策树规则打印出来,你可以直观看到模型是怎么根据花瓣长度和宽度做判断的。这个可视化对理解决策树特别有用,比看任何教材都直观。

3. 从零训练四个模型:代码、参数与评估指标

3.1 MNIST 用 PyTorch 写一个 CNN:三层卷积的参数量与训练循环

MNIST 用全连接层也能到 97%,但用 CNN 能到 99% 以上,而且参数量更少。我一般用两层卷积加两层全连接,结构简单,训练快。

import torch.nn as nn import torch.nn.functional as F import torch.optim as optim class Net(nn.Module): def __init__(self): super().__init__() self.conv1 = nn.Conv2d(1, 32, 3, 1) # 输入1通道,输出32通道,3x3卷积 self.conv2 = nn.Conv2d(32, 64, 3, 1) self.dropout1 = nn.Dropout(0.25) self.dropout2 = nn.Dropout(0.5) self.fc1 = nn.Linear(9216, 128) # 64*12*12=9216 self.fc2 = nn.Linear(128, 10) def forward(self, x): x = self.conv1(x) x = F.relu(x) x = self.conv2(x) x = F.relu(x) x = F.max_pool2d(x, 2) x = self.dropout1(x) x = torch.flatten(x, 1) x = self.fc1(x) x = F.relu(x) x = self.dropout2(x) x = self.fc2(x) return F.log_softmax(x, dim=1) model = Net() optimizer = optim.Adam(model.parameters(), lr=1e-3) criterion = nn.NLLLoss() for epoch in range(5): model.train() for data, target in train_loader: optimizer.zero_grad() output = model(data) loss = criterion(output, target) loss.backward() optimizer.step() print(f"Epoch {epoch+1}, Loss: {loss.item():.4f}")

Conv2d(1, 32, 3, 1)里的参数分别是输入通道、输出通道、卷积核大小、步长。MNIST 是单通道灰度图,所以输入通道是 1。9216是第二次卷积后特征图的大小:28 经过两次 3×3 卷积变成 24,再池化变成 12,64 通道就是 64×12×12=9216。Dropout(0.25)和Dropout(0.5)是防过拟合的,如果训练集准确率远高于测试集,就把 dropout 调大。lr=1e-3是 Adam 的常用学习率,如果 loss 震荡就降到 1e-4。

3.2 Sonar 用 SVM 还是随机森林:小样本高维数据的选型对比

Sonar 只有 208 个样本,60 个特征,属于典型的小样本高维数据。这种数据上,SVM 和随机森林通常比深度学习表现好。我一般先跑一个基线对比。

from sklearn.svm import SVC from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score # SVM:RBF 核,C 和 gamma 是核心参数 svm = SVC(kernel='rbf', C=10, gamma='scale', random_state=42) svm_scores = cross_val_score(svm, X_train, y_train, cv=5) # 随机森林:n_estimators 和 max_depth 是核心参数 rf = RandomForestClassifier(n_estimators=200, max_depth=8, random_state=42) rf_scores = cross_val_score(rf, X_train, y_train, cv=5) print(f"SVM 交叉验证准确率: {svm_scores.mean():.4f} ± {svm_scores.std():.4f}") print(f"随机森林交叉验证准确率: {rf_scores.mean():.4f} ± {rf_scores.std():.4f}")

C=10是惩罚系数,越大越容易过拟合,越小越容易欠拟合。gamma='scale'是 sklearn 的默认值,等于 1/(特征数×方差),一般不用改。随机森林的n_estimators=200是树的数量,再多提升有限但计算变慢。max_depth=8是防止每棵树长得太深。在 Sonar 上,SVM 通常能到 85% 左右,随机森林在 80% 到 84% 之间波动。如果 SVM 明显更好,就选 SVM;如果两者差不多,选随机森林,因为特征重要性可以解释。

3.3 垃圾短信识别:朴素贝叶斯为什么是文本分类的基线

文本分类的经典基线是朴素贝叶斯,尤其是 MultinomialNB。它假设特征独立,虽然这个假设在文本里明显不成立,但实际效果往往出奇地好,而且训练速度极快。

from sklearn.naive_bayes import MultinomialNB from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report # 先划分训练测试集 from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) nb = MultinomialNB(alpha=1.0) nb.fit(X_train, y_train) y_pred_nb = nb.predict(X_test) lr = LogisticRegression(max_iter=1000, class_weight='balanced') lr.fit(X_train, y_train) y_pred_lr = lr.predict(X_test) print("朴素贝叶斯:") print(classification_report(y_test, y_pred_nb)) print("逻辑回归:") print(classification_report(y_test, y_pred_lr))

alpha=1.0是拉普拉斯平滑参数,防止某个词在训练集没出现导致概率为零。class_weight='balanced'在逻辑回归里很重要,因为垃圾短信只占 13%,不加这个参数模型会倾向于把所有短信判为 ham。评估的时候不要只看准确率,要看 spam 类的 recall 和 f1-score。我见过准确率 98% 但 spam 召回率只有 60% 的模型,等于漏掉四成垃圾短信。

3.4 Iris 上的决策树与 KNN:两个模型的可解释性对比

Iris 上最常用的两个模型是决策树和 KNN。决策树可解释性强,KNN 简单但预测慢。我一般两个都跑,对比一下。

from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import confusion_matrix # KNN:n_neighbors 是核心参数 knn = KNeighborsClassifier(n_neighbors=5) knn.fit(X_train, y_train) y_pred_knn = knn.predict(X_test) # 决策树 from sklearn.tree import DecisionTreeClassifier dt = DecisionTreeClassifier(max_depth=3, random_state=42) dt.fit(X_train, y_train) y_pred_dt = dt.predict(X_test) print("KNN 混淆矩阵:") print(confusion_matrix(y_test, y_pred_knn)) print("决策树混淆矩阵:") print(confusion_matrix(y_test, y_pred_dt))

n_neighbors=5是 KNN 的默认值,在 Iris 上一般 3 到 7 都差不多。决策树的max_depth=3让树最多三层,你可以用export_text打印出来,看到它怎么根据花瓣宽度把三个类别分开。KNN 没有可解释性,但它在 Iris 上通常比决策树准一点点。如果要做特征重要性分析,用决策树;如果只要准确率,用 KNN。

4. 避坑与排查:四个数据集上我踩过的真实坑

4.1 MNIST 归一化参数用错导致 loss 不下降

现象:训练了 10 个 epoch,loss 一直在 2.3 左右震荡,准确率只有 10%,等于随机猜。

原因:我用了 ImageNet 的均值和标准差(0.485, 0.456, 0.406)和(0.229, 0.224, 0.225)来归一化 MNIST。MNIST 是灰度图,像素分布和 ImageNet 完全不同,归一化后数据分布被扭曲了。

解决:换成 MNIST 自己的均值和标准差(0.1307,)和(0.3081,)。如果不确定,可以先算一下训练集的均值和标准差,或者干脆只做ToTensor不做Normalize,也能到 97% 以上。

4.2 Sonar 标准化在划分数据集之前做导致数据泄露

现象:交叉验证准确率 92%,但测试集只有 78%,差距巨大。

原因:我先对整个数据集做了StandardScaler().fit_transform(X),然后再划分训练测试集。这样测试集的均值和方差信息泄露到了训练过程中,交叉验证的分数虚高。

解决:先train_test_split,再在训练集上fit,然后transform测试集。这个顺序在任何预处理步骤里都不能反,包括归一化、PCA、特征选择。

4.3 垃圾短信识别只看准确率导致 spam 漏判严重

现象:模型准确率 98.5%,看起来很好,但实际用的时候发现大量垃圾短信没被拦住。

原因:数据里 ham 占 87%,spam 占 13%。模型只要把所有短信都判成 ham,准确率就有 87%。我训练的时候没有加class_weight='balanced',模型倾向于多数类。

解决:加class_weight='balanced',或者用 SMOTE 过采样 spam 类。评估指标换成 spam 类的 recall 和 f1-score,不要只看 accuracy。如果 recall 低于 90%,就继续调参。

4.4 Iris 决策树不限制深度导致训练集 100% 测试集 70%

现象:决策树在训练集上准确率 100%,测试集只有 70%。

原因:DecisionTreeClassifier默认不限制深度,树可以一直长到每个叶子只有一个样本。Iris 只有 150 个样本,树完全记住了训练集。

解决:设max_depth=3或min_samples_leaf=5。max_depth=3在 Iris 上通常能到 95% 以上,而且规则可以打印出来。如果还是过拟合,就减小max_depth或者增大min_samples_leaf。

4.5 四个数据集混用同一个随机种子导致结果不可复现

现象:同样的代码,两次运行结果不一样,准确率差 2 到 3 个点。

原因:train_test_split和模型初始化都用了随机数,但没有固定random_state。PyTorch 的权重初始化也是随机的。

解决:在所有涉及随机的地方加random_state=42,PyTorch 里加torch.manual_seed(42)。如果要完全复现,还要设torch.backends.cudnn.deterministic = True,但会牺牲一点速度。入门阶段固定种子就够了,不用追求完全确定性。

5. 把四个项目串成一个入门工具箱:我的进阶用法与验证习惯

跑通四个数据集之后,不要就扔在那里。我一般会做三件事:第一,写一个统一的评估脚本,输入任意数据集和模型,输出准确率、召回率、f1 和混淆矩阵。第二,把四个数据集的预处理封装成函数,以后换模型只改模型部分。第三,用 Iris 做快速验证,用 MNIST 做最终验证,中间两个做专项验证。

from sklearn.metrics import accuracy_score, f1_score, recall_score def evaluate(model, X_test, y_test, name): y_pred = model.predict(X_test) print(f"=== {name} ===") print(f"准确率: {accuracy_score(y_test, y_pred):.4f}") print(f"宏平均 F1: {f1_score(y_test, y_pred, average='macro'):.4f}") print(f"宏平均召回率: {recall_score(y_test, y_pred, average='macro'):.4f}") # 用法:evaluate(svm, X_test, y_test, "Sonar SVM")

这个脚本看起来简单,但能帮你省掉大量重复代码。每次试新模型,只要调evaluate就行。注意average='macro'在类别不平衡时比weighted更能反映少数类的表现。

还有一个习惯:每次调参只改一个参数,记录改之前和改之后的分数。我见过太多人一次改三四个参数,结果好了不知道是哪个起作用,坏了也不知道该回退哪个。用表格记录最清楚。

数据集基线模型基线分数调参后模型调参后分数关键参数
MNIST全连接97.2%CNN99.1%lr=1e-3, dropout=0.25
Sonar随机森林81.0%SVM85.6%C=10, gamma=scale
SMS Spam朴素贝叶斯97.8%逻辑回归98.2%class_weight=balanced
IrisKNN95.6%决策树95.6%max_depth=3

这张表是我自己用的模板,每次跑完新实验就填一行。时间长了你会发现,大部分调参的收益在 1 到 2 个点以内,真正决定效果的是数据预处理和模型选型。所以不要一上来就调参,先把数据搞清楚。

最后说一个我自己的教训:刚开始学的时候,我总想一次把四个数据集都做到最好,结果每个都只跑了个基线就换下一个。后来我强迫自己把 MNIST 从 97% 调到 99%,把 Sonar 从 80% 调到 86%,这个过程里学到的比跑十个数据集都多。深度比广度重要,尤其是在入门阶段。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询