☰
sklearn准确度评估:accuracy_score与平衡准确度实战
2026/10/26 12:48:26 网站建设 项目流程

1. 准确度这个指标,为什么值得单独拎出来讲

accuracy(准确度)大概是所有分类评估指标里最没门槛的一个。公式简单到一句话就能说完:预测对的样本数除以总样本数。正因为太简单,很多人在项目里就是一行accuracy_score(y_true, y_pred)完事,然后拿着一个 0.97 的数字去汇报,结果上线之后业务方反馈模型效果一塌糊涂。问题不出在 sklearn 的代码有 bug,而是准确度这个指标本身对数据分布极度敏感,对调用方式也有一堆隐性要求。

我在实际项目里见过太多次类似情况:某开发者拿到一份样本比例接近 1:99 的数据集,训练完准确度 0.98,全组都觉得模型不错,最后把混淆矩阵打印出来才发现,模型其实把所有样本都判成了多数类——那个 0.98 跟"什么都不预测,直接蒙多数类"的基线完全一样,模型等于什么都没学到。还有一种情况更隐蔽:把predict_proba输出的概率矩阵直接传给了accuracy_score,sklearn 不报错,安安静静返回一个 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_{i=1}^{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_weight
balanced_accuracy_score各类别召回率的宏平均类别不平衡、关心少数类adjusted、sample_weight
top_k_accuracy_score真实标签落在前 k 个高分预测中排序推荐、类别很多k、labels、normalize
zero_one_loss误判比例(准确度的补数)需要"损失越小越好"的表述normalize、sample_weight

balanced_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, *, normalize=True, sample_weight=None)

注意normalize和sample_weight是关键字参数,必须带参数名传,位置参数硬塞会报错。normalize=True时返回 0 到 1 之间的浮点数,也就是我们通常理解的准确率;normalize=False时返回的是预测正确的样本个数,一个整数。

这个开关在什么场景下有用?主要是需要做统计推断或者分批次汇总的时候。举个具体例子:你要在多个数据切片上分别评估,然后算整体准确度,这时候就不能简单地把各个切片的准确率取平均。假设切片 A 有 1000 个样本、准确率 0.9,切片 B 有 10 个样本、准确率 1.0,直接平均得到 0.95,但真实整体准确度是(900 + 10) / 1010 ≈ 0.901。正确做法是用normalize=False拿到各切片的正确个数,加起来除以总样本数。这个坑在小样本切片特别多的场景(比如按品类拆分的商品分类)里非常容易踩。

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, normalize=False) 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_weight=weights)) # 加权后不同

用的时候注意两点:一是权重数组长度必须和样本数一致,二是加权准确度的解释会变得更模糊,汇报时最好说明权重来源,不然别人看到 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_samples=5000, n_features=20, n_informative=5, n_redundant=2, weights=[0.95, 0.05], flip_y=0.02, class_sep=0.8, random_state=42, ) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, stratify=y, random_state=42 ) print("训练集类别分布:", np.bincount(y_train)) print("测试集类别分布:", np.bincount(y_test))

注意stratify=y这个参数,分层抽样保证了训练集和测试集的类别比例一致。这一点在不平衡数据上几乎是必选项,否则可能抽出一个测试集里正样本只剩个位数,评估结果波动巨大,完全不可信。

接下来先跑一个"什么都不学"的基线模型:

dummy = DummyClassifier(strategy="most_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_iter=1000, class_weight=None).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_weight="balanced"打开之后再跑一次,准确度会降下来一些,但平衡准确度会明显提升,混淆矩阵里少数类的召回率也能从接近 0 变成可用的水平。

4.2 混淆矩阵比单个数字信息量大得多

准确度是一个把整个混淆矩阵压缩成一个标量的过程,压缩必然带来信息损失。所以我养成一个习惯:任何时候看到准确度,都要顺手把混淆矩阵和分类报告打出来,成本就几行代码。

cm = confusion_matrix(y_test, pred) print("混淆矩阵:") print(cm) print() print(classification_report(y_test, pred, digits=4))

混淆矩阵的四宫格对应:左上 TN、右上 FP、左下 FN、右下 TP。通过它可以直接手算验证准确度:

tn, fp, fn, tp = cm.ravel() manual_acc = (tp + tn) / cm.sum() print(f"手算准确度: {manual_acc:.6f}") print(f"sklearn 准确度: {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_splits=5, shuffle=True, random_state=42) scores = cross_val_score(clf, X, y, cv=cv, scoring="accuracy") print("各折准确度:", np.round(scores, 4)) print(f"均值: {scores.mean():.4f} 标准差: {scores.std():.4f}") print(f"95% 参考区间: {scores.mean() - 2*scores.std():.4f} ~ " f"{scores.mean() + 2*scores.std():.4f}")

这里的scoring="accuracy"就是调用accuracy_score。StratifiedKFold保证每折的类别比例和整体一致,这一点在 sklearn 里用cv=5时对分类器基本是默认行为,但显式写出来更保险,也方便之后切换到自定义折数。

如果只有一个固定的测试集,想给准确度加一个置信区间,可以用二项分布的 Wilson 区间。样本量 500 的时候,准确度 0.95 的 95% 置信区间大约在 0.928 到 0.966 之间,跨度接近 4 个百分点。这个数字很有说服力——当两个模型的准确度只差 1 个百分点时,很可能完全没有统计学差异,别急着宣称新模型更好。

from scipy import stats def accuracy_ci(y_true, y_pred, alpha=0.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_division=0), 4), "查全率": round(recall_score(y_test, p_, zero_division=0), 4), "F1": round(f1_score(y_test, p_, zero_division=0), 4), }) import pandas as pd print(pd.DataFrame(rows).to_string(index=False))

跑完这张表你会发现一个很有意思的现象:准确度在阈值较高(比如 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, axis=1)) # 正确做法二:直接调用 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(shuffle=True, random_state=42),拿到的折和手写循环切出来的折根本不是同一批数据。

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) manual_scores = [] for tr_idx, te_idx in cv.split(X, y): m = LogisticRegression(max_iter=1000).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或逐标签评估
分片汇总后的准确度偏高各片准确率直接取平均改用normalize=False累加正确个数

还有一个小细节值得提:classification_report在某个类别完全没被预测到时,precision 会算出 0 并给出警告,新版 sklearn 提供了zero_division参数来控制这个行为,设成 0 或 1 会影响最终展示。这个设置不影响准确度,但会影响你对模型的判断,建议显式写出来,别用默认值。

在项目里跑过几十次评估之后,我的体会是:准确度这个指标本身没有好坏,问题出在"只看准确度"这个习惯上。把它和平衡准确度、混淆矩阵、少数类召回率摆在一起看,再配上交叉验证的均值和置信区间,基本上就不会被一个漂亮的数字骗过去了。真正麻烦的从来不是算不出来,而是算出来之后没意识到它代表什么。

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

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

立即咨询