1. 先把准确度这件事掰开:accuracy_score 到底在算什么
很多人第一次接触 sklearn 的accuracy_score,场景都差不多:模型fit完了,predict出来一列结果,然后把真实标签和预测标签往里一塞,打印出一个 0.9 几的数字,心里踏实了,报告也写完了。我自己早年也这么干过,直到有一次在一个严重偏斜的业务数据集上,把模型换成一个"永远预测多数类"的空壳分类器,准确度依然有 0.96,才发现这个指标远比表面上看起来要滑头。准确度(Accuracy)在 sklearn 里的正式实现就是sklearn.metrics.accuracy_score,它回答的问题极其朴素——在所有样本里,预测标签和真实标签完全一样的比例是多少。但就是这个朴素到几乎没有门槛的指标,在实际项目里翻车的次数,比大多数复杂指标都多。
这篇内容我打算把accuracy_score从函数签名、内部计算逻辑、参数语义,一直到多分类、样本加权、交叉验证、类别不平衡下的失真,全部拆一遍。适合两类人看:刚学 sklearn、只会写accuracy_score(y_true, y_pred)的新手,以及用了两三年但一直把它当"万能打分器"、没细究过适用边界的老手。文中所有代码都可以直接跑,数据全部用make_classification现场生成,不需要额外下载。我会尽量把每个参数的"为什么"讲透,而不是只丢一段 API 文档翻译。
1.1 一行代码背后的四格表与除法
accuracy_score的实现没有黑魔法。二分类场景下,它先根据y_true和y_pred构造出混淆矩阵的四个格子:真正例 TP、假正例 FP、假反例 FN、真反例 TN,然后算一个除法——
$$\text{Accuracy} = \frac{TP + TN}{TP + TN + FP + FN}$$
分母就是样本总数,分子是预测对的样本数。多分类场景只是一个推广:把混淆矩阵的主对角线元素加起来,除以矩阵所有元素之和。所以准确度的本质是"对角线占比",它把所有类别的错误一视同仁地压成一个数字,不区分你是把 A 类错判成 B 类,还是把 A 类错判成 C 类。这一点非常关键,后面讲不平衡数据时,所有的坑都从这里长出来。
我用一个小例子把数字摆出来。假设二分类测试集 100 条,混淆矩阵是 TP=40、FP=10、FN=5、TN=45。准确度 = (40+45)/100 = 0.85。同一个矩阵下,精确率 = 40/(40+10) = 0.80,召回率 = 40/(40+5) ≈ 0.889。三个数字各不相同,但只有准确度会让人产生"整体还不错"的错觉,因为它被 45 个轻松判对的负样本抬高了。你在写评估报告时如果只报 0.85,读者根本不知道你的模型在正例上其实漏了 11%。
顺带说一个能帮你建立直觉的等价写法:在不考虑样本权重的前提下,accuracy_score(y_true, y_pred)的结果和np.mean(y_true == y_pred)完全一致。我第一次意识到这一点之后,就再也不把这个函数当成需要敬畏的黑盒了——它就是一个逐元素比较再求均值。sklearn 额外做的事情只有三件:校验两个输入长度是否一致、处理sample_weight、以及把结果按normalize参数决定返回比例还是计数。
1.2 normalize 和 sample_weight:两个参数,两种语义
函数签名长这样(1.x 版本):
accuracy_score(y_true, y_pred, *, normalize=True, sample_weight=None)注意那个星号,它意味着normalize和sample_weight只能按关键字传。这是个历史遗留的兼容性坑:早期版本允许位置传参,accuracy_score(y, p, False)这种写法在老代码里很常见,升级到 0.24 之后的版本会直接抛TypeError。我维护过一个跑了好几年的评估脚本,就是被这个改动干掉的,报错信息还挺含糊,排查了半天。
normalize=True返回正确率(0 到 1 的浮点数),normalize=False返回预测正确的样本条数(整数)。后者在需要报"错了多少条"的场景里比比例更有用,比如给业务方解释"这批 1 万条数据里,模型判错了 137 条",比说"准确率 98.63%"更直观。我一般两个都打,日志里一行比例一行绝对数,事后追查问题方便很多。
sample_weight是个被严重低估的参数。传入权重数组后,计算变成加权正确率:
$$\text{Accuracy}_{w} = \frac{\sum_i w_i \cdot \mathbb{1}[y_i = \hat{y}_i]}{\sum_i w_i}$$
在normalize=False时返回的是加权后的分子(浮点数,不再是整数)。它最实用的场景是"样本重要性不同"——比如同一批数据里,近三个月的样本权重给 1.0,半年前的给 0.3,你就能得到一个偏向近期表现的准确度,而不用真的去采样或删数据。另一个用法是做分层评估加权:如果你把测试集按业务线拆成几段分别评估,想合成一个总体数字,直接对各段准确度做算术平均是错的(各段样本量不同),正确做法是把各段样本数当权重再算一次。
import numpy as np from sklearn.metrics import accuracy_score y_true = np.array([1, 0, 1, 1, 0, 0, 1, 0]) y_pred = np.array([1, 0, 0, 1, 0, 1, 1, 0]) w = np.array([1.0, 1.0, 3.0, 1.0, 1.0, 3.0, 1.0, 1.0]) print(accuracy_score(y_true, y_pred)) # 0.75 print(accuracy_score(y_true, y_pred, normalize=False)) # 6 print(accuracy_score(y_true, y_pred, sample_weight=w)) # 0.6666...上面这个例子里,两条判错的样本恰好都被赋了 3 倍权重,准确度从 0.75 掉到 0.667。这个数字变化本身就是信息:如果高权重样本恰好是模型容易错的那些,说明模型在关键样本上表现不行,不能靠总体准确度蒙混过去。
1.3 为什么它是最容易"看起来对"的指标
准确度有一个其他指标都不具备的特点:它的取值永远在 0 到 1 之间,永远看起来像个"分数",永远能用百分比讲给非技术同事听。这种"人畜无害"的外表,导致它被过度使用。我见过太多评估报告,通篇只有一行准确度,没有混淆矩阵,没有分类报告,没有基线对比。这样的报告在类别均衡、代价对称的场景下勉强能看,一旦碰上信用卡欺诈、设备故障预警、罕见病筛查这类正例占比不到 5% 的问题,准确度基本等于噪声。
判断能不能用准确度,我给一个很粗暴但好用的经验法则:先看多数类的占比。如果"全猜多数类"这个基线策略的准确度已经超过 0.85,而你的模型只比它高两三个点,那这个准确度数字就别拿出来说事了,先去看召回率和混淆矩阵。这条法则帮我省掉过至少三次在评审会上被追问的尴尬。
还有一个隐蔽的问题:准确度对"错误类型"完全不敏感。把高风险客户判成低风险,和把低风险判成高风险,在accuracy_score看来都是"错了一条",权重一模一样。但业务上的代价可能差几十倍。这种时候要么用sample_weight把代价编码进去,要么干脆换指标,别硬套。
2. 数值是怎么来的:手算一遍比读十遍文档管用
我一直建议身边的人,学任何评估指标都先手算一遍,再用 sklearn 验证。手算的过程会逼你面对每一个中间量,而调包只会给你一个结果。这一节我把二分类、多分类、top-k 三种情况都走一遍,顺便把几个容易搞混的边界说清楚。
2.1 二分类场景的完整推导过程
假设我们有一批 200 条测试样本,模型预测完之后统计出:TP=72、FP=18、FN=8、TN=102。先验证分母:72+18+8+102=200,没问题。分子 = 72+102 = 174。准确度 = 174/200 = 0.87。
现在把同一组数字换成其他几个指标,你会看到准确度有多"宽容":
| 指标 | 计算式 | 结果 |
|---|---|---|
| 准确度 Accuracy | (72+102)/200 | 0.870 |
| 精确率 Precision | 72/(72+18) | 0.800 |
| 召回率 Recall | 72/(72+8) | 0.900 |
| 特异度 Specificity | 102/(102+18) | 0.850 |
| F1 | 2×0.8×0.9/(0.8+0.9) | 0.847 |
准确度 0.87 落在精确率和召回率之间,看起来是个"综合"数字。但它综合的方式很粗暴:它给 TP 和 TN 同等权重,而 TN 有 102 个,TP 只有 72 个,所以这个数字实际上被负样本主导了。如果业务关心的是"漏掉了多少正例",那 8 个 FN 才是重点,准确度完全体现不出来。
用代码复现这个矩阵,可以这样写:
import numpy as np from sklearn.metrics import accuracy_score, confusion_matrix # 按上面的四格表构造标签 y_true = np.array([1]*80 + [0]*120) y_pred = np.array([1]*72 + [0]*8 + [1]*18 + [0]*102) print(confusion_matrix(y_true, y_pred)) # [[102 18] # [ 8 72]] print(accuracy_score(y_true, y_pred)) # 0.87注意confusion_matrix默认的行是真实类别、列是预测类别,顺序按标签排序(0 在前、1 在后)。所以左上角是 TN,右下角是 TP。这个顺序经常有人记反,尤其是标签是字符串的时候,排序规则变成字典序,"no"会排在"yes"前面,矩阵看起来就"反"了。遇到这种情况,显式传labels=["yes", "no"]把顺序钉死,别靠默认值猜。
2.2 多分类:子集准确率与 top-k 的区别
多分类下,accuracy_score要求预测标签和真实标签完全相等才计为正确。这被称为子集准确率(subset accuracy)或者严格匹配率。举个三分类的例子,混淆矩阵如下:
[[50, 3, 2], [ 4, 40, 6], [ 1, 5, 44]]行和分别是 55、50、50,总计 155;对角线 50+40+44 = 134;准确度 = 134/155 ≈ 0.8645。每一类的召回率分别是 50/55≈0.909、40/50=0.8、44/50=0.88。可以看到第二类的表现明显更差,但总体准确度 0.865 把它藏起来了。这就是为什么我评估多分类模型时,一定同时看classification_report,光一个总准确度根本定位不到问题类别。
另一个容易混淆的概念是 top-k 准确率。有些模型(比如图像分类、推荐排序)输出的不只是一个标签,而是一个按概率排序的候选列表。业务上"正确答案排在前 3 名里"就算命中,这时候不能用accuracy_score,要用top_k_accuracy_score:
from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, top_k_accuracy_score X, y = make_classification( n_samples=6000, n_features=30, n_informative=12, n_classes=10, n_clusters_per_class=1, random_state=0) X_tr, X_te, y_tr, y_te = train_test_split( X, y, test_size=0.3, random_state=0, stratify=y) clf = LogisticRegression(max_iter=3000, multi_class="multinomial") clf.fit(X_tr, y_tr) y_pred = clf.predict(X_te) y_prob = clf.predict_proba(X_te) print("top-1:", accuracy_score(y_te, y_pred)) # 约 0.7x print("top-3:", top_k_accuracy_score(y_te, y_prob, k=3, labels=clf.classes_))top_k_accuracy_score的第二个参数要的是概率矩阵或者 decision function 的形状(n_samples × n_classes),不是硬标签。labels参数强烈建议显式传,虽然新版本能从列数推断,但一旦你的概率矩阵列顺序和classes_不一致(比如做了列筛选或重排),推断出来的结果会静默错位,准确度虚高而没有任何警告。这种 bug 极难发现,我吃过一次亏之后,所有涉及概率矩阵的评估代码里都强制写上labels=。
2.3 概率输出不能直接喂给 accuracy_score
这是新手最高频的错误之一。模型predict_proba出来的是 0 到 1 的连续值,直接传进accuracy_score会得到:
ValueError: Classification metrics can't handle a mix of binary and continuous targets报错信息已经写得很明白了:分类指标处理不了连续目标。但很多人看到报错后的第一反应是"那我把概率四舍五入一下",于是写了np.round(y_prob),结果也不对——多分类场景下概率向量会被舍入成一堆 0 和 1,形状都变了。
正确姿势是两步:先决定阈值,再二值化。二分类用predict_proba(...)[:, 1] >= 阈值,多分类用argmax:
proba = clf.predict_proba(X_te) y_pred_05 = clf.classes_[proba.argmax(axis=1)] # 等价于 clf.predict二分类手动调阈值时,注意一个细节:clf.predict()内部用的阈值固定是 0.5,而且是在"正类概率"上判断。如果你的正类在classes_里的位置是 0 而不是 1,predict_proba的列顺序就会跟着变。稳妥的写法是按classes_找索引:
pos_idx = list(clf.classes_).index(1) proba_pos = clf.predict_proba(X_te)[:, pos_idx] for thr in (0.3, 0.4, 0.5, 0.6): pred = (proba_pos >= thr).astype(int) print(thr, accuracy_score(y_te, pred))这段循环跑下来,你经常会看到准确度在 0.4 到 0.5 之间最高。很多人会本能地选那个让准确度最大的阈值,然后就把它固化进代码。我的建议是:如果业务代价并非对称,阈值绝不应该由准确度决定,而应该由精确率和召回率的可接受区间决定。准确度选阈值的做法,本质上是在最大化"总体判对率",这跟"少漏掉一个正例"的业务目标可能完全相反。
3. 三个能直接复现的实操案例
光讲理论没意思,这一节给三个完整案例,从最基础的流水线到不平衡数据的失真现场,代码都能独立运行。我会把每一步的意图和输出解释清楚,你抄过去改改数据就能用。
3.1 案例一:最干净的二分类流水线
先用一个类别相对均衡的数据集,把标准流程跑通,建立正确的心智模型。
import numpy as np from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split, StratifiedKFold, cross_val_score from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import (accuracy_score, confusion_matrix, classification_report, balanced_accuracy_score) X, y = make_classification( n_samples=5000, n_features=25, n_informative=10, n_redundant=5, weights=[0.6, 0.4], flip_y=0.02, class_sep=1.0, random_state=42) X_tr, X_te, y_tr, y_te = train_test_split( X, y, test_size=0.25, random_state=42, stratify=y) clf = RandomForestClassifier( n_estimators=300, max_depth=None, min_samples_leaf=2, n_jobs=-1, random_state=42) clf.fit(X_tr, y_tr) y_pred = clf.predict(X_te) print("acc :", round(accuracy_score(y_te, y_pred), 4)) print("acc_cnt :", accuracy_score(y_te, y_pred, normalize=False)) print("bacc :", round(balanced_accuracy_score(y_te, y_pred), 4)) print(confusion_matrix(y_te, y_pred)) print(classification_report(y_te, y_pred, digits=4))几个设计点值得说。train_test_split里加stratify=y是为了保持训练集和测试集的正负比例一致,不加的话小数据集上可能出现测试集正例占比偏几个百分点的情况,准确度会飘。make_classification里flip_y=0.02是故意注入 2% 的标签噪声,模拟真实数据的脏乱,不加这个参数的话随机森林的准确度容易直接冲到 0.99,失去参考价值。
RandomForestClassifier的min_samples_leaf=2是我个人习惯的起点,比默认的 1 稍微稳一点,尤其在噪声标签存在时,叶子节点样本太少会去拟合噪声。这个参数不是必须的,但在多跑几次不同随机种子之后,你会发现它的方差确实小一些。
交叉验证部分单独写一段:
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(clf, X, y, cv=cv, scoring="accuracy", n_jobs=-1) print(scores.mean(), scores.std())StratifiedKFold比默认的KFold更合适分类问题,它保证每一折的类别比例和全量一致。shuffle=True必须配合random_state用,否则每次跑出来的折都不一样,没法复现。scores.std()这个数别丢,它反映模型在不同数据切片上的稳定性,标准差异常大的时候,说明模型对数据分布很敏感,这时候光看均值会过于乐观。
还有一个很多人不知道的等价关系:clf.score(X_te, y_te)在分类器上返回的就是准确度,和accuracy_score(y_te, clf.predict(X_te))完全一样。这个score方法在所有 sklearn 分类器上都存在,但要注意回归器的score返回的是 R²,别搞混。我在写快速试验脚本时常用clf.score省一行代码,但在正式评估代码里一定写全accuracy_score,因为显式调用能让读代码的人一眼看出用的是哪个指标。
3.2 案例二:交叉验证下同时盯住多个指标
只跑一个准确度,等于只测了一个维度。我现在的习惯是cross_validate一次跑多个指标,把训练集得分也带出来,这样一眼能看出过拟合程度。
from sklearn.model_selection import cross_validate scoring = ["accuracy", "balanced_accuracy", "f1_macro", "roc_auc"] res = cross_validate( clf, X, y, cv=cv, scoring=scoring, return_train_score=True, n_jobs=-1) for k in scoring: tr = res[f"train_{k}"].mean() te = res[f"test_{k}"].mean() print(f"{k:>18}: train={tr:.4f} test={te:.4f} gap={tr-te:+.4f}")输出的gap那一列是我最看重的。准确度的训练集和验证集差距在 0.02 以内,基本可以认为没有明显过拟合;差距到 0.1 以上,就得回头看树的深度、叶子样本数这些正则参数了。这里有个反直觉的点:准确度的 gap 可能很小,但roc_auc的 gap 很大。原因是指标准确度只看 0.5 阈值附近的表现,而 AUC 看的是整体排序能力,模型在排序上学到的"过拟合细节"不一定体现在硬分类结果上。所以多个指标一起看,比单看一个准确度信息量大得多。
scoring列表里的字符串必须是 sklearn 认可的 scorer 名。常用的几个:accuracy、balanced_accuracy、f1、f1_macro、f1_weighted、precision、recall、roc_auc。注意roc_auc在多分类默认是 one-vs-rest 的宏平均,二分类直接用正类概率。如果你传了一个拼错的名字,cross_validate会抛ValueError: 'xxx' is not a valid scoring value,还会把可用列表打出来,照着抄就行。
3.3 案例三:不平衡数据下准确度是怎么骗人的
这个案例我建议每个做分类的人都亲手跑一遍。构造一个 98:2 的极端不平衡数据集,然后分别用"永远猜多数类"的空模型和一个正经的逻辑回归去跑,对比准确度。
from sklearn.datasets import make_classification from sklearn.dummy import DummyClassifier from sklearn.linear_model import LogisticRegression from sklearn.metrics import (accuracy_score, balanced_accuracy_score, confusion_matrix, classification_report, precision_score, recall_score) X, y = make_classification( n_samples=20000, n_features=20, n_informative=6, n_redundant=4, weights=[0.98, 0.02], flip_y=0.01, class_sep=0.9, random_state=7) X_tr, X_te, y_tr, y_te = train_test_split( X, y, test_size=0.25, random_state=7, stratify=y) dummy = DummyClassifier(strategy="most_frequent").fit(X_tr, y_tr) print("空模型 acc:", round(accuracy_score(y_te, dummy.predict(X_te)), 4)) lr = LogisticRegression(max_iter=2000, class_weight="balanced") lr.fit(X_tr, y_tr) pred = lr.predict(X_te) print("LR acc :", round(accuracy_score(y_te, pred), 4)) print("LR bacc :", round(balanced_accuracy_score(y_te, pred), 4)) print("LR prec :", round(precision_score(y_te, pred), 4)) print("LR rec :", round(recall_score(y_te, pred), 4)) print(confusion_matrix(y_te, pred))跑出来的典型结果是这样:空模型准确度大约 0.97 到 0.98,逻辑回归加class_weight="balanced"之后准确度掉到 0.90 左右,看起来"变差了"。但平衡准确度从 0.5 跳到 0.85 上下,正例召回率从 0 跳到 0.8 以上。这时候如果你只看准确度,会把一个真正有用的模型毙掉,留下一个什么都不做的空壳。
class_weight="balanced"的作用是按类别频率反比给权重,公式是n_samples / (n_classes * np.bincount(y))。它对逻辑回归这种线性模型效果直接,因为它改变了损失函数里每个样本的贡献。但要注意,加权之后的predict输出概率不再校准,输出的概率值不能直接当真实概率用,只能用来排序或按阈值切。这一点在需要输出风险分值的业务里很要命,得配合CalibratedClassifierCV重新校准。
这一段我想强调的核心就一句:准确度在不平衡数据上几乎不携带信息。它衡量的是"你有没有学会猜多数类",而这件事连DummyClassifier都能做到。看到 0.98 的准确度先别高兴,先算一下多数类占比,两者差不到 5 个点就基本没意义。
4. 常见问题与排查实录
这一节全部来自实际调试现场,不是文档摘抄。每一条我都尽量给出触发条件和判断方法。
4.1 报错与警告速查表
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
Classification metrics can't handle a mix of binary and continuous targets | 把predict_proba的概率直接传给了accuracy_score | 先阈值化或argmax,再传入 |
Found input variables with inconsistent numbers of samples | y_true和y_pred长度不一致,常见于筛选数据后忘了同步 | 检查两者shape[0],或统一reset_index(drop=True) |
TypeError: accuracy_score() takes 2 positional arguments | 老代码用位置参数传normalize | 改成normalize=False关键字写法 |
| 准确度恒为 0 或极低,但模型明明能训练 | 标签类型不一致,如y_true是字符串、y_pred是整数 | 统一用LabelEncoder或在预测后映射回原标签 |
| 准确度比交叉验证高出很多 | 评估时用了训练集,或做了特征筛选后再划分 | 严格先划分再拟合任何预处理 |
top_k_accuracy_score结果明显偏高 | labels缺失或与概率矩阵列顺序不一致 | 显式传labels=clf.classes_ |
| 多标签任务准确度极低 | accuracy_score在多标签下是子集准确率,要求整行完全匹配 | 改用hamming_loss或f1_score(average="samples") |
表格里第一条和第四条是我遇到最多的。第四条尤其阴险:sklearn 在某些版本里不会报错,而是把所有样本都判为"不匹配",直接返回 0.0。你看到 0 的时候第一反应通常是"模型坏了",其实是标签没对齐。快速验证方法:打印set(y_true)和set(y_pred),看两个集合有没有交集。没交集就是类型或编码问题,跟模型无关。
4.2 数值对不上时的排查顺序
我给自己定了一套固定的排查顺序,避免东一榔头西一棒子:
第一步,确认样本量。len(y_true) == len(y_pred),且等于你预期的测试集大小。差一条都会让结果偏移。
第二步,确认标签空间。两个数组的unique值集合是否一致。如果不一致,看是类型问题还是真的有类别缺失。
第三步,手算一遍。np.mean(np.asarray(y_true) == np.asarray(y_pred)),和accuracy_score的结果比。如果两者不一致,说明中间有sample_weight或者数据类型隐式转换在捣鬼。
第四步,检查索引对齐。这是 pandas 用户专属的坑:如果你对 DataFrame 做了sort_values或者dropna之后没有reset_index,索引会变得稀疏。sklearn 内部会把输入转成 numpy 数组,不按索引对齐,只按位置对齐。结果就是你以为在比较同一批样本,实际上张冠李戴。这种错误不会报错,只会给你一个偏低但看起来合理的数字,极难发现。
提醒:所有传给 sklearn 指标的标签,最好在传入前统一
.values或np.asarray(),把 pandas 的索引语义彻底切断。多写这一句,能省掉后面几小时的排查。
4.3 我在实际项目里踩过的坑
第一个坑是随机种子。同一份代码跑两次,准确度差 0.03,我一度以为是数据读错了,查了半天发现是train_test_split没设random_state。小数据集上这个波动很正常,因为测试集本身只有几百条,划分一变结果就变。后来我养成了固定种子的习惯,同时在报告里写清楚"单次划分结果,波动范围约 ±0.02",不让别人把小数点后两位当真。
第二个坑是多次评估取最优。有些人会跑十几个随机种子,挑准确度最高的那次写进报告。这在技术上不算错,但在汇报上是自欺欺人。正确做法是固定划分跑一次,或者用交叉验证报均值和标准差,把不确定性一起呈现出来。
第三个坑是预处理泄漏。有一次我先对全量数据做了标准化,再划分训练测试集,准确度虚高了好几个点。原因是标准化时用到了测试集的均值和方差,等于提前泄露了信息。正确的顺序永远是:划分 → 在训练集上fit预处理器 → 用同一个预处理器transform测试集。用Pipeline能强制你遵守这个顺序,我现在的项目里几乎没有裸着调用StandardScaler的地方。
第四个坑比较隐蔽:多分类里某个类别在测试集中一条样本都没有。accuracy_score照常计算,不会报错,但这个类别的信息实际上完全没被评估到。这种情况在小样本多分类里很常见,解决办法是用train_test_split(stratify=y)保证每类都进测试集,或者干脆用分层 K 折。如果某个类别样本数少于折数,StratifiedKFold会直接报错提醒你,这反而是好事——早报错早处理。
5. 什么时候该用 accuracy,什么时候必须换指标
这一节是全篇的落脚点。我不打算给一个"永远用 XXX"的结论,因为指标选型从来都是看场景的。
5.1 一张判断表解决大部分选型问题
| 数据情况 | 关注点 | 推荐主指标 | accuracy 的角色 |
|---|---|---|---|
| 类别均衡、错误代价对称 | 总体判对率 | accuracy | 主指标,够用 |
| 类别不均衡(多数类 > 80%) | 少数类识别 | balanced_accuracy / recall / f1 | 仅作参考,不单独汇报 |
| 假正例代价高(误报骚扰用户) | 精确率 | precision / f1 | 辅助 |
| 假反例代价高(漏检) | 召回率 | recall / PR 曲线 | 辅助 |
| 多分类且各类同等重要 | 类间均衡 | macro f1 / balanced_accuracy | 辅助 |
| 多分类且允许候选列表 | 前 k 命中 | top_k_accuracy | 不用 |
| 多标签 | 逐标签表现 | hamming_loss / samples f1 | 不用(子集准确率过于严格) |
| 需要概率校准 | 概率可靠性 | brier_score / log_loss | 不用 |
这张表的用法是:先看第一列定位自己的场景,再看第三列确定主指标,最后用第四列决定 accuracy 放在什么位置。绝大多数情况下 accuracy 不是不能用,而是不能"只用"。
5.2 把准确度用对的几个工程习惯
第一个习惯:永远同时报基线。训练任何分类模型之前,先跑一遍DummyClassifier,把它的准确度记下来。你的模型准确度减去这个基线,才是"模型真正学到的增量"。这个数字在 0.03 以内的时候,我基本会怀疑模型没学到东西,或者特征里根本没有可用信号。
第二个习惯:准确度和混淆矩阵绑在一起出现。单独一个accuracy_score的输出信息量太低,配上confusion_matrix才能定位到"哪一类被错判成了哪一类"。我在代码里封装了一个评估函数,固定打印准确度、平衡准确度、混淆矩阵、分类报告四件套,任何模型都走这个函数,避免临时起意漏掉关键信息。
第三个习惯:把阈值和指标放在一起讨论。前面提过,predict()的 0.5 阈值是一个默认约定,不是最优选择。如果你的业务能接受"多报一些、少漏一些",那就应该主动把阈值调低,然后报出对应阈值下的准确率、精确率、召回率。只报默认阈值下的准确度,等于隐藏了一个重要假设。
第四个习惯:样本量小的时候,别报小数点后四位。500 条测试集上,一条样本的差异就是 0.2%,报 0.8743 这种精度纯属自欺欺人。我一般报两位小数,同时附上交叉验证的标准差,让读者自己判断这个数字有多稳。
注意:
balanced_accuracy_score在多分类下是各类召回率的算术平均,对少数类更敏感。它和accuracy一起看,能快速判断"准确度高是因为真的学会了,还是因为多数类太多"。这两个数字差距超过 0.1 的时候,就该警惕类别不平衡了。
最后分享一个我自己常用的小检查:把测试集按预测置信度排序,取置信度最低的 5% 样本单独算一次准确度。如果这 5% 的准确度低到离谱(比如 0.1),说明模型在边界样本上基本靠猜,整体准确度是被高置信度样本撑起来的。这个分层视角在很多评估报告里看不到,但对判断模型能不能上线很有参考价值——毕竟线上出问题,往往就出在那些模型"拿不准"的样本上。