Spec-Zone.ru › scikit-learn

Примечание

Перейти к концу для скачивания полного примера кода. Или запустить этот пример в вашем браузере через JupyterLite или Binder

Настройка порога принятия решения для обучения с учётом затрат

После обучения классификатора, метод predict выводит предсказания меток классов, соответствующие пороговому значению, либо функции decision_function, либо результату predict_proba. Для бинарного классификатора значение по умолчанию определяется как оценка апостериорной вероятности 0,5 или значение функции принятия решения 0,0.

Однако, эта стратегия по умолчанию, скорее всего, не оптимальна для задачи. Здесь мы используем набор данных «Statlog» по кредитам немцев [1] для иллюстрации использования. В этом наборе данных задача заключается в предсказании того, имеет ли человек «хороший» или «плохой» кредит. Кроме того, предоставляется матрица затрат, которая определяет затраты на неправильную классификацию. В частности, неправильная классификация «плохого» кредита как «хорошего» в среднем в пять раз дороже, чем неправильная классификация «хорошего» кредита как «плохого».

Мы используем TunedThresholdClassifierCV для выбора точки отсечения функции принятия решения, которая минимизирует заданные бизнес-затраты.

Во второй части примера мы далее расширяем этот подход, рассматривая проблему выявления мошенничества при кредитных картах: в этом случае бизнес-метрика зависит от суммы каждой отдельной транзакции.

Ссылки

[1] (1,2,3)

«Statlog (German Credit Data) Data Set», Репозиторий машинного обучения UCI, Ссылка.

[2] (1,2,3,4,5)

Charles Elkan, «The Foundations of Cost-Sensitive Learning», Международная совместная конференция по искусственному интеллекту. Том 17. № 1. Lawrence Erlbaum Associates Ltd, 2001.

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

Чувствительное к затратам обучение с постоянными выигрышами и затратами

В этом первом разделе мы проиллюстрируем использование класса TunedThresholdClassifierCV в задаче обучения, чувствительного к затратам, когда выигрыши и затраты, связанные с каждой ячейкой матрицы ошибок, являются постоянными. Мы используем проблему, представленную в [2], используя набор данных «Statlog» по немецким кредитным заявкам [1].

Набор данных «Statlog» по немецким кредитным заявкам

Мы загружаем набор данных по немецким кредитным заявкам из OpenML.

import sklearn
from sklearn.datasets import fetch_openml

sklearn.set_config(transform_output="pandas")

german_credit = fetch_openml(data_id=31, as_frame=True, parser="pandas")
X, y = german_credit.data, german_credit.target

Мы проверяем типы признаков, доступные в X.

X.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1000 entries, 0 to 999
Data columns (total 20 columns):
 #   Column                  Non-Null Count  Dtype
---  ------                  --------------  -----
 0   checking_status         1000 non-null   category
 1   duration                1000 non-null   int64
 2   credit_history          1000 non-null   category
 3   purpose                 1000 non-null   category
 4   credit_amount           1000 non-null   int64
 5   savings_status          1000 non-null   category
 6   employment              1000 non-null   category
 7   installment_commitment  1000 non-null   int64
 8   personal_status         1000 non-null   category
 9   other_parties           1000 non-null   category
 10  residence_since         1000 non-null   int64
 11  property_magnitude      1000 non-null   category
 12  age                     1000 non-null   int64
 13  other_payment_plans     1000 non-null   category
 14  housing                 1000 non-null   category
 15  existing_credits        1000 non-null   int64
 16  job                     1000 non-null   category
 17  num_dependents          1000 non-null   int64
 18  own_telephone           1000 non-null   category
 19  foreign_worker          1000 non-null   category
dtypes: category(13), int64(7)
memory usage: 69.9 KB

Многие признаки являются категориальными и обычно закодированы строками. Нам нужно закодировать эти категории при разработке нашей предсказательной модели. Давайте проверим целевые значения.

y.value_counts()
class
good    700
bad     300
Name: count, dtype: int64

Еще одной особенностью является то, что набор данных несбалансирован. При оценке нашей предсказательной модели нужно проявлять осторожность и использовать набор метрик, адаптированных к этой ситуации.

Кроме того, мы наблюдаем, что целевая переменная закодирована строками. Некоторые метрики (например, точность и полнота) требуют предоставления целевого метки, также называемой «положительной меткой». Здесь мы определяем, что наша цель — предсказать, является ли образец «плохим» кредитом.

pos_label, neg_label = "bad", "good"

Для проведения нашего анализа мы разделим наш набор данных с помощью одного стратифицированного разделения.

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=0)

Мы готовы разработать нашу предсказательную модель и связанную с ней стратегию оценки.

Метрики оценки

В этом разделе мы определим набор метрик, которые будем использовать позже. Чтобы увидеть эффект настройки точки отсечения, мы оценим предсказательную модель с помощью ROC-кривой и кривой точность-полнота. Значения, отображаемые на этих графиках, являются поэтому истинным положительным значением (ИПЗ), также известным как полнота или чувствительность, и ложным положительным значением (ЛПЗ), также известным как специфичность, для ROC-кривой, и точность и полнота для кривой точность-полнота.

Из этих четырёх метрик scikit-learn не предоставляет оценщик для ЛПЗ. Поэтому нам нужно определить небольшую пользовательскую функцию для его вычисления.

from sklearn.metrics import confusion_matrix


def fpr_score(y, y_pred, neg_label, pos_label):
    cm = confusion_matrix(y, y_pred, labels=[neg_label, pos_label])
    tn, fp, _, _ = cm.ravel()
    tnr = tn / (tn + fp)
    return 1 - tnr

Как уже упоминалось, «положительная метка» не определена как значение «1», и вызов некоторых метрик с этим нестандартным значением вызывает ошибку. Нам необходимо указать метки «положительной метки» для метрик.

Поэтому нам необходимо определить оценщик scikit-learn, используя make_scorer, где информация передается. Все пользовательские оценщики хранятся в словаре. Для их использования нам необходимо передать обученную модель, данные и целевую переменную, по которым мы хотим оценить предсказательную модель.

from sklearn.metrics import make_scorer, precision_score, recall_score

tpr_score = recall_score  # TPR and recall are the same metric
scoring = {
    "precision": make_scorer(precision_score, pos_label=pos_label),
    "recall": make_scorer(recall_score, pos_label=pos_label),
    "fpr": make_scorer(fpr_score, neg_label=neg_label, pos_label=pos_label),
    "tpr": make_scorer(tpr_score, pos_label=pos_label),
}

Кроме того, в оригинальном исследовании [1] определена пользовательская бизнес-метрика. Мы называем «бизнес-метрикой» любую функцию метрики, которая стремится к количественному определению того, как предсказания (правильные или неправильные) могут повлиять на бизнес-ценность развертывания данной модели машинного обучения в определенном контексте приложения. Для нашей задачи прогнозирования кредитов авторы предоставляют пользовательскую матрицу затрат, которая кодирует, что классификация «плохого» кредита как «хорошего» в среднем в 5 раз дороже, чем обратное: для финансового учреждения менее затратно не предоставлять кредит потенциальному клиенту, который не просрочит (и, следовательно, потерять хорошего клиента, который в противном случае выплатил бы кредит и проценты), чем предоставить кредит клиенту, который просрочит.

Мы определяем функцию Python, которая взвешивает матрицу ошибок и возвращает общую стоимость.

import numpy as np


def credit_gain_score(y, y_pred, neg_label, pos_label):
    cm = confusion_matrix(y, y_pred, labels=[neg_label, pos_label])
    # The rows of the confusion matrix hold the counts of observed classes
    # while the columns hold counts of predicted classes. Recall that here we
    # consider "bad" as the positive class (second row and column).
    # Scikit-learn model selection tools expect that we follow a convention
    # that "higher" means "better", hence the following gain matrix assigns
    # negative gains (costs) to the two kinds of prediction errors:
    # - a gain of -1 for each false positive ("good" credit labeled as "bad"),
    # - a gain of -5 for each false negative ("bad" credit labeled as "good"),
    # The true positives and true negatives are assigned null gains in this
    # metric.
    #
    # Note that theoretically, given that our model is calibrated and our data
    # set representative and large enough, we do not need to tune the
    # threshold, but can safely set it to the cost ration 1/5, as stated by Eq.
    # (2) in Elkan paper [2]_.
    gain_matrix = np.array(
        [
            [0, -1],  # -1 gain for false positives
            [-5, 0],  # -5 gain for false negatives
        ]
    )
    return np.sum(cm * gain_matrix)


scoring["credit_gain"] = make_scorer(
    credit_gain_score, neg_label=neg_label, pos_label=pos_label
)

Простая предсказательная модель

Мы используем HistGradientBoostingClassifier в качестве предсказательной модели, которая изначально обрабатывает категориальные признаки и пропущенные значения.

from sklearn.ensemble import HistGradientBoostingClassifier

model = HistGradientBoostingClassifier(
    categorical_features="from_dtype", random_state=0
).fit(X_train, y_train)
model
HistGradientBoostingClassifier(random_state=0)
В среде Jupyter, пожалуйста, перезапустите этот ячейку, чтобы показать HTML представление или доверьтесь блокноту.
На GitHub, HTML представление не может отобразиться, пожалуйста, попробуйте загрузить эту страницу с nbviewer.org.
HistGradientBoostingClassifier(random_state=0)


Мы оцениваем производительность нашей предсказательной модели с помощью ROC- и Precision-Recall-кривых.

import matplotlib.pyplot as plt

from sklearn.metrics import PrecisionRecallDisplay, RocCurveDisplay

fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(14, 6))

PrecisionRecallDisplay.from_estimator(
    model, X_test, y_test, pos_label=pos_label, ax=axs[0], name="GBDT"
)
axs[0].plot(
    scoring["recall"](model, X_test, y_test),
    scoring["precision"](model, X_test, y_test),
    marker="o",
    markersize=10,
    color="tab:blue",
    label="Default cut-off point at a probability of 0.5",
)
axs[0].set_title("Precision-Recall curve")
axs[0].legend()

RocCurveDisplay.from_estimator(
    model,
    X_test,
    y_test,
    pos_label=pos_label,
    ax=axs[1],
    name="GBDT",
    plot_chance_level=True,
)
axs[1].plot(
    scoring["fpr"](model, X_test, y_test),
    scoring["tpr"](model, X_test, y_test),
    marker="o",
    markersize=10,
    color="tab:blue",
    label="Default cut-off point at a probability of 0.5",
)
axs[1].set_title("ROC curve")
axs[1].legend()
_ = fig.suptitle("Evaluation of the vanilla GBDT model")
Evaluation of the vanilla GBDT model, Precision-Recall curve, ROC curve

Мы помним, что эти кривые дают представление о статистической производительности предсказательной модели для различных точек отсечения. Для кривой точность-полнота сообщаемыми метриками являются точность и полнота, а для ROC-кривой – ИПЗ (то же, что и полнота) и ЛПЗ.

Здесь разные точки отсечения соответствуют разным уровням оценок вероятности, изменяющихся от 0 до 1. По умолчанию model.predict использует точку отсечения при оценке вероятности 0,5. Метрики для такой точки отсечения отображаются синей точкой на кривых: это соответствует статистической производительности модели при использовании model.predict.

Однако, мы помним, что первоначальной целью было минимизировать затраты (или максимизировать выигрыш), как определено бизнес-метрикой. Мы можем вычислить значение бизнес-метрики:

print(f"Business defined metric: {scoring['credit_gain'](model, X_test, y_test)}")
Business defined metric: -232

На данном этапе нам неизвестно, может ли любая другая точка отсечения привести к большему выигрышу. Чтобы найти оптимальную, нам нужно вычислить стоимость-выигрыш с помощью бизнес-метрики для всех возможных точек отсечения и выбрать лучшую. Эта стратегия может быть довольно трудоёмкой для ручной реализации, но класс TunedThresholdClassifierCV поможет нам. Он автоматически вычисляет стоимость-выигрыш для всех возможных точек отсечения и оптимизирует для scoring.

Настройка точки отсечения

Мы используем TunedThresholdClassifierCV для настройки точки отсечения. Нам необходимо указать оптимизируемую бизнес-метрику и положительную метку. Внутренне оптимальная точка отсечения выбирается таким образом, чтобы максимизировать бизнес-метрику с помощью перекрестной проверки. По умолчанию используется 5-кратная стратифицированная перекрестная проверка.

from sklearn.model_selection import TunedThresholdClassifierCV

tuned_model = TunedThresholdClassifierCV(
    estimator=model,
    scoring=scoring["credit_gain"],
    store_cv_results=True,  # necessary to inspect all results
)
tuned_model.fit(X_train, y_train)
print(f"{tuned_model.best_threshold_=:0.2f}")
tuned_model.best_threshold_=0.02

Мы строим ROC- и Precision-Recall-кривые для простой модели и настроенной модели. Также мы отображаем точки отсечения, которые будут использоваться каждой моделью. Поскольку мы повторно используем этот код позже, мы определим функцию для генерации графиков.

def plot_roc_pr_curves(vanilla_model, tuned_model, *, title):
    fig, axs = plt.subplots(nrows=1, ncols=3, figsize=(21, 6))

    linestyles = ("dashed", "dotted")
    markerstyles = ("o", ">")
    colors = ("tab:blue", "tab:orange")
    names = ("Vanilla GBDT", "Tuned GBDT")
    for idx, (est, linestyle, marker, color, name) in enumerate(
        zip((vanilla_model, tuned_model), linestyles, markerstyles, colors, names)
    ):
        decision_threshold = getattr(est, "best_threshold_", 0.5)
        PrecisionRecallDisplay.from_estimator(
            est,
            X_test,
            y_test,
            pos_label=pos_label,
            linestyle=linestyle,
            color=color,
            ax=axs[0],
            name=name,
        )
        axs[0].plot(
            scoring["recall"](est, X_test, y_test),
            scoring["precision"](est, X_test, y_test),
            marker,
            markersize=10,
            color=color,
            label=f"Cut-off point at probability of {decision_threshold:.2f}",
        )
        RocCurveDisplay.from_estimator(
            est,
            X_test,
            y_test,
            pos_label=pos_label,
            linestyle=linestyle,
            color=color,
            ax=axs[1],
            name=name,
            plot_chance_level=idx == 1,
        )
        axs[1].plot(
            scoring["fpr"](est, X_test, y_test),
            scoring["tpr"](est, X_test, y_test),
            marker,
            markersize=10,
            color=color,
            label=f"Cut-off point at probability of {decision_threshold:.2f}",
        )

    axs[0].set_title("Precision-Recall curve")
    axs[0].legend()
    axs[1].set_title("ROC curve")
    axs[1].legend()

    axs[2].plot(
        tuned_model.cv_results_["thresholds"],
        tuned_model.cv_results_["scores"],
        color="tab:orange",
    )
    axs[2].plot(
        tuned_model.best_threshold_,
        tuned_model.best_score_,
        "o",
        markersize=10,
        color="tab:orange",
        label="Optimal cut-off point for the business metric",
    )
    axs[2].legend()
    axs[2].set_xlabel("Decision threshold (probability)")
    axs[2].set_ylabel("Objective score (using cost-matrix)")
    axs[2].set_title("Objective score as a function of the decision threshold")
    fig.suptitle(title)
title = "Comparison of the cut-off point for the vanilla and tuned GBDT model"
plot_roc_pr_curves(model, tuned_model, title=title)
Comparison of the cut-off point for the vanilla and tuned GBDT model, Precision-Recall curve, ROC curve, Objective score as a function of the decision threshold

Первый вывод состоит в том, что у обоих классификаторов абсолютно одинаковые ROC- и Precision-Recall-кривые. Это ожидаемо, так как классификатор по умолчанию обучен на одних и тех же обучающих данных. В последующем разделе мы более подробно рассмотрим доступные варианты переобучения модели и перекрестной проверки.

Второй вывод состоит в том, что точки отсечения простой модели и настроенной модели отличаются. Чтобы понять, почему настроенная модель выбрала эту точку отсечения, можно посмотреть на график справа, который отображает целевую метрику, то есть нашу бизнес-метрику. Мы видим, что оптимальный порог соответствует максимуму целевой метрики. Этот максимум достигается при значении порога, намного меньшем, чем 0,5: настроенная модель обладает гораздо большей полнотой за счёт значительно меньшей точности: настроенная модель гораздо охотнее предсказывает метку класса «плохой» для большей части людей.

Теперь мы можем проверить, приводит ли выбор этой точки отсечения к лучшему результату на тестовом наборе:

print(f"Business defined metric: {scoring['credit_gain'](tuned_model, X_test, y_test)}")
Business defined metric: -134

Мы наблюдаем, что настройка порога принятия решения почти удваивает наш бизнес-выигрыш.

Рассмотрение переобучения модели и перекрестной проверки

В приведенном выше эксперименте мы использовали стандартные настройки TunedThresholdClassifierCV. В частности, точка отсечения настраивается с помощью 5-кратной стратифицированной перекрестной проверки. Кроме того, базовую предсказательную модель переобучают на всех данных обучения после выбора точки отсечения.

Эти две стратегии можно изменить, предоставив параметры refit и cv. Например, можно предоставить обученную модель estimator и установить cv="prefit", в этом случае точка отсечения находится на всех данных, предоставленных при обучении. Также базовую классификатор не переобучают, установив refit=False. Здесь мы можем попробовать провести такой эксперимент.

model.fit(X_train, y_train)
tuned_model.set_params(cv="prefit", refit=False).fit(X_train, y_train)
print(f"{tuned_model.best_threshold_=:0.2f}")
tuned_model.best_threshold_=0.28

Затем мы оцениваем нашу модель тем же способом, что и ранее:

title = "Tuned GBDT model without refitting and using the entire dataset"
plot_roc_pr_curves(model, tuned_model, title=title)
Tuned GBDT model without refitting and using the entire dataset, Precision-Recall curve, ROC curve, Objective score as a function of the decision threshold

Мы наблюдаем, что оптимальная точка отсечения отличается от той, что была найдена в предыдущем эксперименте. Если посмотреть на график справа, мы видим, что коммерческая выгода имеет большой плато близкой к оптимальной выгоды 0 для большого диапазона пороговых значений решений. Такое поведение является признаком переобучения. Поскольку мы отключили перекрестную проверку, мы настраивали точку отсечения на том же наборе данных, что и модель была обучена, и это причина наблюдаемого переобучения.

Поэтому этот вариант следует использовать с осторожностью. Необходимо убедиться, что данные, предоставленные при обучении TunedThresholdClassifierCV, не совпадают с данными, используемыми для обучения базового классификатора. Это иногда может произойти, когда цель — настроить предсказательную модель на полностью новом наборе проверки без дорогостоящей полной переобучения.

Если перекрестная проверка слишком затратна, потенциальной альтернативой является использование одного разделения на обучающую и тестовую выборки, предоставив число с плавающей точкой в диапазоне [0, 1] параметру cv. Оно разбивает данные на обучающую и тестовую выборки. Давайте исследуем этот вариант:

tuned_model.set_params(cv=0.75).fit(X_train, y_train)
TunedThresholdClassifierCV(cv=0.75,
                           estimator=HistGradientBoostingClassifier(random_state=0),
                           refit=False,
                           scoring=make_scorer(credit_gain_score, response_method='predict', neg_label=good, pos_label=bad),
                           store_cv_results=True)
В среде Jupyter, пожалуйста, перезапустите эту ячейку, чтобы отобразить HTML-представление, или доверьтесь блокноту.
На GitHub HTML-представление не может отобразиться, пожалуйста, попробуйте загрузить эту страницу с nbviewer.org.
TunedThresholdClassifierCV(cv=0.75,
                           estimator=HistGradientBoostingClassifier(random_state=0),
                           refit=False,
                           scoring=make_scorer(credit_gain_score, response_method='predict', neg_label=good, pos_label=bad),
                           store_cv_results=True)
HistGradientBoostingClassifier(random_state=0)
HistGradientBoostingClassifier(random_state=0)


title = "Tuned GBDT model without refitting and using the entire dataset"
plot_roc_pr_curves(model, tuned_model, title=title)
Tuned GBDT model without refitting and using the entire dataset, Precision-Recall curve, ROC curve, Objective score as a function of the decision threshold

Что касается точки отсечения, мы наблюдаем, что оптимальное значение аналогично случаю с многократной перекрестной проверкой. Однако следует помнить, что одно разделение не учитывает изменчивость процесса подбора/предсказания, и поэтому мы не можем знать, есть ли вариация в точке отсечения. Многократная перекрестная проверка усредняет этот эффект.

Другое наблюдение касается кривых ROC и точность-полнота настроенной модели. Как ожидалось, эти кривые отличаются от кривых обычной модели, поскольку мы обучили базовый классификатор на подмножестве данных, предоставленных при обучении, и зарезервировали набор проверки для настройки точки отсечения.

Чувствительное к стоимости обучение, когда выгоды и затраты не являются постоянными

Как указано в [2], выгоды и затраты в реальных задачах обычно не являются постоянными. В этом разделе мы используем аналогичный пример, как в [2], для задачи обнаружения мошенничества в данных о транзакциях по кредитным картам.

Набор данных по кредитным картам

credit_card = fetch_openml(data_id=1597, as_frame=True, parser="pandas")
credit_card.frame.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 284807 entries, 0 to 284806
Data columns (total 30 columns):
 #   Column  Non-Null Count   Dtype
---  ------  --------------   -----
 0   V1      284807 non-null  float64
 1   V2      284807 non-null  float64
 2   V3      284807 non-null  float64
 3   V4      284807 non-null  float64
 4   V5      284807 non-null  float64
 5   V6      284807 non-null  float64
 6   V7      284807 non-null  float64
 7   V8      284807 non-null  float64
 8   V9      284807 non-null  float64
 9   V10     284807 non-null  float64
 10  V11     284807 non-null  float64
 11  V12     284807 non-null  float64
 12  V13     284807 non-null  float64
 13  V14     284807 non-null  float64
 14  V15     284807 non-null  float64
 15  V16     284807 non-null  float64
 16  V17     284807 non-null  float64
 17  V18     284807 non-null  float64
 18  V19     284807 non-null  float64
 19  V20     284807 non-null  float64
 20  V21     284807 non-null  float64
 21  V22     284807 non-null  float64
 22  V23     284807 non-null  float64
 23  V24     284807 non-null  float64
 24  V25     284807 non-null  float64
 25  V26     284807 non-null  float64
 26  V27     284807 non-null  float64
 27  V28     284807 non-null  float64
 28  Amount  284807 non-null  float64
 29  Class   284807 non-null  category
dtypes: category(1), float64(29)
memory usage: 63.3 MB

Набор данных содержит информацию о записях по кредитным картам, некоторые из которых являются мошенническими, а другие — законными. Цель состоит в том, чтобы предсказать, является ли запись по кредитной карте мошеннической или нет.

columns_to_drop = ["Class"]
data = credit_card.frame.drop(columns=columns_to_drop)
target = credit_card.frame["Class"].astype(int)

Сначала мы проверяем распределение классов в наборе данных.

target.value_counts(normalize=True)
Class
0    0.998273
1    0.001727
Name: proportion, dtype: float64

Набор данных сильно несбалансирован, при этом мошеннические транзакции составляют всего 0,17% данных. Поскольку мы заинтересованы в обучении модели машинного обучения, мы также должны убедиться, что в классе меньшинства достаточно образцов для обучения модели.

target.value_counts()
Class
0    284315
1       492
Name: count, dtype: int64

Мы наблюдаем, что у нас около 500 образцов, что находится на нижнем пределе числа образцов, необходимого для обучения модели машинного обучения. В дополнение к распределению целевой переменной, мы проверяем распределение суммы мошеннических транзакций.

fraud = target == 1
amount_fraud = data["Amount"][fraud]
_, ax = plt.subplots()
ax.hist(amount_fraud, bins=30)
ax.set_title("Amount of fraud transaction")
_ = ax.set_xlabel("Amount (€)")
Amount of fraud transaction

Решение проблемы с бизнес-метрикой

Теперь мы создаем бизнес-метрику, зависящую от суммы каждой транзакции. Мы определяем матрицу затрат аналогично [2]. Принятие законной транзакции обеспечивает выгоду в 2% от суммы транзакции. Однако принятие мошеннической транзакции приводит к убыткам в размере суммы транзакции. Как указано в [2], выгоды и потери, связанные с отклонениями (мошеннических и законных транзакций), нетривиально определить. Здесь мы определяем, что отклонение законной транзакции оценивается в 5€ убытка, а отклонение мошеннической транзакции оценивается в 50€ выгоды. Таким образом, мы определяем следующую функцию для вычисления общей выгоды от данного решения:

def business_metric(y_true, y_pred, amount):
    mask_true_positive = (y_true == 1) & (y_pred == 1)
    mask_true_negative = (y_true == 0) & (y_pred == 0)
    mask_false_positive = (y_true == 0) & (y_pred == 1)
    mask_false_negative = (y_true == 1) & (y_pred == 0)
    fraudulent_refuse = mask_true_positive.sum() * 50
    fraudulent_accept = -amount[mask_false_negative].sum()
    legitimate_refuse = mask_false_positive.sum() * -5
    legitimate_accept = (amount[mask_true_negative] * 0.02).sum()
    return fraudulent_refuse + fraudulent_accept + legitimate_refuse + legitimate_accept

Из этой бизнес-метрики мы создаем инструмент оценки scikit-learn, который, принимая обученный классификатор и тестовый набор, вычисляет бизнес-метрику. В этом отношении мы используем фабрику make_scorer. Переменная amount — это дополнительная метаданные, передаваемые инструменту оценки, и нам необходимо использовать маршрутизацию метаданных, чтобы учесть эту информацию.

sklearn.set_config(enable_metadata_routing=True)
business_scorer = make_scorer(business_metric).set_score_request(amount=True)

Итак, на этом этапе мы видим, что сумма транзакции используется дважды: как признак для обучения нашей предсказательной модели и как метаданные для вычисления бизнес-метрики и, следовательно, статистической эффективности нашей модели. При использовании в качестве признака нам требуется только столбец в data, содержащий сумму каждой транзакции. Чтобы использовать эту информацию как метаданные, нам нужна внешняя переменная, которую мы можем передать в инструмент оценки или модель, которая внутренне передает эти метаданные в инструмент оценки. Поэтому давайте создадим эту переменную.

amount = credit_card.frame["Amount"].to_numpy()
from sklearn.model_selection import train_test_split

data_train, data_test, target_train, target_test, amount_train, amount_test = (
    train_test_split(
        data, target, amount, stratify=target, test_size=0.5, random_state=42
    )
)

Сначала мы оценим некоторые базовые политики, которые послужат эталоном. Напоминаем, что класс «0» — это класс законных транзакций, а класс «1» — класс мошеннических транзакций.

from sklearn.dummy import DummyClassifier

always_accept_policy = DummyClassifier(strategy="constant", constant=0)
always_accept_policy.fit(data_train, target_train)
benefit = business_scorer(
    always_accept_policy, data_test, target_test, amount=amount_test
)
print(f"Benefit of the 'always accept' policy: {benefit:,.2f}€")
Benefit of the 'always accept' policy: 221,445.07€

Политика, которая рассматривает все транзакции как законные, создаст прибыль около 220 000€. Мы проводим аналогичную оценку для классификатора, который предсказывает все транзакции как мошеннические.

always_reject_policy = DummyClassifier(strategy="constant", constant=1)
always_reject_policy.fit(data_train, target_train)
benefit = business_scorer(
    always_reject_policy, data_test, target_test, amount=amount_test
)
print(f"Benefit of the 'always reject' policy: {benefit:,.2f}€")
Benefit of the 'always reject' policy: -698,490.00€

Такая политика повлечёт катастрофические убытки: около 670 000€. Это ожидаемо, поскольку подавляющее большинство транзакций — законные, а политика будет отклонять их с ненулевой стоимостью.

Предсказательная модель, которая адаптирует решения о принятии/отклонении для каждой транзакции, в идеале должна позволить нам получить прибыль, большую, чем 220 000€ — наилучший из наших постоянных базовых политик.

Мы начинаем с модели логистической регрессии с порогом принятия решений по умолчанию 0,5. Здесь мы настраиваем гиперпараметр C логистической регрессии с правилом оценки (логарифмическая потеря), чтобы гарантировать, что вероятностные предсказания модели, возвращаемые ее методом predict_proba, максимально точны, независимо от выбора значения порога принятия решений.

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

logistic_regression = make_pipeline(StandardScaler(), LogisticRegression())
param_grid = {"logisticregression__C": np.logspace(-6, 6, 13)}
model = GridSearchCV(logistic_regression, param_grid, scoring="neg_log_loss").fit(
    data_train, target_train
)
model
GridSearchCV(estimator=Pipeline(steps=[('standardscaler', StandardScaler()),
                                       ('logisticregression',
                                        LogisticRegression())]),
             param_grid={'logisticregression__C': array([1.e-06, 1.e-05, 1.e-04, 1.e-03, 1.e-02, 1.e-01, 1.e+00, 1.e+01,
       1.e+02, 1.e+03, 1.e+04, 1.e+05, 1.e+06])},
             scoring='neg_log_loss')
В среде Jupyter, пожалуйста, перезапустите эту ячейку, чтобы показать HTML-представление, или доверьтесь блокноту.
На GitHub HTML-представление не может отобразиться, попробуйте загрузить эту страницу с помощью nbviewer.org.
GridSearchCV(estimator=Pipeline(steps=[('standardscaler', StandardScaler()),
                                       ('logisticregression',
                                        LogisticRegression())]),
             param_grid={'logisticregression__C': array([1.e-06, 1.e-05, 1.e-04, 1.e-03, 1.e-02, 1.e-01, 1.e+00, 1.e+01,
       1.e+02, 1.e+03, 1.e+04, 1.e+05, 1.e+06])},
             scoring='neg_log_loss')
Pipeline(steps=[('standardscaler', StandardScaler()),
                ('logisticregression',
                 LogisticRegression(C=np.float64(100.0)))])
StandardScaler()
LogisticRegression(C=np.float64(100.0))


print(
    "Benefit of logistic regression with default threshold: "
    f"{business_scorer(model, data_test, target_test, amount=amount_test):,.2f}€"
)
Benefit of logistic regression with default threshold: 244,919.87€

Бизнес-метрика показывает, что наша предсказательная модель с порогом принятия решений по умолчанию уже превосходит базовые политики с точки зрения прибыли, и её использование для принятия или отклонения транзакций уже будет выгодным, вместо принятия всех транзакций.

Настройка порога принятия решения

Теперь вопрос в том: оптимальна ли наша модель для типа решения, которое мы хотим принять? До сих пор мы не оптимизировали порог принятия решения. Мы используем TunedThresholdClassifierCV, чтобы оптимизировать решение, учитывая наш бизнес-оценщик. Чтобы избежать вложенной кросс-валидации, мы будем использовать лучший оценочный модуль, найденный во время предыдущего поиска по сетке.

tuned_model = TunedThresholdClassifierCV(
    estimator=model.best_estimator_,
    scoring=business_scorer,
    thresholds=100,
    n_jobs=2,
)

Поскольку наш бизнес-оценщик требует суммы каждой транзакции, нам необходимо передать эту информацию в метод fit. Класс TunedThresholdClassifierCV отвечает за автоматическую передачу этих метаданных в базовый оценщик.

tuned_model.fit(data_train, target_train, amount=amount_train)
TunedThresholdClassifierCV(estimator=Pipeline(steps=[('standardscaler',
                                                      StandardScaler()),
                                                     ('logisticregression',
                                                      LogisticRegression(C=np.float64(100.0)))]),
                           n_jobs=2,
                           scoring=make_scorer(business_metric, response_method='predict'))
В среде Jupyter, пожалуйста, перезапустите эту ячейку, чтобы отобразить HTML-представление, или доверьтесь блокноту.
На GitHub HTML-представление не может быть отображено, попробуйте загрузить эту страницу с помощью nbviewer.org.
TunedThresholdClassifierCV(estimator=Pipeline(steps=[('standardscaler',
                                                      StandardScaler()),
                                                     ('logisticregression',
                                                      LogisticRegression(C=np.float64(100.0)))]),
                           n_jobs=2,
                           scoring=make_scorer(business_metric, response_method='predict'))
Pipeline(steps=[('standardscaler', StandardScaler()),
                ('logisticregression',
                 LogisticRegression(C=np.float64(100.0)))])
StandardScaler()
LogisticRegression(C=np.float64(100.0))


Заметим, что настроенный порог принятия решения сильно отличается от значения по умолчанию 0,5:

print(f"Tuned decision threshold: {tuned_model.best_threshold_:.2f}")
Tuned decision threshold: 0.03
print(
    "Benefit of logistic regression with a tuned threshold: "
    f"{business_scorer(tuned_model, data_test, target_test, amount=amount_test):,.2f}€"
)
Benefit of logistic regression with a tuned threshold: 249,433.39€

Мы наблюдаем, что настройка порога принятия решения увеличивает ожидаемую прибыль при развертывании нашей модели, как показано по бизнес-метрик. Поэтому, когда это возможно, целесообразно оптимизировать порог принятия решения относительно бизнес-метрики.

Ручное задание порога принятия решения вместо его настройки

В предыдущем примере мы использовали TunedThresholdClassifierCV для поиска оптимального порога принятия решения. Однако в некоторых случаях у нас может быть предварительное знание о рассматриваемой проблеме, и мы можем быть удовлетворены ручным заданием порога принятия решения.

Класс FixedThresholdClassifier позволяет нам вручную задать порог принятия решения. При прогнозировании он ведет себя как предыдущая настроенная модель, но поиск не выполняется во время процесса обучения. Обратите внимание, что здесь мы используем FrozenEstimator, чтобы обернуть предсказательную модель, чтобы избежать повторного обучения.

Здесь мы повторно используем порог принятия решения, найденный в предыдущем разделе, чтобы создать новую модель и проверить, что она дает те же результаты.

from sklearn.frozen import FrozenEstimator
from sklearn.model_selection import FixedThresholdClassifier

model_fixed_threshold = FixedThresholdClassifier(
    estimator=FrozenEstimator(model), threshold=tuned_model.best_threshold_
)
business_score = business_scorer(
    model_fixed_threshold, data_test, target_test, amount=amount_test
)
print(f"Benefit of logistic regression with a tuned threshold:  {business_score:,.2f}€")
Benefit of logistic regression with a tuned threshold:  249,433.39€

Мы наблюдаем, что получили точно такие же результаты, но процесс обучения был намного быстрее, так как мы не выполняли поиск по гиперпараметрам.

Наконец, оценка (средней) бизнес-метрики сама по себе может быть ненадежной, особенно когда количество точек данных в классе меньшинства очень мало. Любой бизнес-влияние, оцениваемый с помощью кросс-валидации бизнес-метрики на исторических данных (офлайн-оценка), в идеале должен быть подтвержден A/B-тестированием на реальных данных (онлайн-оценка). Тем не менее, моделирование A/B-тестирования выходит за рамки самой библиотеки scikit-learn.

Общее время выполнения скрипта: (0 минут 31.200 секунд)

Launch binder
Launch JupyterLite

Download Jupyter notebook: plot_cost_sensitive_learning.ipynb

Download Python source code: plot_cost_sensitive_learning.py

Download zipped: plot_cost_sensitive_learning.zip

Похожие примеры

Постобъектная настройка точки отсечения функции принятия решений

Основные моменты выпуска scikit-learn 1.5

Точность-полнота

Настраиваемая стратегия повторного обучения поиска по сетке с кросс-валидацией

© 2007–2025 The scikit-learn developers
Licensed under the 3-clause BSD License.
https://scikit-learn.org/1.6/auto_examples/model_selection/plot_cost_sensitive_learning.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API