¿Cómo funciona el parámetro class_weight en scikit-learn?

116

Tengo muchos problemas para entender cómo funciona el class_weightparámetro de Regresión logística de scikit-learn.

La situación

Quiero usar la regresión logística para hacer una clasificación binaria en un conjunto de datos muy desequilibrado. Las clases están etiquetadas como 0 (negativo) y 1 (positivo) y los datos observados están en una proporción de aproximadamente 19: 1 y la mayoría de las muestras tienen un resultado negativo.

Primer intento: preparación manual de datos de entrenamiento

Dividí los datos que tenía en conjuntos separados para entrenamiento y pruebas (alrededor de 80/20). Luego hice una muestra aleatoria de los datos de entrenamiento a mano para obtener datos de entrenamiento en diferentes proporciones que 19: 1; desde 2: 1 -> 16: 1.

Luego entrené la regresión logística en estos diferentes subconjuntos de datos de entrenamiento y tracé el recuerdo (= TP / (TP + FN)) en función de las diferentes proporciones de entrenamiento. Por supuesto, la recuperación se calculó en las muestras de PRUEBA disjuntos que tenían las proporciones observadas de 19: 1. Tenga en cuenta que, aunque entrené los diferentes modelos en diferentes datos de entrenamiento, calculé la recuperación para todos ellos en los mismos datos de prueba (disjuntos).

Los resultados fueron los esperados: la recuperación fue de alrededor del 60% en proporciones de entrenamiento de 2: 1 y cayó bastante rápido cuando llegó a 16: 1. Hubo varias proporciones 2: 1 -> 6: 1 en las que el retiro fue decentemente superior al 5%.

Segundo intento: búsqueda en cuadrícula

A continuación, quería probar diferentes parámetros de regularización, por lo que usé GridSearchCV e hice una cuadrícula de varios valores del Cparámetro, así como del class_weightparámetro. Para traducir mis n: m proporciones de muestras de entrenamiento negativas: positivas al idioma del diccionario de class_weight, pensé que solo especifico varios diccionarios de la siguiente manera:

{ 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

y también incluí Noney auto.

Esta vez los resultados fueron totalmente descabellados. Todos mis retiros resultaron pequeños (<0.05) para cada valor de class_weightexcepto auto. Así que solo puedo asumir que mi comprensión de cómo configurar el class_weightdiccionario es incorrecta. Curiosamente, el class_weightvalor de 'auto' en la búsqueda de la cuadrícula fue de alrededor del 59% para todos los valores de C, y supuse que se equilibra en 1: 1.

Mis preguntas

  1. ¿Cómo se usa correctamente class_weightpara lograr diferentes equilibrios en los datos de entrenamiento de los que realmente les da? Específicamente, ¿a qué diccionario paso para class_weightusar n: m proporciones de muestras de entrenamiento negativas: positivas?

  2. Si pasa varios class_weightdiccionarios a GridSearchCV, durante la validación cruzada, ¿reequilibrará los datos del pliegue de entrenamiento de acuerdo con el diccionario, pero utilizará las proporciones de muestra reales dadas para calcular mi función de puntuación en el pliegue de prueba? Esto es fundamental ya que cualquier métrica solo me es útil si proviene de datos en las proporciones observadas.

  3. ¿Qué hace el autovalor de class_weighten cuanto a proporciones? Leí la documentación y supongo que "equilibra los datos de forma inversamente proporcional a su frecuencia", solo significa que los hace 1: 1. ¿Es esto correcto? Si no, ¿alguien puede aclararlo?

kilgoretrout
fuente
Cuando se usa class_weight, la función de pérdida se modifica. Por ejemplo, en lugar de entropía cruzada, se convierte en entropía cruzada ponderada. forwarddatascience.com/…
prashanth

Respuestas:

123

En primer lugar, puede que no sea bueno limitarse a recordar solo. Simplemente puede lograr una recuperación del 100% clasificando todo como la clase positiva. Por lo general, sugiero usar AUC para seleccionar parámetros y luego encontrar un umbral para el punto de operación (digamos, un nivel de precisión dado) que le interesa.

Cómo class_weightfunciona: penaliza los errores en muestras de class[i]con en class_weight[i]lugar de 1. Por lo tanto, un peso de clase más alto significa que desea poner más énfasis en una clase. Por lo que dice, parece que la clase 0 es 19 veces más frecuente que la clase 1. Por lo tanto, debe aumentar el class_weightde la clase 1 en relación con la clase 0, digamos {0: .1, 1: .9}. Si class_weightno suma 1, básicamente cambiará el parámetro de regularización.

Para saber cómo class_weight="auto"funciona, puede echar un vistazo a esta discusión . En la versión dev puedes usar class_weight="balanced", que es más fácil de entender: básicamente significa replicar la clase más pequeña hasta tener tantas muestras como en la más grande, pero de forma implícita.

Andreas Mueller
fuente
1
¡Gracias! Pregunta rápida: mencioné el retiro para mayor claridad y, de hecho, estoy tratando de decidir qué AUC usar como mi medida. Tengo entendido que debería maximizar el área bajo la curva ROC o el área bajo recuperación frente a la curva de precisión para encontrar parámetros. Después de elegir los parámetros de esta manera, creo que elijo el umbral para la clasificación deslizándome a lo largo de la curva. ¿Es esto lo que quisiste decir? Si es así, ¿cuál de las dos curvas tiene más sentido para mirar si mi objetivo es capturar tantos TP como sea posible? ¡¡¡Además, gracias por su trabajo y contribuciones a scikit-learn !!!
kilgoretrout
1
Creo que usar ROC sería la forma más estándar de hacerlo, pero no creo que haya una gran diferencia. Sin embargo, necesita algún criterio para elegir el punto en la curva.
Andreas Mueller
3
@MiNdFrEaK Creo que lo que Andrew quiere decir es que el estimador replica muestras en la clase minoritaria, por lo que la muestra de diferentes clases está equilibrada. Es simplemente un sobremuestreo de una manera implícita.
Shawn TIAN
8
@MiNdFrEaK y Shawn Tian: los clasificadores basados ​​en SV no producen más muestras de las clases más pequeñas cuando usa 'balanceado'. Literalmente penaliza los errores cometidos en las clases más pequeñas. Decir lo contrario es un error y es engañoso, especialmente en grandes conjuntos de datos cuando no puede permitirse crear más muestras. Esta respuesta debe editarse.
Pablo Rivas
4
scikit-learn.org/dev/glossary.html#term-class-weight Las ponderaciones de clase se utilizarán de forma diferente según el algoritmo: para modelos lineales (como SVM lineal o regresión logística), las ponderaciones de clase alterarán la función de pérdida por ponderando la pérdida de cada muestra por su peso de clase. Para los algoritmos basados ​​en árboles, las ponderaciones de clase se utilizarán para volver a ponderar el criterio de división. Sin embargo, tenga en cuenta que este reequilibrio no tiene en cuenta el peso de las muestras en cada clase.
prashanth
2

La primera respuesta es buena para comprender cómo funciona. Pero quería entender cómo debería usarlo en la práctica.

RESUMEN

  • para datos moderadamente desequilibrados SIN ruido, no hay mucha diferencia en la aplicación de ponderaciones de clase
  • para datos moderadamente desequilibrados CON ruido y fuertemente desequilibrados, es mejor aplicar ponderaciones de clase
  • param class_weight="balanced"funciona decente en ausencia de que desee optimizar manualmente
  • con class_weight="balanced"captura más eventos verdaderos (mayor recuperación VERDADERA) pero también es más probable que reciba alertas falsas (menor precisión VERDADERA)
    • como resultado, el% TRUE total podría ser más alto que el real debido a todos los falsos positivos
    • Las AUC pueden engañarlo aquí si las falsas alarmas son un problema
  • no es necesario cambiar el umbral de decisión al% de desequilibrio, incluso para un desequilibrio fuerte, está bien mantener 0.5 (o en algún lugar alrededor de eso, dependiendo de lo que necesite)

nótese bien

El resultado puede diferir al utilizar RF o GBM. sklearn no tiene class_weight="balanced" para GBM pero lightgbm tieneLGBMClassifier(is_unbalance=False)

CÓDIGO

# 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
citynorman
fuente