决策函数切分点的事后调优#

一旦二元分类器训练完成,predict 方法会根据 decision_functionpredict_proba 输出的阈值处理结果来输出类别标签预测。默认阈值被定义为后验概率估计 0.5 或决策分值 0.0。然而,这种默认策略对于手头的任务可能并非最优。

此示例展示了如何使用 TunedThresholdClassifierCV 根据感兴趣的指标来调优决策阈值。

# Authors: The scikit-learn developers
# SPDX-License-Identifier: BSD-3-Clause

糖尿病数据集#

为了演示决策阈值的调优,我们将使用糖尿病数据集。该数据集可在 OpenML 上获取:https://www.openml.org/d/37。我们使用 fetch_openml 函数来获取此数据集。

from sklearn.datasets import fetch_openml

diabetes = fetch_openml(data_id=37, as_frame=True, parser="pandas")
data, target = diabetes.data, diabetes.target

我们观察目标变量以了解我们正在处理的问题类型。

target.value_counts()
class
tested_negative    500
tested_positive    268
Name: count, dtype: int64

我们可以看到我们正在处理的是一个二元分类问题。由于标签未被编码为 0 和 1,我们明确指定将标签为 “tested_negative” 的类视为负类(这也是最频繁出现的类),将标签为 “tested_positive” 的类视为正类。

neg_label, pos_label = target.value_counts().index

我们还可以观察到这个二元问题存在轻微的不平衡,负类的样本数量大约是正类样本的两倍。在进行评估时,我们应该考虑这一方面以解释结果。

我们的原始(Vanilla)分类器#

我们定义一个基础预测模型,由一个缩放器(scaler)和紧随其后的逻辑回归(logistic regression)分类器组成。

from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

model = make_pipeline(StandardScaler(), LogisticRegression())
model
Pipeline(steps=[('standardscaler', StandardScaler()),
                ('logisticregression', LogisticRegression())])
在 Jupyter 环境中,请重新运行此单元格以显示 HTML 表示形式或信任 notebook。
在 GitHub 上,HTML 表示形式无法渲染,请尝试使用 nbviewer.org 加载此页面。


我们使用交叉验证来评估模型。我们使用准确率(accuracy)和平衡准确率(balanced accuracy)来报告模型的性能。平衡准确率是一个对类别不平衡不太敏感的指标,它能让我们客观看待准确率得分。

交叉验证允许我们研究决策阈值在不同数据切分下的方差。然而,数据集相当小,使用超过 5 折的验证来评估离散度将是有害的。因此,我们使用 RepeatedStratifiedKFold,即应用多次 5 折交叉验证的重复。

import pandas as pd

from sklearn.model_selection import RepeatedStratifiedKFold, cross_validate

scoring = ["accuracy", "balanced_accuracy"]
cv_scores = [
    "train_accuracy",
    "test_accuracy",
    "train_balanced_accuracy",
    "test_balanced_accuracy",
]
cv = RepeatedStratifiedKFold(n_splits=5, n_repeats=10, random_state=42)
cv_results_vanilla_model = pd.DataFrame(
    cross_validate(
        model,
        data,
        target,
        scoring=scoring,
        cv=cv,
        return_train_score=True,
        return_estimator=True,
    )
)
cv_results_vanilla_model[cv_scores].aggregate(["mean", "std"]).T
mean std
train_accuracy (训练准确率) 0.779751 0.007822
test_accuracy (测试准确率) 0.770926 0.030585
train_balanced_accuracy (训练平衡准确率) 0.732913 0.009788
test_balanced_accuracy (测试平衡准确率) 0.723665 0.035914


我们的预测模型成功掌握了数据与目标之间的关系。训练得分和测试得分非常接近,这意味着我们的预测模型没有出现过拟合。我们还可以观察到,由于之前提到的类别不平衡问题,平衡准确率低于准确率。

对于此分类器,我们将用于将正类概率转换为类别预测的决策阈值保留为默认值:0.5。然而,这个阈值可能并非最优。如果我们的目标是最大化平衡准确率,我们应该选择另一个能使该指标最大化的阈值。

TunedThresholdClassifierCV 元评估器允许在给定感兴趣指标的情况下调优分类器的决策阈值。

调优决策阈值#

我们创建一个 TunedThresholdClassifierCV 并将其配置为最大化平衡准确率。我们使用与之前相同的交叉验证策略来评估该模型。

from sklearn.model_selection import TunedThresholdClassifierCV

tuned_model = TunedThresholdClassifierCV(estimator=model, scoring="balanced_accuracy")
cv_results_tuned_model = pd.DataFrame(
    cross_validate(
        tuned_model,
        data,
        target,
        scoring=scoring,
        cv=cv,
        return_train_score=True,
        return_estimator=True,
    )
)
cv_results_tuned_model[cv_scores].aggregate(["mean", "std"]).T
mean std
train_accuracy (训练准确率) 0.752470 0.015579
test_accuracy (测试准确率) 0.739950 0.036592
train_balanced_accuracy (训练平衡准确率) 0.757915 0.009747
test_balanced_accuracy (测试平衡准确率) 0.744029 0.035445


与原始模型相比,我们观察到平衡准确率得分有所提高。当然,这是以降低准确率得分为代价的。这意味着我们的模型现在对正类更加敏感,但在负类上会犯更多错误。

然而,值得注意的是,这个经过调优的预测模型在内部与原始模型是同一个模型:它们具有相同的拟合系数。

import matplotlib.pyplot as plt

vanilla_model_coef = pd.DataFrame(
    [est[-1].coef_.ravel() for est in cv_results_vanilla_model["estimator"]],
    columns=diabetes.feature_names,
)
tuned_model_coef = pd.DataFrame(
    [est.estimator_[-1].coef_.ravel() for est in cv_results_tuned_model["estimator"]],
    columns=diabetes.feature_names,
)

fig, ax = plt.subplots(ncols=2, figsize=(12, 4), sharex=True, sharey=True)
vanilla_model_coef.boxplot(ax=ax[0])
ax[0].set_ylabel("Coefficient value")
ax[0].set_title("Vanilla model")
tuned_model_coef.boxplot(ax=ax[1])
ax[1].set_title("Tuned model")
_ = fig.suptitle("Coefficients of the predictive models")
Coefficients of the predictive models, Vanilla model, Tuned model

在交叉验证期间,仅改变了每个模型的决策阈值。

decision_threshold = pd.Series(
    [est.best_threshold_ for est in cv_results_tuned_model["estimator"]],
)
ax = decision_threshold.plot.kde()
ax.axvline(
    decision_threshold.mean(),
    color="k",
    linestyle="--",
    label=f"Mean decision threshold: {decision_threshold.mean():.2f}",
)
ax.set_xlabel("Decision threshold")
ax.legend(loc="upper right")
_ = ax.set_title(
    "Distribution of the decision threshold \nacross different cross-validation folds"
)
Distribution of the decision threshold  across different cross-validation folds

平均而言,0.32 左右的决策阈值能使平衡准确率最大化,这与默认的决策阈值 0.5 不同。因此,当预测模型的输出用于决策时,调优决策阈值尤为重要。此外,应谨慎选择用于调优决策阈值的指标。在这里,我们使用了平衡准确率,但它可能不是手头问题最合适的指标。选择“正确”的指标通常取决于具体问题,并可能需要一些领域知识。有关更多详细信息,请参阅标题为 代价敏感学习的决策阈值后调优 的示例。

脚本总运行时间: (0 分 41.564 秒)

相关示例

后验调整成本敏感学习的决策阈值

后验调整成本敏感学习的决策阈值

scikit-learn 1.5 发布亮点

scikit-learn 1.5 发布亮点

不同自训练阈值的影响

不同自训练阈值的影响

使用 FrozenEstimator 的示例

使用 FrozenEstimator 的示例

由 Sphinx-Gallery 生成的图库