分类与回归的全面评估

前言

选择正确的评估指标是模型开发的关键。不同的业务场景需要不同的指标,本文全面介绍分类和回归任务的常用评估指标。


分类评估指标

混淆矩阵

二分类混淆矩阵:

  预测正类 预测负类
实际正类 TP FN
实际负类 FP TN
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (confusion_matrix, accuracy_score, precision_score,
                             recall_score, f1_score, classification_report)
import seaborn as sns

np.random.seed(42)

# 生成不平衡数据
X, y = make_classification(n_samples=1000, n_features=20, n_informative=10,
                           weights=[0.9, 0.1], random_state=42)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 训练模型
lr = LogisticRegression(random_state=42, max_iter=1000)
lr.fit(X_train, y_train)
y_pred = lr.predict(X_test)

# 混淆矩阵
cm = confusion_matrix(y_test, y_pred)

plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=['Negative', 'Positive'],
            yticklabels=['Negative', 'Positive'])
plt.xlabel('预测')
plt.ylabel('实际')
plt.title('混淆矩阵')
plt.show()

tn, fp, fn, tp = cm.ravel()
print(f"TN={tn}, FP={fp}, FN={fn}, TP={tp}")

准确率(Accuracy)

\[\text{Accuracy} = \frac{TP + TN}{TP + TN + FP + FN}\]
acc = accuracy_score(y_test, y_pred)
print(f"准确率: {acc:.4f}")

# 手动计算
acc_manual = (tp + tn) / (tp + tn + fp + fn)
print(f"手动计算准确率: {acc_manual:.4f}")

注意:类别不平衡时准确率可能具有误导性。

精确率(Precision)

\[\text{Precision} = \frac{TP}{TP + FP}\]

预测为正的样本中,有多少真的是正。

prec = precision_score(y_test, y_pred)
print(f"精确率: {prec:.4f}")

应用场景:垃圾邮件检测(不想把正常邮件标为垃圾)

召回率(Recall / Sensitivity)

\[\text{Recall} = \frac{TP}{TP + FN}\]

所有正样本中,有多少被正确预测。

rec = recall_score(y_test, y_pred)
print(f"召回率: {rec:.4f}")

应用场景:癌症检测(不想漏掉任何患者)

F1分数

精确率和召回率的调和平均:

\[F1 = 2 \cdot \frac{\text{Precision} \cdot \text{Recall}}{\text{Precision} + \text{Recall}}\]
f1 = f1_score(y_test, y_pred)
print(f"F1分数: {f1:.4f}")

# 完整报告
print("\n分类报告:")
print(classification_report(y_test, y_pred))

F-beta分数

\[F_\beta = (1 + \beta^2) \cdot \frac{\text{Precision} \cdot \text{Recall}}{\beta^2 \cdot \text{Precision} + \text{Recall}}\]
  • $\beta > 1$:更重视召回率
  • $\beta < 1$:更重视精确率
from sklearn.metrics import fbeta_score

f2 = fbeta_score(y_test, y_pred, beta=2)  # 更重视召回率
f05 = fbeta_score(y_test, y_pred, beta=0.5)  # 更重视精确率

print(f"F2分数: {f2:.4f}")
print(f"F0.5分数: {f05:.4f}")

概率预测评估

ROC曲线与AUC

from sklearn.metrics import roc_curve, roc_auc_score

# 获取概率预测
y_proba = lr.predict_proba(X_test)[:, 1]

# 计算ROC曲线
fpr, tpr, thresholds = roc_curve(y_test, y_proba)
auc = roc_auc_score(y_test, y_proba)

plt.figure(figsize=(10, 5))

plt.subplot(1, 2, 1)
plt.plot(fpr, tpr, 'b-', linewidth=2, label=f'ROC (AUC={auc:.3f})')
plt.plot([0, 1], [0, 1], 'k--', alpha=0.5, label='随机猜测')
plt.fill_between(fpr, tpr, alpha=0.3)
plt.xlabel('假正例率 (FPR)')
plt.ylabel('真正例率 (TPR)')
plt.title('ROC曲线')
plt.legend()
plt.grid(True, alpha=0.3)

# 阈值对应的指标
plt.subplot(1, 2, 2)
plt.plot(thresholds, tpr[:-1], 'b-', label='TPR')
plt.plot(thresholds, fpr[:-1], 'r-', label='FPR')
plt.plot(thresholds, tpr[:-1] - fpr[:-1], 'g-', label='TPR-FPR')
plt.xlabel('阈值')
plt.ylabel('')
plt.title('阈值选择')
plt.legend()
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

PR曲线

from sklearn.metrics import precision_recall_curve, average_precision_score

precision, recall, thresholds_pr = precision_recall_curve(y_test, y_proba)
ap = average_precision_score(y_test, y_proba)

plt.figure(figsize=(8, 6))
plt.plot(recall, precision, 'g-', linewidth=2, label=f'PR (AP={ap:.3f})')
plt.fill_between(recall, precision, alpha=0.3)
plt.xlabel('召回率')
plt.ylabel('精确率')
plt.title('PR曲线')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

ROC vs PR

场景 推荐曲线
类别平衡 ROC
类别不平衡 PR
关注正类 PR

多分类评估

from sklearn.datasets import load_iris
from sklearn.metrics import confusion_matrix, classification_report

# 加载多分类数据
iris = load_iris()
X_iris, y_iris = iris.data, iris.target

X_train_i, X_test_i, y_train_i, y_test_i = train_test_split(
    X_iris, y_iris, test_size=0.2, random_state=42)

lr_multi = LogisticRegression(random_state=42, max_iter=200)
lr_multi.fit(X_train_i, y_train_i)
y_pred_i = lr_multi.predict(X_test_i)

# 混淆矩阵
cm_multi = confusion_matrix(y_test_i, y_pred_i)

plt.figure(figsize=(8, 6))
sns.heatmap(cm_multi, annot=True, fmt='d', cmap='Blues',
            xticklabels=iris.target_names, yticklabels=iris.target_names)
plt.xlabel('预测')
plt.ylabel('实际')
plt.title('多分类混淆矩阵')
plt.show()

print(classification_report(y_test_i, y_pred_i, target_names=iris.target_names))

平均方式

# 不同的平均方式
print("多分类F1分数:")
for average in ['micro', 'macro', 'weighted']:
    f1_avg = f1_score(y_test_i, y_pred_i, average=average)
    print(f"  {average}: {f1_avg:.4f}")
平均方式 计算方法
micro 全局计算TP, FP, FN
macro 各类别指标的简单平均
weighted 按样本数加权平均

回归评估指标

from sklearn.datasets import fetch_california_housing
from sklearn.linear_model import LinearRegression
from sklearn.metrics import (mean_squared_error, mean_absolute_error,
                             r2_score, mean_absolute_percentage_error)

# 加载回归数据
housing = fetch_california_housing()
X_house, y_house = housing.data, housing.target

X_train_h, X_test_h, y_train_h, y_test_h = train_test_split(
    X_house, y_house, test_size=0.2, random_state=42)

# 训练模型
reg = LinearRegression()
reg.fit(X_train_h, y_train_h)
y_pred_h = reg.predict(X_test_h)

MSE与RMSE

\[\text{MSE} = \frac{1}{N}\sum_{i=1}^{N}(y_i - \hat{y}_i)^2\] \[\text{RMSE} = \sqrt{\text{MSE}}\]
mse = mean_squared_error(y_test_h, y_pred_h)
rmse = np.sqrt(mse)

print(f"MSE: {mse:.4f}")
print(f"RMSE: {rmse:.4f}")

MAE

\[\text{MAE} = \frac{1}{N}\sum_{i=1}^{N}|y_i - \hat{y}_i|\]
mae = mean_absolute_error(y_test_h, y_pred_h)
print(f"MAE: {mae:.4f}")
指标 对离群点 单位
MSE 敏感 平方单位
RMSE 敏感 原始单位
MAE 稳健 原始单位

R²决定系数

\[R^2 = 1 - \frac{\sum(y_i - \hat{y}_i)^2}{\sum(y_i - \bar{y})^2}\]
r2 = r2_score(y_test_h, y_pred_h)
print(f"R²: {r2:.4f}")

解释:模型解释了目标变量多少比例的方差。

MAPE

\[\text{MAPE} = \frac{100\%}{N}\sum_{i=1}^{N}\left|\frac{y_i - \hat{y}_i}{y_i}\right|\]
mape = mean_absolute_percentage_error(y_test_h, y_pred_h)
print(f"MAPE: {mape:.4f}")

残差分析

residuals = y_test_h - y_pred_h

fig, axes = plt.subplots(1, 3, figsize=(15, 5))

# 残差vs预测值
axes[0].scatter(y_pred_h, residuals, alpha=0.5)
axes[0].axhline(0, color='r', linestyle='--')
axes[0].set_xlabel('预测值')
axes[0].set_ylabel('残差')
axes[0].set_title('残差图')

# 残差分布
axes[1].hist(residuals, bins=50, edgecolor='black')
axes[1].set_xlabel('残差')
axes[1].set_ylabel('频数')
axes[1].set_title('残差分布')

# Q-Q图
from scipy import stats
stats.probplot(residuals, dist="norm", plot=axes[2])
axes[2].set_title('Q-Q图')

plt.tight_layout()
plt.show()

指标选择指南

分类任务

场景 推荐指标
类别平衡 准确率、F1
类别不平衡 F1、PR-AUC
重视漏检 召回率、F2
重视误报 精确率、F0.5
概率预测 AUC-ROC、Log Loss

回归任务

场景 推荐指标
通用 RMSE、R²
有离群点 MAE
相对误差重要 MAPE
业务解释 MAE(更直观)

多指标综合评估

from sklearn.model_selection import cross_validate
from sklearn.ensemble import RandomForestClassifier

# 多指标交叉验证
rf = RandomForestClassifier(random_state=42)

scoring = {
    'accuracy': 'accuracy',
    'precision': 'precision',
    'recall': 'recall',
    'f1': 'f1',
    'roc_auc': 'roc_auc'
}

cv_results = cross_validate(rf, X, y, cv=5, scoring=scoring)

print("交叉验证结果:")
for metric in scoring.keys():
    scores = cv_results[f'test_{metric}']
    print(f"  {metric}: {scores.mean():.4f} ± {scores.std():.4f}")

常见问题

Q1: 精确率和召回率如何权衡?

取决于业务需求:

  • 医疗诊断:宁可误报(高召回率)
  • 垃圾邮件:宁可漏检(高精确率)
  • 无明显偏好:使用F1分数

Q2: AUC=0.5意味着什么?

模型等同于随机猜测,没有预测能力。

Q3: R²可以为负吗?

可以。当模型比简单均值预测更差时,R²为负。

Q4: 如何处理多标签分类评估?

使用各指标的微平均或宏平均,或单独评估每个标签。


总结

任务 主要指标 补充指标
二分类 F1、AUC Precision、Recall
多分类 Macro-F1 Confusion Matrix
回归 RMSE、R² MAE、MAPE

参考资料

  • scikit-learn 文档:Model evaluation
  • 《机器学习》周志华 第2章
  • Davis, J., & Goadrich, M. (2006). “The relationship between Precision-Recall and ROC curves”

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:《 机器学习基础系列——模型评估指标 》

本文链接:http://localhost:3015/ai/%E6%A8%A1%E5%9E%8B%E8%AF%84%E4%BC%B0%E6%8C%87%E6%A0%87.html

本文最后一次更新为 天前,文章中的某些内容可能已过时!