Maîtriser le processing des variables numériques pour l’algorithme SMOTE

Les effets du SMOTE sur l’apprentissage des modèles dépendent largement de la façon dont les données numériques sont processées. L’idéal est de normaliser les variables continues en amont du SMOTE et de transformer les variables discrètes en aval.
SMOTE numeric - SMOTE et modèles processing
Sommaire

Cet article est le quatrième de notre série consacrée aux données déséquilibrées.

Nous avons vu dans un article précédent sur le SMOTE que la génération d’individus synthétiques dépend fortement de la structure des variables numériques. Cette structure intervient particulièrement à deux étapes :

  • Le calcul des plus proches voisins. Pour identifier les plus proches voisins d’un point, on calcule les distances à ce point.
  • La création d’un nouvel individu. L’individu synthétique est positionné aléatoirement sur le segment entre 2 individus minoritaires originaux.

Nous présentons dans cet article comment mieux traiter les variables numériques, en distinguant les variables discrètes et continues.

Nous reprenons le cadre des articles précédents : une variable binaire à deux classes ($Y = 0/1$), où les individus minoritaires sont les individus positifs ($Y = 1$) que l’on cherche à prédire.

Scaler les variables numériques (continues ou discrètes)

Normaliser les variables

Le SMOTE repose sur l’algorithme des k-NN (k-Nearest Neighbors ou k-plus proches voisins). Cet algorithme utilise la distance euclidienne, qui peut être biaisée par l’échelle des variables. Ainsi, une variable qui prend des valeurs entre 0 et 100 aura plus d’importance dans les calculs de distance qu’une variable qui prend des valeurs entre 0 et 1.

Il est donc important – comme pour tous les algorithmes métriques – de normaliser (scaling) les données pour utiliser les différentes variables de façon non biaisée. Il existe différentes façons de normaliser les données (voir [1] pour une discussion approfondie), dont les principales sont :

  • Min Max Scaler : normalise chaque variable dans un intervalle (feature range) défini, par défaut entre 0 et 1. Cette méthode est très sensible aux outliers.
  • Standard Scaler : centre (retrait de la moyenne) et réduit (divise par la variance) chaque variable.
  • Robust Scaler : centre en utilisant la médiane et réduit en utilisant l’interquartile range. Ces statistiques sont plus robustes aux outliers que celles du Standard Scaler.

Scaling des variables numériques, un exemple 

Pour normaliser (ou rescaler) un jeu de données sous Python avant d’appliquer le SMOTE, le plus simple est d’utiliser les fonctions scikit-learn disponibles dans le module sklearn.preprocessing [2]. A titre d’exemple, utilisons le StandardScaler pour observer l’influence de la normalisation sur un petit nombre de données :

				
					from sklearn.preprocessing import StandardScaler
 
scaler = StandardScaler()
X_norm = scaler.fit_transform(X)
				
			

Observons l’effet de la normalisation des données sur les résultats du k-Nearest Neighbors :

SMOTE numeric - NN search avant après normalisation

Figure 1. Effet de la normalisation sur la recherche du plus proche voisin.
A gauche, recherche du plus proche voisin sur des données non normalisées. A droite, recherche du plus proche voisin sur des données normalisées.
En bleu les points candidats, en orange le point dont on cherche le plus proche voisin et en vert le plus proche voisin obtenu.

Dans cet exemple, la variable en ordonnée a une échelle de plus grande amplitude que celle en abscisse. Elle a donc plus d’importance dans le calcul des distances et le plus proche voisin obtenu correspond au point qui a l’ordonnée la plus proche.

Lorsque les variables sont normalisées, elles ont une importance similaire et le plus proche voisin n’est plus le point d’ordonnée la plus proche. C’est un autre point qui devient le plus proche voisin.

Il est donc essentiel de normaliser vos variables pour vous assurer que le k-NN exploite l’information de l’ensemble des variables.

Re-discrétiser les variables numériques discrètes

SMOTE et variables discrètes : la création d’un signal anormal 

Nous avons vu que le SMOTE génère des individus synthétiques positionnés entre les individus réels. Les variables numériques de ces nouveaux individus prennent donc des valeurs différentes de celles réellement observées. Cette propriété est un avantage pour les variables continues car elle permet au modèle de mieux généraliser son apprentissage, comme nous l’avons vu dans l’article explicatif du SMOTE.

Pour les variables numériques discrètes, le résultat est différent. Une variable discrète est une variable prenant un faible nombre de valeurs fixes, par exemple le nombre de contrats d’un client, ou l’âge arrondi à l’année. Pour ces variables le SMOTE conduit à un pattern particulier, illustré par l’histogramme suivant :

SMOTE numeric - Hist discrete SMOTE zone isolées

Figure 2. Création de “zones synthétiques irréelles” par le SMOTE pour une variable numérique discrète
En vert les individus majoritaires, en orange les individus minoritaires originaux et en bleu les individus minoritaires synthétiques.
On observe la création d’individus minoritaires synthétiques dans des régions (encadrées en rouge) où aucune valeur réelle n’existe.

En présence d’une variable discrète, le SMOTE crée des individus synthétiques dans des régions où aucun individu réel ne se trouve. Nommons ces régions les “zones synthétiques irréelles”. Elles correspondent aux encadrés rouges dans la figure ci-dessus.

Dans ces zones synthétiques irréelles, le signal est pur : elles contiennent uniquement des individus positifs. Un modèle avec une bonne capacité de séparation de l’espace comme un algorithme par arbre (tree-based learner) ou un réseau de neurones profond (deep neural network) va donc concentrer une partie des ses efforts d’apprentissage sur ces régions. En effet, s’il isole parfaitement une zone synthétique irréelle, il prédira une probabilité de 1 dans cette zone et fera donc une prédiction correcte à 100%. Il augmentera ainsi sa performance sur les données Train.

Plusieurs points sont à noter :

  • Ces régions ne seront jamais rencontrées dans les données de validation/test : elles sont donc “apprises pour rien” et n’impactent pas la performance du modèle mesurée sur validation/test (tant que vous ne faites pas l’erreur de faire du SMOTE sur ces données).
  • Mais elles génèrent une forte variance (écart de performance entre les données Train et Validation/Test) et sont susceptibles de déformer le signal appris par le modèle, qui devient alors moins robuste et moins interprétable.
  • Enfin elles rallongent le temps d’apprentissage du modèle, qui dépense du temps à apprendre ces régions inutiles.

Comment éviter les zones synthétiques irréelles ? Re-discrétiser les variables

La solution la plus simple est de retraiter les individus synthétiques après le SMOTE, en arrondissant les valeurs obtenues pour les variables discrètes à la valeur réelle la plus proche.

Notons qu’une autre approche pourrait consister à traiter les numériques discrètes comme des catégorielles via l’algorithme SMOTE-NC (voir l’article précédent), mais cela conduirait à perdre la notion d’ordre des variables numériques et n’est donc pas recommandé.

La version de SMOTE implémentée dans imbalanced-learn est celle de l’article initial [3] et ne retraite pas les variables discrètes. Nous proposons donc une implémentation simple de la re-discrétisation, avec une fonction nearest_existing (code en annexe).

Le code pour appliquer le SMOTE aux données Train et retraiter les variables numériques discrètes avec la fonction re_discretize est le suivant :

				
					from imblearn.over_sampling import SMOTE
 
# Utilisation du SMOTE sur les données d'apprentissage
smote = SMOTE(sampling_strategy=0.5)
X_train_res, y_train_res = smote.fit_resample(X_train, y_train)
 
# Liste contenant les indices des variables numériques discrètes
numeric_discrete_col_list = [3, 4]
 
for numeric_discrete_col in numeric_discrete_col_list:
    X_train_res[:, numeric_discrete_col] = re_discretize(
        np.unique(X_train[:, numeric_discrete_col]),
        X_train_res[:, numeric_discrete_col],
    )

				
			

Cette re-discrétisation empêche la création de zones synthétiques irréelles par le SMOTE, comme le montre cet histogramme :

SMOTE numeric - Processing discrete before after

Figure 3. Effet de la re-discrétisation des individus synthétiques après le SMOTE
A gauche, répétition de la figure 2 : création de zones synthétiques irréelles
A droite, après re-discrétisation, les individus synthétiques prennent uniquement des valeurs observées sur les données d’origine (ils sont similaires aux individus réels).

Comparaison des modèles avec/sans re-discrétisation

Illustrons l’effet de ce retraitement sur un modèle de Machine Learning en constituant un jeu de données avec cinq variables, dont une variable continue prédictive et une variable discrète non-informative. Pour ce faire, nous utilisons la fonction datasets.make_classification de sklearn, et transformons la variable non-prédictive en variable discrète (avec une fonction de binning).

Observons ces données après SMOTE uniquement, et après SMOTE + re-discrétisation :

SMOTE numeric - Processing discrete before after 2D

Figure 4. Répartition des individus synthétiques sans/avec re-discrétisation.
Abscisse : Variable non-prédictive discrète. Ordonnée : Variable prédictive continue.
A gauche, sans re-discrétisation, des individus irréels sont générés. A droite, après re-discrétisation, les individus générés prennent uniquement des valeurs observées sur les données d’origine.

Nous entraînons ensuite un Gradient Boosting [4] sur ces deux jeux de données. Observons les zones de forte probabilité prédites par le modèle après SMOTE, selon que l’on retraite ou non la variable discrète :

SMOTE numeric - Predicted probabilities processing

Figure 5. Prédictions du modèle sans et avec retraitement des variables discrètes.
Pointillés bleus : valeurs réelles (originales) prises par la variable discrète.
A gauche : prédictions du modèle entraîné directement après le SMOTE.
A droite : prédictions du modèle entraîné après SMOTE + re-discrétisation.

En l’absence de re-discrétisation (graphique de gauche), on observe que la probabilité prédite dépend des valeurs de la variable discrète. La création d’individus synthétiques entre les valeurs discrètes a donc conduit le modèle à extraire une information fausse, puisque la variable n’est pas prédictive. 

A l’inverse, lorsqu’il y a re-discrétisation après le SMOTE, la probabilité prédite par le modèle dépend très peu de la variable discrète : les informations extraites par le modèle sont justes.

On confirme cette observation en mesurant l’importance des variables :

SMOTE numeric - Feature importances processing

Figure 6. Importance des variables sans et avec re-discrétisation
En bleu l’importance de la variable discrète, en orange l’importance de la variable continue explicative. Les trois autres variables ne sont pas représentées. L’importance est mesurée par le total gain.
A gauche : importances des variables avant le traitement des variables discrètes dans le SMOTE. A droite : importances après le traitement des variables discrètes dans le SMOTE.

Ces résultats confirment que sans retraitement, le modèle attribue une importance significative à la variable discrète alors qu’elle n’est pas prédictive. Lorsqu’il y a eu re-discrétisation, on obtient un modèle plus fiable, dans lequel la variable discrète reçoit moins de 5% d’importance.

La re-discrétisation permet donc d’obtenir un modèle plus interprétable. Rappelons qu’il aura cependant la même performance sur les données de validation/test en général (en termes d’AUC-PR par exemple) car les zones synthétiques irréelles ne se retrouvent pas dans les données d’évaluation.

Conclusion

Cet article éclaire deux méthodes essentielles dans l’utilisation de l’algorithme SMOTE. Le scaling des variables numériques est un aspect connu, mais la re-discrétisation des variables discrètes l’est beaucoup moins. En effet, à notre connaissance aucune ressource disponible – articles de recherche ou de blogs – ne mentionne ce problème.

Résumons :

  • Il est indispensable de normaliser les variables numériques afin qu’elles soient d’égale importance dans la génération des individus synthétiques.
  • En présence de variables discrètes, le SMOTE génère un faux signal dans les données, sous forme de “zones synthétiques irréelles”. Ceci peut conduire à attribuer une valeur prédictive à des variables qui n’en ont pas.

Une solution simple pour pallier ce problème est de re-discrétiser les variables en sortie du SMOTE. Le code pour réaliser ce post-processing est fourni dans l’article.

Annexe : Traitement des variables numériques discrètes en python

				
					def re_discretize(discrete_values, values_to_discretize):
""" This function is useful for re-discretizing the values of
    synthetic individuals after SMOTE in any numerical variable that
    is originally discrete.
    It rounds the values in the array values_to_discretize to the
    closest value in the array discrete_values (which represents the
    truly existing values of the discrete variable)

    Parameters
    ----------
    discrete_values : Array-like
        Original discrete values, sorted in ascending/descending order
    values_to_discretize : Array-like
        Values to be discretized

    Returns
    -------
    np.ndarray
        Same shape as values_to_discretize, rounded to the
        nearest value found in discrete_values
    """
    # The function searchsorted identifies for every value in 
    # values_to_discretize the index that should be used if inserting
    # values, to maintain an ordered array
    insertion_index = np.searchsorted(discrete_values, values_to_discretize)

    # We use this index to get the closest lower index and the closest upper
    # index
    index_closest_lower = (insertion_index - 1).clip(min=0)
    index_closest_upper = insertion_index.clip(max=len(discrete_values) - 1)

    # We use these indexes to compute the distances between a value in 
    # values_to_discretize and its closest values (lower and upper) in
    # discrete_values
    closest_indexes = np.vstack((index_closest_lower, index_closest_upper))
    closest_values = np.take(discrete_values, closest_indexes)
    shortest_distances = abs(closest_values - values_to_discretize)

    # We then use the value that is the nearest between these lower 
    # and upper bounds
    nearest_value = shortest_distances.argmin(axis=0)
    discretized_values = closest_values[
        nearest_value, np.arange(len(values_to_discretize))
    ]

    return discretized_values
				
			

 

 

Références :

  1. B. Roy, « All about Feature Scaling », Medium, 7 avril 2020. https://towardsdatascience.com/all-about-feature-scaling-bcc0ad75cb35

  2. « 6.3. Preprocessing data », scikit-learn. https://scikit-learn/stable/modules/preprocessing.html

  3. N. V. Chawla, K. W. Bowyer, L. O. Hall, et W. P. Kegelmeyer, « SMOTE: Synthetic Minority Over-sampling Technique », J. Artif. Intell. Res., vol. 16, p. 321‑357, juin 2002.

  4. « XGBClassifier – Python API Reference — xgboost 1.5.2 documentation ». https://xgboost.readthedocs.io/en/stable/python/python_api.html#xgboost.XGBClassifier

Sommaire

Voir aussi

Voir aussi

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *