☰
逻辑回归鸢尾花分类实战:从手写实现到高分报告
2026/10/10 6:24:15 网站建设 项目流程

简介:面向机器学习课程设计与期末大作业的常见选题,逻辑回归对鸢尾花数据集进行分类是经典入门实践。资源以 Python 语言实现,覆盖数据加载、特征处理、模型训练、分类评估等完整流程,代码附有详细注释,能帮助新手理解逻辑回归的建模思路与关键参数。同时附有实验报告与文档说明,从算法原理到实验结论均有论述,适合作为期末大作业、课程设计的高分参考。压缩包采用 zip 格式,整体约 192.11MB,以 Python 源码、实验报告和文档说明为主,目录结构清晰,简单部署即可运行。已有 265 人学习,对于有机器学习基础、需要快速完成分类任务并撰写报告的学生来说,可以直接对照源码与文档,节省从零搭建和排版时间,也能借此掌握鸢尾花分类这一典型项目的完整实现方法;资源整体设计完整,既可作为平时练习,也能满足期末提交要求。

1. 逻辑回归与鸢尾花分类:为什么这个老模型仍是机器学习大作业的首选

如果你正为“机器学习大作业”选题发愁,想找一个能讲清楚原理、能跑出结果、能被老师追问也答得上来的项目,那“利用逻辑回归进行鸢尾花的分类”几乎是绕不开的模板级方案。它不需要 GPU,不需要大数据集,一个笔记本就能跑完从数据读取到实验报告的全流程。别小看这个看起来“太入门”的项目——真正区分作业分数高低的,从来不是模型本身,而是你有没有把“逻辑回归为什么在鸢尾花上能分类”“三类花怎么用二分类模型去分”“参数怎么调才不翻车”讲透。这篇笔记就把这个项目从数据到报告完整拆开,给你一条直接能照着走的路。

2. 鸢尾花数据集与特征工程:逻辑回归的输入到底怎么准备

2.1 数据集结构:150 条样本、4 个特征、3 个类别

鸢尾花数据集是机器学习里最经典的公开数据集之一,很多初学者第一次接触分类任务就是从它开始的。它包含 150 条样本,每条样本有 4 个数值特征:花萼长度、花萼宽度、花瓣长度、花瓣宽度,单位是厘米;标签是 3 种鸢尾花类别。这个数据规模放在今天看非常小,但正因为小,你才能把每一行数据、每一个特征的分布都看清楚,也才适合用来验证你对逻辑回归原理的理解是否正确。

实际动手前,先花 5 分钟用代码把数据结构和分布摸清楚。不要跳过这一步,后面所有“为什么这个模型能分对”的解释,都建立在你对数据的直观认识上。

import pandas as pd import seaborn as sns import matplotlib.pyplot as plt from sklearn.datasets import load_iris iris = load_iris() df = pd.DataFrame(iris.data, columns=iris.feature_names) df['target'] = iris.target df['species'] = df['target'].map({0: 'setosa', 1: 'versicolor', 2: 'virginica'}) print(df.head()) print(df.describe()) print(df.groupby('species').size())

逻辑说明:load_iris()返回的是 Bunch 对象,data部分是特征矩阵,target部分是整数标签,target_names是标签名。这里把特征和标签合并成一个 DataFrame,方便后续做分组统计和可视化。describe()输出每个特征的均值、标准差、最小值、最大值,能快速看出数值范围差异——注意,花萼宽度的范围和三列花瓣特征不在一个量级上,这通常是后面需要做标准化的直接信号。groupby('species').size()确认三类样本各 50 条,类别完全均衡,所以评估时可以直接看准确率,不用太担心类别不平衡带来的误导。

2.2 特征分布:哪两类花是逻辑回归的“硬骨头”

用 seaborn 画一组特征两两散点图,你会发现一个非常关键的事实:setosa 这个类别几乎可以只用花瓣长度和花瓣宽度就和其他两类完全分开,而 versicolor 和 virginica 在多数特征组合下有明显重叠区域。这个观察决定了你实验报告里“为什么选用逻辑回归”“为什么准确率不是 100%”这两个问题的答案。

sns.pairplot(df, hue='species', vars=iris.feature_names) plt.savefig('pairplot.png', dpi=150) print("散点图已保存")

逻辑说明:pairplot会生成 4x4 的特征两两散点矩阵,对角线上是单特征分布直方图。hue参数按类别着色,能直接看到三类样本在特征空间中的分布。保存图片到本地文件,是为了后面写实验报告时直接引用。注意这里特意把图片分辨率设为 150 dpi——报告里插图清晰度不够,是导师追问“你实验到底做没做”时最常见的破绽之一。

从图里你能得出三个结论:第一,setosa 与另外两类在花瓣相关特征上线性可分性很强;第二,versicolor 与 virginica 在多数特征组合下相互重叠,单靠一条直线做二分类不可能完全分开;第三,因此逻辑回归在这个数据集上的理论准确率上限不是 100%,通常在 95% 到 97% 左右。把这个上限预判写进报告的“预期结果”部分,会让整个实验显得更有思考深度。

2.3 标签处理:从二分类到三分类的 OvR 策略

逻辑回归本身的数学形式是二分类:输出一个 0 到 1 之间的概率,用阈值 0.5 判断正负。那面对三类鸢尾花怎么处理?常见的做法有两种:One-vs-Rest(OvR)和 Multinomial(Softmax)。OvR 的思路是“拆成三个二分类问题”,即分别训练“是不是 setosa”“是不是 versicolor”“是不是 virginica”三个分类器,预测时选置信度最高的那个。Multinomial 则是用 Softmax 直接一次输出三个类别的概率分布。

在 sklearn 的LogisticRegression里,multi_class参数控制这个策略。multi_class='ovr'用 OvR 方式,multi_class='multinomial'用 Softmax 方式。对鸢尾花这种三个类别、样本量很小的任务,两种方式准确率差别不大,但损失函数形式、训练迭代行为有区别。而如果你想完整地“复现逻辑回归原理”,手动写一个二分类逻辑回归、再用 OvR 思路套到三分类上,会比直接调一个现成的多分类模型更能讲清楚原理。

3. 从手写梯度下降到 sklearn 实现:两种路线的完整代码

3.1 手写逻辑回归:最小可运行的梯度下降训练循环

很多同学觉得自己“懂了逻辑回归原理”,但一被追问“sigmoid 输出怎么变成类别”“梯度下降每一步在更新什么”就说不清。用手写代码逼自己把公式变成程序,是最有效的检验方式。下面给出一个结构完整、可直接运行的手写二分类逻辑回归实现,然后用它配合 OvR 完成三分类。

import numpy as np from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler class LogisticRegressionManual: def __init__(self, lr=0.1, n_iters=1000): self.lr = lr # 学习率,控制每一步参数更新的步长 self.n_iters = n_iters # 梯度下降迭代轮数 self.weights = None # 特征权重 self.bias = 0 # 偏置项 def sigmoid(self, z): return 1 / (1 + np.exp(-z)) def fit(self, X, y): n_samples, n_features = X.shape self.weights = np.zeros(n_features) self.bias = 0 self.loss_history = [] # 记录每轮损失,用于画收敛曲线 for _ in range(self.n_iters): linear = np.dot(X, self.weights) + self.bias y_pred = self.sigmoid(linear) # 二元交叉熵损失(加一个极小 epsilon 防 log(0)) loss = -np.mean(y * np.log(y_pred + 1e-9) + (1 - y) * np.log(1 - y_pred + 1e-9)) self.loss_history.append(loss) # 梯度计算:这里是整个方法的核心 dw = (1 / n_samples) * np.dot(X.T, (y_pred - y)) db = (1 / n_samples) * np.sum(y_pred - y) self.weights -= self.lr * dw self.bias -= self.lr * db def predict_proba(self, X): linear = np.dot(X, self.weights) + self.bias return self.sigmoid(linear) def predict(self, X, threshold=0.5): proba = self.predict_proba(X) return (proba >= threshold).astype(int)

逻辑说明:sigmoid把线性输出压缩到 0 到 1 之间,解释为“属于正类的概率”;fit里每一轮先算预测值,再算交叉熵损失,然后计算权重和偏置的梯度,用学习率乘以梯度做参数更新。loss_history是给后面画损失迭代曲线用的——没有这条曲线,你的实验报告里“模型收敛”就是一句空话。predict_proba输出原始概率,predict用 0.5 阈值转成类别。

参数说明:学习率lr=0.1对这个标准化后的数据是合适的;如果学习率设置到 1 以上,损失曲线大概率会震荡甚至发散。迭代次数n_iters=1000在这类小数据集上足够,但注意这里只训练了二分类,要分三类鸢尾花还需要用 OvR 包一层。

3.2 用 OvR 把二分类逻辑回归扩展成三分类

上面实现的是二分类器,面对三类鸢尾花,按 OvR 思路要训练三个二分类器。这里可以用sklearn.multiclass.OneVsRestClassifier直接包装,也可以手动循环。我一般建议手动循环一次,因为这样你能真正理解“每个分类器学到的边界是什么”。下面给出 sklearn 风格的 OvR 包装代码,和手动方式做一个对比。

from sklearn.multiclass import OneVsRestClassifier # 数据准备 X = iris.data y = iris.target # 原始标签是 0, 1, 2 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 方法一:用 sklearn 的 OneVsRestClassifier 包装手写模型 clf_manual = OneVsRestClassifier( LogisticRegressionManual(lr=0.1, n_iters=500) ) clf_manual.fit(X_train_scaled, y_train) acc_manual = clf_manual.score(X_test_scaled, y_test) print(f"手写逻辑回归 + OvR 测试准确率: {acc_manual:.3f}") # 方法二:手动循环训练并预测,便于理解原理 classifiers = [] for cls in range(3): y_binary = (y_train == cls).astype(int) # 当前类别为正类,其余为负类 model = LogisticRegressionManual(lr=0.1, n_iters=500) model.fit(X_train_scaled, y_binary) classifiers.append(model) # 预测时取三个分类器中概率最高的类别 probas = np.column_stack([ model.predict_proba(X_test_scaled) for model in classifiers ]) y_pred_manual = np.argmax(probas, axis=1) acc_manual_loop = np.mean(y_pred_manual == y_test) print(f"手动循环 OvR 测试准确率: {acc_manual_loop:.3f}")

逻辑说明:两种写法等价。单分类器训练时把y_train == cls转成 0/1 二分类标签,三个分类器各负责一个“是某个类别 vs 不是某个类别”的问题;预测时把三个分类器输出的概率拼成矩阵,argmax取概率最高的类别。注意stratify=y让训练集和测试集里三类花的比例与原数据一致,这是防止某类花恰好全落到训练集或测试集的关键。

参数说明:test_size=0.3表示 30% 数据做测试,45 条样本足够评估。random_state=42固定随机种子,保证结果可复现。标准化这里用fit_transform适配训练集、transform适配测试集,绝对不能用全量数据做fit_transform再划分——这是数据泄漏,后面避坑章节还会具体展开。

3.3 sklearn 官方实现:一行代码但参数要懂

手写模型验证了原理,最终提交的作业里通常还是会用 sklearn 的标准实现收尾,因为它在数值稳定性和收敛性上经过了充分优化。但“一行代码跑通”不是终点,你要能解释清楚每个参数为什么这样设。

from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, classification_report, confusion_matrix model = LogisticRegression( multi_class='ovr', # 多分类策略 solver='lbfgs', # 优化算法 max_iter=200, # 最大迭代次数 C=1.0 # 正则化强度的倒数 ) model.fit(X_train_scaled, y_train) y_pred = model.predict(X_test_scaled) print(f"sklearn 逻辑回归准确率: {accuracy_score(y_test, y_pred):.3f}") print(classification_report(y_test, y_pred, target_names=iris.target_names)) print("混淆矩阵:\n", confusion_matrix(y_test, y_pred))

逻辑说明:accuracy_score给出整体正确率,classification_report输出每个类别的精确率、召回率、F1 分数,confusion_matrix给出具体的错分情况。对鸢尾花项目来说,混淆矩阵是最有信息量的输出——它能直接告诉你 versicolor 和 virginica 之间有多少条被混了,而 setosa 永远不出错。你的报告里如果只有准确率而没有混淆矩阵,等于把最有分析价值的一张图漏掉了。

参数说明:multi_class='ovr'这里与手写实现保持一致。solver='lbfgs'是拟牛顿法,适合中小数据集;如果改成liblinear,它只支持 OvR 模式。max_iter=200在标准化后的数据上通常足够,如果收敛警告出现就增大到 500。C=1.0是正则化强度alpha的倒数,C 越大正则化越弱,模型越倾向拟合训练集。

4. 实验报告的组织与写作:一份能拿高分的大作业文档结构

4.1 报告骨架:从“做了什么”到“为什么这样做”

很多大作业的失败不在代码而在报告——模型跑出了 96% 准确率,报告却写成了 pure API 调用说明,导师自然怀疑这不是你自己的理解。我建议报告正文采用固定六段式结构:问题定义、数据描述、模型原理、实验设计、结果分析、结论与改进方向。这里不是让你凑字数,而是每一段都有明确的问答目标。

章节要回答的问题关键内容
问题定义你要解决什么三类鸢尾花分类,类型定义、类别数量
数据描述数据什么样子150 条、4 特征、类别均衡、分布图
模型原理逻辑回归凭什么能分类sigmoid 函数、概率输出、决策边界、OvR
实验设计你怎么做的数据划分比例、标准化操作、参数取值
结果分析结果说明什么准确率、混淆矩阵、错分样本分析
结论与改进还能怎么提升换成 SVM 或调 C 参数后的对比

一个很容易踩的坑是“模型原理”部分从网上抄一段逻辑回归公式就完事。更好的做法是结合你自己的数据写:比如“sigmoid 输出的概率阈值决定了决策边界,而数据在花瓣长度维度上的线性分布让边界可以比较清晰地把 setosa 分出来”。把公式和你画的散点图对应起来,这是“原理”被真正消化的表现。

4.2 可视化必须有的三张图

实验报告里图表不是装饰,是证据。我要求自己至少给出三张图,缺一张都容易被追问。第一张是特征两两分布的 pairplot,证明你对数据有过全貌观察;第二张是损失随迭代变化的曲线,证明模型训练过程是收敛的;第三张是测试集上的混淆矩阵热力图,展示分类结果的细节。

import matplotlib.pyplot as plt import seaborn as sns from sklearn.metrics import ConfusionMatrixDisplay # 损失曲线:用手写模型的 loss_history 画图 plt.figure(figsize=(6, 4)) plt.plot(range(len(clf_manual.estimators_[0].loss_history)), clf_manual.estimators_[0].loss_history) plt.xlabel("Iteration") plt.ylabel("Binary Cross Entropy Loss") plt.title("Loss Curve of Logistic Regression (OvR, class 0)") plt.savefig('loss_curve.png', dpi=150) plt.show() # 混淆矩阵热力图 disp = ConfusionMatrixDisplay(confusion_matrix(y_test, y_pred), display_labels=iris.target_names) disp.plot(cmap='Blues') plt.savefig('confusion_matrix.png', dpi=150)

逻辑说明:损失曲线取的是 OvR 第一个分类器(即“是否为 setosa”)的历史损失,因为 setosa 最容易分,它的损失收敛过程最清晰。如果你想展示三类整体的损失,就画三张子图。ConfusionMatrixDisplay把混淆矩阵封装成可直接显示的对象,cmap='Blues'让数值越大颜色越深,这种图放到报告里时建议用 matplotlib 重新调整坐标轴字体和图片尺寸,避免默认样式太丑。

4.3 实验设计部分:超参数为什么要这样选

报告里写“学习率设成 0.1”很简单,但写“为什么是 0.1”才是拉开差距的地方。我的建议是做一个简短的学习率对比实验:分别用 0.01、0.1、0.5 跑一次,把三条损失曲线叠加到一张图上,然后用一两句话说明差异。这种“对比实验”本身就是最能体现工程素养的部分。

lrs = [0.01, 0.1, 0.5] fig, ax = plt.subplots(figsize=(8, 5)) for lr in lrs: clf = LogisticRegressionManual(lr=lr, n_iters=300) # 用类别 0 的训练数据训练,标准化已在前面完成 clf.fit(X_train_scaled, (y_train == 0).astype(int)) ax.plot(clf.loss_history, label=f'lr = {lr}') ax.set_xlabel('Iteration') ax.set_ylabel('Loss') ax.legend() plt.savefig('lr_comparison.png', dpi=150) print("学习率对比图已保存")

逻辑说明:lr=0.01时损失曲线下降缓慢且最终损失较高,说明收敛不充分;lr=0.5时损失曲线可能出现震荡,因为步长过大越过最优点;lr=0.1是权衡后的选择。这组对比实验放进报告后,你对“超参数怎么调”的解释就有了直接证据。注意这里同样是只对类别 0 做了二分类训练,目的是对比学习率,不需要完整三分类。

5. 避坑清单:鸢尾花逻辑回归最常见的 5 个翻车现场

5.1 没做特征标准化,手写模型损失曲线乱跳

现象:手写的逻辑回归用原始特征训练,损失曲线不下降或者乱蹦,最终测试准确率远低于 sklearn 版本;甚至出现“sklearn 能跑通、手写就废了”的反差。

原因:梯度下降对特征尺度敏感。花萼宽度数值范围在 2 到 4 之间,而花瓣长度范围在 1 到 7 之间,不同维度梯度量级差异大,固定学习率下参数更新方向被大尺度特征主导,落到局部震荡。sklearn 的lbfgs求解器内部有尺度处理机制,所以不像手写实现那么敏感。

解决:训练前用StandardScaler做标准化,把每个特征转成均值 0、方差 1。注意必须在划分训练集后,用训练集的统计量去transform测试集,不能全量数据一起标准化再划分,否则测试集信息泄漏进训练过程,评估出的准确率是虚高的。

5.2 sklearn 的 multi_class 与 solver 搭配不对

现象:设置multi_class='multinomial'加solver='liblinear'时直接报错;或者换成lbfgs + ovr后准确率和之前不一样,自己说不清原因。

原因:在旧版本 sklearn 中,solver和multi_class存在兼容矩阵。liblinear只支持ovr,不支持multinomial;lbfgs两种都支持但默认迭代策略不同。初学者如果随手抄了一段网上代码,参数匹配不对就会翻车。

解决:统一用solver='lbfgs',multi_class显式指定为'ovr'或'multinomial'。做实验时建议两种各跑一次,把准确率差写进报告——“两种策略在鸢尾花上结果几乎一致,说明数据本身对策略选择不敏感”,这就是一个很自然的加分论述。

5.3 只看准确率,不分析混淆矩阵

现象:报告里只有一行acc: 0.96,导师追问“那 4% 错在哪一类、为什么错”,答不上来。

原因:准确率是一个宏观指标,掩盖了类别之间的差异。在鸢尾花数据上,正确的 96% 大概率是把所有 setosa 都分对了,把个别 versicolor 分成了 virginica。只看准确率会错过“错分集中在相邻类别”这个最有价值的观察。

解决:每次训练完先打印classification_report和confusion_matrix,并分析“setosa 不出错,versicolor 与 virginica 互相混”这个现象——它在报告里是绝佳的讨论素材,因为你从这里引出了“类别重叠区域是线性模型边界极限”的结论。

5.4 数据划分不走 stratify,导致某类花在测试集里缺位

现象:测试准确率很高或很低且每次运行都不一样,或者报错说某个类别在预测时没出现过。

原因:train_test_split默认按随机顺序划分。150 条样本里每类只有 50 条,划分比例 70/30 时测试集只有 45 条,随机划分有可能让某种类别在训练集或测试集中数量偏少甚至为 0。

解决:划分时加stratify=y,让训练集与测试集中各类别比例保持一致。另外设置固定random_state,保证多人复核时结果一致。这个小细节同时是报告里“实验方法严谨性”的一种体现。

5.5 报告里图模糊不清,表格没有任何说明

现象:报告的图片是截屏或低分辨率 PNG,文字描述与图表数据对不上;表格贴了一大堆数据却不解释。

原因:报告排版时把图缩小、用默认 72 dpi 截图,导师在屏幕上放大后像素花成一片。数据表格没有上下文,就成了数字垃圾。

解决:所有图片生成时设dpi=150并保存为 PNG 文件;报告中每张图下方配两到三行文字说明“图里能看到什么、说明什么”。表格只保留模型参数表和结果对比表,其他处理过程建议用文字描述。记住一个原则:图表是证明你的实验做过的证据,证据必须清晰、有说明。

6. 进阶技巧:决策边界可视化与在报告里讲透“为什么能分对”

如果你想让这份大作业从“完成”变成“优秀”,我建议你做一张决策边界图。鸢尾花数据有 4 个特征,完整可视化所有维度不现实,但可以选两个最有区分度的特征——通常选花瓣长度和花瓣宽度——在二维平面上画出分类决策区域。做法是生成一个密集的网格点,让模型对每个网格点预测类别,再用等高线填充图展示边界位置。

import numpy as np import matplotlib.pyplot as plt from sklearn.inspection import DecisionBoundaryDisplay X_two = df[['petal length (cm)', 'petal width (cm)']].values X_train_2, X_test_2, y_train_2, y_test_2 = train_test_split( X_two, y, test_size=0.3, random_state=42, stratify=y ) model_2d = LogisticRegression(max_iter=200, multi_class='ovr') model_2d.fit(X_train_2, y_train_2) # 用全部数据作为网格背景 DecisionBoundaryDisplay.from_estimator( model_2d, X_two, response_method='predict', alpha=0.5, grid_resolution=200 ) plt.scatter(X_two[:, 0], X_two[:, 1], c=y, edgecolors='k', cmap='viridis') plt.xlabel('petal length (cm)') plt.ylabel('petal width (cm)') plt.title('Decision Boundary: Logistic Regression on Petal Features') plt.savefig('decision_boundary.png', dpi=150) print("决策边界图已保存")

逻辑说明:DecisionBoundaryDisplay.from_estimator负责在二维特征空间里生成预测网格并填充颜色,grid_resolution=200控制网格密度,数值越大边界越平滑。注意这里只用两个特征做可视化,模型的准确率会低于用 4 个特征的完整模型,这是正常的。这张图的最大价值在于:你亲眼看到了逻辑回归学到的决策边界是“直线”形状,然后就能自然地引出结论——线性模型的能力边界就是“只能画直线”,而 versicolor 和 virginica 的重叠区域决定了准确率上限。

我的个人习惯是,把这张图放在报告的“结论与讨论”部分,配上这样一段话:“从决策边界可以看出,setosa 与其余两类的边界非常清晰,这解释了 100% 的召回率;而 versicolor 与 virginica 在花瓣长度与宽度上有真实分布重叠,任何线性模型都无法完全分开,此误差属于不可约误差。”这段分析比任何调优都能打动审阅者。整个项目跑下来,你收获的不仅是 95% 以上的准确率,更是对“模型边界到底在哪里”的直观理解。希望帮到你。

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

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

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

立即咨询