Come funziona il parametro class_weight in scikit-learn?


116

Ho molti problemi a capire come funziona il class_weightparametro nella Regressione logistica di scikit-learn.

La situazione

Voglio utilizzare la regressione logistica per eseguire la classificazione binaria su un set di dati molto sbilanciato. Le classi sono etichettate 0 (negativo) e 1 (positivo) ei dati osservati sono in un rapporto di circa 19: 1 con la maggior parte dei campioni con esito negativo.

Primo tentativo: preparazione manuale dei dati di allenamento

Ho diviso i dati che avevo in set disgiunti per l'addestramento e il test (circa 80/20). Quindi ho campionato a caso i dati di allenamento a mano per ottenere dati di allenamento in proporzioni diverse da 19: 1; da 2: 1 -> 16: 1.

Ho quindi addestrato la regressione logistica su questi diversi sottoinsiemi di dati di addestramento e ho tracciato il richiamo (= TP / (TP + FN)) in funzione delle diverse proporzioni di addestramento. Naturalmente, il richiamo è stato calcolato sui campioni di TEST disgiunti che avevano le proporzioni osservate di 19: 1. Nota, anche se ho addestrato i diversi modelli su dati di allenamento diversi, ho calcolato il richiamo per tutti sugli stessi dati di test (disgiunti).

I risultati sono stati quelli attesi: il richiamo è stato di circa il 60% con proporzioni di allenamento 2: 1 ed è diminuito piuttosto velocemente quando è arrivato a 16: 1. C'erano diverse proporzioni 2: 1 -> 6: 1 dove il ricordo era decentemente superiore al 5%.

Secondo tentativo: ricerca sulla griglia

Successivamente, volevo testare diversi parametri di regolarizzazione e quindi ho usato GridSearchCV e ho creato una griglia di diversi valori del Cparametro e del class_weightparametro. Per tradurre le mie proporzioni n: m di campioni di allenamento negativi: positivi nella lingua del dizionario di class_weightho pensato di specificare solo diversi dizionari come segue:

{ 0:0.67, 1:0.33 } #expected 2:1
{ 0:0.75, 1:0.25 } #expected 3:1
{ 0:0.8, 1:0.2 }   #expected 4:1

e ho anche incluso Nonee auto.

Questa volta i risultati sono stati totalmente stravaganti. Tutti i miei richiami sono risultati minuscoli (<0,05) per ogni valore di class_weighttranne auto. Quindi posso solo presumere che la mia comprensione di come impostare il class_weightdizionario sia sbagliata. È interessante notare che il class_weightvalore di "auto" nella ricerca sulla griglia era di circa il 59% per tutti i valori di C, e ho indovinato che equilibra a 1: 1?

Le mie domande

  1. Come si utilizza correttamente class_weightper ottenere equilibri diversi nei dati di allenamento da ciò che effettivamente gli si fornisce? In particolare, a quale dizionario devo passare per class_weightutilizzare le proporzioni n: m di campioni di allenamento negativi: positivi?

  2. Se si passano vari class_weightdizionari a GridSearchCV, durante la convalida incrociata verranno riequilibrati i dati della piega di addestramento in base al dizionario ma utilizzerà le proporzioni reali del campione per calcolare la mia funzione di punteggio nella cartella di prova? Questo è fondamentale poiché qualsiasi metrica mi è utile solo se proviene da dati nelle proporzioni osservate.

  3. Che autovalore ha per class_weightquanto riguarda le proporzioni? Ho letto la documentazione e presumo che "bilancia i dati inversamente proporzionali alla loro frequenza" significa solo che lo rende 1: 1. È corretto? In caso contrario, qualcuno può chiarire?


Quando si usa class_weight, la funzione di perdita viene modificata. Ad esempio, invece di entropia incrociata, diventa entropia incrociata pesata. versodatascience.com/…
prashanth,

Risposte:


123

Prima di tutto, potrebbe non essere buono andare solo per il richiamo. Puoi semplicemente ottenere un richiamo del 100% classificando tutto come classe positiva. Di solito suggerisco di utilizzare l'AUC per selezionare i parametri e quindi trovare una soglia per il punto operativo (ad esempio un dato livello di precisione) a cui sei interessato.

Per come class_weightfunziona: penalizza gli errori nei campioni class[i]con class_weight[i]invece di 1. Quindi un peso di classe più alto significa che vuoi mettere più enfasi su una classe. Da quello che dici sembra che la classe 0 sia 19 volte più frequente della classe 1. Quindi dovresti aumentare il valore class_weightdella classe 1 rispetto alla classe 0, ad esempio {0: .1, 1: .9}. Se laclass_weight somma non è 1, sostanzialmente cambierà il parametro di regolarizzazione.

Per come class_weight="auto"funziona, puoi dare un'occhiata a questa discussione . Nella versione dev si può usare class_weight="balanced", il che è più facile da capire: in pratica significa replicare la classe più piccola fino ad avere tanti sample quanti in quella più grande, ma in modo implicito.


1
Grazie! Domanda veloce: ho menzionato il richiamo per chiarezza e infatti sto cercando di decidere quale AUC usare come misura. La mia comprensione è che dovrei massimizzare l'area sotto la curva ROC o l'area sotto la curva di richiamo rispetto alla curva di precisione per trovare i parametri. Dopo aver selezionato i parametri in questo modo, credo di aver scelto la soglia per la classificazione scorrendo lungo la curva. È questo quello che volevi dire? In tal caso, quale delle due curve ha più senso guardare se il mio obiettivo è catturare il maggior numero possibile di TP? Inoltre, grazie per il tuo lavoro e per i contributi a scikit-learn !!!
kilgoretrout

1
Penso che usare ROC sarebbe il modo più standard per andare, ma non credo che ci sarà una grande differenza. Tuttavia, hai bisogno di qualche criterio per scegliere il punto sulla curva.
Andreas Mueller

3
@MiNdFrEaK Penso che ciò che Andrew intende è che lo stimatore replica i campioni nella classe di minoranza, in modo che il campione di classi diverse sia bilanciato. È solo un sovracampionamento in modo implicito.
Shawn TIAN

8
@MiNdFrEaK e Shawn Tian: i classificatori basati su SV non producono più campioni delle classi più piccole quando si utilizza "bilanciato". Penalizza letteralmente gli errori commessi nelle classi minori. Dire il contrario è un errore ed è fuorviante, specialmente in set di dati di grandi dimensioni quando non puoi permetterti di creare più campioni. Questa risposta deve essere modificata.
Pablo Rivas

4
scikit-learn.org/dev/glossary.html#term-class-weight I pesi delle classi verranno utilizzati in modo diverso a seconda dell'algoritmo: per i modelli lineari (come SVM lineare o regressione logistica), i pesi delle classi altereranno la funzione di perdita di ponderando la perdita di ciascun campione in base al peso della sua classe. Per gli algoritmi ad albero, i pesi delle classi verranno utilizzati per riponderare il criterio di divisione. Si noti tuttavia che questo ribilanciamento non tiene conto del peso dei campioni in ciascuna classe.
prashanth

2

La prima risposta è utile per capire come funziona. Ma volevo capire come dovrei usarlo nella pratica.

SOMMARIO

  • per dati moderatamente sbilanciati SENZA rumore, non c'è molta differenza nell'applicazione dei pesi delle classi
  • per dati moderatamente sbilanciati CON rumore e fortemente sbilanciati, è meglio applicare i pesi delle classi
  • param class_weight="balanced" funziona in modo decente se non si desidera ottimizzare manualmente
  • con class_weight="balanced" l'acquisizione di più eventi veri (richiamo TRUE più alto) ma anche più probabilità di ricevere falsi allarmi (precisione TRUE inferiore)
    • di conseguenza, la% TRUE totale potrebbe essere superiore a quella effettiva a causa di tutti i falsi positivi
    • L'AUC potrebbe fuorviarti qui se i falsi allarmi sono un problema
  • non è necessario modificare la soglia di decisione allo squilibrio%, anche per uno squilibrio forte, ok per mantenere 0,5 (o da qualche parte intorno a quello a seconda di ciò di cui hai bisogno)

NB

Il risultato potrebbe differire quando si utilizza RF o GBM. sklearn non ha class_weight="balanced" per GBM ma lightgbm haLGBMClassifier(is_unbalance=False)

CODICE

# scikit-learn==0.21.3
from sklearn import datasets
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score, classification_report
import numpy as np
import pandas as pd

# case: moderate imbalance
X, y = datasets.make_classification(n_samples=50*15, n_features=5, n_informative=2, n_redundant=0, random_state=1, weights=[0.8]) #,flip_y=0.1,class_sep=0.5)
np.mean(y) # 0.2

LogisticRegression(C=1e9).fit(X,y).predict(X).mean() # 0.184
(LogisticRegression(C=1e9).fit(X,y).predict_proba(X)[:,1]>0.5).mean() # 0.184 => same as first
LogisticRegression(C=1e9,class_weight={0:0.5,1:0.5}).fit(X,y).predict(X).mean() # 0.184 => same as first
LogisticRegression(C=1e9,class_weight={0:2,1:8}).fit(X,y).predict(X).mean() # 0.296 => seems to make things worse?
LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X).mean() # 0.292 => seems to make things worse?

roc_auc_score(y,LogisticRegression(C=1e9).fit(X,y).predict(X)) # 0.83
roc_auc_score(y,LogisticRegression(C=1e9,class_weight={0:2,1:8}).fit(X,y).predict(X)) # 0.86 => about the same
roc_auc_score(y,LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X)) # 0.86 => about the same

# case: strong imbalance
X, y = datasets.make_classification(n_samples=50*15, n_features=5, n_informative=2, n_redundant=0, random_state=1, weights=[0.95])
np.mean(y) # 0.06

LogisticRegression(C=1e9).fit(X,y).predict(X).mean() # 0.02
(LogisticRegression(C=1e9).fit(X,y).predict_proba(X)[:,1]>0.5).mean() # 0.02 => same as first
LogisticRegression(C=1e9,class_weight={0:0.5,1:0.5}).fit(X,y).predict(X).mean() # 0.02 => same as first
LogisticRegression(C=1e9,class_weight={0:1,1:20}).fit(X,y).predict(X).mean() # 0.25 => huh??
LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X).mean() # 0.22 => huh??
(LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict_proba(X)[:,1]>0.5).mean() # same as last

roc_auc_score(y,LogisticRegression(C=1e9).fit(X,y).predict(X)) # 0.64
roc_auc_score(y,LogisticRegression(C=1e9,class_weight={0:1,1:20}).fit(X,y).predict(X)) # 0.84 => much better
roc_auc_score(y,LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X)) # 0.85 => similar to manual
roc_auc_score(y,(LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict_proba(X)[:,1]>0.5).astype(int)) # same as last

print(classification_report(y,LogisticRegression(C=1e9).fit(X,y).predict(X)))
pd.crosstab(y,LogisticRegression(C=1e9).fit(X,y).predict(X),margins=True)
pd.crosstab(y,LogisticRegression(C=1e9).fit(X,y).predict(X),margins=True,normalize='index') # few prediced TRUE with only 28% TRUE recall and 86% TRUE precision so 6%*28%~=2%

print(classification_report(y,LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X)))
pd.crosstab(y,LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X),margins=True)
pd.crosstab(y,LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X),margins=True,normalize='index') # 88% TRUE recall but also lot of false positives with only 23% TRUE precision, making total predicted % TRUE > actual % TRUE
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.