XGBoost pour la classification binaire: choisir le bon seuil

8

Je travaille sur un ensemble de données à étiquetage binaire très déséquilibré, où le nombre de véritables étiquettes est à seulement 7% de l'ensemble de données. Mais une combinaison de fonctionnalités pourrait produire un nombre supérieur à la moyenne de celles d'un sous-ensemble.

Par exemple, nous avons le jeu de données suivant avec une seule entité (couleur):

180 échantillons rouges - 0

20 échantillons rouges - 1

300 échantillons verts - 0

100 échantillons verts - 1

Nous pouvons construire un arbre de décision simple:

                      (color)

                red /       \ green

 P(1 | red) = 0.1              P(1 | green) = 0.25

P (1) = 0,2 pour l'ensemble de données global

Si j'exécute XGBoost sur cet ensemble de données, il peut prédire des probabilités ne dépassant pas 0,25. Ce qui signifie que si je prends une décision au seuil de 0,5:

  • 0 - P <0,5
  • 1 - P> = 0,5

Ensuite, j'obtiendrai toujours tous les échantillons étiquetés comme des zéros . J'espère que j'ai clairement décrit le problème.

Maintenant, sur l'ensemble de données initial, j'obtiens le tracé suivant (seuil sur l'axe des x):

entrez la description de l'image ici

Avoir un maximum de f1_score au seuil = 0,1. Maintenant, j'ai deux questions:

  • dois-je même utiliser f1_score pour un ensemble de données d'une telle structure?
  • est-il toujours raisonnable d'utiliser un seuil de 0,5 pour mapper les probabilités aux étiquettes lors de l'utilisation de XGBoost pour la classification binaire?

Mise à jour. Je vois que ce sujet suscite un certain intérêt. Ci-dessous se trouve le code Python pour reproduire l'expérience rouge / vert en utilisant XGBoost. Il génère en fait les probabilités attendues:

from xgboost import XGBClassifier
from sklearn.model_selection import train_test_split
import numpy as np

X0_0 = np.zeros(180) # red - 0
Y0_0 = np.zeros(180)

X0_1 = np.zeros(20) # red - 1
Y0_1 = np.ones(20)

X1_0 = np.ones(300) # green - 0
Y1_0 = np.zeros(300)

X1_1 = np.ones(100) # green  - 1
Y1_1 = np.ones(100)

X = np.concatenate((X0_0, X0_1, X1_0, Y1_1))
Y = np.concatenate((Y0_0, Y0_1, Y1_0, Y1_1))

# reshaping into 2-dim array
X = X.reshape(-1, 1)

import xgboost as xgb

xgb_dmat = xgb.DMatrix(X_train, label=y_train)

param = {'max_depth': 1,
         'eta': 0.01,
         'objective': 'binary:logistic',
         'eval_metric': 'error',
         'nthread': 4}

model = xgb.train(param, xg_mat, 400)

X0_sample = np.array([[0]])
X1_sample = np.array([[1]])

print('P(1 | red), predicted: ' + str(model.predict(xgb.DMatrix(X0_sample))))
print('P(1 | green), predicted: ' + str(model.predict(xgb.DMatrix(X1_sample))))

Production:

P(1 | red), predicted: [ 0.1073855]
P(1 | green), predicted: [ 0.24398108]
Denis Kulagin
la source

Réponses:

5

Vous devez décider ce que vous voulez maximiser.

Le classement en comparant la probabilité à 0,5 est approprié si vous souhaitez maximiser la précision. Ce n'est pas approprié si vous souhaitez maximiser la métrique f1.

Si vous voulez maximiser la précision, toujours prédire zéro est le classificateur optimal.

Alternativement, étant donné un score de probabilité p, une autre option consiste à lancer au hasard une pièce biaisée; avec probabilitép, classification de sortie 1, sinon classification de sortie 0. Cela ne prédit pas toujours zéro. Cependant, ce n'est probablement pas mieux en aucune façon utile.

Si vous souhaitez maximiser la métrique f1, une approche consiste à former votre classificateur pour prédire une probabilité, puis choisissez un seuil qui maximise le score f1. Le seuil ne sera probablement pas 0,5.

Une autre option consiste à comprendre le coût des erreurs de type I par rapport aux erreurs de type II, puis d'affecter des pondérations de classe en conséquence.

DW
la source
1
Je veux juste mentionner deux autres choses: (a) au lieu du score F1, l'OP peut également utiliser une précision pondérée, ou même maximiser une métrique de classement telle que AUC ROC (b) xgboostprend en charge les poids de classe, l'OP devrait jouer avec ceux-ci s'il n'est pas satisfait de la métrique qu'il souhaite maximiser.
Ricardo Cruz
@RicardoCruz, merci - bonnes suggestions! (J'ai mentionné brièvement les poids de classe - dernier paragraphe de la réponse - mais je suis heureux que vous l'ayez souligné.)
DW
BTW, un class_weights couramment utilisés sont des fréquences inverses: n_samples / (n_classes * np.bincount(y)). Cela évite au classificateur de donner plus de poids aux classes plus peuplées.
Ricardo Cruz