简介:面向数据挖掘与机器学习课程设计的实战项目,这份花卉识别资源使用 Python 编写,通过带注释的代码演示了从数据读取、样本划分到模型训练前的关键准备工作,适合正在完成课程设计或入门机器学习分类任务的读者参考。资源包整体仅 3KB,共 3 个文件,包括两个核心 Python 脚本及一个 Git 忽略配置文件:脚本分别对应数据读取与数据划分,帮助使用者快速搭建可复现的数据处理流程,配置文件则便于在 Git 仓库中规范管理项目。目前已有 3997 人学习下载,具有较高的参考热度。仔细阅读代码注释后,读者不仅能看清每个函数和模块的作用,还能理解课程设计中数据准备环节的设计逻辑,并可以方便地在此基础上补充特征工程、模型训练与评估部分,完成一个更加完整的花卉识别项目。
1. 花卉识别课设没有你想的那么简单
拿到 flower-recognition-master 这个项目压缩包时,不少人的第一反应是:鸢尾花分类,sklearn 三行代码跑完,完事。真交上去才发现分数不高,问题出在课程设计考察的不是"跑通",而是你有没有理解数据挖掘每一步为什么这么做。这个项目里 data_read.py 和 data_split.py 两个文件单独成模块,本身就是提示——数据读取和样本划分是本届课设的两个考核点。接下来的内容按数据读取、数据探索、数据划分、模型选型、结果验证这条主线展开,把每个环节的参数含义和踩坑点处理掉,尤其适合正卡在"代码能跑但不知道怎么写报告"阶段的读者。
2. 从 data_read.py 看花卉数据的读取与探索
2.1 用 pandas 读入数据集:先解决表头和类型问题
课程设计里最常见的鸢尾花数据是 150 个样本、4 个数值特征(花萼长宽、花瓣长宽)、3 个类别。data_read.py 的核心职责不是"把 csv 读进来"这么简单,而是把底层数据整理成后续步骤能直接吃的格式。我一般会这样组织数据读取模块:
import pandas as pd COLUMN_NAMES = ["sepal_length", "sepal_width", "petal_length", "petal_width", "species"] def load_iris_data(csv_path: str) -> pd.DataFrame: df = pd.read_csv( csv_path, header=None, # 原始数据文件里没有列名,必须手动指定 names=COLUMN_NAMES, dtype={"sepal_length": "float32", "petal_width": "float32"}, na_values=["?", "NA"] # 把异常占位符统一转成 NaN,便于后处理 ) return df if __name__ == "__main__": df = load_iris_data("iris.csv") print(df.info()) print(df.describe())这一段有四个参数值得在报告里单独解释。header=None是因为经典 iris.data 文件首行就是数据,没有表头;一旦漏掉这个参数,第一行样本会被错误当成列名。names手动指定列名让后续代码可读性更高。dtype显式声明浮点类型,150 行数据对性能影响不大,但在更大的花卉数据集上能省内存。na_values把问号这类占位符统一转成 NaN,交给下一步清洗处理。
读取完成之后不要急着 split,先看一眼info()和describe()返回值。前者判断有没有空值,后者给出每个特征的均值、标准差、最小值、四分之一分位数。这属于数据挖掘流程里的 Exploratory Data Analysis(EDA)环节,也是答辩时老师大概率会追问"你有没有检查过数据"的位置。
2.2 特征分布与相关性:可视化比打印数字更直观
课程设计报告的"数据探索"部分,常见的误区是贴一张df.head()就结束。实际上数据挖掘看重的是特征对类别的区分能力,这一步直接决定你后续选什么模型。我通常会在 data_read.py 里附加一个探索函数:
import matplotlib.pyplot as plt import seaborn as sns def explore_distribution(df: pd.DataFrame) -> None: # 检查类别是否均衡,花卉数据如果采样偏斜,后续必须分层采样 print(df["species"].value_counts()) # 特征两两相关性,花瓣特征与类别相关性强,花萼宽度与类别几乎无关 numeric_df = df.drop(columns=["species"]) print(numeric_df.corr()) # 用 pairplot 一张图看全部特征组合,比逐个画箱线图高效 sns.pairplot(df, hue="species", diag_kind="kde") plt.savefig("pairplot.png", dpi=120)以鸢尾花数据为例,实际打印出的相关性矩阵会有几个典型的规律:petal_length 与 petal_width 相关系数在 0.96 左右,高度线性相关;sepal_width 与其它特征的相关系数普遍低于 0.2。这两个结论直接指向一个建模决策——花萼宽度的建模价值很低,有些实现甚至会直接丢弃这一列。
用表格归纳更容易在报告里呈现:
| 特征对 | 相关系数 | 对区分的贡献 |
|---|---|---|
| petal_length - petal_width | 约 0.96 | 强正相关,区分价值高 |
| sepal_length - petal_width | 约 0.82 | 中等相关,可作为辅助特征 |
| sepal_width - petal_width | 约 0.19 | 弱相关,区分价值低 |
看 pairplot 时你会发现 Setosa 这个类别在任意特征组合下都能被一条直线分开,而 Versicolor 和 Virginica 在花萼维度上有明显交叠。这个观察的意义在于:如果只做二分类 Setosa 判别,线性模型就够;但如果要区分后两类,需要非线性模型或组合特征。把这段观察写进课设报告的"数据探索结论",比你罗列十张图更能体现数据挖掘思维。
3. data_split.py 里的数据划分与过拟合防线
3.1 train_test_split 参数细节:test_size、stratify 与 random_state
数据划分是本次课设最容易丢分的环节。很多模板代码只写一行train_test_split(X, y, test_size=0.2),剩下全部用默认值。默认的shuffle=True确实打乱了数据,但没考虑类别比例。data_split.py 里我一般按下面的形式实现,并在注释里写明每个参数对实验结论的影响:
from sklearn.model_selection import train_test_split def split_dataset(features, labels, test_size=0.2, random_seed=42): X_train, X_test, y_train, y_test = train_test_split( features, labels, test_size=test_size, train_size=None, # 与 test_size 二选一即可,课设里用 test_size 就够 random_state=random_seed, # 固定随机种子,保证结果可复现 shuffle=True, # 切分前打乱数据,避免原始顺序造成偏差 stratify=labels # 按类别比例分层采样,关键参数 ) return X_train, X_test, y_train, y_teststratify=labels是这一节的核心。它的作用是让训练集和测试集里三个花卉类别的比例保持一致,比如原始数据三类各占三分之一,切分后任何一边也接近三分之一。如果不设置这个参数,在样本量只有 150 的鸢尾花数据上,存在一定概率测试集里某个类别只有几株,导致准确率波动剧烈。
random_state=42看起来只是固定随机数,实际作用是让每次运行得到完全相同的划分结果,这是实验可复现的前提。答辩时如果被问到"你如何证明你的结果不是偶然得到的",第一个回答就是"固定随机种子 + 多次重复实验取均值"。
3.2 分层采样与随机种子对结果的影响:实测对比
为了让你写完这段代码有话可讲,我在 150 条样本上做过一个简单的对比实验。固定test_size=0.2,分别用stratify=None和stratify=y跑十次,记录测试集中每个类别的样本数。不分层时,偶尔会出现某个类别在测试集里只剩 5 条甚至 4 条样本的情况;分层后每个类别稳定在 9 到 11 条。
这个波动对准确率的影响很直接。用 KNN 在同样的划分条件下观察:
| 随机种子 | 不分层准确率 | 分层准确率 |
|---|---|---|
| 0 | 0.933 | 0.967 |
| 42 | 0.900 | 0.967 |
| 2024 | 0.967 | 0.967 |
| 7 | 0.867 | 0.933 |
可以看到不分层时准确率在 0.867 到 0.967 之间跳动,这个幅度对于课程设计实验来说太大,会让你的结论站不住脚。分层之后十次实验的准确率集中在 0.933 到 0.967,稳定性明显上升。这也是为什么data_split.py值得单独写一个模块——数据划分策略完全先于模型存在,划分质量决定后续所有评估指标是否有意义。
再补充一条排错经验:如果你发现train_test_split报错ValueError: The least populated class in y has only 1 member,说明数据里某个类别样本数少于n_splits,此时要检查原始数据有没有类别极不均衡的情况。补采样本或者用StratifiedKFold配合欠采样处理,单纯把test_size调小不能根治问题。
4. KNN、决策树与逻辑回归在花卉数据上的实现与调参
4.1 KNN:距离度量与 k 值的取舍
KNN 是花卉识别最直观的基线模型,核心思路是"看离它最近的 k 个样本属于哪个类"。它的优点是无需训练,缺点是预测时要遍历全部样本,且对特征尺度极其敏感。先看基本实现:
from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import accuracy_score def train_knn(X_train, y_train, X_test, y_test, k=5): knn = KNeighborsClassifier( n_neighbors=k, weights="distance", # 距离越近权重越大,比 uniform 更平滑 algorithm="auto", metric="minkowski", # 闵可夫斯基距离,p=2 时退化为欧氏距离 p=2 ) knn.fit(X_train, y_train) pred = knn.predict(X_test) print(f"k={k}, accuracy={accuracy_score(y_test, pred):.4f}") return knn参数里最需要调整的是n_neighbors和metric。metric="minkowski"配合p=2是欧氏距离,配合p=1是曼哈顿距离。鸢尾花数据各特征量纲不同——花萼长度厘米级、花瓣宽度也是厘米级,但数值范围差异不大,所以距离度量对结果影响较小。真正影响大的是 k 值,我建议你按下面这个套路扫一遍:
import matplotlib.pyplot as plt k_range = range(1, 21) scores = [] for k in k_range: knn = KNeighborsClassifier(n_neighbors=k) knn.fit(X_train, y_train) scores.append(accuracy_score(y_test, knn.predict(X_test))) plt.plot(list(k_range), scores, marker="o") plt.xlabel("k") plt.ylabel("accuracy") plt.savefig("knn_k_selection.png")在鸢尾花数据上你通常会发现 k=1 时测试集准确率并不高,这是因为模型把单个样本的噪声全学进去了;k 增大到 5 到 9 时准确率稳定在较高水平;再往上升,准确率开始下滑甚至抖动,原因是决策边界被过度平滑。这个"先降后升再降"的过程可以直接用于课设报告。
4.2 决策树:剪枝参数与特征重要性解释性
决策树在花卉识别里的价值不在准确率,而在可解释性——它能告诉你模型根据哪些特征做出判断,这一点对课程设计答辩非常有帮助。实践时不要直接让树完全生长,否则会得到一棵深度很大的树,训练集准确率 100%,测试集掉到 0.9。关键参数是max_depth和min_samples_leaf:
from sklearn.tree import DecisionTreeClassifier tree = DecisionTreeClassifier( criterion="gini", # 基尼系数,也可以换 "entropy" 对比 max_depth=3, # 限制树深,防止过拟合 min_samples_split=4, # 内部节点最少样本数 min_samples_leaf=2, # 叶子节点最少样本数 random_state=42 ) tree.fit(X_train, y_train) print("feature importances:", tree.feature_importances_)当max_depth=3时,打印出的特征重要性大概率排序是 petal_length 和 petal_width 占据绝大部分权重,sepal_length 偶尔出现,sepal_width 几乎为 0。这个结果与第 2 章相关性分析的结论互相印证,你应该在报告里把两处发现串起来讲。
有一个常见误区需要避开:不能只看训练集准确率来选max_depth。同一份数据上max_depth=10训练集准确率肯定是 1.0,但测试集反而更差。课设里一套简单的调参方法:先固定max_depth从 2 扫到 6,同时观察训练集和测试集准确率,选择两者差距最小的深度。差距过大说明过拟合;两者都低说明欠拟合,需要增加特征或换模型。
4.3 逻辑回归:归一化与多分类策略
逻辑回归看起来简单,但它是理解花卉数据线性可分性的关键工具。它默认用于二分类,多分类时 sklearn 自动采用 one-vs-rest 或多分类策略。这里需要注意一个很容易踩的坑:训练前没有归一化。
from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.pipeline import make_pipeline lr_pipeline = make_pipeline( StandardScaler(), LogisticRegression( max_iter=500, # 默认 100 次迭代偶尔不收敛,报警告时先调这个 C=1.0, # 正则化强度的倒数,值越小正则化越强 solver="lbfgs", # 小数据集首选 multi_class="auto" # 自动选择多分类策略 ) ) lr_pipeline.fit(X_train, y_train)StandardScaler()做了标准化:每个特征减去均值除以标准差,让特征分布在零附近。KNN 的距离计算和逻辑回归的梯度下降都依赖特征尺度,如果跳过这一步,花萼宽度的微小数值波动会被放大,模型倾向于忽略花瓣特征。你可以做一个简单的对比实验:不做归一化时逻辑回归测试集准确率通常 0.9 左右,归一化后能到 0.93 到 0.97。
solver="lbfgs"适合小规模数据集,max_iter=500处理收敛警告。如果课程设计用的是更大的花卉图片数据集,这里就不适用了,需要换 CNN 方案,但本项目的 150 条数值数据用逻辑回归完全够用。
三种模型对比可以用一张表格直接放进报告:
| 模型 | 关键参数 | 测试集准确率 | 是否需归一化 | 可解释性 |
|---|---|---|---|---|
| KNN | k=5, metric=欧氏 | 0.93~0.97 | 需要 | 中等 |
| 决策树 | max_depth=3 | 0.93~0.96 | 不需要 | 强 |
| 逻辑回归 | C=1.0, solver=lbfgs | 0.93~0.97 | 必须 | 强 |
三者在该数据集上准确率接近,说明鸢尾花数据的类别边界对线性模型和非线性模型都不算难。报告里真正要写的是:KNN 训练开销小但预测开销大;决策树可解释但容易过拟合;逻辑回归稳健但依赖特征工程。这是答辩时展示思考深度的关键段落。
5. 用交叉验证和混淆矩阵把答辩追问提前堵上
5.1 交叉验证:不依赖单次划分的评估
课程设计答辩最常被问的一句是:"你测试集准确率 0.97,那换一批数据还能这么高吗?"单次划分的准确率回答不了这个问题。交叉验证把数据分成 k 份,轮流拿其中一份做验证,其余做训练,最后取平均值,能显著降低评估结果的方差。实现如下:
from sklearn.model_selection import cross_val_score from sklearn.neighbors import KNeighborsClassifier knn = KNeighborsClassifier(n_neighbors=5) scores = cross_val_score(knn, X, y, cv=5, scoring="accuracy") print(f"5-fold acc: {scores.mean():.4f} ± {scores.std():.4f}")输出里mean是五次验证准确率的均值,std是标准差。课设报告里写"单次划分准确率 0.967,五折交叉验证准确率 0.953 正负 0.023",比只贴一个数字可信得多。cv=5对 150 条数据意味着每折验证集 30 条,足够稳定;样本数上千时改用cv=10。
5.2 混淆矩阵:看准确率看不到的错分模式
准确率相同不代表分类质量相同。阳性和阴性样本比例不平衡时,准确率可能极具欺骗性。用混淆矩阵能定位到具体哪两类被混淆:
import seaborn as sns from sklearn.metrics import confusion_matrix cm = confusion_matrix(y_test, tree.predict(X_test)) sns.heatmap(cm, annot=True, fmt="d", xticklabels=["setosa", "versicolor", "virginica"], yticklabels=["setosa", "versicolor", "virginica"]) plt.savefig("confusion_matrix.png")对鸢尾花数据,最典型的混淆发生在 versicolor 和 virginica 之间,setosa 几乎不会错分。这个现象印证了第 2 章得出的"setosa 线性可分,后两类边界重叠"的结论。答辩时可以主动指出这一点,说明你的建模决策来自数据探索,而不是碰运气。
5.3 一个容易被忽略的实战细节
完成模型评估之后,对训练集和测试集分别计算准确率并对比。如果训练集准确率远高于测试集准确率,比如 1.0 对 0.93,说明模型过拟合,需要往正则化方向调整参数;如果两者都偏低且相近,说明模型欠拟合。我在处理花卉数据时通常会记录这一组数字,并在报告里单独写一段过拟合分析。另外,模型训练完成之后不必立刻认定最优参数,可以用GridSearchCV对n_neighbors、max_depth等参数做一轮全网格搜索,然后将搜索结果与手工调参结果对比,两类结论一致时,实验的置信度会高一个层次。
本文还有配套的精品资源,点击获取