鸢尾花线性回归做分类:从连续输出到阈值决策的工程实践
简介这份资源围绕鸢尾花数据集展开线性回归的Python实现面向刚接触机器学习、希望用经典案例打通建模流程的初学者与自学者。它要解决的是如何借助花萼长度、花萼宽度、花瓣长度、花瓣宽度四个特征对Setosa、Versicolour、Virginica三个品种进行分类预测这一基础问题是理解多重变量分析与监督学习思路的典型入口。压缩包为rar格式共2个文件包含1个data数据文件与1个py脚本前者提供建模所需的鸢尾花样本后者承载线性回归的完整代码逻辑整体体积约2KB轻量易取。目前已有11534人学习下载热度较高说明该案例在入门阶段具备较强的参考价值。读者可据此直接运行脚本、观察特征与类别之间的对应关系并在此基础上调整参数、替换模型或扩展特征工程逐步建立对机器学习建模全流程的直观认识。1. 鸢尾花线性回归到底在拟合什么别被三分类标签骗了很多人第一次在鸢尾花数据集上跑线性回归都会卡在一个反直觉的地方标签明明是三个品种线性回归输出却是连续值这俩怎么对上我见过不少新手直接把0/1/2当成连续目标去拟合然后拿predict出来的1.37去四舍五入当类别结果准确率忽高忽低还以为是模型玄学。其实问题不在模型在于你没想清楚「线性回归 鸢尾花」这个组合到底在解决什么任务。鸢尾花数据集Iris一共 150 条样本4 个特征花萼长度、花萼宽度、花瓣长度、花瓣宽度3 个类别各 50 条。线性回归本身是回归器不是分类器。所以标题里这个组合真正能立住的落地路径只有两条一是把三分类拆成「某一类 vs 其余」的二分类用线性回归去拟合 0/1 概率再卡阈值二是干脆只做特征与某个连续量的回归比如用花瓣尺寸去预测花萼长度纯当回归练手。前者是分类入门后者是回归入门选哪条取决于你要验证什么。这篇笔记我按第一条路径走因为它更贴近「鸢尾花 线性回归」这个搜索意图也更能暴露真实工程里的坑阈值怎么定、类别不平衡怎么办、为什么R²在分类任务上会骗你。适合已经会sklearn基本 API、但没认真想过「回归器做分类」边界的人。下面从数据加载一路写到评估每一步都给可复现的代码和参数解释。2. 用 scikit-learn 在本地跑通鸢尾花线性回归的最小闭环2.1 数据加载与二分类目标的构造先把环境钉死避免版本差异带来的FutureWarning干扰判断。我一般用scikit-learn1.3 以上、pandas2.x、numpy1.24 以上Python 3.10 或 3.11 都行。数据集直接用sklearn.datasets.load_iris不依赖外网下载这是它比很多玩具数据集省心的地方。import numpy as np import pandas as pd from sklearn.datasets import load_iris from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error, r2_score # 加载鸢尾花数据集 iris load_iris() X iris.data # shape (150, 4) y iris.target # 0setosa, 1versicolor, 2virginica # 构造二分类目标把 virginica(2) 当作正类其余为负类 y_binary (y 2).astype(int) print(特征形状:, X.shape) print(正类样本数:, y_binary.sum(), 负类样本数:, len(y_binary) - y_binary.sum())这段代码的关键在y_binary (y 2).astype(int)。为什么选 virginica 当正类因为 setosa 和 versicolor 在花瓣长度上几乎线性可分拿它们做二分类太容易看不出问题virginica 和 versicolor 在花瓣宽度上有重叠更能检验线性回归的边界。正负样本比例是 50:100属于轻度不平衡后面评估时要留意。提示不要用iris.target直接当回归目标。三个类别编码成 0/1/2 后数值大小没有数学意义线性回归会强行拟合出「类别 2 是类别 0 的两倍」这种荒谬关系。2.2 划分训练集与测试集时的分层策略二分类任务里如果正类只有 50 条随机划分很可能让测试集里正类只剩个位数指标波动会非常大。所以train_test_split必须加stratify。X_train, X_test, y_train, y_test train_test_split( X, y_binary, test_size0.3, # 30% 做测试150 条里留 45 条 random_state42, # 固定随机种子保证可复现 stratifyy_binary # 按标签比例分层抽样 ) print(训练集正类比例:, y_train.mean().round(3)) print(测试集正类比例:, y_test.mean().round(3))stratifyy_binary保证训练集和测试集里正类比例都接近 1/3。random_state42不是玄学是为了让你我跑出来的数字能对上排查问题时不会因为换了个种子就「时好时坏」。test_size0.3是中小数据集的常见选择150 条样本留 45 条测试既够评估又不至于让训练数据太紧张。2.3 训练线性回归并查看原始输出分布现在直接上LinearRegression但先别急着卡阈值先看它输出的连续值长什么样。model LinearRegression() model.fit(X_train, y_train) # 查看测试集上的原始连续输出 y_pred_continuous model.predict(X_test) print(预测值范围: [{:.3f}, {:.3f}].format(y_pred_continuous.min(), y_pred_continuous.max())) print(前 10 个预测值:, np.round(y_pred_continuous[:10], 3)) print(真实标签前 10 个:, y_test[:10])跑完你会看到预测值大概落在-0.2到1.3之间而不是干净的 0 或 1。这就是线性回归做分类的核心特征它输出的是「倾向正类的程度」不是类别本身。model.coef_是 4 个特征的权重model.intercept_是截距可以打印出来看哪个特征贡献最大。通常花瓣长度和花瓣宽度的系数绝对值最大这和鸢尾花数据的常识一致。2.4 阈值选择0.5 不是唯一答案最直觉的做法是y_pred_continuous 0.5判为正类。但 0.5 只在你把正负类同等看待时才合理。如果业务上漏判正类代价更高阈值就该往下压。from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score for threshold in [0.3, 0.4, 0.5, 0.6, 0.7]: y_pred_label (y_pred_continuous threshold).astype(int) cm confusion_matrix(y_test, y_pred_label) precision precision_score(y_test, y_pred_label, zero_division0) recall recall_score(y_test, y_pred_label, zero_division0) f1 f1_score(y_test, y_pred_label, zero_division0) print(f阈值{threshold} | 混淆矩阵{cm.tolist()} | P{precision:.3f} R{recall:.3f} F1{f1:.3f})这段循环把阈值从 0.3 扫到 0.7每个阈值都输出混淆矩阵、精确率、召回率、F1。你会观察到阈值降低召回率上升但精确率下降阈值升高则相反。选哪个取决于你的场景——如果这是花卉品种初筛宁可多报也别漏报就选低阈值如果是自动定种错报代价高就选高阈值。我一般会先看 F1 最高的那个阈值再结合业务微调。2.5 用回归指标做 sanity check但别用它下结论线性回归自带MSE和R²很多人顺手就拿来评估分类效果这是典型的翻车点。mse mean_squared_error(y_test, y_pred_continuous) r2 r2_score(y_test, y_pred_continuous) print(fMSE{mse:.4f}, R²{r2:.4f})R²在二分类 0/1 目标上通常不会太高0.6 到 0.8 都算正常。它衡量的是连续预测值对 0/1 的拟合程度不是分类准确率。你可以把它当「模型有没有学到东西」的快速检查如果R²接近 0 甚至为负说明特征和目标基本没关系得回去查数据。但最终汇报分类效果必须用混淆矩阵、精确率、召回率、F1 这一套别拿R²当准确率说事。3. 特征工程与参数调整让线性回归在鸢尾花上更稳3.1 四个特征的相关性排查与冗余处理鸢尾花四个特征里花瓣长度和花瓣宽度高度相关花萼长度和花瓣长度也有中等相关。线性回归对多重共线性敏感虽然预测值不一定崩但系数会变得不稳定解释性变差。df pd.DataFrame(X, columnsiris.feature_names) corr_matrix df.corr().round(3) print(corr_matrix)打印相关性矩阵后如果看到某两个特征相关系数超过 0.9可以考虑去掉一个。鸢尾花里花瓣长度和花瓣宽度的相关系数大约 0.96我一般会保留花瓣长度、去掉花瓣宽度或者用PCA压成两个主成分。但注意去掉特征后要重新跑一遍阈值扫描因为预测值分布会变。3.2 标准化到底要不要做LinearRegression本身不要求标准化因为它用最小二乘法解闭式解量纲不影响拟合能力。但如果你后面要加正则化Ridge、Lasso或者想比较系数大小标准化就必须做。from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline pipe Pipeline([ (scaler, StandardScaler()), (lr, LinearRegression()) ]) pipe.fit(X_train, y_train) y_pred_scaled pipe.predict(X_test) print(标准化后预测范围: [{:.3f}, {:.3f}].format(y_pred_scaled.min(), y_pred_scaled.max()))用Pipeline的好处是标准化参数只在训练集上拟合避免测试集信息泄漏。这是很多人手写fit_transform时容易犯的错先在全量数据上fit再划分测试集分布就「污染」了训练过程。标准化后预测值范围会变阈值扫描要重做。3.3 用 Ridge 回归压制过拟合的尝试鸢尾花只有 150 条特征 4 个过拟合风险不大但如果你想练手正则化可以把LinearRegression换成Ridge。from sklearn.linear_model import Ridge ridge Ridge(alpha1.0) ridge.fit(X_train, y_train) y_pred_ridge ridge.predict(X_test) print(Ridge 系数:, np.round(ridge.coef_, 3)) print(Ridge 预测范围: [{:.3f}, {:.3f}].format(y_pred_ridge.min(), y_pred_ridge.max()))alpha控制正则强度越大系数越往 0 压。鸢尾花上alpha1.0和普通线性回归差别很小但你可以扫alpha从 0.01 到 100观察系数和预测值范围的变化。如果alpha太大预测值会被压向均值分类效果反而下降这就是正则化过头的表现。3.4 交叉验证看阈值稳定性单次划分的测试集只有 45 条指标波动大。用交叉验证能看出阈值在不同数据子集上稳不稳。from sklearn.model_selection import cross_val_predict # 用 5 折交叉验证获取每个样本的连续预测值 y_pred_cv cross_val_predict(LinearRegression(), X, y_binary, cv5) for threshold in [0.4, 0.5, 0.6]: y_pred_label (y_pred_cv threshold).astype(int) f1 f1_score(y_binary, y_pred_label, zero_division0) print(fCV 阈值{threshold} | F1{f1:.3f})cross_val_predict返回的是每个样本在它没参与训练的那一折上的预测值比单次划分更可靠。如果某个阈值在 CV 下 F1 明显高于其他阈值那它就更值得选。注意cv5在 150 条数据上每折 30 条正类约 10 条指标仍有噪声但比单次 45 条测试集稳。4. 鸢尾花线性回归做分类的避坑清单4.1 坑一把predict输出直接当类别用现象model.predict(X_test)返回0.87、1.12这种值有人直接round或astype(int)当类别结果1.12变成 10.87变成 1-0.2变成 0看似能用但边界样本全错。原因线性回归输出是连续实数没有概率归一化也没有类别语义。round只是碰巧在 0.5 附近切了一刀和真正的分类决策边界不是一回事。解决显式卡阈值并且把阈值当超参数来调。用(y_pred threshold).astype(int)不要用round。4.2 坑二用R²或MSE汇报分类准确率现象有人写报告说「模型 R²0.78效果不错」但混淆矩阵显示正类召回率只有 0.4。原因R²衡量连续拟合优度对类别不平衡不敏感。正类全预测成 0.33 也能拿到不低的R²但分类完全没抓到正类。解决分类任务只用混淆矩阵、精确率、召回率、F1。R²只用来做「模型有没有学到东西」的快速检查不进入最终结论。4.3 坑三不做分层抽样导致测试集正类太少现象train_test_split没加stratify测试集 45 条里正类只有 5 条召回率算出来要么 0 要么 1波动极大。原因随机划分在样本量小、类别不平衡时正类可能被集中分到训练集。解决二分类必须加stratifyy_binary。如果做多分类stratifyy同样适用。4.4 坑四标准化在划分之前做现象先StandardScaler().fit_transform(X)再train_test_split测试集信息泄漏到训练过程评估指标虚高。原因标准化用到了全量数据的均值和方差测试集分布被提前「看到」。解决用Pipeline把标准化和模型串起来fit只在训练集上发生。手写的话scaler.fit(X_train)之后transform(X_train)和transform(X_test)分开做。4.5 坑五忽略特征相关性导致系数解释翻车现象打印系数发现花瓣宽度系数是负的但常识上花瓣越宽越可能是 virginica符号反了。原因花瓣长度和花瓣宽度高度共线线性回归系数在共线特征间会互相抵消符号不稳定。解决要么去掉一个共线特征要么用Ridge正则化要么只看预测效果不解释单个系数。如果非要解释先算 VIF 或相关性矩阵。5. 把线性回归当基线进阶用法与验证习惯线性回归在鸢尾花上做二分类准确率通常能到 0.93 到 0.97作为基线完全够用。但它的价值不在最终精度而在给你一个「最简可解释模型」的参照系。我一般会拿它和逻辑回归对比同样特征、同样划分逻辑回归的predict_proba输出天然在 0 到 1 之间阈值扫描更顺滑F1 通常略高一点。如果线性回归和逻辑回归差距很大说明数据里有非线性结构该上树模型了。验证习惯上我坚持三件事。第一固定random_state所有对比实验用同一个种子否则数字没法比。第二阈值扫描必须做而且要在交叉验证下做单次划分的阈值不可信。第三混淆矩阵每次都要看不能只看一个 F1 数字正类召回和负类精确率哪个低决定了下一步是调阈值还是换模型。# 一个可复用的对比模板 from sklearn.linear_model import LogisticRegression models { LinearRegression: LinearRegression(), LogisticRegression: LogisticRegression(max_iter200) } for name, clf in models.items(): clf.fit(X_train, y_train) if name LinearRegression: pred (clf.predict(X_test) 0.5).astype(int) else: pred clf.predict(X_test) f1 f1_score(y_test, pred, zero_division0) print(f{name}: F1{f1:.3f})这段模板把线性回归和逻辑回归放在同一划分上跑输出 F1 对比。LogisticRegression的max_iter200是为了避免默认迭代次数不够导致的收敛警告。如果逻辑回归 F1 明显更高说明概率校准和 sigmoid 边界更适合这个任务如果差不多线性回归的简洁性就是优势。最后说个我自己的教训早期我总想一步到位上复杂模型后来发现鸢尾花这种小数据线性回归加阈值扫描就能把问题讲清楚反而更容易定位是特征问题还是阈值问题。把简单模型跑透再上复杂模型排查问题时才有参照。希望帮到你。本文还有配套的精品资源点击获取