Sintonizzati: ottimizzazione della soglia decisionale con TunedThresholdClassifierCV di scikit-learn |  di Kevin Arvai |  Maggio 2024

 | Intelligenza-Artificiale

Usa casi e codice per esplorare la nuova classe che aiuta a ottimizzare le soglie decisionali in scikit-learn

La versione 1.5 di scikit-learn include una nuova classe, TunedThresholdClassifierCVsemplificare l'ottimizzazione delle soglie decisionali dai classificatori scikit-learn. Una soglia decisionale è un punto limite che converte le probabilità previste ottenute da un modello di machine learning in classi discrete. La soglia decisionale predefinita del .predict() Il metodo dei classificatori scikit-learn in un'impostazione di classificazione binaria è 0,5. Sebbene si tratti di un'impostazione predefinita sensata, raramente è la scelta migliore per le attività di classificazione.

Questo post introduce la classe TunedThresholdClassifierCV e dimostra come può ottimizzare le soglie decisionali per varie attività di classificazione binaria. Questo nuovo corso aiuterà a colmare il divario tra i data scientist che costruiscono modelli e le parti interessate aziendali che prendono decisioni in base all'output del modello. Perfezionando le soglie decisionali, i data scientist possono migliorare le prestazioni del modello e allinearsi meglio agli obiettivi aziendali.

Questo post tratterà le seguenti situazioni in cui l'ottimizzazione delle soglie decisionali è vantaggiosa:

  1. Massimizzare una metrica: utilizzare questa opzione quando si sceglie una soglia che massimizza una metrica di punteggio, come il punteggio F1.
  2. Apprendimento sensibile ai costi: regolare la soglia quando il costo della classificazione errata di un falso positivo non è uguale al costo della classificazione errata di un falso negativo e si dispone di una stima dei costi.
  3. Accordatura sotto vincoli: ottimizza il punto operativo sulla curva ROC o richiamo di precisione per soddisfare specifici vincoli prestazionali.

Il codice utilizzato in questo post e i collegamenti ai set di dati sono disponibili su GitHub.

Cominciamo! Innanzitutto, importa le librerie necessarie, leggi i dati e dividi i dati di training e di test.

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.compose import make_column_selector as selector
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
RocCurveDisplay,
f1_score,
make_scorer,
recall_score,
roc_curve,
confusion_matrix,
)
from sklearn.model_selection import TunedThresholdClassifierCV, train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

RANDOM_STATE = 26120

Massimizzare una metrica

Prima di iniziare il processo di creazione del modello in qualsiasi progetto di machine learning, è fondamentale collaborare con le parti interessate per determinare quali metriche ottimizzare. Prendere questa decisione in anticipo garantisce che il progetto sia in linea con gli obiettivi previsti.

L'utilizzo di una metrica di precisione nei casi d'uso di rilevamento delle frodi per valutare le prestazioni del modello non è l'ideale perché i dati sono spesso sbilanciati e la maggior parte delle transazioni non sono fraudolente. Il punteggio F1 è la media armonica di precisione e richiamo ed è una metrica migliore per set di dati sbilanciati come il rilevamento delle frodi. Usiamo il TunedThresholdClassifierCV classe per ottimizzare la soglia decisionale di un modello di regressione logistica per massimizzare il punteggio F1.

Useremo il Set di dati Kaggle per il rilevamento delle frodi sulle carte di credito per introdurre la prima situazione in cui dobbiamo mettere a punto una soglia decisionale. Innanzitutto, dividi i dati in set di training e test, quindi crea una pipeline di scikit-learn per ridimensionare i dati e addestrare un modello di regressione logistica. Adattare la pipeline ai dati di training in modo da poter confrontare le prestazioni del modello originale con le prestazioni del modello ottimizzato.

creditcard = pd.read_csv("data/creditcard.csv")
y = creditcard("Class")
X = creditcard.drop(columns=("Class"))

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

# Only Time and Amount need to be scaled
original_fraud_model = make_pipeline(
ColumnTransformer(
(("scaler", StandardScaler(), ("Time", "Amount"))),
remainder="passthrough",
force_int_remainder_cols=False,
),
LogisticRegression(),
)
original_fraud_model.fit(X_train, y_train)

Non è stata ancora effettuata alcuna messa a punto, ma arriverà nel prossimo blocco di codice. Gli argomenti a favore TunedThresholdClassifierCV sono simili ad altri CV lezioni in scikit-learn, come GridSearchCV. Come minimo, l'utente deve solo superare lo stimatore originale e TunedThresholdClassifierCV memorizzerà la soglia decisionale che massimizza la precisione bilanciata (impostazione predefinita) utilizzando la convalida incrociata K-fold stratificata 5 volte (impostazione predefinita). Utilizza questa soglia anche durante la chiamata .predict(). Tuttavia, qualsiasi metrica scikit-learn (o richiamabile) può essere utilizzata come file scoring metrico. Inoltre, l'utente può trasmettere il familiare cv argomento per personalizzare la strategia di convalida incrociata.

Crea il TunedThresholdClassifierCV istanza e adattare il modello ai dati di addestramento. Passa il modello originale e imposta il punteggio su “f1”. Vorremo anche impostare store_cv_results=True per accedere alle soglie valutate durante la convalida incrociata per la visualizzazione.

tuned_fraud_model = TunedThresholdClassifierCV(
original_fraud_model,
scoring="f1",
store_cv_results=True,
)

tuned_fraud_model.fit(X_train, y_train)

# average F1 across folds
avg_f1_train = tuned_fraud_model.best_score_
# Compare F1 in the test set for the tuned model and the original model
f1_test = f1_score(y_test, tuned_fraud_model.predict(X_test))
f1_test_original = f1_score(y_test, original_fraud_model.predict(X_test))

print(f"Average F1 on the training set: {avg_f1_train:.3f}")
print(f"F1 on the test set: {f1_test:.3f}")
print(f"F1 on the test set (original model): {f1_test_original:.3f}")
print(f"Threshold: {tuned_fraud_model.best_threshold_: .3f}")

Average F1 on the training set: 0.784
F1 on the test set: 0.796
F1 on the test set (original model): 0.733
Threshold: 0.071

Ora che abbiamo trovato la soglia che massimizza il controllo del punteggio F1 tuned_fraud_model.best_score_ per scoprire qual è stato il miglior punteggio medio F1 tra le pieghe nella convalida incrociata. Possiamo anche vedere quale soglia ha generato tali risultati utilizzando tuned_fraud_model.best_threshold_. È possibile visualizzare i punteggi delle metriche attraverso le soglie decisionali durante la convalida incrociata utilizzando objective_scores_ E decision_thresholds_ attributi:

fig, ax = plt.subplots(figsize=(5, 5))
ax.plot(
tuned_fraud_model.cv_results_("thresholds"),
tuned_fraud_model.cv_results_("scores"),
marker="o",
linewidth=1e-3,
markersize=4,
color="#c0c0c0",
)
ax.plot(
tuned_fraud_model.best_threshold_,
tuned_fraud_model.best_score_,
"^",
markersize=10,
color="#ff6700",
label=f"Optimal cut-off point = {tuned_fraud_model.best_threshold_:.2f}",
)
ax.plot(
0.5,
f1_test_original,
label="Default threshold: 0.5",
color="#004e98",
linestyle="--",
marker="X",
markersize=10,
)
ax.legend(fontsize=8, loc="lower center")
ax.set_xlabel("Decision threshold", fontsize=10)
ax.set_ylabel("F1 score", fontsize=10)
ax.set_title("F1 score vs. Decision threshold -- Cross-validation", fontsize=12)
png
Immagine creata dall'autore.
# Check that the coefficients from the original model and the tuned model are the same
assert (tuned_fraud_model.estimator_(-1).coef_ ==
original_fraud_model(-1).coef_).all()

Abbiamo utilizzato lo stesso modello di regressione logistica sottostante per valutare due diverse soglie decisionali. I modelli sottostanti sono gli stessi, evidenziato dal coefficiente di uguaglianza nella dichiarazione di cui sopra. Ottimizzazione dentro TunedThresholdClassifierCV viene ottenuto utilizzando tecniche di post-elaborazione, che vengono applicate direttamente alle probabilità previste in uscita dal modello. Tuttavia, è importante notarlo TunedThresholdClassifierCV utilizza la convalida incrociata per impostazione predefinita per trovare la soglia decisionale per evitare un adattamento eccessivo ai dati di addestramento.

Apprendimento sensibile ai costi

L'apprendimento sensibile ai costi è un tipo di apprendimento automatico che assegna un costo a ciascun tipo di classificazione errata. Ciò traduce le prestazioni del modello in unità comprensibili alle parti interessate, come i dollari risparmiati.

Utilizzeremo il Set di dati sull'abbandono dei clienti TELCOun set di dati di classificazione binaria, per dimostrare il valore dell'apprendimento sensibile ai costi. L'obiettivo è prevedere se un cliente abbandonerà o meno, date le caratteristiche relative ai dati demografici del cliente, i dettagli del contratto e altre informazioni tecniche sull'account del cliente. La motivazione per utilizzare questo set di dati (e parte del codice) proviene da Il corso di Dan Becker sull'ottimizzazione della soglia decisionale.

data = pd.read_excel("data/Telco_customer_churn.xlsx")
drop_cols = (
"Count", "Country", "State", "Lat Long", "Latitude", "Longitude",
"Zip Code", "Churn Value", "Churn Score", "CLTV", "Churn Reason"
)
data.drop(columns=drop_cols, inplace=True)

# Preprocess the data
data("Churn Label") = data("Churn Label").map({"Yes": 1, "No": 0})
data.drop(columns=("Total Charges"), inplace=True)

X_train, X_test, y_train, y_test = train_test_split(
data.drop(columns=("Churn Label")),
data("Churn Label"),
test_size=0.2,
random_state=RANDOM_STATE,
stratify=data("Churn Label"),
)

Configura una pipeline di base per elaborare i dati e generare probabilità previste con un modello di foresta casuale. Questo servirà come base per il confronto con TunedThresholdClassifierCV.

preprocessor = ColumnTransformer(
transformers=(("one_hot", OneHotEncoder(),
selector(dtype_include="object"))),
remainder="passthrough",
)

original_churn_model = make_pipeline(
preprocessor, RandomForestClassifier(random_state=RANDOM_STATE)
)
original_churn_model.fit(X_train.drop(columns=("customerID")), y_train);

La scelta della preelaborazione e del tipo di modello non è importante per questo tutorial. L'azienda vuole offrire sconti ai clienti che si prevede abbandoneranno. Durante la collaborazione con le parti interessate, impari che offrire uno sconto a un cliente che non rinuncerà (un falso positivo) costerebbe $ 80. Impari anche che vale 200 dollari offrire uno sconto a un cliente che avrebbe rinunciato. Puoi rappresentare questa relazione in una matrice di costi:

def cost_function(y, y_pred, neg_label, pos_label):
cm = confusion_matrix(y, y_pred, labels=(neg_label, pos_label))
cost_matrix = np.array(((0, -80), (0, 200)))
return np.sum(cm * cost_matrix)

cost_scorer = make_scorer(cost_function, neg_label=0, pos_label=1)

Abbiamo anche inserito la funzione di costo in uno scorer personalizzato con scikit-learn. Questo marcatore verrà utilizzato come scoring argomento nel TunedThresholdClassifierCV e per valutare il profitto sul set di test.

tuned_churn_model = TunedThresholdClassifierCV(
original_churn_model,
scoring=cost_scorer,
store_cv_results=True,
)

tuned_churn_model.fit(X_train.drop(columns=("CustomerID")), y_train)

# Calculate the profit on the test set
original_model_profit = cost_scorer(
original_churn_model, X_test.drop(columns=("CustomerID")), y_test
)
tuned_model_profit = cost_scorer(
tuned_churn_model, X_test.drop(columns=("CustomerID")), y_test
)

print(f"Original model profit: {original_model_profit}")
print(f"Tuned model profit: {tuned_model_profit}")

Original model profit: 29640
Tuned model profit: 35600

Il profitto è maggiore nel modello messo a punto rispetto all'originale. Ancora una volta, possiamo tracciare la metrica oggettiva rispetto alle soglie decisionali per visualizzare la selezione della soglia decisionale sui dati di training durante la convalida incrociata:

fig, ax = plt.subplots(figsize=(5, 5))
ax.plot(
tuned_churn_model.cv_results_("thresholds"),
tuned_churn_model.cv_results_("scores"),
marker="o",
markersize=3,
linewidth=1e-3,
color="#c0c0c0",
label="Objective score (using cost-matrix)",
)
ax.plot(
tuned_churn_model.best_threshold_,
tuned_churn_model.best_score_,
"^",
markersize=10,
color="#ff6700",
label="Optimal cut-off point for the business metric",
)
ax.legend()
ax.set_xlabel("Decision threshold (probability)")
ax.set_ylabel("Objective score (using cost-matrix)")
ax.set_title("Objective score as a function of the decision threshold")
Immagine creata dall'autore.

In realtà, assegnare un costo statico a tutte le istanze classificate erroneamente nello stesso modo non è realistico dal punto di vista aziendale. Esistono metodi più avanzati per ottimizzare la soglia assegnando un peso a ciascuna istanza nel set di dati. Questo è coperto Esempio di apprendimento sensibile ai costi di scikit-learn.

Accordatura sotto vincoli

Questo metodo non è attualmente trattato nella documentazione di scikit-learn, ma è un caso aziendale comune per i casi d'uso della classificazione binaria. Il metodo di ottimizzazione sotto vincolo trova una soglia decisionale identificando un punto sulla curva ROC o su quella di richiamo di precisione. Il punto sulla curva rappresenta il valore massimo di un asse mentre vincola l'altro asse. Per questa procedura dettagliata, utilizzeremo il set di dati sul diabete degli indiani Pima. Si tratta di un compito di classificazione binaria per prevedere se un individuo ha il diabete.

Immagina che il tuo modello venga utilizzato come test di screening per una popolazione a rischio medio applicato a milioni di persone. Si stima che negli Stati Uniti ci siano 38 milioni di persone affette da diabete. Si tratta di circa l'11,6% della popolazione, quindi la specificità del modello dovrebbe essere elevata in modo da non diagnosticare erroneamente milioni di persone con diabete e sottoporle a test di conferma non necessari. Supponiamo che il tuo CEO immaginario abbia comunicato che non tollererà più di un tasso di falsi positivi del 2%. Costruiamo un modello che raggiunga questo obiettivo utilizzando TunedThresholdClassifierCV.

Per questa parte del tutorial, definiremo una funzione di vincolo che verrà utilizzata per trovare il tasso massimo di veri positivi con un tasso di falsi positivi del 2%.

def max_tpr_at_tnr_constraint_score(y_true, y_pred, max_tnr=0.5):
fpr, tpr, thresholds = roc_curve(y_true, y_pred, drop_intermediate=False)
tnr = 1 - fpr
tpr_at_tnr_constraint = tpr(tnr >= max_tnr).max()
return tpr_at_tnr_constraint

max_tpr_at_tnr_scorer = make_scorer(
max_tpr_at_tnr_constraint_score, max_tnr=0.98)
data = pd.read_csv("data/diabetes.csv")

X_train, X_test, y_train, y_test = train_test_split(
data.drop(columns=("Outcome")),
data("Outcome"),
stratify=data("Outcome"),
test_size=0.2,
random_state=RANDOM_STATE,
)

Costruisci due modelli, uno di regressione logistica che fungerà da modello di base e l'altro, TunedThresholdClassifierCV che avvolgerà il modello di regressione logistica di base per raggiungere l’obiettivo delineato dal CEO. Nel modello sintonizzato, imposta scoring=max_tpr_at_tnr_scorer. Ancora una volta, la scelta del modello e della preelaborazione non è importante per questo tutorial.

# A baseline model
original_model = make_pipeline(
StandardScaler(), LogisticRegression(random_state=RANDOM_STATE)
)
original_model.fit(X_train, y_train)

# A tuned model
tuned_model = TunedThresholdClassifierCV(
original_model,
thresholds=np.linspace(0, 1, 150),
scoring=max_tpr_at_tnr_scorer,
store_cv_results=True,
cv=8,
random_state=RANDOM_STATE,
)
tuned_model.fit(X_train, y_train)

Confrontare la differenza tra la soglia decisionale predefinita degli stimatori scikit-learn, 0,5, e quella trovata utilizzando l'approccio di ottimizzazione sotto vincolo sulla curva ROC.

# Get the fpr and tpr of the original model
original_model_proba = original_model.predict_proba(X_test)(:, 1)
fpr, tpr, thresholds = roc_curve(y_test, original_model_proba)
closest_threshold_to_05 = (np.abs(thresholds - 0.5)).argmin()
fpr_orig = fpr(closest_threshold_to_05)
tpr_orig = tpr(closest_threshold_to_05)

# Get the tnr and tpr of the tuned model
max_tpr = tuned_model.best_score_
constrained_tnr = 0.98

# Plot the ROC curve and compare the default threshold to the tuned threshold
fig, ax = plt.subplots(figsize=(5, 5))
# Note that this will be the same for both models
disp = RocCurveDisplay.from_estimator(
original_model,
X_test,
y_test,
name="Logistic Regression",
color="#c0c0c0",
linewidth=2,
ax=ax,
)
disp.ax_.plot(
1 - constrained_tnr,
max_tpr,
label=f"Tuned threshold: {tuned_model.best_threshold_:.2f}",
color="#ff6700",
linestyle="--",
marker="o",
markersize=11,
)
disp.ax_.plot(
fpr_orig,
tpr_orig,
label="Default threshold: 0.5",
color="#004e98",
linestyle="--",
marker="X",
markersize=11,
)
disp.ax_.set_ylabel("True Positive Rate", fontsize=8)
disp.ax_.set_xlabel("False Positive Rate", fontsize=8)
disp.ax_.tick_params(labelsize=8)
disp.ax_.legend(fontsize=7)

png
Immagine creata dall'autore.

Il metodo sintonizzato sotto vincolo ha rilevato una soglia di 0,80, che ha comportato una sensibilità media del 19,2% durante la convalida incrociata dei dati di addestramento. Confrontare la sensibilità e la specificità per vedere come regge la soglia nel set di test. Il modello ha soddisfatto i requisiti di specificità del CEO nel set di test?

# Average sensitivity and specificity on the training set
avg_sensitivity_train = tuned_model.best_score_

# Call predict from tuned_model to calculate sensitivity and specificity on the test set
specificity_test = recall_score(
y_test, tuned_model.predict(X_test), pos_label=0)
sensitivity_test = recall_score(y_test, tuned_model.predict(X_test))

print(f"Average sensitivity on the training set: {avg_sensitivity_train:.3f}")
print(f"Sensitivity on the test set: {sensitivity_test:.3f}")
print(f"Specificity on the test set: {specificity_test:.3f}")

Average sensitivity on the training set: 0.192
Sensitivity on the test set: 0.148
Specificity on the test set: 0.990

Conclusione

Il nuovo TunedThresholdClassifierCV Il corso è uno strumento potente che può aiutarti a diventare un data scientist migliore condividendo con i leader aziendali come sei arrivato a una soglia decisionale. Hai imparato come utilizzare il nuovo scikit-learn TunedThresholdClassifierCV classe per massimizzare una metrica, eseguire un apprendimento sensibile ai costi e ottimizzare una metrica sotto vincolo. Questo tutorial non intende essere completo o avanzato. Volevo introdurre la nuova funzionalità ed evidenziarne la potenza e la flessibilità nella risoluzione dei problemi di classificazione binaria. Consulta la documentazione di scikit-learn, la guida per l'utente e gli esempi per esempi di utilizzo approfonditi.

Un enorme ringraziamento a Guillaume Lemaitre per il suo lavoro su questa funzione.

Grazie per aver letto. Buona accordatura.

Licenze dati:
Frode con carta di credito: DbCL
Diabete degli indiani Pima: CC0
Abbandono TELCO: uso commerciale OK

Fonte: towardsdatascience.com

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *