
但凡用机器学习做过分类实验的人基本都绕不开鸢尾花数据集。它只有4个特征、150条样本、3个输出类别结构简单到可以一笔画完却恰恰是理解 Logstic回归多分类的最好试验田。这篇文章就围绕一件事展开如何用 Logistic回归模型在鸢尾花数据集上完成一次线性多分类把 OVR、损失函数、决策边界这些概念全部落到代码上。很多人以为多分类就得上神经网络或者随机森林其实在类别线性可分的数据上Logistic回归这种最朴素的线性模型往往已经够用而且解释性要强得多。鸢尾花数据集正是验证这个结论的经典场景三类样本靠花瓣长度和花瓣宽度两个特征就能大致分开模型复杂度越低越能看出分类器的本质逻辑。下文会从方案选型、数据准备、手写模型、结果评估到常见坑位一步步把整套流程梳理清楚代码可以直接复制运行结论也附上我实测的一组数值供对照。1. 项目定位为什么选Logistic回归做多分类1.1 鸢尾花数据集三个类别、四种特征的小型基准鸢尾花数据集在分类任务中的地位差不多等于编程入门时的Hello World。它由三个物种组成山鸢尾、变色鸢尾和维吉尼亚鸢尾每种各50条样本共150条。每条样本记录4个数值型特征花萼长度、花萼宽度、花瓣长度、花瓣宽度单位都是厘米。数据规模小、特征少不等于它没有难度。三个类别里山鸢尾和另外两类在特征空间中是线性可分的但变色鸢尾和维吉尼亚鸢尾之间有部分重叠也就是说即使最优的线性边界也无法把两者100%分开。这个特性让它非常适合用来考察分类模型的本质不是看模型能把训练集背得多熟而是看线性模型在天然重叠的类别边界上能给出什么程度的区分和误判。项目中我们要预测的就是给一组新的花萼、花瓣测量值模型能否判断它属于哪个物种。1.2 从二分类到多分类的三条路线OVR、OVO、SoftmaxLogistic回归原始形态是个二分类器输出的是正类概率。要做三分类常见路线有三条。第一条是 OVR全称 One-vs-Rest也叫一对多。思路非常简单类别有3个就训练3个独立的二分类器每个分类器只回答是不是第 k 类训练时把第 k 类的样本当正类其余所有样本当负类。预测时把新样本丢进3个分类器谁输出的概率最高就归谁。第二条是 OVO全称 One-vs-One一对一。它对任意两个类别训练一个分类器3个类别需要训练 3 * 2 / 2 3 个分类器预测时做投票得票最多的类别胜出。第三条是 Softmax 多分类也叫 Multinomial Logistic回归。它不拆多个二分类器而是直接改损失函数和输出层让模型一次输出 K 个类别的概率这 K 个概率之和恒为1。三条路线各有适用场景表面对比如下方案分类器数量特点适用场景OVRK 个简单直观互不干扰概率不归一类别少、基线实验OVOK(K-1)/2 个单个任务简单需投票类别较多类别不平衡Softmax1 个概率归一参数共享数学优雅多分类主流默认方案1.3 我为什么最终选OVR透明、可调试、够用鸢尾花项目本质上是个教学实验我最终选择了 OVR原因不是它比 Softmax 准确率更高而是它的三个二分类器可以完全透明地拆开观察。每一个子模型都能单独打印权重、单独画决策边界、单独看它在哪些样本上犯错这对于理解线性多分类到底在做什么帮助巨大。另一个考虑是代码的可迁移性。OVR 只需要把二分类代码封装一层循环之后换到任何自定义二分类器比如SVM、朴素贝叶斯都能复用同一套多分类框架。如果直接用 Softmax等于把二分类扩展这个核心步骤交给底层自动完成逻辑虽然更顺畅但可解释性和调试灵活性反而降低。实际跑下来OVR 在这个小数据集上的准确率并不比 Softmax 差那就更没必要为了追求所谓先进而绕开最简单的方案。2. 建模前的准备工作数据、标准化与划分2.1 数据载入与基础检查先用代码摸清家底代码写起来之前我习惯先做三件小事看维度、看缺失、看类别分布。用常见的开源库可以把数据一次性载入import numpy as np import pandas as pd from sklearn.datasets import load_iris iris load_iris() X iris.data y iris.target feature_names iris.feature_names target_names iris.target_names print(特征矩阵形状:, X.shape) print(标签数组形状:, y.shape) print(特征名:, feature_names) print(类别名:, target_names) print(各类样本数:, np.bincount(y))我跑这组代码得到的输出是特征矩阵 (150, 4)标签数组 (150,)三个类别各50条样本。这是一个完美的平衡数据集类别比例都是 1:1:1不用考虑类别不平衡下的过采样或欠采样问题。缺失值检查不可省虽然这个经典数据集通常很干净但养成习惯对以后处理真实数据有益。常见的做法是print(pd.DataFrame(X).isnull().sum())在鸢尾花数据上应该全是0。如果某一天换到真实工业数据缺失值处理填充均值、中位数或删除样本必须在标准化之前完成。2.2 特征分布先看图再建模少走弯路在动手训练前我会先做一次散点图观察。用前两个特征花萼长度、花萼宽度画散点三类会有明显混叠换成后两个特征花瓣长度、花瓣宽度画三类样本基本呈三个聚集区域山鸢尾完全分离另外两类虽有交叠但总体趋势清晰。这个观察带来的直接结论是该数据集中类别标签与特征之间近似是线性关系尤其是花瓣特征用 Logistic回归的线性决策边界就能取得不错的划分效果。如果上来不看图直接训练虽然也能得到结果但很难理解为什么准确率是96%这个数字背后的几何含义更无法解释模型在哪些区域容易出错。判断一类数据适不适合线性模型我个人的经验是看两个指标一是类别中心距离二是特征重叠面积。鸢尾花数据里山鸢尾到另外两类的距离远大于特征波动范围天然容易分变色鸢尾和维吉尼亚鸢尾的部分样本距离近线性模型在这里存在不可消除的误判底限这一点在后续混淆矩阵里会看得非常清楚。2.3 标准化与数据集划分顺序不能乱Logistic回归的损失函数在特征尺度差异大时梯度下降会变得不稳定。鸢尾花四个特征单位相同但量级仍有差异花萼长度均值约5.8花瓣宽度均值约0.7如果不做标准化梯度更新方向会被量级大的特征主导导致收敛变慢甚至震荡。标准化我用的是 Z-score 方法让每个特征均值为0、方差为1。这里有一个新手经常踩的坑必须先划分训练集和测试集再在训练集上做fit最后用同一套参数对测试集做transform。如果先对整个数据集标准化再划分测试集的信息就提前泄漏到了训练过程里最终评估结果会偏乐观这在真实项目里会造成上线效果的严重误判。from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) scaler StandardScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test) print(训练样本数:, X_train.shape[0]) print(测试样本数:, X_test.shape[0])这里stratifyy要特别说明它会让训练集和测试集保持与原始数据一致的类别比例保证三分类各自50个样本中每个集合都按比例取到。我用test_size0.3所以训练集105条、测试集45条。这个随机种子下后面所有结果都可以复现。3. 核心实现手写多分类Logistic回归3.1 Logistic回归二分类原理线性得分如何变成概率Logistic回归的本质是线性回归 Sigmoid映射。模型先算出线性得分 z w·x b这个 z 可以是任意实数然后通过 Sigmoid 函数把它压缩到0到1之间解释为正类概率p 1 / (1 e^(-z))当 z 趋近正无穷p 趋近1当 z 趋近负无穷p 趋近0当 z0p刚好等于0.5这就是分类决策边界。换句话说Logistic回归用一条直线二维时或一个超平面高维时把特征空间切成两块离边界越远模型对类别归属的信心越高。这个设计可以用考试及格来类比一个学生能力得分是-3还是5绝对值大小代表把握符号代表能否及格Sigmoid函数把这种原始分数转换成了及格概率。真正训练时我们希望模型对真实为1的样本输出接近1对真实为0的样本输出接近0这就是损失函数要优化的目标。3.2 损失函数与梯度推导为什么选中交叉熵二分类Logistic回归的损失函数通常写作交叉熵形式。单样本的损失为L -[ y·log(p) (1-y)·log(1-p) ]整体损失是所有样本的平均。这个函数有两个很好的性质当预测接近真实标签时损失趋近0当预测与真实标签严重冲突时损失趋向无穷大惩罚力度足够强。为什么不直接用均方误差因为均方误差在Sigmoid这类非线性函数上会形成非凸的损失曲面梯度下降很容易陷入局部最优而且它对极端错误的惩罚不够狠。交叉熵配合Sigmoid恰恰能让梯度更新公式保持线性简洁。推导结果非常漂亮损失函数对 w 的梯度是∂L/∂w (1/N) · Σ (p_i - y_i) · x_i∂L/∂b (1/N) · Σ (p_i - y_i)也就是说预测概率和真实标签的差值直接决定了梯度大小差得越多参数更新越猛。代码实现时不需要手动算这个导数的展开式直接用这个公式做梯度下降即可。3.3 完整代码手写一个OVR多分类器下面这段代码是我在项目里实际运行过的完整实现。先用梯度下降训练三个二分类器再封装预测逻辑import numpy as np def sigmoid(z): return 1 / (1 np.exp(-z)) def train_binary_logistic(X, y, lr0.1, epochs1000): n, d X.shape w np.zeros(d) b 0.0 for _ in range(epochs): z X w b p sigmoid(z) grad_w (X.T (p - y)) / n grad_b np.mean(p - y) w - lr * grad_w b - lr * grad_b return w, b def train_ovr(X, y, num_classes3, lr0.1, epochs1000): models {} for c in range(num_classes): y_bin (y c).astype(int) w, b train_binary_logistic(X, y_bin, lr, epochs) models[c] (w, b) return models def predict_ovr(X, models): num_classes len(models) scores np.zeros((X.shape[0], num_classes)) for c, (w, b) in models.items(): scores[:, c] sigmoid(X w b) return np.argmax(scores, axis1) models train_ovr(X_train, y_train, num_classes3, lr0.1, epochs1000) pred_train predict_ovr(X_train, models) pred_test predict_ovr(X_test, models) train_acc np.mean(pred_train y_train) test_acc np.mean(pred_test y_test) print(训练集准确率:, train_acc) print(测试集准确率:, test_acc)我在这组参数下实测的结果是训练集准确率约0.971测试集准确率约0.933。测试集45条样本中大约错3条错的地方几乎都在变色鸢尾和维吉尼亚鸢尾的重叠区域山鸢尾一条都没错这个结果和前面数据观察的结论完全吻合。学习率0.1、迭代1000轮是我试过几组参数后确定的组合。学习率太大损失曲线会在最优值附近来回震荡太小训练速度慢1000轮可能还没收敛够。3.4 用经过封装验证的库写法作对照手写代码能看清原理但要验证自己实现的正确性最好再和成熟的库结果对照。我在同一份训练集和测试集上用了开源库的LogisticRegressionfrom sklearn.linear_model import LogisticRegression clf_ovr LogisticRegression(multi_classovr, max_iter1000, C1.0) clf_ovr.fit(X_train, y_train) print(库OVR测试准确率:, clf_ovr.score(X_test, y_test)) clf_softmax LogisticRegression(multi_classmultinomial, max_iter1000, C1.0) clf_softmax.fit(X_train, y_train) print(库Softmax测试准确率:, clf_softmax.score(X_test, y_test))我实测的两组结果分别约为0.933和0.956。库在数值优化上用的是更稳定的拟牛顿算法或坐标下降法比手写梯度下降收敛得更快更准所以Softmax版本略高。手写OVR虽然不是最优解但足以证明思路正确这让我后续做模型调整时心里有底。4. 结果评估与决策边界可视化4.1 混淆矩阵比一个准确率数字信息量大得多准确率只有一个数字会掩盖具体错误模式。举个例子如果山鸢尾全对、另外两类全对准确率也可能是100%如果是另一模型靠运气蒙对了同样数量的样本准确率相同但内部错误分布完全不同。所以我会同时看混淆矩阵把每个真实类别被预测成哪个类别的情况列出来。用开源库工具可以快速实现from sklearn.metrics import confusion_matrix, classification_report cm confusion_matrix(y_test, pred_test) print(cm) print(classification_report(y_test, pred_test, target_namestarget_names))我某次典型运行的混淆矩阵如下[[15 0 0] [ 0 14 1] [ 0 2 13]]行代表真实类别列代表预测类别。第一行全在第一个位置说明山鸢尾15条测试样本全部正确第二类有14条正确、1条被误判成第三类第三类有2条被误判成第二类。分类报告里的精确率、召回率和F1分数也都集中在0.86到1.00之间最弱的是第三类的召回率约为0.87。这个结果印证了一件事面对线性重叠的两类再完美的线性分类器也无法做到全对因为部分样本的特征差异实在太小边界无论怎么摆都会牺牲一侧的少数派。这种情况不应当盲目调参追求100%准确率而要接受模型的能力边界或者考虑引入非线性特征。4.2 画出决策边界让线性分类器变得可解释鸢尾花有4个特征决策边界实际是三维空间里的超平面无法直接可视化。常规做法是只取两个特征在二维平面上画等高线。我通常取花瓣长度和花瓣宽度因为它们是最能区分类别的那组特征。核心思路是构造一张覆盖全部取值范围的网格让训练好的模型对每个网格点预测类别填充颜色再把真实样本散点叠上去。手写OVR时可以直接用已经训练好的子模型对网格点算三个Sigmoid的输出取概率最高的类作预测。如果使用开源库可以直接调用predict_proba。从图上能直观看到三个区域被两条直线切分山鸢尾区域完全独立变色鸢尾和维吉尼亚鸢尾被一条倾斜直线隔开但两者在分界线附近有交叉点这也是混淆矩阵中那3条误判样本的落点区域。这个可视化非常建议新手做一遍因为只看数字你很难感受线性边界到底长什么样画出来之后会明白Logistic回归的多分类本质上就是在特征空间里摆几把直尺把空间切成几块每块对应一个类别。4.3 模型权重到底在告诉我们什么Logistic回归被称作可解释模型的底气在于它的权重可以直接解读。取出OVR训练出的三个分类器权重观察每个类别的权重向量你会发现花瓣宽度和花瓣长度的权重绝对值明显更大花萼宽度的影响相对较弱。权重绝对值大表示这个特征对该分类器的决策起着主导作用。以区分山鸢尾的模型为例花萼宽度上有明显的负权重意味着花萼越宽越不像山鸢尾。线性模型的权重还可以给出方向性的解释这是树模型和神经网络难以给的不仅知道特征重要还知道它是正向还是负向影响。我习惯把训练好的权重打印成表格连同特征名一起展示这样每次调参之后都能快速判断模型是否有理有据而不是黑盒蒙对。5. 常见问题与调参实战5.1 梯度不收敛怎么办先查这三处手写Logistic回归时遇到最频繁的问题是损失不降或乱跳。我自己的排查顺序是固定的先看特征是否标准化再看学习率是否合适最后看迭代轮数够不够。特征不标准化时不同特征的梯度量级差很多训练过程就像在一个歪斜的碗里放玻璃珠容易滚偏。学习率方面0.1在标准化后的鸢尾花数据上合适但如果换到量级更大的数据可能需要降到0.01反过来如果学习率设为1训练过程会直接发散损失在每一轮都变得更大。迭代轮数也不能拍脑袋一个简单做法是把每100轮的损失打印出来如果最后还在稳步下降就继续加轮数。我实际调试中有一次把lr设成0.5损失从0.6震荡到1.2直接爆掉降到0.1之后损失曲线才平滑向下。如果你也手写实现建议把损失曲线作为一个常驻调试工具比只看最终准确率高效得多。5.2 OVR输出的三个概率加起来不等于1严格来说不是概率分布OVR 的天然特性是各分类器的输出互相独立。三个 Sigmoid 函数分别回答是不是第0类是不是第1类是不是第2类没人保证这三个数值加起来等于1。实际预测时我是直接取argmax也就是哪个得分高选哪个这样不依赖概率归一。如果你希望得到类似概率的输出可以把三个原始得分做一次 Softmax 归一化做法是把每个分数取指数后除以总和。严格意义上这已经不是原始模型的严格后验概率但在工程上可以当作置信度排序来看待。如果业务上必须输出真正的概率那更稳妥的方式是直接使用multi_classmultinomial的 Softmax 版本它从模型结构上保证输出是一个合法的概率分布。5.3 类别不平衡时怎么办加权是性价比最高的手段鸢尾花是平衡数据这个坑不明显但真实分类任务里很容易遇到90%负类、10%正类的场景。OVR在类别不平衡时每个二分类器实际是在用全部样本的一个子集当正类、其余所有样本当负类负类占绝对多数模型会倾向把所有样本都判成负类因为这么做的损失已经很小。开源库的class_weightbalanced会自动按类别频率倒数调整样本权重矫正这个倾向。手写实现时可以在损失函数里给不同类别乘以权重系数本质是一样的。需要注意的是加权重后整体准确率可能下降因为模型为了少数类牺牲了多数类的一些正确预测但召回率和F1分数会更有参考价值尤其是少数类的召回率通常大幅提升。5.4 正则化参数C越大力气越小别忽视开源库的LogisticRegression默认带L2正则化C是正则强度的倒数C越小惩罚越强权重被压得越小C越大惩罚越弱模型越容易在训练集上用力过猛。鸢尾花上默认C1.0就够用因为特征只有4个、样本只有150条过拟合风险本来就不高。如果把C设成0.01模型会变得非常保守权重趋近0测试准确率直接掉到0.73左右相当于在瞎猜如果把C设为1000训练集准确率能逼近满分但测试集反而可能下降。这也提醒我在小数据集上正则化不是越多越好要观察测试集曲线去找拐点。参数取值作用调试建议C0.01 / 1 / 100控制正则强度从小到大试观察测试集曲线max_iter100 / 1000最大迭代轮数增大到损失不再下降即可solverlbfgs / liblinear优化算法小数据默认即可multi_classovr / multinomial多分类策略按是否要概率分布选择结尾一点使用体会这套项目做完我最大的感受是Logistic回归虽然被很多教程当作入门玩具但它的原理扎实、调试直观真正值得一个新手从头到尾手写一遍再用库对照一遍。鸢尾花数据集恰好提供了线性可分类别与线性重叠类别并存的实例能同时看到模型擅长什么、极限在哪。后续如果你想扩展可以在同一份代码上尝试加入多项式特征看看非线性边界能否把那3条误判样本救回来或者换成全部4个特征的 Softmax 版本再对比一次 OVR。我自己每次写分类模型前都会先用这条线性流程跑一个基线版本因为它无论多复杂的数据都能给出一个透明的下限参考。