资讯详情

资讯详情

多分类逻辑回归如何决定AI可信度?从鸢尾花实验看概率校准

如果你拿同一张蘑菇照片问两个 AI一个说能吃一个说不能吃你会听谁的我最近没有去野外采蘑菇而是把这个问题降维成了一个很老但很合适的数据集——鸢尾花用多分类逻辑回归把整个过程完整跑了一遍。结论比想象中有意思真正决定 AI 值不值得信的其实不是它最后吐出来的那个标签而是它额外输出的一组概率以及这组概率有没有被认真校准。这篇文章写给谁主要是刚接触分类模型、想搞清楚“AI 说能吃”这种结论背后到底有没有依据的人。我会从数据准备、Softmax 原理、sklearn 实现、混淆矩阵解读、概率校准这五个角度把“AI 能不能信”从一句口号变成一串可以操作的检查步骤。即使你之前只听说过逻辑回归也可以跟着代码在自己的电脑上跑一遍。1. 把“蘑菇能不能吃”翻译成一个可复现的AI实验1.1 原本想问AI的问题为什么绕不开鸢尾花在野外问“这朵蘑菇能吃吗”本质上是在问 AI 一个三分类问题这朵蘑菇属于“可食用”“有毒”“未知/待查”中的哪一类。如果 AI 只回一句“能吃”它至少隐瞒了两个关键信息它用了多少特征去做判断它对“能吃”这个结论有多确定这两个问题如果答不上来结论就无从验证。可是真拿蘑菇数据来跑实验问题会变得非常难不同地区的蘑菇形态差异大图片拍摄角度、光线、背景都会干扰判断更麻烦的是有毒蘑菇在样本里往往很稀少模型很容易学到一个“总说能吃”的偷懒策略——因为这样可以拿到很高的总体准确率虽然代价是偶尔漏检一次后果却很严重。所以我把问题抽象出来在一个样本量小、类别清、特征能直接算的多分类任务上先看清楚多分类逻辑回归到底是怎么工作的。鸢尾花数据集恰好是干这件事最经典的试验台。它只有 150 个样本4 个特征3 个类别用逻辑回归几分钟就能跑完而且每一步都能摊开检查。1.2 先约定一个可复现的“可信度”判定流程说“AI 能不能信”不能靠拍脑袋我给自己定了一个可复现的流程固定数据切分方式和随机种子保证实验可以重跑。只让模型看到训练集然后用测试集判断泛化能力。不只看准确率还要看混淆矩阵里哪两类最容易被混淆。要求模型输出每一类的概率而不只是“最可能的类别”。检查概率是否校准模型说有 90% 把握时它是不是真的 10 次里对 9 次。最后一步最容易被忽略。很多入门教程只教你算 accuracy却没有告诉你一个模型完全可能一边准确率很高一边在错误回答时给出非常高的置信度。放到“AI 说这蘑菇能吃”的场景里这就是最危险的地方。2. 鸢尾花数据集与多分类逻辑回归动手前先避开三个坑2.1 数据版本、标签和特征的含义鸢尾花数据集来自测量三种鸢尾的花萼和花瓣分别是 setosa、versicolor、virginica。每条样本有四个特征花萼长度、花萼宽度、花瓣长度、花瓣宽度单位都是厘米。早期版本可能有数据缺失但现在 scikit-learn 里内置的版本比较干净直接加载就行。类别样本数特征数常见的线性可分程度setosa (0)504与其他两类非常容易分开versicolor (1)504与 virginica 存在重叠virginica (2)504与 versicolor 重叠区最多这个结构和蘑菇场景有很强的可比性有些类别很容易识别有些类别在特征空间里挨得很近。换句话说模型可能在 setosa 上做到百分百正确却在 versicolor 和 virginica 之间频繁犯错。如果我们只看总准确率这种不对称的错误分布会被完全藏起来。读取数据的代码是这样from sklearn.datasets import load_iris iris load_iris(as_frameTrue) X iris.data y iris.target print(X.shape) # (150, 4) print(y.value_counts().sort_index())2.2 特征缩放为什么比调参还重要逻辑回归的表达式里有一个权重向量和特征做点积。如果某个特征的数值范围比其他特征大很多模型对它的权重更新就会更敏感L2 正则化也会把不同尺度的权重一视同仁地惩罚结果就是在没有标准化的情况下模型可能把不重要的特征误认为重要。尤其注意标准化的均值和方法必须在训练集上计算再应用到训练集和测试集。这是一个容易出错但也很容易被忽视的细节。from sklearn.preprocessing import StandardScaler scaler StandardScaler().fit(X_train) X_train_scaled scaler.transform(X_train) X_test_scaled scaler.transform(X_test)有人可能会问鸢尾花的特征单位都是厘米为什么还要缩放因为花萼宽度和花瓣长度的标准差差异很大实际计算时梯度下降的路径会变得很扭曲。对于逻辑回归这种线性模型缩放不是可选项而是基本操作。2.3 别再让随机种子毁掉你的实验固定随机种子的价值在于让下一次跑代码的人能看到和你一样的结果。尤其是数据量小到只有 150 条时一次不同的划分可能让准确率波动好几个百分点。另外我还推荐使用分层划分因为鸢尾花每类 50 个样本如果不分层某个类别可能只出现在训练集或测试集里导致实验失真。from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, stratifyy, random_state42 )这样下来训练集有 105 条测试集有 45 条三个类别在两边大致都占三分之一。这个小决定看似无关紧要但在小数据集上它的影响比很多调参动作都大。3. Softmax到底在算什么手写一个多分类逻辑回归3.1 从线性打分到“类内竞争”的概率分布二分类逻辑回归用的是 Sigmoid 函数把分数压到 0 到 1 之间。多分类逻辑回归则用 Softmax它接收所有类别的原始分数再把它们变成一组加起来等于 1 的概率。每个类别都有自己的权重向量 w_k 和偏置 b_k对第 k 类计算一个得分z_k w_k・x b_k然后通过 Softmax 获得每一类的概率P(y k | x) exp(z_k) / (exp(z_1) exp(z_2) exp(z_3))我把这个公式读作“类内竞争”每个类都在争取一个更高的原始分数而 Softmax 把这种竞争变成概率。如果一个类的 z_k 比其他类大出很多它的概率就会非常接近 1模型也会显得非常“自信”。这里有一个非常值得向初学者强调的点多分类逻辑回归不是先训练三个独立的二分类器再硬凑结果。它让所有类别共享输入特征同时把各自的原始分数放在同一个分母里竞争。这正是“AI 说能吃”这种判断应该有的结构模型必须知道说“能吃”不只是它自己说了算还要打败“有毒”和“未知”这两个候选答案。3.2 交叉熵损失让模型在正确类别上“加注”训练多分类逻辑回归时常用的损失函数是交叉熵。它的写法可以非常直观如果真实类别是 k模型给 k 的概率是 p那么这条样本的损失就是L -log(p)如果模型说正确类别的概率是 0.9损失是 -log(0.9) ≈ 0.105如果模型说正确类别的概率只有 0.2损失就会飙升到约 1.609。模型在训练中会拼命给正确类别加概率同时压低其他类别的概率。用 NumPy 手写这个计算反而能让概念更清楚import numpy as np def softmax(logits): # 减去最大值是为了数值稳定防止 exp 溢出 shifted logits - np.max(logits, axis-1, keepdimsTrue) exp_logits np.exp(shifted) return exp_logits / exp_logits.sum(axis-1, keepdimsTrue) def cross_entropy(probs, y_true): n len(y_true) return -np.mean(np.log(probs[np.arange(n), y_true] 1e-12))我曾在真实项目里见过有人把 logits 直接丢进 exp 不处理结果相关性稍微大一点就变成 NaN。减掉最大值只是平移不会改变概率结果却能让计算稳定很多。这个细节看起来小实际排除故障时能省半天时间。3.3 为什么不直接训练多个二分类器有人会问三分类问题能不能训练三个“是不是”的二分类器比如“是不是 setosa”“是不是 versicolor”“是不是 virginica”确实能但这样做有两个问题。第一三个二分类器各自独立训练它们的置信度很难对齐。第二个分类器说“是 versicolor”的概率是 0.7第三个分类器说“是 virginica”的概率也是 0.7那你到底信谁第二使用 one-vs-rest 策略时三个概率加起来不一定是 1模型给“能吃”和“有毒”都打高分的现象很难解释。Softmax 回归天然解决了这个问题所有类别的原始分数被放进同一个 normalize 操作里输出天然归一化。它适合需要明确概率边界和排他性判断的场景比如“可食用、有毒、未知”这类只有一种真实答案的情况。4. 用scikit-learn先跑基线再算一次“AI说能吃”的过程4.1 训练代码和分数不要只看准确率下面是完整的建模代码。我特意使用multi_classmultinomial让 scikit-learn 内部使用真正的 Softmax / 多项逻辑回归而不是 one-vs-rest 的方式。from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, confusion_matrix model LogisticRegression( multi_classmultinomial, solverlbfgs, max_iter1000, random_state42 ) model.fit(X_train_scaled, y_train) train_acc model.score(X_train_scaled, y_train) test_acc model.score(X_test_scaled, y_test) print(ftrain accuracy: {train_acc:.4f}) print(ftest accuracy: {test_acc:.4f})在我固定随机种子的环境下测试集准确率大约在 0.955 到 0.978 之间换句话说45 个测试样本里通常只错 1 到 2 个。如果你在自己电脑上跑出来的数字有小幅波动不用太惊讶因为不同版本的 scikit-learn 在数值优化上可能有细微差异重点是观察错误的分布。4.2 把模型的决策路径完整手算一遍为了让“AI 说能吃”不只是一个黑盒输出我们拆解一次预测过程。模型训练完后model.coef_里保存了每个类别的权重向量model.intercept_保存了每个类别的偏置。某个测试样本的特则向量经过标准化后乘以权重再加偏置得到三个原始分数。比如模型对某个样本可能会给出这样的原始分数setosa: 3.2 versicolor: 1.1 virginica: -2.5用 Softmax 转换后概率大概是setosa: 0.81 versicolor: 0.17 virginica: 0.02这时你就能看懂模型为什么说“这是 setosa”是因为 setosa 的分数比 versicolor 高出不少转化后概率占了优而不是因为模型内部有什么神秘直觉。在实际代码里你不需要自己手动算这些predict_proba做的就是这件事sample X_test_scaled[:1] proba model.predict_proba(sample) print(model.classes_) print(proba)predict_proba返回的每一行是三个类别的概率predict只是取其中最大的一个。这个结构本身就提醒我们AI 的判断不是一个孤立的标签而是一组竞争的分数。4.3 参数差异从基线到更好的效果很多人会纠结solver选哪个。对鸢尾花这种小样本、特征数也不多的任务lbfgs足够稳定也不需要特别多的迭代次数。max_iter1000是为了避免默认最大迭代次数不够导致无法收敛的警告。不要一上来就调正则化参数先用默认的 C1把数据集和特征工程弄干净再回过头来看模型是否过拟合或欠拟合。逻辑回归的可解释性正是它最大的优势你能看到每个类别对每个特征的权重能说出来为什么这个样本被判成“能吃”。换成随机森林或深度网络解释成本要高得多。5. 真正决定“能不能信”的是混淆矩阵和每一类的表现5.1 识别最容易出错的类别比平均分数更关键准确率是一个平均值它不问错误发生在哪里。可对蘑菇场景来说错误发生在哪决定了一切把“可食用”误判成“有毒”浪费食物把“有毒”误判成“可食用”则可能致命。在鸢尾花实验里最典型的错误发生在 versicolor 和 virginica 之间因为它们在花瓣长宽上的分布有重叠。我随机种子下的混淆矩阵形态大概像下面这样真实\预测setosaversicolorvirginicasetosa1500versicolor0141virginica0015如果只看准确率44/45 似乎很不错但看混淆矩阵你马上会发现问题集中在哪一类以及这一类错误的方向是什么。对真实业务而言错误方向比错误数量更重要。5.2 精确率、召回率和F1的多分类读法在多分类任务里每一类都可以单独看三个指标精确率模型说“这是 versicolor”的时候说对的概率。召回率所有真实 versicolor 样本里模型找回来的比例。F1精确率和召回率的调和平均数值偏向两者中较低的那个。以“AI 说这蘑菇能吃”为例真正需要关心的不是总 F1而是“有毒”这一类的召回率真实有毒的蘑菇里有多少条被模型错误地放进了“能吃”这个桶里。只要这类错误不为零模型的平均指标再高也不能用于真实决策。print(confusion_matrix(y_test, model.predict(X_test_scaled))) print(classification_report(y_test, model.predict(X_test_scaled), target_namesiris.target_names))classification_report会把每个类别的精确率、召回率、F1 都列出来。我习惯先看最容易被混淆的类别再看测试准确率而不是反过来。5.3 当概率分布也被输出时风险信息在哪除了标签和混淆矩阵概率分布本身也是一层信息。比如一个模型对某个样本给出setosa 0.98 versicolor 0.02 virginica 0.00和另一个样本给出setosa 0.55 versicolor 0.44 virginica 0.01这两者虽然argmax可能都是 setosa但可信度完全不同。前一个值得进一步确认后一个我们几乎应该要求模型“拒绝回答”。这就是早期入门的同学最容易忽略的地方模型不是只能输出标签它本来就可以输出一个完整概率分布。你要求它只输出“能吃”等于是把最有价值的风险提示丢掉了。6. 对“AI说能吃”该持有的信任边界藏在概率校准里6.1 校准0.8的概率应当恰好有80%是对的模型输出的概率高不代表它一定可靠。概率校准讨论的是模型说 0.8 的那批预测实际正确率是不是也接近 0.8。如果模型总是过度自信就会在错误预测时也给出接近 0.99 的概率。这种情况在神经网络里非常常见在逻辑回归里也可能出现特别是类别重叠区域比较大时。检查校准最简单的方式是把测试集预测概率按最大值分桶看每个桶里的实际准确率。你可以这样写probs model.predict_proba(X_test_scaled) max_probs probs.max(axis1) pred_labels model.predict(X_test_scaled) correct pred_labels y_test.to_numpy() # 粗略分箱单桶太多样本时再细化 for bucket in [(0.0, 0.6), (0.6, 0.9), (0.9, 1.0)]: mask (max_probs bucket[0]) (max_probs bucket[1]) if mask.sum() 0: print(bucket, 样本数, mask.sum(), 实际准确率, correct[mask].mean())如果第一个桶里样本很少不需要太担心但如果高概率桶里的准确率明显低于概率值就要考虑温度缩放或其他校准方法。6.2 温度缩放一个很轻但有效的补救办法温度缩放的思路特别简单在 Softmax 之前把所有原始分数除以一个标量 T。当 T 大于 1 时Softmax 输出的概率会被“拉平”当 T 小于 1 时概率会更尖锐。def temperature_scale(logits, T): return softmax(logits / T)温度缩放不改变模型的预测标签也就是说它不会改变混淆矩阵和准确率只会改变置信度。它是在模型训练完之后在验证集上寻找一个 T让概率更贴近真实频率。它不能拯救一个泛化很差的模型却能修正“答错的时候还特别自信”的结构性问题。放到“AI 说能吃”的场景里校准的意义就是当模型说“能吃”的概率只有 0.62 时它应该在真实场景中也只有六成多把握而不是藏着“其实我完全没把握”的底细。6.3 落到蘑菇场景我会怎么做先说清楚我不建议任何人把 AI 当作野外蘑菇的最终判定工具这篇文章的目标是在鸢尾花上理解多分类逻辑回归的运作逻辑而不是给出采食建议。真实蘑菇识别需要高质量的图像数据、覆盖不同地区的样本、针对误判代价专门设计的拒识策略以及人工复核流程。但我会把从鸢尾花实验里得到的判断方法带过去。如果非要用 AI 辅助我不会只问“能吃不能吃”我会要求它输出完整概率分布并且只看那些概率足够高、且经过独立验证集校准过的结果。只要是概率不够集中、模型在两个类别之间犹豫的判断我会统一当作“不知道”处理。个人经验是多数问题并不出在模型“说不清”而是出在使用者只想要一个明确答案。可分类模型最诚实的表达是一组概率把选择权和责任放在人自己身上。我跑完这个鸢尾花实验后最大的体会是AI 说的不是一个“结论”而是一个“判断的分布”。当你下次看到“AI 说这能吃”这种句子先去问三个问题它输出概率了吗它有没有把每个类别分开交代它有没有在自己拿不准的时候主动拒绝这三个问题比那个标签本身更有价值。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →