机器学习入门四大练手项目:手写数字识别、声呐分类、垃圾短信识别与鸢尾花分类实战 简介这份资源面向机器学习零基础或刚入门的学习者围绕四个经典项目展开手写数字识别、声呐目标分类、垃圾短信识别与鸢尾花分类帮助读者在动手实践中理解分类任务的基本流程与常见算法。压缩包共13个文件约630KB包含4个csv数据集、3个ipynb交互式笔记本、3个py源码脚本、2个txt说明及1个md文档覆盖数据读取、模型训练与结果验证等环节结构清晰便于按项目逐个学习。目前已有142人学习下载适合作为课程实验或自学练手材料。读者可借助源码与笔记本直接运行调试掌握特征处理、模型评估与调参思路并对照四个案例举一反三快速建立机器学习项目的完整认知。1. 四个练手项目里哪个最容易被小白低估很多人第一次接触机器学习都是从手写数字识别开始的。MNIST 这个数据集几乎成了行业里的“Hello World”网上教程一抓一大把跑通一个 98% 准确率的模型可能只需要二十行代码。但问题恰恰出在这里跑通不等于理解准确率高不等于你真的会做机器学习。我见过太多人拿着现成的 notebook 跑完一遍换一个数据集就完全不知道从哪下手。标题里提到的四个项目——手写数字识别、声呐分类、垃圾短信识别、鸢尾花分类——其实是一条非常完整的入门路径。它们分别对应了图像、信号、文本、表格四种数据形态覆盖了分类任务里最典型的几个场景。鸢尾花帮你建立最基本的特征与标签概念手写数字让你接触图像数据的预处理和卷积网络垃圾短信识别带你进入文本向量化和朴素贝叶斯的领域声呐分类则是一个被严重低估的练手项目——它的特征维度高、样本量小非常考验你对过拟合的理解。这篇文章面向的是想用这四个项目真正入门机器学习的人。我不会只给你一段能跑的代码就结束而是把每个项目的选型理由、关键参数、常见翻车点都讲清楚。你跟着做一遍应该能建立起一套可复用的分类任务流程而不是只会跑某个特定数据集。2. 四个项目的数据形态与模型选型逻辑2.1 为什么鸢尾花和手写数字适合作为起点鸢尾花数据集只有 150 个样本4 个特征3 个类别。它的价值不在于难度而在于让你把“特征矩阵 X”和“标签向量 y”这两个概念刻进肌肉记忆里。很多人跳过这一步直接上深度学习结果连训练集和测试集为什么要分开都说不清楚。手写数字识别用的是 28x28 的灰度图每个样本 784 个像素特征。它的核心教学点是图像数据怎么归一化、为什么要做归一化、卷积核到底在提取什么。我一般会建议先用全连接网络跑一个基线再换成卷积网络对比效果这样你能直观感受到“局部感受野”带来的差异。2.2 垃圾短信识别和声呐分类的独特价值垃圾短信识别是典型的文本二分类问题。它的流程比图像多了一步文本要转成数值向量。常见做法是词袋模型加 TF-IDF 加权然后接朴素贝叶斯或线性 SVM。这个项目能让你理解“特征工程”在文本任务里的分量——同样的分类器特征做得好不好效果可能差十几个百分点。声呐分类用的是 UCI 的 Sonar 数据集60 个特征208 个样本二分类。它的特点是特征维度接近样本量模型非常容易过拟合。这个项目是检验你正则化功底的试金石。如果你在鸢尾花上随便调参都能到 95%在声呐数据上可能会发现训练集准确率 100%、测试集只有 70%。这种落差感恰恰是让你真正理解泛化能力的时刻。2.3 四个项目的统一实验框架与其每个项目写一套代码不如搭一个统一的实验框架。下面这个结构我用了很久四个项目都能套进去# ml_pipeline.py # 统一的分类任务实验框架四个项目共用 import numpy as np from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.metrics import classification_report, confusion_matrix def load_and_split(data, label, test_size0.2, random_state42): 统一的数据划分函数 data: 特征矩阵shape(n_samples, n_features) label: 标签向量shape(n_samples,) test_size: 测试集比例小样本数据集建议 0.2~0.3 random_state: 固定随机种子保证结果可复现 X_train, X_test, y_train, y_test train_test_split( data, label, test_sizetest_size, random_staterandom_state, stratifylabel ) # stratify 保证训练集和测试集的类别比例一致小样本时尤其重要 return X_train, X_test, y_train, y_test def normalize(X_train, X_test): 标准化用训练集的均值和方差去变换测试集 这是最容易翻车的地方——千万不要用全量数据 fit scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) # 只在训练集上 fit X_test_scaled scaler.transform(X_test) # 测试集只做 transform return X_train_scaled, X_test_scaled, scaler def evaluate(model, X_test, y_test, target_namesNone): 统一的评估输出分类报告 混淆矩阵 y_pred model.predict(X_test) print(classification_report(y_test, y_pred, target_namestarget_names)) print(混淆矩阵) print(confusion_matrix(y_test, y_pred)) return y_pred这段代码里最关键的参数是stratifylabel。在小样本数据集上如果不做分层抽样可能出现某个类别在测试集里一个样本都没有的情况评估结果就完全失真了。另一个重点是标准化那一步fit_transform只能用在训练集上测试集必须用训练集学到的均值和方差来做transform。这个坑我见过太多人踩包括一些已经工作了一两年的工程师。注意标准化不是必须的。鸢尾花和声呐数据建议做手写数字如果像素值已经归一化到 0-1 之间可以跳过垃圾短信的 TF-IDF 向量本身已经是归一化的。3. 手写数字识别从全连接到卷积的完整实现3.1 数据加载与预处理的三个关键参数手写数字识别我一般用两种方式加载数据一种是直接从 sklearn 的load_digits拿 8x8 的小图另一种是从原始 IDX 文件读 28x28 的完整版。前者适合快速验证后者更接近真实场景。# mnist_baseline.py # 手写数字识别基线模型 from sklearn.datasets import load_digits from sklearn.linear_model import LogisticRegression from sklearn.neural_network import MLPClassifier from ml_pipeline import load_and_split, normalize, evaluate # 加载 8x8 版本1797 个样本 digits load_digits() X, y digits.data, digits.target # 划分数据集手写数字样本量适中测试集 20% 足够 X_train, X_test, y_train, y_test load_and_split(X, y, test_size0.2) # 像素值范围是 0-16做标准化 X_train, X_test, scaler normalize(X_train, X_test) # 基线模型逻辑回归 lr LogisticRegression(max_iter5000, C1.0, solverlbfgs) lr.fit(X_train, y_train) print( 逻辑回归基线 ) evaluate(lr, X_test, y_test) # 多层感知机 mlp MLPClassifier( hidden_layer_sizes(128, 64), # 两层隐藏层 activationrelu, alpha0.001, # L2 正则化系数 max_iter500, random_state42 ) mlp.fit(X_train, y_train) print( MLP 模型 ) evaluate(mlp, X_test, y_test)LogisticRegression的max_iter默认是 100在手写数字这种多分类任务上经常不收敛调到 5000 是常规操作。C是正则化强度的倒数越小正则化越强。MLPClassifier的alpha参数控制 L2 惩罚0.001 是一个比较稳的起点。hidden_layer_sizes(128, 64)表示两层隐藏层第一层 128 个神经元第二层 64 个。这个结构在 8x8 数据上已经足够再深就容易过拟合。3.2 用卷积网络处理 28x28 完整版数据如果你用的是完整版 MNIST全连接网络的效果会明显下降因为 784 维输入里大部分像素是冗余的。这时候卷积网络的优势就体现出来了。# mnist_cnn.py # 用 PyTorch 搭建一个简单的卷积网络 import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset class SimpleCNN(nn.Module): def __init__(self, num_classes10): super(SimpleCNN, self).__init__() # 第一个卷积块1 通道输入8 个卷积核3x3 self.conv1 nn.Sequential( nn.Conv2d(1, 8, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(kernel_size2) # 28x28 - 14x14 ) # 第二个卷积块8 通道输入16 个卷积核 self.conv2 nn.Sequential( nn.Conv2d(8, 16, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(kernel_size2) # 14x14 - 7x7 ) # 全连接层16*7*7 784 维输入 self.fc nn.Sequential( nn.Linear(16 * 7 * 7, 64), nn.ReLU(), nn.Dropout(0.3), # 丢弃率 0.3防止过拟合 nn.Linear(64, num_classes) ) def forward(self, x): x self.conv1(x) x self.conv2(x) x x.view(x.size(0), -1) # 展平 x self.fc(x) return x # 训练配置 device torch.device(cuda if torch.cuda.is_available() else cpu) model SimpleCNN().to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) # 假设 train_loader 和 test_loader 已经准备好 # 训练循环 for epoch in range(10): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() print(fEpoch {epoch1}, Loss: {running_loss/len(train_loader):.4f})卷积核的数量我选了 8 和 16这是比较保守的配置。如果你用 32 和 64训练时间会翻倍但在 MNIST 上准确率提升不到 0.5 个百分点。Dropout(0.3)放在全连接层之前是防止过拟合的常规手段。学习率用 Adam 的默认值 0.001 就行MNIST 这个任务不太需要精细调学习率。3.3 训练过程中要看哪些指标不要只盯着准确率。我一般会同时看三个东西训练 loss 的下降曲线、验证集准确率的变化、以及混淆矩阵里哪些数字容易被搞混。MNIST 上最常见的混淆是 4 和 9、3 和 8、5 和 6。如果你发现某两个数字的混淆特别严重可以针对性地做数据增强比如对这两类样本做轻微的旋转和平移。另一个容易被忽略的点是测试集准确率 98% 和 99% 之间的差距可能比你想象的大。从 98% 到 99%意味着错误率降低了一半。在工业场景里这个提升可能需要付出几倍的努力。所以入门阶段不要追求极致准确率先把流程跑通、把每个参数的含义搞清楚。4. 声呐分类与垃圾短信识别小样本和高维特征的挑战4.1 声呐数据的过拟合陷阱与正则化手段声呐数据集有 60 个特征208 个样本。如果你直接上一个全连接网络训练集准确率可以轻松到 100%但测试集可能只有 70% 出头。这就是典型的过拟合。# sonar_classify.py # 声呐分类对比不同正则化策略 import pandas as pd import numpy as np from sklearn.model_selection import cross_val_score, StratifiedKFold from sklearn.linear_model import LogisticRegression from sklearn.svm import SVC from sklearn.ensemble import RandomForestClassifier from ml_pipeline import load_and_split, normalize, evaluate # 加载声呐数据假设已下载 sonar.csv # 数据格式60 个特征列 1 个标签列R 或 M df pd.read_csv(sonar.csv, headerNone) X df.iloc[:, :-1].values y df.iloc[:, -1].values # 标签编码R - 0, M - 1 from sklearn.preprocessing import LabelEncoder le LabelEncoder() y le.fit_transform(y) X_train, X_test, y_train, y_test load_and_split(X, y, test_size0.25) X_train, X_test, scaler normalize(X_train, X_test) # 策略一L2 正则化的逻辑回归 lr_l2 LogisticRegression(penaltyl2, C0.1, max_iter5000) lr_l2.fit(X_train, y_train) print( L2 逻辑回归 ) evaluate(lr_l2, X_test, y_test, target_names[Rock, Mine]) # 策略二L1 正则化自带特征选择效果 lr_l1 LogisticRegression(penaltyl1, C0.5, solversaga, max_iter5000) lr_l1.fit(X_train, y_train) print( L1 逻辑回归 ) evaluate(lr_l1, X_test, y_test, target_names[Rock, Mine]) # 策略三SVM 交叉验证选参 svm SVC(kernelrbf, C1.0, gammascale, probabilityTrue) svm.fit(X_train, y_train) print( SVM ) evaluate(svm, X_test, y_test, target_names[Rock, Mine]) # 用交叉验证评估稳定性 cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores cross_val_score(svm, X_train, y_train, cvcv, scoringaccuracy) print(fSVM 5折交叉验证准确率{scores.mean():.4f} (/- {scores.std():.4f}))C0.1的 L2 正则化是我在声呐数据上试出来比较稳的参数。C 越小正则化越强模型越保守。L1 正则化会让部分特征权重变成 0相当于自动做了特征选择但需要配合saga求解器。SVM 的gammascale是 sklearn 的默认值相当于 1/(n_features * X.var())在特征维度高的时候比auto更稳。交叉验证那一步很重要。单次划分的测试集准确率波动可能很大5 折交叉验证的均值和标准差能告诉你模型到底稳不稳定。如果标准差超过 0.05说明模型对数据划分太敏感需要换更简单的模型或者加更强的正则化。4.2 垃圾短信识别的文本向量化流程垃圾短信识别的核心不在分类器而在文本怎么转成向量。我一般用 TF-IDF因为它比简单的词频统计更能反映词语的重要性。# spam_classify.py # 垃圾短信识别TF-IDF 朴素贝叶斯 import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # 加载数据假设是两列label, message df pd.read_csv(spam.csv, encodinglatin-1) df df[[v1, v2]].rename(columns{v1: label, v2: message}) df[label_num] df[label].map({ham: 0, spam: 1}) X_train, X_test, y_train, y_test train_test_split( df[message], df[label_num], test_size0.2, random_state42, stratifydf[label_num] ) # 用 Pipeline 把向量化和分类器串起来 pipeline Pipeline([ (tfidf, TfidfVectorizer( max_features5000, # 最多保留 5000 个词 ngram_range(1, 2), # 考虑单个词和相邻词对 min_df2, # 至少出现在 2 个文档里的词才保留 max_df0.95, # 出现在超过 95% 文档里的词丢弃 stop_wordsenglish # 去掉英文停用词 )), (clf, MultinomialNB(alpha0.1)) # 朴素贝叶斯alpha 是平滑参数 ]) pipeline.fit(X_train, y_train) y_pred pipeline.predict(X_test) print(classification_report(y_test, y_pred, target_names[Ham, Spam]))ngram_range(1, 2)表示同时考虑单个词和相邻的两个词组合。比如“free”和“free entry”都会被当作特征。min_df2过滤掉只出现一次的词减少噪声。max_df0.95过滤掉几乎每篇文档都有的词这些词没有区分能力。alpha0.1是拉普拉斯平滑的变体防止某个词在训练集里没出现过导致概率为 0。朴素贝叶斯在文本分类上往往能打败更复杂的模型因为它的独立性假设在词袋模型下反而成了一种有效的正则化。如果你换成逻辑回归可能需要更仔细地调正则化参数。4.3 两个项目的交叉验证与模型持久化小样本项目一定要做交叉验证而且最好用分层 K 折。模型训练完之后用joblib保存下来下次直接加载不用重新训练。# model_persist.py # 模型保存与加载 import joblib from sklearn.model_selection import cross_val_score # 交叉验证 cv_scores cross_val_score(pipeline, X_train, y_train, cv5, scoringf1) print(fF1 分数{cv_scores.mean():.4f} (/- {cv_scores.std():.4f})) # 保存模型 joblib.dump(pipeline, spam_classifier.pkl) print(模型已保存) # 加载模型 loaded_model joblib.load(spam_classifier.pkl) new_message [Congratulations! Youve won a free ticket] prediction loaded_model.predict(new_message) print(f预测结果{Spam if prediction[0] 1 else Ham})用 F1 分数而不是准确率来评估是因为垃圾短信数据通常是不平衡的正常短信远多于垃圾短信。如果只看准确率一个把所有短信都判为正常的模型也能到 85% 以上但它的 F1 分数会很低。joblib比pickle更适合保存 sklearn 模型因为它在处理 numpy 数组时效率更高。5. 避坑指南四个项目里最容易翻车的五个地方5.1 标准化时用全量数据 fit现象模型在测试集上表现异常好但上线后效果一落千丈。原因在划分训练集和测试集之前就做了标准化导致测试集的统计信息泄露到了训练过程中。解决永远先划分数据集再在训练集上fit测试集只做transform。这个坑在鸢尾花和声呐数据上尤其致命因为它们的特征尺度差异大标准化对结果影响明显。5.2 小样本数据集不做分层抽样现象某次运行结果特别好换一个随机种子结果掉十几个百分点。原因train_test_split默认是随机抽样在小样本上可能导致某个类别在测试集里几乎没有样本。解决加上stratifylabel参数保证训练集和测试集的类别比例一致。声呐数据只有 208 个样本不做分层的话测试集里可能只有三五个正样本评估结果完全没有参考价值。5.3 文本分类时在划分前做 TF-IDF现象交叉验证分数很高但实际预测新短信时效果很差。原因TF-IDF 的fit过程会统计词频和逆文档频率如果在全量数据上做测试集的词汇信息就泄露了。解决用Pipeline把向量化和分类器串起来这样交叉验证时fit只会作用在训练折上。我见过有人手动做 TF-IDF 然后交叉验证分数虚高得离谱。5.4 手写数字识别不做像素归一化现象模型训练 loss 震荡严重收敛很慢。原因原始像素值范围是 0-255输入到神经网络后梯度太大。解决把像素值除以 255 归一化到 0-1 之间或者做标准化。这个操作在图像任务里几乎是必须的但很多人从表格数据转过来会忽略。5.5 声呐分类盲目追求复杂模型现象用了深度网络训练集准确率 100%测试集只有 65%。原因60 个特征、208 个样本模型参数量远大于样本量过拟合不可避免。解决从简单的线性模型开始逐步增加复杂度。逻辑回归加 L2 正则化在声呐数据上通常能到 80% 左右SVM 调好参数能到 85%。如果这些简单模型都做不好换深度网络只会更糟。6. 把四个项目串成一条可复用的分类任务流水线四个项目做下来你应该已经发现它们共享同一套流程加载数据、划分数据集、预处理、选模型、训练、评估、调参。区别只在于数据形态不同导致预处理和模型选型有差异。我一般会把这套流程固化成一个模板新任务来了直接套。# template.py # 通用分类任务模板 def run_classification(X, y, preprocess_fn, model, test_size0.2): X: 特征矩阵 y: 标签向量 preprocess_fn: 预处理函数接收 X_train, X_test返回处理后的数据 model: 分类器实例 # 1. 划分 X_train, X_test, y_train, y_test train_test_split( X, y, test_sizetest_size, random_state42, stratifyy ) # 2. 预处理 X_train, X_test preprocess_fn(X_train, X_test) # 3. 训练 model.fit(X_train, y_train) # 4. 评估 from sklearn.metrics import classification_report y_pred model.predict(X_test) print(classification_report(y_test, y_pred)) return model这个模板的价值在于它强迫你把预处理和模型训练分开考虑。很多人写代码时把这两步混在一起导致交叉验证没法做因为预处理会泄露信息。把preprocess_fn作为参数传进来就能保证每次交叉验证时预处理只在训练折上 fit。最后一个技巧用sklearn.pipeline.Pipeline把预处理和模型串起来然后直接对 pipeline 做GridSearchCV。这样调参的时候预处理参数和模型参数可以一起搜而且不会泄露信息。from sklearn.pipeline import Pipeline from sklearn.model_selection import GridSearchCV from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC pipe Pipeline([ (scaler, StandardScaler()), (svm, SVC()) ]) param_grid { svm__C: [0.1, 1, 10], svm__gamma: [scale, auto, 0.01], svm__kernel: [rbf, linear] } grid GridSearchCV(pipe, param_grid, cv5, scoringf1, n_jobs-1) grid.fit(X_train, y_train) print(f最佳参数{grid.best_params_}) print(f最佳分数{grid.best_score_:.4f})n_jobs-1表示用所有 CPU 核心并行搜索。scoringf1在不平衡数据上比准确率更可靠。grid.best_params_里的参数名带svm__前缀是因为它们在 pipeline 里的步骤名是svm。我自己做项目的习惯是每接触一个新数据集先用最简单的模型跑一个基线记录下准确率和 F1。然后逐步增加复杂度每次只改一个地方观察指标变化。如果复杂模型没有明显提升就退回简单模型。这个习惯帮我省了很多时间也避免了很多“为了复杂而复杂”的翻车。希望帮到你。本文还有配套的精品资源点击获取