首页 / 资讯中心 / 文章详情

sklearn准确度评估:accuracy_score与平衡准确度实战

sklearn准确度评估:accuracy_score与平衡准确度实战 ★ FEATURED ARTICLE
1. 准确度这个指标为什么值得单独拎出来讲accuracy准确度大概是所有分类评估指标里最没门槛的一个。公式简单到一句话就能说完预测对的样本数除以总样本数。正因为太简单很多人在项目里就是一行accuracy_score(y_true, y_pred)完事然后拿着一个 0.97 的数字去汇报结果上线之后业务方反馈模型效果一塌糊涂。问题不出在 sklearn 的代码有 bug而是准确度这个指标本身对数据分布极度敏感对调用方式也有一堆隐性要求。我在实际项目里见过太多次类似情况某开发者拿到一份样本比例接近 1:99 的数据集训练完准确度 0.98全组都觉得模型不错最后把混淆矩阵打印出来才发现模型其实把所有样本都判成了多数类——那个 0.98 跟什么都不预测直接蒙多数类的基线完全一样模型等于什么都没学到。还有一种情况更隐蔽把predict_proba输出的概率矩阵直接传给了accuracy_scoresklearn 不报错安安静静返回一个 0.03 左右的分数人还以为是模型太差需要调参。这篇内容就是把sklearn里跟准确度相关的东西彻底拆开讲一遍accuracy_score到底在算什么normalize参数的作用多分类和多标签场景下判定逻辑的差异balanced_accuracy_score和top_k_accuracy_score分别解决什么痛点以及在交叉验证里怎么把准确度的置信区间一起算出来。适合刚入门分类任务、想把评估环节做扎实的读者也适合已经写过不少模型但没系统梳理过指标口径的人。代码全部基于常见的 Python 数据栈可以直接复制到 notebook 里跑。2. 准确度的三种口径与 sklearn 里的四个函数2.1 一个公式三种完全不同的判定逻辑准确度的数学定义看着统一实际落到代码里sklearn会根据输入数组的维度选择不同的比较策略这一点是很多人没有意识到的。二分类和多分类情况下y_true和y_pred都是一维的标签数组accuracy_score做的是逐元素相等比较等价于$$\text{accuracy}(y, \hat{y}) \frac{1}{n}\sum_{i1}^{n} \mathbb{1}(\hat{y}_i y_i)$$换成混淆矩阵的语言二分类下就是(TP TN) / (TP TN FP FN)多分类下就是混淆矩阵对角线元素之和除以总数用 numpy 写出来就是np.trace(cm) / cm.sum()。这两句代码算出来的结果和accuracy_score完全一致我在排查问题时经常用它们互相验证成本极低。到了多标签场景逻辑就变了。假设一个样本可能同时属于科技财经体育多个类别y_true和y_pred都是形状为(n_samples, n_labels)的二维 0/1 矩阵。这个时候accuracy_score的判定标准是子集完全匹配subset accuracy只有当某一行样本的预测标签集合和真实标签集合一模一样这个样本才算预测正确。比如真实是[1, 0, 1]预测是[1, 0, 0]虽然三个位置对了两个但整行不算对。这个口径非常严格标签一多分数就会掉得很难看也不反映部分正确的价值。所以拿到一个准确度数字之前先问自己一句我这次评估的是哪种任务如果是多标签是否应该换成jaccard_score按位置平均或者每个标签单独算二分类指标这个判断做错了后面所有调参都是在错误的目标上优化。我个人的经验是多标签任务里 subset accuracy 只适合标签组合本身就是业务单位的场景比如一份工单必须同时被归到三个部门才算分派正确如果只是想做内容打标用 Jaccard 或 micro-F1 更贴合实际。2.2 sklearn 中与准确度相关的四个函数sklearn.metrics里跟准确度沾边的函数不止一个选错了会得到方向完全相反的结论。下面这张表是我自己在项目里做的速查卡把最常用的四个列出来对比函数核心口径适合什么场景主要参数accuracy_score整体预测正确的比例类别相对均衡、错判代价相近normalize、sample_weightbalanced_accuracy_score各类别召回率的宏平均类别不平衡、关心少数类adjusted、sample_weighttop_k_accuracy_score真实标签落在前 k 个高分预测中排序推荐、类别很多k、labels、normalizezero_one_loss误判比例准确度的补数需要损失越小越好的表述normalize、sample_weightbalanced_accuracy_score值得单独说一下。它的计算方式是先算每个类别的召回率该类被正确识别的比例再对这些召回率做算术平均不做样本量加权。所以在一份 1:99 的数据上多数类召回率 1.0、少数类召回率 0.0普通准确度是 0.99而平衡准确度只有 0.5。这个数字一眼就能看出模型其实没有区分能力非常适合放进周报里做防自欺指标。top_k_accuracy_score则解决另一个问题当类别有几百上千个时要求模型把正确答案排第一过于苛刻。这个函数的输入不是y_pred而是y_score通常是predict_proba的输出只要真实标签出现在概率最高的前k个位置里就算命中。用它之前必须显式传labels参数把概率矩阵的列索引和真实标签值对齐否则会抛错或算错。zero_one_loss本质上就是1 - accuracy返回的是错误率。它存在的价值在于符合损失函数越小越好的统一表述习惯在写自动化评估脚本时和回归任务的 MSE、MAE 放在一起看比较整齐。功能上和准确度没有区别别指望它能提供额外信息。3. accuracy_score 的参数细节与常见误用3.1 normalize 参数一个开关切换两种返回值accuracy_score的函数签名大致是这样accuracy_score(y_true, y_pred, *, normalizeTrue, sample_weightNone)注意normalize和sample_weight是关键字参数必须带参数名传位置参数硬塞会报错。normalizeTrue时返回 0 到 1 之间的浮点数也就是我们通常理解的准确率normalizeFalse时返回的是预测正确的样本个数一个整数。这个开关在什么场景下有用主要是需要做统计推断或者分批次汇总的时候。举个具体例子你要在多个数据切片上分别评估然后算整体准确度这时候就不能简单地把各个切片的准确率取平均。假设切片 A 有 1000 个样本、准确率 0.9切片 B 有 10 个样本、准确率 1.0直接平均得到 0.95但真实整体准确度是(900 10) / 1010 ≈ 0.901。正确做法是用normalizeFalse拿到各切片的正确个数加起来除以总样本数。这个坑在小样本切片特别多的场景比如按品类拆分的商品分类里非常容易踩。from sklearn.metrics import accuracy_score import numpy as np # 分片评估后汇总避免小样本切片权重被放大 total_correct 0 total_n 0 for yt, yp in slice_generator: total_correct accuracy_score(yt, yp, normalizeFalse) total_n len(yt) overall_acc total_correct / total_n print(f汇总准确度: {overall_acc:.4f})需要提醒的是不同版本的 sklearn 对这个参数的处理有过调整某些较新版本在调用时可能给出弃用提示。我的习惯是在正式脚本里加一句版本检查或者直接用np.mean(np.asarray(y_true) np.asarray(y_pred))这种等价写法完全绕开版本差异问题运行速度还更快一些。3.2 标签类型要一致不然结果会静默出错accuracy_score做的是严格相等比较1和1.0在 numpy 里比较结果是 True但如果一边是字符串1、另一边是整数1比较结果是 False而且不会抛异常。这个特性导致过不少诡异问题从 CSV 读进来的标签列是字符串模型训练前用LabelEncoder转成了整数评估时忘了同步转换结果准确度直接掉到 0.01查了半天以为是模型崩了。还有一种情况是y_pred的类型和y_true不一样比如y_true是 numpy 数组、y_pred是 pandas Series两者做逐元素比较时会按索引对齐。如果 Series 的索引被之前的筛选操作打乱过对齐过程会引入 NaN比较结果变成 False准确度莫名其妙变低。这类问题最难受的地方在于代码不报错只能靠分数异常来发现。import numpy as np import pandas as pd from sklearn.metrics import accuracy_score y_true pd.Series([0, 1, 1, 0, 1], index[10, 11, 12, 13, 14]) y_pred pd.Series([0, 1, 0, 0, 1], index[0, 1, 2, 3, 4]) # 直接比较会因为索引对不齐而产生 NaN print(accuracy_score(y_true, y_pred)) # 结果不可靠 # 稳妥写法统一转成 numpy 数组 print(accuracy_score(y_true.to_numpy(), y_pred.to_numpy()))我的做法是在评估函数入口处强制做一次类型归一化np.asarray(y_true).ravel()和np.asarray(y_pred).ravel()顺手把多余维度压平。多写这两行能省掉后面大量的排查时间。3.3 sample_weight不是所有样本都该一视同仁sample_weight参数允许给每个样本赋一个权重计算准确度时用权重求和代替简单计数。这个功能在实际业务里比想象中更常用。举个场景某个风控模型对近 30 天的样本更敏感历史样本只作为参考这时候可以给时间近的样本更高权重。或者数据里有一部分是标注质量很高的专家标注另一部分是众包标注两者可信度不同也可以用权重区分。from sklearn.metrics import accuracy_score y_true [1, 0, 1, 1, 0] y_pred [1, 0, 0, 1, 1] weights [2.0, 1.0, 1.0, 3.0, 1.0] print(accuracy_score(y_true, y_pred)) # 0.6 print(accuracy_score(y_true, y_pred, sample_weightweights)) # 加权后不同用的时候注意两点一是权重数组长度必须和样本数一致二是加权准确度的解释会变得更模糊汇报时最好说明权重来源不然别人看到 0.6 和 0.75 两个数字会一头雾水。我一般只在确有必要的时候才用评估阶段还是优先保证口径简单可解释。4. 完整实操从造数据到把准确度算明白4.1 构造一份不平衡数据先把基线立出来光看公式没什么感觉直接上一份可以跑通的完整流程。下面这段代码用make_classification造一份正负样本比例接近 1:19 的数据其中用 5 个特征控制类别可分性剩下的都是噪声特征。这样设置是为了模拟真实业务中信号弱、噪声多、样本偏的典型情况。import numpy as np from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.dummy import DummyClassifier from sklearn.metrics import ( accuracy_score, balanced_accuracy_score, confusion_matrix, classification_report ) X, y make_classification( n_samples5000, n_features20, n_informative5, n_redundant2, weights[0.95, 0.05], flip_y0.02, class_sep0.8, random_state42, ) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, stratifyy, random_state42 ) print(训练集类别分布:, np.bincount(y_train)) print(测试集类别分布:, np.bincount(y_test))注意stratifyy这个参数分层抽样保证了训练集和测试集的类别比例一致。这一点在不平衡数据上几乎是必选项否则可能抽出一个测试集里正样本只剩个位数评估结果波动巨大完全不可信。接下来先跑一个什么都不学的基线模型dummy DummyClassifier(strategymost_frequent).fit(X_train, y_train) dummy_pred dummy.predict(X_test) print(基线准确度:, accuracy_score(y_test, dummy_pred)) print(基线平衡准确度:, balanced_accuracy_score(y_test, dummy_pred))跑出来的基线准确度大概在 0.94 到 0.95 之间因为测试集里本来就有大约 95% 是多数类。而基线平衡准确度会落在 0.5 附近。这两个数字放在一起看含义非常清楚这个模型完全没有区分能力但普通准确度给了它一个看起来很体面的分数。然后再训练一个正经模型clf LogisticRegression(max_iter1000, class_weightNone).fit(X_train, y_train) pred clf.predict(X_test) print(模型准确度:, accuracy_score(y_test, pred)) print(模型平衡准确度:, balanced_accuracy_score(y_test, pred))实测下来如果不加class_weight逻辑回归的准确度可能只比基线高一点点甚至持平因为优化目标就是整体误差最小模型有充分的理由偏向多数类。这就是准确度指标看起来没退步实际毫无价值的经典场面。把class_weightbalanced打开之后再跑一次准确度会降下来一些但平衡准确度会明显提升混淆矩阵里少数类的召回率也能从接近 0 变成可用的水平。4.2 混淆矩阵比单个数字信息量大得多准确度是一个把整个混淆矩阵压缩成一个标量的过程压缩必然带来信息损失。所以我养成一个习惯任何时候看到准确度都要顺手把混淆矩阵和分类报告打出来成本就几行代码。cm confusion_matrix(y_test, pred) print(混淆矩阵:) print(cm) print() print(classification_report(y_test, pred, digits4))混淆矩阵的四宫格对应左上 TN、右上 FP、左下 FN、右下 TP。通过它可以直接手算验证准确度tn, fp, fn, tp cm.ravel() manual_acc (tp tn) / cm.sum() print(f手算准确度: {manual_acc:.6f}) print(fsklearn 准确度: {accuracy_score(y_test, pred):.6f})两个数字必然一致如果不一致说明数据或调用方式有问题这时候就该去查样本是否被意外过滤、预测结果是否顺序错乱。我一般在写评估脚本时就把这个交叉校验放进去当断言用跑通了心里才踏实。classification_report里每一类的 precision、recall、f1 也值得逐个看。少数类的 recall 如果只有 0.1 以下那不管整体准确度多高这个模型在实际业务里基本都是废的。相比之下我更愿意盯着少数类 recall 和 macro avg 的 f1 看准确度只作为一个参考背景值。4.3 交叉验证下的准确度与置信区间单次划分的测试集准确度本身就是一个随机变量换个random_state结果可能浮动好几个百分点。只报一个点估计是不够的至少要知道这个数字的波动范围。做法是用分层 K 折交叉验证拿到多次评估结果然后算均值和标准差。from sklearn.model_selection import cross_val_score, StratifiedKFold import numpy as np cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores cross_val_score(clf, X, y, cvcv, scoringaccuracy) print(各折准确度:, np.round(scores, 4)) print(f均值: {scores.mean():.4f} 标准差: {scores.std():.4f}) print(f95% 参考区间: {scores.mean() - 2*scores.std():.4f} ~ f{scores.mean() 2*scores.std():.4f})这里的scoringaccuracy就是调用accuracy_score。StratifiedKFold保证每折的类别比例和整体一致这一点在 sklearn 里用cv5时对分类器基本是默认行为但显式写出来更保险也方便之后切换到自定义折数。如果只有一个固定的测试集想给准确度加一个置信区间可以用二项分布的 Wilson 区间。样本量 500 的时候准确度 0.95 的 95% 置信区间大约在 0.928 到 0.966 之间跨度接近 4 个百分点。这个数字很有说服力——当两个模型的准确度只差 1 个百分点时很可能完全没有统计学差异别急着宣称新模型更好。from scipy import stats def accuracy_ci(y_true, y_pred, alpha0.05): yt np.asarray(y_true).ravel() yp np.asarray(y_pred).ravel() n len(yt) p np.mean(yt yp) z stats.norm.ppf(1 - alpha / 2) denom 1 z**2 / n center (p z**2 / (2 * n)) / denom half z * np.sqrt(p * (1 - p) / n z**2 / (4 * n**2)) / denom return p, max(0.0, center - half), min(1.0, center half) p, lo, hi accuracy_ci(y_test, pred) print(f准确度 {p:.4f}95% 置信区间 [{lo:.4f}, {hi:.4f}])用 Wilson 区间而不是最朴素的p ± z*sqrt(p(1-p)/n)原因是后者在准确度接近 1 或者样本量小的时候会算出超过 1 的上界看起来很不专业。Wilson 区间的公式多几行结果始终落在 0 到 1 之间实测表现更稳。4.4 阈值扫描把准确度变成可调的旋钮分类器的predict内部其实藏着一个阈值二分类下默认是 0.5。这个默认值在均衡数据上勉强能用在不平衡数据上往往不是最优的。既然准确度会随阈值变化那就把它画成一条曲线看比纠结单一数字有意义得多。from sklearn.metrics import precision_score, recall_score, f1_score proba clf.predict_proba(X_test)[:, 1] rows [] for thr in np.arange(0.05, 0.96, 0.05): p_ (proba thr).astype(int) rows.append({ 阈值: round(thr, 2), 准确度: round(accuracy_score(y_test, p_), 4), 平衡准确度: round(balanced_accuracy_score(y_test, p_), 4), 查准率: round(precision_score(y_test, p_, zero_division0), 4), 查全率: round(recall_score(y_test, p_, zero_division0), 4), F1: round(f1_score(y_test, p_, zero_division0), 4), }) import pandas as pd print(pd.DataFrame(rows).to_string(indexFalse))跑完这张表你会发现一个很有意思的现象准确度在阈值较高比如 0.5 以上时表现最好因为它倾向于把样本都判成多数类而 F1 和平衡准确度在阈值 0.2 到 0.3 附近才达到峰值。两套指标指向的最优阈值完全不同。到底取哪个取决于业务上错判的代价——漏掉一个正样本的成本是误报的多少倍。这个权衡不是算法能替你做的必须和业务方对齐之后再定。我踩过的一个坑是调完阈值之后把阈值写死在评估脚本里结果上线时模型服务用的是默认 0.5两边对不上线上效果比离线差了一大截。后来规矩就改了阈值作为模型产物的一个字段一起保存加载模型时一并读出来不再依赖任何默认值。5. 排查实录准确度算不对的六种典型情况5.1 概率矩阵直接喂给 accuracy_score这是新手最容易踩、也最难自查的一个坑。y_pred期待的是类别标签如果传进去的是predict_proba输出的二维概率矩阵accuracy_score会做逐元素相等比较得到一个接近 0 但又不是 0 的诡异分数全程不报任何错。proba clf.predict_proba(X_test) wrong accuracy_score(y_test, proba) print(错误用法结果:, wrong) # 接近 0但不会报错 # 正确做法一先转成标签 correct accuracy_score(y_test, np.argmax(proba, axis1)) # 正确做法二直接调用 predict correct accuracy_score(y_test, clf.predict(X_test))自查方法很简单在评估函数开头加一句assert np.asarray(y_pred).ndim 1把二维输入直接拦下来。多标签任务除外那种情况需要放宽到二维但限制取值范围是 0/1。这一行断言的性价比极高我在所有评估脚本里都加上了。5.2 索引错位与数据泄漏前面提过 pandas 索引对齐的问题这里再强调一次因为它造成的分数偏差看起来很合理不容易引起怀疑。典型的链路是先对整个数据集做了缺失值过滤y_test少了几行但y_pred是基于未过滤的数据生成的两边长度一样、顺序不同准确度就会莫名其妙地低几个百分点。另一种更隐蔽的是数据泄漏。特征构造的时候不小心把目标变量编码进去了或者标准化时用了全量数据的均值方差导致准确度高得不真实。区分方法是交叉验证分数和测试集分数差距特别大或者分数高到 0.99 以上还伴随一个很奇怪的特征重要性排序。遇到这种情况先怀疑泄漏别急着庆祝。排查手段上我一般会打印前 20 个样本的真实标签和预测标签做肉眼对照同时检查两者的长度、dtype、索引是否一致。这三项一过绝大多数低级错误就露馅了。5.3 交叉验证分数和手算对不上有人会发现在cross_val_score里拿到五折分数手动切分数据自己算一遍结果和某一折对不上。原因通常是切分方式不同cross_val_score对分类器默认使用分层策略而且shuffle的默认值是 False不同版本可能略有差异。如果不显式指定StratifiedKFold(shuffleTrue, random_state42)拿到的折和手写循环切出来的折根本不是同一批数据。cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) manual_scores [] for tr_idx, te_idx in cv.split(X, y): m LogisticRegression(max_iter1000).fit(X[tr_idx], y[tr_idx]) manual_scores.append(accuracy_score(y[te_idx], m.predict(X[te_idx]))) print(手算:, np.round(manual_scores, 4)) print(cross_val_score:, np.round(scores, 4))只要cv对象一致、模型初始化参数一致两组数字必然逐个相等。这个对照是我验证评估流程正确性的标准动作尤其是在自己封装了评估函数之后一定要和 sklearn 原生结果对齐一次。5.4 常见问题速查表把上面这些以及我遇到过的其他问题整理成一张表出问题的时候对着扫一遍能省不少时间现象可能原因排查动作准确度接近 0 但不报错传入了概率矩阵或多维数组检查y_pred.ndim是否为 1准确度异常低模型其他指标正常标签类型不一致str vs int打印两者dtype并统一转换准确度 0.95 但业务效果差类别严重不平衡全判多数类看混淆矩阵和少数类召回率交叉验证分数波动极大未分层、样本量小、正样本稀少换StratifiedKFold增加折数或样本量离线高线上低阈值写死、特征处理不一致把阈值和预处理管线一起持久化多标签任务分数极低误用了子集准确度口径改用jaccard_score或逐标签评估分片汇总后的准确度偏高各片准确率直接取平均改用normalizeFalse累加正确个数还有一个小细节值得提classification_report在某个类别完全没被预测到时precision 会算出 0 并给出警告新版 sklearn 提供了zero_division参数来控制这个行为设成 0 或 1 会影响最终展示。这个设置不影响准确度但会影响你对模型的判断建议显式写出来别用默认值。在项目里跑过几十次评估之后我的体会是准确度这个指标本身没有好坏问题出在只看准确度这个习惯上。把它和平衡准确度、混淆矩阵、少数类召回率摆在一起看再配上交叉验证的均值和置信区间基本上就不会被一个漂亮的数字骗过去了。真正麻烦的从来不是算不出来而是算出来之后没意识到它代表什么。
阅读完成 · 觉得有帮助?
咨询建站