机器学习期末作业:基于鸢尾花数据集的算法比较与评估
简介:机器学习期末作业中的鸢尾花数据集算法比较资料,适合计算机、电子信息工程、数学等专业学生在课程设计、期末大作业或毕业设计中快速搭建分类算法对比实验。资源自带鸢尾花数据集,无需额外准备数据即可直接运行。打包共七份文件,核心为四个Python脚本,分别实现逻辑回归、随机森林、KNN与决策树四种经典分类算法,另附一份Markdown文档说明,整体仅9KB,结构紧凑便于阅读与修改。已有396人学习下载。代码采用参数化编写,注释详细,且附带了运行结果,便于理解算法思路并自由替换数据集或调节参数;文档部分对实验过程、算法原理和性能结论做了梳理,能帮助读者掌握鸢尾花分类任务中多种算法的比较方法,减少从零调试的弯路。
1. 期末作业年年有,为什么鸢尾花数据集成了算法比较的“默认考场”
如果你正在为机器学习期末作业发愁,大概率已经听过“鸢尾花数据集”这个名字。它只有 150 条样本、4 个特征、3 个类别,规模小到 Excel 就能打开,却偏偏是无数课程设计、期末作业和入门比赛的首选素材。原因不复杂:数据干净、标签明确、类别均衡,算法之间的差异不会被数据噪声淹没。这份作业的核心诉求也直白——拿同一个数据集,跑几种常见分类算法,比一比准确率,写清楚为什么有的算法表现好、有的表现差,再附上完整源代码和文档说明。整个过程本质上是一次“算法比较基准实验”:控制数据不变,只改变模型,观察评估指标的变化。这篇笔记就把这条路径完整拆开,讲清楚数据集怎么用、算法怎么选、源代码怎么组织、验证怎么做,以及最容易让作业翻车的几个隐蔽问题。
2. 鸢尾花数据集的底细:先摸清数据,再做算法比较
2.1 看一眼数据的真实结构:三分类、四特征与线性边界
鸢尾花数据集之所以被反复用于算法比较,不是因为它“简单”,而是它的结构刚好能暴露不同算法的边界。150 条样本分成 setosa、versicolor、virginica 三个品种,每个品种正好 50 条。四个特征分别是花萼长度、花萼宽度、花瓣长度、花瓣宽度,单位是厘米,全部是连续数值,不存在缺失值。用 pandas 快速加载看分布,是作业里“数据探索”部分的标准起手式:
import pandas as pd from sklearn.datasets import load_iris iris = load_iris() df = pd.DataFrame(iris.data, columns=iris.feature_names) df['target'] = iris.target print(df.shape) # 看一眼样本量和特征数 print(df['target'].value_counts()) # 确认三类是否均衡 print(df.groupby('target').mean()) # 各类在四个特征上的均值差异这段代码的用意是帮你在写任何模型之前,先建立对数据的直观印象。df.shape输出(150, 5),告诉你样本量和特征维度;value_counts()检查类别均衡度——鸢尾花数据集是标准的 50/50/50,所以后续计算准确率时不会出现“多数类压倒少数类”的假象;groupby('target').mean()则能一眼看出 setosa 的花瓣长度均值远小于另外两类,说明这个特征本身就带有很强的分类信息。
有一个关键结构值得注意:setosa 和另外两类在花瓣长度、花瓣宽度上完全线性可分,而 versicolor 和 virginica 存在部分重叠区域。这意味着线性模型(逻辑回归、线性 SVM)有希望达到高准确率,但难以做到 100% 完美分类;而 KNN、决策树这类非线性模型则有机会在重叠区域做出更细的划分。算法比较的“看点”恰恰落在这种结构上——模型差异在重叠样本上才真正显现。
2.2 先把评估口径定死:分层采样、交叉验证与随机种子
算法比较最怕的不是模型不好,而是评估口径不一致。比如 A 同学用train_test_split默认参数划分数据,B 同学固定random_state=0,C 同学做了五折交叉验证,三个人跑同一个 KNN,准确率分别是 95%、97%、93%,然后各自写进报告,谁也说服不了谁。正确的做法是在动手训练之前,先把“怎么切数据、怎么评估”定成一套固定流程。
我一般会把下面这段代码当作整个作业的实验骨架,任何算法都在同一套切分方案下做比较:
from sklearn.model_selection import train_test_split, StratifiedKFold, cross_val_score import numpy as np X_train, X_test, y_train, y_test = train_test_split( iris.data, iris.target, test_size=0.3, random_state=42, stratify=iris.target ) cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)stratify=iris.target是第一个容易被忽略的参数。鸢尾花数据本身类别均衡,理论上随机切分也不太会歪,但分层采样能保证训练集和测试集里三个类别的比例都和全量数据一致,这在样本量只有 150 条的情况下很重要——万一随机切分把某个类别的样本大量分到测试集,训练集就“缺了一类”,模型学到的决策边界就会明显偏向多数类。random_state=42则是整个实验的“后悔药”,固定它之后,无论跑多少次,切分结果都完全一致,报告里写出的每个数字都能被复核。
交叉验证跑出来的分数才是算法比较的核心证据。单次切分的准确率受运气影响太大;交叉验证把数据切成 5 份,轮流拿 4 份训练、1 份验证,相当于在 5 组不同数据上做了 5 次实验,取平均值和标准差,比单次结果可靠得多。这里的shuffle=True保证每折的样本顺序被打乱,避免原始数据中的潜在排列规律干扰结果。
2.3 指标怎么选:准确率之外,混淆矩阵才是重点
很多期末作业的算法比较部分只写一个 accuracy,这是最大的偷懒。150 条样本上,95% 和 93% 的差异可能只是 3 条样本的预测结果不同,光靠一个数字说明不了任何算法优势。比较合理的做法是同时报告准确率、混淆矩阵和分类报告,这样才算“把结果讲清楚了”。
from sklearn.metrics import classification_report, confusion_matrix y_pred = model.predict(X_test) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, target_names=iris.target_names))混淆矩阵的价值在于它能指出错误发生在哪里。比如 KNN 的混淆矩阵显示 setosa 全部预测正确,但 versicolor 有 2 条被错判成 virginica;换成 SVM 后 versicolor 的错判减少到 1 条——这才是算法比较应该关注的细节。classification_report输出的 precision、recall、f1-score 是针对每个类别的,能看出模型是否对某个特定品种存在系统性偏见。如果只写准确率,这些信息全都会被丢掉。
到这里,数据侧的准备已经完成。接下来进入整个作业的核心分歧点:到底把哪些算法放进比较名单里,依据是什么。
3. 算法选型不是越多越好:五类模型的比较逻辑
3.1 基线先行:为什么先跑一个最朴素的模型
算法比较的第一步不是上最强模型,而是立一个基线。基线的意义在于给你一把“尺子”——如果一个复杂模型连简单模型都打不过,那它的复杂就毫无价值。常见做法是用多数类投票(ZeroR)或逻辑回归作为基线,因为逻辑回归在鸢尾花这样的小规模低维数据上通常表现不差。
我在布置这类作业时,建议至少比较下面五类模型:K近邻、决策树、逻辑回归、支持向量机、朴素贝叶斯。选它们的理由各不相同,但覆盖了“非参数方法、树模型、线性模型、间隔最大化方法、概率生成模型”五个典型思路,写文档说明时每一类都可以独立成段。
3.2 KNN 与决策树:两类非参数方法的脾气差别
KNN 的思路是最朴素的“近朱者赤”:预测一个样本的类别时,找离它最近的 K 个已知样本,投票决定。它不学任何参数,训练阶段几乎不耗时,真正的计算发生在预测阶段。K 值是 KNN 唯一的敏感参数——K 太小容易过拟合,K 太大则会把远处的样本也拉进投票,导致边界过于平滑。鸢尾花数据集上常见的 K 值在 3 到 15 之间,再大就会开始丢精度。
决策树则是另一种极端:它显式学习一组“如果—那么”规则,按特征值逐层切分样本空间。它与 KNN 最大的区别是可解释性——树可以画出来,每一个分支都是一条判断路径。但决策树对深度极其敏感:不设max_depth,树会一直生长到每个叶节点只剩一种类别,训练集准确率冲到 100%,测试集反而下降,这是典型的过拟合表现。作业里我建议把max_depth限制在 3 到 5,既保留可解释性,又避免树长得太复杂。
3.3 逻辑回归与 SVM:两种“找分界线”的线性思维
逻辑回归虽然名字里带“回归”,本质是分类算法。它做的事情是找一条线性决策边界,让样本被分到边界两侧的概率最大。在鸢尾花数据上,逻辑回归面对的关键问题是:setosa 很容易分,versicolor 和 virginica 的部分样本在特征空间重叠,线性边界必然会造成一些错分。它的优势是训练快、输出有概率解释,缺点是边界形式受限,无法表达复杂的非线性关系。
SVM 的出发点不同:它不追求概率最大,而是找一个“间隔最大”的分界线——让离边界最近的样本(支持向量)到边界的距离尽可能大。配合核函数之后,SVM 可以在低维线性不可分的数据上“升维”找到可分边界。在鸢尾花数据上用 RBF 核,往往能比线性逻辑回归多纠正几条 versicolor 和 virginica 之间的误分。代价是 SVM 对参数更敏感,C值控制对误分类的惩罚,gamma控制单个样本的影响半径,两个参数调过头都容易过拟合。
3.4 朴素贝叶斯:独立性假设在四个特征上还成立吗
朴素贝叶斯是所有模型里“假设最强”的一个:它认为特征之间在给定类别的前提下相互独立。鸢尾花的四个特征显然不完全独立——花瓣长度和花瓣宽度就有很强的相关性,但朴素贝叶斯在实践中的表现往往出乎意料地好。原因在于它只需要估计每个特征在每个类别下的均值和方差,4 个特征、3 个类别、150 条样本,参数数量足够少,反而降低了过拟合风险。
把这个模型放进比较名单的教学价值在于:它让你看到“错误的假设不一定导致差的结果”。如果朴素贝叶斯的准确率和其他模型差不多,你可以顺着这个现象展开讨论——说明在特征相关性不极端的情况下,独立性假设带来的偏差没大到影响分类结论。这比单纯罗列模型得分更有写头,也是文档说明里最容易拿分的分析点。
3.5 模型统一接口:用字典把比较逻辑“规范化”
前面提到的五类模型各有各的超参数,但训练和评估的流程完全一致。写作业代码时,一个好的习惯是把所有模型放进一个字典,用统一循环处理。这个做法能省掉大量复制粘贴,也能防止“某个模型少写了一行 fit”这类低级错误:
from sklearn.neighbors import KNeighborsClassifier from sklearn.tree import DecisionTreeClassifier from sklearn.linear_model import LogisticRegression from sklearn.svm import SVC from sklearn.naive_bayes import GaussianNB models = { 'KNN': KNeighborsClassifier(n_neighbors=5), 'DecisionTree': DecisionTreeClassifier(max_depth=3, random_state=42), 'LogisticRegression': LogisticRegression(max_iter=500, random_state=42), 'SVM': SVC(kernel='rbf', C=1.0, gamma='scale', random_state=42), 'NaiveBayes': GaussianNB() }字典的键是模型名,值是实例化好的分类器对象。这样设计的好处是:后续无论要跑单次评估还是交叉验证,只需要遍历models.items(),训练、预测、打分都能写在同一套代码里。文档说明里贴数据表时,也能直接按字典顺序输出,不会漏模型。每个模型的参数都已做过初步约束——max_depth=3防决策树过拟合,max_iter=500保证逻辑回归收敛,gamma='scale'让 SVM 按特征数量自动调整核宽度,这些都是保证算法比较公平性的基础。
4. 从数据到结论:一套可复现的算法比较源代码
4.1 搭建实验骨架:统一训练循环与逐模型评估
模型字典定义好之后,下一步就是写训练和评估主循环。作业的源代码里这部分必须清晰到“换个人也能跑通”,因此我会把单次切分评估和交叉验证分开写,先出单次结果,再做稳健性验证。
results = [] for name, model in models.items(): model.fit(X_train, y_train) y_pred = model.predict(X_test) acc = accuracy_score(y_test, y_pred) results.append((name, acc, model)) print(f'{name}: test accuracy = {acc:.3f}')这段循环做的事情很直接:每个模型依次在训练集上fit,在测试集上predict,最后计算准确率。关键在于results列表把每个模型的名称、得分、训练好的模型对象都存了下来,后面的混淆矩阵分析和可视化直接从这个列表里取,不需要重新训练。注意每轮循环里model.fit()是必不可少的——很多新手直接拿初始化好的模型去predict,报错说“模型还没训练”,就是在这一步漏了 fit。
跑完这段代码,你大概率会看到所有模型的准确率都在 90% 到 100% 之间。这是正常的——鸢尾花数据集本身分类难度不高,算法差异不是体现在“谁对谁错”的天壤之别,而是体现在“谁错得更少、错在哪里”。如果某类模型的准确率明显偏低(比如低于 85%),需要回头检查参数设置,而不是急着下结论说“这个算法不行”。
4.2 交叉验证与混淆矩阵:把“比较结果”写成可论证的结论
单次切分结果容易受随机性影响,交叉验证才是作业里更有说服力的证据。继续复用同一个模型字典,用前面定义好的cv对象跑五折交叉验证:
cv_results = {} for name, model in models.items(): scores = cross_val_score(model, iris.data, iris.target, cv=cv, scoring='accuracy') cv_results[name] = scores print(f'{name}: CV accuracy = {scores.mean():.3f} (+/- {scores.std():.3f})')cross_val_score的cv=cv用的是我们预先定义好的StratifiedKFold对象,而不是传一个整数——这样能保证分层采样和随机种子全局一致。scoring='accuracy'指定评估指标为准确率。输出中的均值代表模型在 5 组不同训练/验证划分下的平均表现,标准差则反映了模型对数据划分的敏感程度。对期末作业来说,这个均值就是报告里“比较表”的核心数据。
交叉验证跑完之后,再对每个模型在测试集上生成混淆矩阵并做可视化。用 matplotlib 的matshow或 seaborn 的heatmap都能完成,但要点是让图里的数字清晰可见,而且同一个数据集上所有模型的色阶范围一致,不能某个模型用 0-50 的色阶、另一个用 0-30 的色阶,否则视觉对比会失真。
import matplotlib.pyplot as plt import seaborn as sns fig, axes = plt.subplots(2, 3, figsize=(12, 8)) axes = axes.flatten() for ax, (name, acc, model) in zip(axes, results): y_pred = model.predict(X_test) cm = confusion_matrix(y_test, y_pred) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=iris.target_names, yticklabels=iris.target_names, ax=ax, vmin=0, vmax=50) ax.set_title(f'{name} (acc={acc:.3f})') plt.tight_layout() plt.savefig('confusion_matrices.png', dpi=150)提示:vmin=0, vmax=50是刻意设置的。因为每类测试样本数量为 15(50 条全量数据的 30%),混淆矩阵主对角线最多也就是 15 以内,50 这个上限能让所有子图的色阶基准一致,看色深浅就能横向比较各模型的错分程度。
4.3 结果解读的次序:先看整体,再看错分,最后看边界
拿到运行结果之后,写文档说明的顺序很有讲究。我见过不少作业直接把代码运行输出贴到报告里,没有任何解读,这等于把最有价值的部分丢掉了。一个合理的解读顺序是:
先看交叉验证的平均准确率表,确定模型的“大致档次”。如果所有模型都在 93% 到 97% 之间,就需要用标准差来区分——标准差小的模型更稳定。然后看混淆矩阵,找出具体错分位置。最后一步是画决策边界或特征分布图,用图像说明“为什么错在这里”。以鸢尾花数据为例,常见结论是:setosa 几乎没有错分,错分全部集中在 versicolor 和 virginica 的交界区域;KNN 和 SVM 在交界区域的错分模式不同,KNN 偏向于把 versicolor 尾部样本拉向 virginica,SVM 则更依赖边界几何形状。能写出这样有具体指向的分析,才说明作业不是“代码跑一遍交差”。
5. 算法比较作业里最常翻车的 5 个问题:现象、原因与修法
5.1 随机种子没固定,两次跑出来的结果排名不一样
现象:同一台电脑上先后运行两次代码,KNN 和 SVM 的准确率排名发生互换,报告里的数字和代码实际输出不一致。
原因:train_test_split和cross_val_score内部都有随机性。没有固定random_state时,每次切分出的训练集和测试集不同,模型学到的边界不同,评估分数自然浮动。在只有 150 条样本的数据集上,这种浮动可以轻易达到 2% 到 3%,足以改变排名。
解决:在整个实验代码的入口处统一固定随机性。每个用到随机过程的函数都显式传random_state=42,从数据切分到模型实例化再到交叉验证,一个都不漏。这样任何人跑你的源代码,都会得到完全一致的输出。这也是“可复现实验”的基本要求。
5.2 把全量数据又拿去训练了一次,测试集成“开卷考试”
现象:交叉验证跑出 100% 准确率,但换成单次切分立刻掉到 93%;或者报告里写了“所有模型准确率均为 100%”,明显不合理。
原因:数据划分顺序错了。常见操作是先用load_iris()拿到全量数据,跑完train_test_split之后,又手滑在训练代码里改成了用原始X, y做fit。模型在测试集上见过的样本已经出现在训练数据里,等于考试前先看了一遍答案。
解决:划分之后,把原始数据引用“收起来”。一个可靠的习惯是划分完立刻del X, y,后续所有代码只用X_train, X_test, y_train, y_test。这样就算想用全量数据训练,也会直接报NameError,从机制上杜绝泄漏。
5.3 决策树不设深度限制,报告里贴出一棵没法看的树
现象:决策树模型准确率很高(甚至 100%),但画出来的树有十几层深,整个图密密麻麻全是分支,文档里根本排不下;换成max_depth=3之后准确率下降了一点点,但树的图清晰可读。
原因:决策树的默认参数max_depth=None表示不限制深度,模型会不断切分直到每个叶子节点纯净化。鸢尾花数据集很小,这种深度生长能轻松记住每一条训练样本。过拟合的直接后果就是在测试集上表现不稳定,而且树的可解释性完全丢失。
解决:作业里把max_depth设为 3 或 4。深度为 3 的决策树最多 8 个叶子节点,画出来能放进一页文档,同时准确率通常保持在 94% 以上,属于“牺牲极少量精度换取可读性”的合理取舍。如果想让参数更有说服力,可以用网格搜索验证一下不同的max_depth在交叉验证下的得分曲线,把结果写进文档。
5.4 只看准确率一个指标,忽略了模型之间的“错位”
现象:KNN 和 SVM 交叉验证得分都是 96.0%,报告里结论写成“两者性能相同”,草草结束比较。
原因:准确率是宏观指标,它掩盖了错分样本的分布结构。两个模型总分相同,但 KNN 把 versicolor 错判为 virginica 的有 3 条,SVM 则是把 virginica 错判为 versicolor 2 条。含义完全不同:前者偏向把中间地带样本归为 virginica,后者偏向 versicolor。
解决:强制自己在文档里附上每个模型的混淆矩阵,并逐条查看错分样本的原始特征。如果两个模型总分一样,对比它们的混淆矩阵也是一种有价值的结论——“在总量相同的错分中,不同算法犯错的偏向不同”。这种观察比“分数一样”深刻得多,也更容易在答辩时讲出东西来。
5.5 报告里的可视化结果和代码输出对不上
现象:文档里的混淆矩阵图显示某个模型错分了 2 条,但代码控制台输出的分类报告显示错分为 3 条;导师一问,支支吾吾解释不清。
原因:绘图代码在模型重新训练之后执行,但train_test_split用了不同的随机状态;或者执行顺序混乱,先跑了绘图再跑评估,变量被后续代码覆盖。
解决:Jupyter Notebook 里养成“从头到尾重跑一遍再截图”的习惯;脚本方式运行则保证数据划分代码只出现一次,后续单元格/函数一律通过参数引用已划分好的数据,不做第二次划分。所有报告图片必须是在最新一次完整运行之后生成的,不能把早先的旧图和新的结果混在一起。
6. 把这次作业做成可复用的实验模板:三个进阶习惯
期末作业交上去只是一种“一次性交付”,但如果把这次鸢尾花算法比较当成一个实验模板来打磨,后面换任何数据集都能直接复用。我自己的习惯有三条。
第一条是把模型字典和评估流程封装成函数。这次实验里的models字典、cross_val_score循环、混淆矩阵绘图三段代码,直接合并成一个compare_models(X, y, models, cv)函数,数据换成乳腺癌数据集、手写数字数据集时,函数体一行都不用改。这个模板沉淀下来,比每次重新写一遍脚本有价值得多。
第二条是用花瓣长度和花瓣宽度画出二维决策边界。取全部 150 条样本的两个特征,在二维平面上生成网格点,用训练好的模型预测网格点的类别并把色块画出来,原始样本点叠加在色块上。这张图能直观显示 KNN 的分界是“锯齿状”的、SVM 的分界是“弧线”的、逻辑回归的分界是“直线”的——视觉差异比数字差异更有冲击力,而且答辩时特别好讲。
第三条是写文档说明时固定一个“模板骨架”:数据集描述、评估方法、算法名单与参数、比较结果表、混淆矩阵图、结论分析。这份骨架就是以后做任何机器学习小项目的文档母版,改数据、改模型、改结论,框架不动。这样做的好处是每次实验的对比口径都一致,不会这次用准确率、下次用 F1 分数,导致结果没法横向比较。
xx, yy = np.meshgrid( np.linspace(0.9, 7.2, 200), np.linspace(0, 2.8, 200) ) grid = np.c_[xx.ravel(), yy.ravel()] # 用训练好的 KNN 模型预测网格点类别 Z = knn_model.predict(grid).reshape(xx.shape) plt.contourf(xx, yy, Z, alpha=0.3, cmap='Set1') plt.scatter(iris.data[:, 2], iris.data[:, 3], c=iris.target, cmap='Set1', edgecolor='k')这段代码里np.meshgrid生成的是一个 200×200 的平面网格,grid的每一行是一对坐标;用knn_model.predict预测每个坐标点的类别后,contourf把类别色块绘制出来,原始样本点则用scatter画在色块上方。注意这里只用花瓣长度(特征索引 2)和花瓣宽度(特征索引 3)两个维度,因为它们在区分三个品种时信息量最大,画出来的边界最清晰。
把这三个习惯装进你的实验流程,鸢尾花作业就不再是“交差”,而是一个能持续复用的起步模板。回头看这次作业,我最后悔的一件事是第一版代码里没有固定随机种子——后来要改参数重新跑一遍,发现之前报告里所有数字都变了,只能全部重来。希望这次的踩坑记录能帮你省掉那个反复重跑的夜晚,希望帮到你。
本文还有配套的精品资源,点击获取