机器学习之模型评估指标详解

摘要: 模型评估是机器学习工作流程中的核心环节,选择合适的评估指标直接影响模型优化的方向与最终效果。本文系统梳理了分类、回归两大任务场景下的核心评估指标,从混淆矩阵出发,深入讲解准确率、精确率、召回率、F1分数等分类指标的理论意义与代码实现;详细阐述ROC曲线与AUC的原理、绘制方法及阈值选择策略;对比PR曲线在不同数据分布下的适用性;并涵盖MAE、MSE、RMSE、R²、MAPE等回归指标的含义与Python实现。通过完整的Scikit-learn代码示例,帮助读者快速掌握模型评估的实战技巧。

关键词: 机器学习;模型评估;混淆矩阵;ROC曲线;AUC;F1分数;scikit-learn


1. 引言

机器学习的核心目标是构建能够泛化到新数据的模型,而模型评估正是检验这一目标是否达成的关键步骤。无论是经典的监督学习还是新兴的深度学习,模型评估指标都是连接训练过程与实际应用的桥梁。一个看似"准确率99%"的模型,可能因为数据不平衡而毫无价值;一个AUC为0.5的分类器,可能在特定阈值下成为业务利器。因此,深入理解各类评估指标的内涵与适用场景,是每一位机器学习从业者的必修课。

本文将按照分类任务与回归任务两大类别,系统讲解主流评估指标的数学原理,并通过scikit-learn给出完整代码示例。读者可根据自身业务场景,快速定位并上手适用的评估方法。


2. 分类评估指标

2.1 混淆矩阵:所有指标的基石

混淆矩阵(Confusion Matrix)是二分类问题评估的核心工具,以矩阵形式直观展示模型预测与真实标签的对照关系。矩阵的每一行代表模型预测的类别,每一列代表真实类别。

对于二分类问题,混淆矩阵的结构如下:

预测为正类 预测为负类
实际为正类 TP(真正例) FN(假负例)
实际为负类 FP(假正例) TN(真负例)

四个基本概念定义如下:

  • TP(True Positive):实际为正,预测为正------预测正确

  • FP(False Positive):实际为负,预测为正------漏诊负类,误报

  • FN(False Negative):实际为正,预测为负------漏诊正类,漏报

  • TN(True Negative):实际为负,预测为负------预测正确

复制代码
# Python代码示例:混淆矩阵的构建与可视化
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report
​
# 1. 生成模拟的二分类数据集
# n_samples=5000:样本数量5000
# n_features=20:特征数量20
# n_informative=15:有信息量的特征数量15
# n_redundant=5:冗余特征数量5
# weights=[0.9, 0.1]:类别权重,负类占90%,正类占10%(模拟不平衡数据)
X, y = make_classification(
    n_samples=5000,
    n_features=20,
    n_informative=15,
    n_redundant=5,
    weights=[0.9, 0.1],  # 不平衡数据集,正类仅占10%
    random_state=42
)
​
# 2. 划分训练集和测试集
# test_size=0.3:30%作为测试集
# random_state=42:保证结果可复现
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)
​
# 3. 训练逻辑回归模型
model = LogisticRegression(max_iter=1000, random_state=42)
model.fit(X_train, y_train)
​
# 4. 在测试集上预测
y_pred = model.predict(X_test)
​
# 5. 计算混淆矩阵
# confusion_matrix 返回值顺序: [[TN, FP], [FN, TP]]
cm = confusion_matrix(y_test, y_pred)
print("混淆矩阵:")
print(cm)
# 输出示例:
# [[1345   32]
#  [  98   25]]
# TN=1345, FP=32, FN=98, TP=25
​
# 6. 可视化混淆矩阵
# titles_options: 可选多种显示格式
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
​
# 左图:带数值标签的混淆矩阵
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=["负类(0)", "正类(1)"])
disp.plot(ax=axes[0], cmap="Blues")
axes[0].set_title("混淆矩阵(数值标签)", fontsize=14)
​
# 右图:归一化后的混淆矩阵(每个单元格表示占比)
cm_normalized = cm.astype("float") / cm.sum(axis=1)[:, np.newaxis]
disp_norm = ConfusionMatrixDisplay(confusion_matrix=cm_normalized, display_labels=["负类(0)", "正类(1)"])
disp_norm.plot(ax=axes[1], cmap="Blues")
axes[1].set_title("混淆矩阵(按行归一化)", fontsize=14)
​
plt.tight_layout()
plt.savefig("confusion_matrix.png", dpi=150, bbox_inches="tight")
plt.show()
​
# 7. 打印详细分类报告(包含各类指标的精确值)
# classification_report 输出每个类别的 Precision/Recall/F1-Score/Support
print("\n分类报告:")
print(classification_report(y_test, y_pred, target_names=["负类(0)", "正类(1)"]))

运行上述代码后,将得到混淆矩阵的热力图与标准化矩阵,从图中可直观看出:虽然整体准确率较高(因负类样本占主导),但正类的召回率极低,说明模型对少数类的识别能力严重不足。

2.2 准确率(Accuracy)

公式:

Accuracy = \\frac{TP + TN}{TP + TN + FP + FN}

准确率是最直观、最常用的分类指标,表示所有预测中正确预测的比例。其取值范围为[0, 1],值越接近1表示模型越好。然而,准确率在数据不平衡场景下具有严重的欺骗性。例如,癌症筛查数据中阳性率仅1%,模型即使把所有样本都预测为阴性,准确率也能达到99%,但这毫无意义。

复制代码
from sklearn.metrics import accuracy_score
​
# 计算准确率
acc = accuracy_score(y_test, y_pred)
print(f"准确率 (Accuracy): {acc:.4f}")
# 说明:在不平衡数据集下,99%的准确率可能掩盖正类召回率仅10%的问题

使用场景: 数据类别相对平衡(各类别样本比例相近)时,准确率是快速评估模型整体性能的不错选择。但面对不平衡数据,务必结合其他指标综合判断。

2.3 精确率(Precision)

公式:

Precision = \\frac{TP}{TP + FP} = \\frac{预测为正类中实际为正类的数量}{所有预测为正类的数量}

精确率衡量的是预测为正的样本中,有多少是真正的正类。它反映的是模型"不误报"的能力。在推荐系统中,精确率高意味着推荐的内容用户大多确实感兴趣;在垃圾邮件检测中,精确率高意味着被标记为垃圾邮件的邮件确实是垃圾邮件。

复制代码
from sklearn.metrics import precision_score
​
# 计算精确率
# zero_division=0:防止分母为0时出现警告
prec = precision_score(y_test, y_pred, zero_division=0)
print(f"精确率 (Precision): {prec:.4f}")

使用场景: 当"误报代价高"时,优先关注精确率。例如:

  • 股票推荐系统:误推荐一只会跌的股票给用户,会造成直接经济损失

  • 医疗诊断:误诊一个健康人为患者,会导致不必要的进一步检查和患者焦虑

2.4 召回率(Recall / Sensitivity)

公式:

Recall = \\frac{TP}{TP + FN} = \\frac{预测为正类中实际为正类的数量}{所有实际为正类的数量}

召回率衡量的是所有正类样本中,模型成功识别出了多少。它反映的是模型"不漏报"的能力。召回率高意味着尽可能把所有正类样本都找出来。

复制代码
from sklearn.metrics import recall_score
​
# 计算召回率
rec = recall_score(y_test, y_pred, zero_division=0)
print(f"召回率 (Recall): {rec:.4f}")

使用场景: 当"漏报代价高"时,优先关注召回率。例如:

  • 癌症筛查:漏检一个真正的癌症患者可能导致延误治疗,代价极高

  • 欺诈检测:漏掉一笔欺诈交易可能造成重大经济损失

  • 地震预测:即使误报也不能漏报,召回率至关重要

2.5 F1分数(F1-Score)

精确率和召回率往往是一对矛盾体------追求高精确率意味着模型更保守(只预测非常有把握的正类),这会导致召回率下降;反之亦然。F1分数正是为调和这一矛盾而设计的。

公式:

F1 = \\frac{2 \\times Precision \\times Recall}{Precision + Recall}

F1是精确率和召回率的调和平均数,相比简单算术平均,它更重视两者的平衡。只有当精确率和召回率都较高时,F1才会高。

复制代码
from sklearn.metrics import f1_score
​
# 计算F1分数
f1 = f1_score(y_test, y_pred, zero_division=0)
print(f"F1分数 (F1-Score): {f1:.4f}")

F1的更一般形式是Fβ分数,允许通过β调节对召回率的重视程度:

F_\\beta = (1 + \\beta\^2) \\times \\frac{Precision \\times Recall}{\\beta\^2 \\times Precision + Recall}

  • β < 1:更重视精确率(如F0.5)

  • β = 1:精确率和召回率同等重要(F1)

  • β > 1:更重视召回率(如F2)

复制代码
from sklearn.metrics import fbeta_score
​
# 计算F2分数(召回率的权重更高)
# 在医疗检测等场景,F2分数比F1更常用
f2 = fbeta_score(y_test, y_pred, beta=2, zero_division=0)
print(f"F2分数 (F2-Score): {f2:.4f}")

2.6 特异性(Specificity)

公式:

Specificity = \\frac{TN}{TN + FP} = \\frac{预测为负类中实际为负类的数量}{所有实际为负类的数量}

特异性衡量的是模型正确识别负类的能力,与召回率(敏感度)相对应。在医学检测中,特异性高意味着健康人不会被误诊为患者。

复制代码
# 手动计算特异性
# 从混淆矩阵中提取 TN, FP
tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel()
specificity = tn / (tn + fp)
print(f"特异性 (Specificity): {specificity:.4f}")
# 或者用 sklearn 的 recall_score,pos_label 参数指定为负类
specificity_sklearn = recall_score(y_test, y_pred, pos_label=0)
print(f"特异性 (使用sklearn): {specificity_sklearn:.4f}")

2.7 敏感度(Sensitivity)

敏感度(Sensitivity)与召回率(Recall)是同一概念,都是指TPR(True Positive Rate):

Sensitivity = TPR = \\frac{TP}{TP + FN}

复制代码
# 敏感度与召回率等价
sensitivity = recall_score(y_test, y_pred)
print(f"敏感度 (Sensitivity): {sensitivity:.4f}")

3. ROC曲线与AUC

3.1 TPR与FPR

在深入ROC曲线之前,需要先理解两个核心概念:

  • TPR(True Positive Rate)= 召回率 = 敏感度:所有正类中被正确识别的比例

  • FPR(False Positive Rate):所有负类中被错误识别为正类的比例

FPR = \\frac{FP}{FP + TN}

ROC曲线(Receiver Operating Characteristic Curve)正是以FPR为横轴,TPR为纵轴,展示模型在不同分类阈值下的表现。

复制代码
# 概念说明:手动计算不同阈值下的 TPR 和 FPR
y_scores = model.predict_proba(X_test)[:, 1]  # 获取正类的预测概率
​
# 手动计算 ROC 曲线上的一些点
thresholds = [0.1, 0.3, 0.5, 0.7, 0.9]
for thresh in thresholds:
    # 大于阈值为正类
    y_pred_thresh = (y_scores >= thresh).astype(int)
    tp = np.sum((y_pred_thresh == 1) & (y_test == 1))
    fp = np.sum((y_pred_thresh == 1) & (y_test == 0))
    tn = np.sum((y_pred_thresh == 0) & (y_test == 0))
    fn = np.sum((y_pred_thresh == 0) & (y_test == 1))
    
    tpr = tp / (tp + fn) if (tp + fn) > 0 else 0
    fpr = fp / (fp + tn) if (fp + tn) > 0 else 0
    print(f"阈值={thresh:.1f}: TPR={tpr:.4f}, FPR={fpr:.4f}")

3.2 ROC曲线绘制

复制代码
from sklearn.metrics import roc_curve, auc, roc_auc_score
import matplotlib.pyplot as plt
​
# 1. 获取预测概率(而非直接预测标签)
# predict_proba 返回每行样本属于各类别的概率,第2列是正类(1)的概率
y_prob = model.predict_proba(X_test)[:, 1]
​
# 2. 计算不同阈值下的 FPR 和 TPR
# fpr, tpr, thresholds:分别为假正率、真正率和对应阈值
fpr_arr, tpr_arr, thresholds = roc_curve(y_test, y_prob)
​
# 3. 计算AUC值
# AUC是ROC曲线下的面积,取值范围[0, 1]
auc_score = auc(fpr_arr, tpr_arr)
print(f"AUC值: {auc_score:.4f}")
​
# 也可以直接用 roc_auc_score(输入真实标签和预测概率)
auc_direct = roc_auc_score(y_test, y_prob)
print(f"AUC值 (直接计算): {auc_direct:.4f}")
​
# 4. 绘制ROC曲线
plt.figure(figsize=(8, 6))
plt.plot(fpr_arr, tpr_arr, color="darkorange", lw=2,
         label=f"ROC曲线 (AUC = {auc_score:.4f})")
plt.plot([0, 1], [0, 1], color="navy", lw=2, linestyle="--",
         label="随机猜测 (AUC = 0.5)")
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel("假正率 (FPR)", fontsize=12)
plt.ylabel("真正率 (TPR)", fontsize=12)
plt.title("ROC曲线", fontsize=14)
plt.legend(loc="lower right", fontsize=11)
plt.grid(alpha=0.3)
​
# 标注一些关键阈值点
for i in range(0, len(thresholds), len(thresholds)//5):
    if thresholds[i] < 1:
        plt.annotate(f"阈值={thresholds[i]:.2f}",
                     (fpr_arr[i], tpr_arr[i]),
                     textcoords="offset points",
                     xytext=(5, -10),
                     fontsize=9,
                     arrowprops=dict(arrowstyle="->", color="gray"))
​
plt.tight_layout()
plt.savefig("roc_curve.png", dpi=150, bbox_inches="tight")
plt.show()

3.3 AUC的含义

AUC(Area Under the Curve)即ROC曲线下的面积,其核心含义是:随机从正负样本中各抽取一个,正类样本的预测分数高于负类样本预测分数的概率

  • AUC = 1.0:完美分类器(理想情况,不存在)

  • AUC = 0.5:随机猜测,等同于无分辨能力

  • AUC < 0.5:比随机猜测还差,说明模型预测方向反了(可通过反转预测结果来改善)

AUC的特点:

  • 不受阈值影响:AUC描述的是模型在所有阈值下的整体排序能力,而非某个特定阈值下的表现

  • 对数据不平衡不敏感:由于只看相对排序,不受正负样本比例影响

  • 衡量模型区分能力:本质上,AUC评估的是模型区分正负样本的能力

复制代码
# AUC的另一个理解:正样本score > 负样本score 的概率
# 验证计算
pos_scores = y_prob[y_test == 1]
neg_scores = y_prob[y_test == 0]
# 计算正样本分数大于负样本分数的比例
count = 0
total = len(pos_scores) * len(neg_scores)
for p in pos_scores:
    for n in neg_scores:
        if p > n:
            count += 1
auc_manual = count / total
print(f"AUC (手动验证): {auc_manual:.4f}")

3.4 如何选择最佳阈值

ROC曲线上的每个点对应一个分类阈值。选择最佳阈值需要权衡TPR和FPR,常用方法包括:

方法一:Youden's J统计量

J = TPR - FPR

选择使J值最大的阈值,即ROC曲线上距离对角线最远的点。

复制代码
# 使用 Youden's J 统计量选择最佳阈值
j_scores = tpr_arr - fpr_arr
best_idx = np.argmax(j_scores)
best_threshold = thresholds[best_idx]
best_tpr = tpr_arr[best_idx]
best_fpr = fpr_arr[best_idx]
​
print(f"最佳阈值 (Youden's J): {best_threshold:.4f}")
print(f"对应 TPR: {best_tpr:.4f}, FPR: {best_fpr:.4f}")

方法二:几何平均(G-Mean)

G\\text{-}Mean = \\sqrt{TPR \\times Specificity}

G-Mean综合考虑正类和负类的识别率,在不平衡数据集上表现良好。

复制代码
# 使用 G-Mean 选择最佳阈值
specificity_arr = 1 - fpr_arr
g_means = np.sqrt(tpr_arr * specificity_arr)
best_g_idx = np.argmax(g_means)
best_g_threshold = thresholds[best_g_idx]
​
print(f"最佳阈值 (G-Mean): {best_g_threshold:.4f}")
print(f"对应 TPR: {tpr_arr[best_g_idx]:.4f}, Specificity: {specificity_arr[best_g_idx]:.4f}")

4. PR曲线

4.1 什么是PR曲线

PR曲线(Precision-Recall Curve)以召回率为横轴,精确率为纵轴 ,展示不同阈值下两者之间的权衡关系。相比ROC曲线,PR曲线更能反映模型在不平衡数据集上的真实表现。

复制代码
from sklearn.metrics import precision_recall_curve, average_precision_score
from sklearn.metrics import auc
​
# 1. 计算PR曲线
# 返回不同阈值下的精确率、召回率和对应阈值
precision_arr, recall_arr, pr_thresholds = precision_recall_curve(y_test, y_prob)
​
# 2. 计算PR曲线下面积(Average Precision)
# AP是PR曲线下面积的近似,等效于AUC在PR空间中的值
ap_score = average_precision_score(y_test, y_prob)
print(f"平均精确率 (Average Precision): {ap_score:.4f}")
​
# 3. 绘制PR曲线
plt.figure(figsize=(8, 6))
plt.plot(recall_arr, precision_arr, color="green", lw=2,
         label=f"PR曲线 (AP = {ap_score:.4f})")
​
# 标注基线(正类比例)
baseline = np.sum(y_test) / len(y_test)
plt.axhline(y=baseline, color="red", linestyle="--", lw=1.5,
            label=f"基线 (正类比例 = {baseline:.4f})")
​
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel("召回率 (Recall)", fontsize=12)
plt.ylabel("精确率 (Precision)", fontsize=12)
plt.title("精确率-召回率曲线 (PR Curve)", fontsize=14)
plt.legend(loc="upper right", fontsize=11)
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("pr_curve.png", dpi=150, bbox_inches="tight")
plt.show()

4.2 为什么PR曲线在不平衡数据下更可靠

当数据集严重不平衡时,ROC曲线的FPR分母(TN+FP)中TN占主导,导致FPR变化不明显,ROC曲线的变化不足以反映模型对少数类的识别能力变化。而PR曲线直接以精确率和召回率为坐标,更敏感地反映少数类的表现。

复制代码
# 对比:在极度不平衡数据下ROC和PR曲线的差异
# 生成极度不平衡数据(正类仅占1%)
X_imbalanced, y_imbalanced = make_classification(
    n_samples=10000,
    n_features=20,
    n_informative=10,
    weights=[0.99, 0.01],  # 1%正类
    random_state=42
)
​
X_train_imb, X_test_imb, y_train_imb, y_test_imb = train_test_split(
    X_imbalanced, y_imbalanced, test_size=0.3, random_state=42, stratify=y_imbalanced
)
​
model_imb = LogisticRegression(max_iter=1000, random_state=42)
model_imb.fit(X_train_imb, y_train_imb)
y_prob_imb = model_imb.predict_proba(X_test_imb)[:, 1]
​
# 计算 ROC AUC 和 PR AUC
roc_auc_imb = roc_auc_score(y_test_imb, y_prob_imb)
ap_imb = average_precision_score(y_test_imb, y_prob_imb)
​
print(f"极度不平衡数据 - ROC AUC: {roc_auc_imb:.4f}")
print(f"极度不平衡数据 - Average Precision (PR AUC): {ap_imb:.4f}")
​
# PR曲线的AP很低说明模型在少数类上的精确率和召回率都很差
# 而ROC AUC看起来可能还不错(因为FPR变化不大)

5. 回归评估指标

回归任务评估的是模型预测的连续值与真实值之间的误差。以下是常用的回归评估指标。

5.1 MAE(平均绝对误差)

公式:

MAE = \\frac{1}{n} \\sum*{i=1}\^{n} \|y_i - \\hat{y}*i\|

MAE是预测值与真实值之差的绝对值的平均,对所有误差赋予相同权重,对异常值相对稳健。

复制代码
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import numpy as np
​
# 生成回归数据集
from sklearn.datasets import make_regression
X_reg, y_reg = make_regression(
    n_samples=1000,
    n_features=10,
    noise=20,
    random_state=42
)
​
X_train_reg, X_test_reg, y_train_reg, y_test_reg = train_test_split(
    X_reg, y_reg, test_size=0.3, random_state=42
)
​
# 训练一个简单的回归模型
from sklearn.linear_model import LinearRegression
reg_model = LinearRegression()
reg_model.fit(X_train_reg, y_train_reg)
y_pred_reg = reg_model.predict(X_test_reg)
​
# 1. MAE:平均绝对误差
mae = mean_absolute_error(y_test_reg, y_pred_reg)
print(f"MAE (平均绝对误差): {mae:.4f}")
​
# 手动验证
mae_manual = np.mean(np.abs(y_test_reg - y_pred_reg))
print(f"MAE (手动验证): {mae_manual:.4f}")

5.2 MSE(均方误差)

公式:

MSE = \\frac{1}{n} \\sum*{i=1}\^{n} (y_i - \\hat{y}*i)\^2

MSE对较大的误差给予更大的惩罚(因为平方项放大了大误差的影响),但对异常值敏感。

复制代码
# 2. MSE:均方误差
mse = mean_squared_error(y_test_reg, y_pred_reg)
print(f"MSE (均方误差): {mse:.4f}")
​
# 手动验证
mse_manual = np.mean((y_test_reg - y_pred_reg) ** 2)
print(f"MSE (手动验证): {mse_manual:.4f}")

5.3 RMSE(均方根误差)

公式:

RMSE = \\sqrt{MSE} = \\sqrt{\\frac{1}{n} \\sum*{i=1}\^{n} (y_i - \\hat{y}*i)\^2}

RMSE是MSE的平方根,与目标变量具有相同的单位,更容易解释。在物理和工程领域,RMSE比MSE更常用。

复制代码
# 3. RMSE:均方根误差
rmse = np.sqrt(mse)
print(f"RMSE (均方根误差): {rmse:.4f}")
​
# sklearn 直接计算
rmse_sklearn = mean_squared_error(y_test_reg, y_pred_reg, squared=False)
print(f"RMSE (sklearn): {rmse_sklearn:.4f}")

5.4 R²(决定系数)

公式:

R\^2 = 1 - \\frac{\\sum*{i=1}\^{n}(y_i - \\hat{y}* i)\^2}{\\sum*{i=1}\^{n}(y_i - \\bar{y})\^2} = 1 - \\frac{SS*{res}}{SS_{tot}}

R²表示模型对目标变量变异性的解释程度,取值范围通常为[0, 1](在OLS框架下),但也可能为负(当模型预测比简单均值还差时)。

  • R² = 1:模型完美预测

  • R² = 0:模型预测效果与简单均值预测相当

  • R² < 0:模型完全失效,预测不如均值

复制代码
# 4. R²:决定系数
r2 = r2_score(y_test_reg, y_pred_reg)
print(f"R² (决定系数): {r2:.4f}")
​
# 手动验证
ss_res = np.sum((y_test_reg - y_pred_reg) ** 2)  # 残差平方和
ss_tot = np.sum((y_test_reg - np.mean(y_test_reg)) ** 2)  # 总平方和
r2_manual = 1 - (ss_res / ss_tot)
print(f"R² (手动验证): {r2_manual:.4f}")
​
# 说明:R²=0.85意味着模型解释了85%的目标变量方差

5.5 MAPE(平均绝对百分比误差)

公式:

MAPE = \\frac{100\\%}{n} \\sum*{i=1}\^{n} \\left\| \\frac{y_i - \\hat{y}*i}{y_i} \\right\|

MAPE以百分比形式表示预测误差,适合业务报告和跨场景比较。但当真实值接近0时,MAPE会趋向无穷大,需要特殊处理。

复制代码
# 5. MAPE:平均绝对百分比误差
# 注意:MAPE在sklearn中需要手动计算
def mean_absolute_percentage_error(y_true, y_pred):
    """计算MAPE,处理y_true中含0的情况"""
    y_true, y_pred = np.array(y_true), np.array(y_pred)
    # 避免除以0:只计算非零样本的MAPE
    non_zero_mask = y_true != 0
    return np.mean(np.abs((y_true[non_zero_mask] - y_pred[non_zero_mask]) / y_true[non_zero_mask])) * 100
​
mape = mean_absolute_percentage_error(y_test_reg, y_pred_reg)
print(f"MAPE (平均绝对百分比误差): {mape:.4f}%")
​
# 注意:MAPE的缺点是当真实值接近0时会变得非常大
# 解决方法是使用对称MAPE(SMAPE)或在真实值极小时限制计算范围

5.6 回归指标对比与选择

复制代码
# 综合对比所有回归指标
print("=" * 50)
print("回归指标综合对比")
print("=" * 50)
print(f"MAE  (平均绝对误差):         {mae:.4f}")
print(f"MSE  (均方误差):             {mse:.4f}")
print(f"RMSE (均方根误差):           {rmse:.4f}")
print(f"R²   (决定系数):             {r2:.4f}")
print(f"MAPE (平均绝对百分比误差):   {mape:.4f}%")
print("=" * 50)
​
# 指标解读
print("\n指标选择指南:")
print("- MAE:误差量纲相同,对异常值稳健,适合评估典型误差")
print("- MSE:惩罚大误差,适合"大错代价高"的场景(如金融风险)")
print("- RMSE:与MAE相同量纲,适合报告和业务沟通")
print("- R²:衡量模型解释力,0.7-0.9通常为较好水平")
print("- MAPE:适合跨尺度比较,但不适合同比接近0的数据")

6. 实战:综合评估示例

下面通过一个完整的实战案例,展示如何针对不同业务场景选择合适的评估指标。

复制代码
# 综合实战:多模型对比与指标选择
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split, cross_val_predict
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.metrics import (
    confusion_matrix, accuracy_score, precision_score, recall_score,
    f1_score, roc_auc_score, average_precision_score,
    classification_report, roc_curve, precision_recall_curve
)
import matplotlib.pyplot as plt
import numpy as np
​
# 1. 准备不平衡数据集(模拟实际业务场景)
X, y = make_classification(
    n_samples=5000, n_features=20, n_informative=15,
    n_redundant=5, weights=[0.85, 0.15],  # 15%正类
    random_state=42
)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)
​
# 2. 定义三个不同类型的模型
models = {
    "逻辑回归": LogisticRegression(max_iter=1000, random_state=42),
    "随机森林": RandomForestClassifier(n_estimators=100, random_state=42),
    "SVM": SVC(kernel="rbf", probability=True, random_state=42)
}
​
# 3. 存储评估结果
results = {}
​
# 4. 对每个模型进行评估
for name, model in models.items():
    print(f"\n{'='*50}")
    print(f"模型: {name}")
    print('='*50)
    
    # 训练模型
    model.fit(X_train, y_train)
    
    # 获取预测结果
    y_pred = model.predict(X_test)
    y_prob = model.predict_proba(X_test)[:, 1]
    
    # 计算各项指标
    cm = confusion_matrix(y_test, y_pred)
    tn, fp, fn, tp = cm.ravel()
    
    accuracy = accuracy_score(y_test, y_pred)
    precision = precision_score(y_test, y_pred, zero_division=0)
    recall = recall_score(y_test, y_pred, zero_division=0)
    f1 = f1_score(y_test, y_pred, zero_division=0)
    specificity = tn / (tn + fp)
    roc_auc = roc_auc_score(y_test, y_prob)
    ap = average_precision_score(y_test, y_prob)
    
    # 存储结果
    results[name] = {
        "Accuracy": accuracy,
        "Precision": precision,
        "Recall": recall,
        "F1": f1,
        "Specificity": specificity,
        "ROC_AUC": roc_auc,
        "AP": ap,
        "y_prob": y_prob
    }
    
    # 打印详细报告
    print(f"混淆矩阵: TN={tn}, FP={fp}, FN={fn}, TP={tp}")
    print(f"准确率 (Accuracy):     {accuracy:.4f}")
    print(f"精确率 (Precision):    {precision:.4f}")
    print(f"召回率 (Recall):       {recall:.4f}")
    print(f"F1分数 (F1-Score):     {f1:.4f}")
    print(f"特异性 (Specificity):  {specificity:.4f}")
    print(f"ROC_AUC:              {roc_auc:.4f}")
    print(f"AP (PR_AUC):          {ap:.4f}")
    print(f"\n详细分类报告:")
    print(classification_report(y_test, y_pred, target_names=["负类", "正类"], zero_division=0))
​
# 5. 汇总对比表格
print("\n" + "="*80)
print("模型综合对比汇总")
print("="*80)
print(f"{'模型':<15} {'Acc':>8} {'Prec':>8} {'Rec':>8} {'F1':>8} {'Spec':>8} {'AUC':>8} {'AP':>8}")
print("-"*80)
for name, metrics in results.items():
    print(f"{name:<15} {metrics['Accuracy']:>8.4f} {metrics['Precision']:>8.4f} "
          f"{metrics['Recall']:>8.4f} {metrics['F1']:>8.4f} {metrics['Specificity']:>8.4f} "
          f"{metrics['ROC_AUC']:>8.4f} {metrics['AP']:>8.4f}")
print("="*80)
​
# 6. 绘制对比图
fig, axes = plt.subplots(2, 2, figsize=(14, 12))
​
# 6.1 ROC曲线对比
ax1 = axes[0, 0]
colors = ["blue", "green", "red"]
for (name, metrics), color in zip(results.items(), colors):
    fpr, tpr, _ = roc_curve(y_test, metrics["y_prob"])
    ax1.plot(fpr, tpr, color=color, lw=2, label=f"{name} (AUC={metrics['ROC_AUC']:.4f})")
ax1.plot([0, 1], [0, 1], "k--", lw=1)
ax1.set_xlabel("FPR")
ax1.set_ylabel("TPR")
ax1.set_title("ROC曲线对比")
ax1.legend()
ax1.grid(alpha=0.3)
​
# 6.2 PR曲线对比
ax2 = axes[0, 1]
for (name, metrics), color in zip(results.items(), colors):
    precision, recall, _ = precision_recall_curve(y_test, metrics["y_prob"])
    ax2.plot(recall, precision, color=color, lw=2, label=f"{name} (AP={metrics['AP']:.4f})")
baseline = np.sum(y_test) / len(y_test)
ax2.axhline(y=baseline, color="gray", linestyle="--", label=f"基线={baseline:.4f}")
ax2.set_xlabel("Recall")
ax2.set_ylabel("Precision")
ax2.set_title("PR曲线对比")
ax2.legend()
ax2.grid(alpha=0.3)
​
# 6.3 指标柱状图
ax3 = axes[1, 0]
metrics_to_plot = ["Precision", "Recall", "F1", "AP"]
x = np.arange(len(metrics_to_plot))
width = 0.25
for i, (name, metrics) in enumerate(results.items()):
    values = [metrics[m] for m in metrics_to_plot]
    ax3.bar(x + i*width, values, width, label=name, color=colors[i], alpha=0.8)
ax3.set_xticks(x + width)
ax3.set_xticklabels(metrics_to_plot)
ax3.set_ylabel("分数")
ax3.set_title("分类指标对比")
ax3.legend()
ax3.grid(alpha=0.3, axis="y")
​
# 6.4 指标雷达图
ax4 = axes[1, 1]
ax4.axis("off")
# 用表格替代雷达图(雷达图在sklearn外需额外库)
cell_text = []
for name, metrics in results.items():
    cell_text.append([
        f"{metrics['Accuracy']:.3f}",
        f"{metrics['Precision']:.3f}",
        f"{metrics['Recall']:.3f}",
        f"{metrics['F1']:.3f}",
        f"{metrics['Specificity']:.3f}",
        f"{metrics['ROC_AUC']:.3f}",
        f"{metrics['AP']:.3f}"
    ])
table = ax4.table(
    cellText=cell_text,
    rowLabels=list(results.keys()),
    colLabels=["Acc", "Prec", "Rec", "F1", "Spec", "AUC", "AP"],
    cellLoc="center",
    rowLoc="center",
    loc="center",
    bbox=[0.1, 0.3, 0.8, 0.5]
)
table.auto_set_font_size(False)
table.set_fontsize(10)
table.scale(1.2, 1.5)
ax4.set_title("指标汇总表", fontsize=12, pad=20)
​
plt.tight_layout()
plt.savefig("model_comparison.png", dpi=150, bbox_inches="tight")
plt.show()

7. 指标选择指南

不同业务场景下,评估指标的侧重点各有不同。以下是常见场景的指标推荐:

7.1 分类任务

场景 主要指标 次要指标 说明
类别平衡数据 Accuracy F1, ROC_AUC 各类别样本比例相近时,准确率即可反映整体性能
不平衡数据(少数类重要) Recall / F1 ROC_AUC, AP 如疾病筛查、欺诈检测,漏报代价高
不平衡数据(误报代价高) Precision F1, ROC_AUC 如推荐系统、垃圾邮件过滤
排序任务 ROC_AUC AP 只关心正类排在负类前面的概率
极度不平衡 PR_AUC (AP) Recall ROC曲线在极度不平衡下可能过于乐观
需要综合权衡 F1 / Fβ - 需要同时控制误报和漏报

7.2 回归任务

指标 特点 适用场景
MAE 对异常值稳健,量纲与目标变量一致 典型误差评估、成本预测
MSE 惩罚大误差,对异常值敏感 金融风险、重大偏差不可接受场景
RMSE 与MAE同量纲,易解释 物理工程领域、报告与沟通
衡量模型解释力,与数据尺度无关 模型解释力评估、论文报告
MAPE 百分比形式,跨尺度可比 业务报告、需求预测

8. 总结

本文系统梳理了机器学习中分类与回归两大任务的核心评估指标:

  1. 分类任务:从混淆矩阵出发,理解TP/FP/TN/FN四个基本概念是掌握所有分类指标的关键。精确率与召回率的平衡是分类评估的核心挑战,F1分数提供了折中方案;ROC-AUC从全局排序能力角度评估模型,在数据不平衡时比准确率更可靠;PR曲线则在极度不平衡场景下提供更准确的性能画像。

  2. 回归任务:MAE、MSE、RMSE从不同角度刻画预测误差,R²反映模型的解释力,MAPE以百分比形式便于跨场景比较。

  3. 实战要点:没有"万能指标",必须结合业务场景和数据特点选择合适的评估体系。数据不平衡时尤其需要谨慎,避免被单一准确率误导。

理解每个指标的数学本质与适用边界,才能在模型开发中做出正确的优化方向决策,真正发挥机器学习模型的价值。


相关推荐
测试员周周1 小时前
【Appium 系列】第03节-驱动初始化 — BaseDriver 的设计与实现
开发语言·人工智能·python·功能测试·appium·测试用例·web app
泰迪智能科技011 小时前
分享|企业数据挖掘平台从“平台工具”到“育人生态”
人工智能·数据挖掘
AI医影跨模态组学1 小时前
Radiology(IF=15.2)中南大学湘雅二医院肖煜东教授等团队:基于CT放射组学的机器学习识别肝细胞癌瘤内纤维化及其潜在血管生成
人工智能·深度学习·论文·医学·医学影像·影像组学
工业机器人销售服务2 小时前
应对频繁换模挑战:伯朗特机器人快换方案实现冲压产线“分钟级”换产
人工智能
2501_921960852 小时前
地图之外:对Lerchner“AI永无意识”论的系统反驳与协同本体论的重建
人工智能·重构
AI医影跨模态组学2 小时前
Eur Radiol 温州医科大学第五附属医院等团队:开发与解释基于双能量CT的深度学习放射组学模型,用于预测颈动脉支架后新出现的脑缺血病灶
人工智能·深度学习·论文·医学·医学影像·影像组学
Bode_20023 小时前
制造企业实现产品服务化的路径
人工智能
Rubin智造社3 小时前
Claude Code开发者大会系列2|“饮鸩止渴”还是“即刻解药”?Anthropic与SpaceX的联姻内幕
大数据·数据库·人工智能·开发者大会·anthropic·claude code
AI机器学习算法3 小时前
机器学习基础知识
数据结构·人工智能·python·深度学习·算法·机器学习·ai学习路线