Travaux pratiques - Introduction à AutoML avec AutoGluon

[Diapositives du cours]

(correspond à 1 séance de TP)

Références externes utiles :

L’objectif de cette séance de TP est d’introduire l’utilisation d’AutoGluon pour trouver automatiquement des modèles optimisés sur des données tabulaires, pour des problèmes de classification ou de régression. Après une Illustration suivant l’exemple de la documentation d’AutoGluon, nous examinerons une Utilisation d’AutoGluon sur South German Credit Dataset.

Installation d’AutoGluon

Vous pouvez vous servir d’AutoGluon soit en l’installant sur votre ordinateur (pour le moment seuls sont acceptés les systèmes linux et MacOS), soit en utilisant le JupyterHub du Cnam auquel vous devriez vous connecter avec vos identifiants Cnam. Les instructions d’installation sont relativement claires. Il est possible également d’installer le support CUDA (pour faire des calculs sur une carte graphique adéquate) ou non. Des calculs sur carte graphique ne sont pas nécessaires pour nos séances de TP.

Illustration suivant l’exemple de la documentation d’AutoGluon

Le premier exemple donné dans la documentation d’AutoGluon concerne le Adult Data Set issu de UCI Machine Learning Repository. Dans ce problème de classification à deux classes l’objectif est de prédire si le revenu d’un individu est inférieur ou supérieur à 50 k$ par an.

Importation des classes indispensables :

import os
os.system("mkdir -p tpautoml/data")
from autogluon.tabular import TabularDataset, TabularPredictor

Les données d’apprentissage (et ultérieurement celles de test) sont téléchargées depuis un site AWS et non depuis le site de l’UCI. Les données, en format CSV (Comma-Separated Values) ont été préalablement divisées en ensemble d’apprentissage et ensemble de test. La lecture des données d’apprentissage et de test peut donc se faire avec :

train_data = TabularDataset('https://autogluon.s3.amazonaws.com/datasets/Inc/train.csv')
test_data = TabularDataset('https://autogluon.s3.amazonaws.com/datasets/Inc/test.csv')

Adult Data Set contient 48842 observations, afin de réduire le temps de calcul pour cette illustration de l’utilisation d’AutoGluon un échantillon de seulement 500 observations est choisi. Une valeur d’initialisation est employée pour le générateur pseudo-aléatoire afin de rendre cette sélection reproductible. Est affiché ensuite un en-tête avec les noms des colonnes et quelques observations :

subsample_size = 500
train_data = train_data.sample(n=subsample_size, random_state=0)
train_data.head()

La variable à expliquer (à prédire) est définie avec label = 'nom_de_la_colonne'. Un résumé de la distribution de ses valeurs est ensuite affiché. AutoGluon est capable de déterminer quelles sont les différentes classes présentes suivant les valeurs trouvées dans les données pour la variable à expliquer. En principe, AutoGluon peut déterminer aussi, à partir de ces valeurs, s’il s’agit d’un problème de classification ou de régression, mais (comme nous le verrons plus loin) une variable quantitative discrète avec relativement peu de valeurs différentes est parfois prise par une variable nominale (où chaque valeur différente indique une classe).

label = 'class'
print("Résumé de la variable de classe : \n", train_data[label].describe())

Le lancement de l’apprentissage est très facile dans sa version par défaut : après avoir indiqué un répertoire local où les modèles résultants doivent être stockés à la fin de l’apprentissage, on peut se contenter de préciser quelles sont les valeurs désirées pour la variable à expliquer (argument label du constructeur TabularPredictor) et quelles sont les données d’apprentissage (argument de .fit()) :

save_path = 'tpautoml/agModels-predictClass'  # répertoire pour stocker les modèles obtenus
predictor = TabularPredictor(label=label, path=save_path).fit(train_data)

Il est utile de suivre les messages donnés lors de l’exécution pour comprendre la séquence des opérations et détecter éventuellement des problèmes (par ex., problème traité comme une classification multi-classe au lieu de régression lorsque la variable à prédire est discrète avec peu de valeurs différentes).

On peut constater que la représentation des valeurs des variables explicatives et de la variable expliquée peut être modifiée. Aussi, plusieurs méthodes sont considérées ; pour chacune, plusieurs modèles sont obtenus, avec différentes valeurs pour les hyper-paramètres, et comparées. Les données de validation sont, par défaut, un échantillon de 20% de train_data, les données d’apprentissage étant le reste de 80% de train_data.

L’exécution peut prendre un certain temps (ici elle sera rapide car il y a peu de données et nous utilisons les paramètres par défaut de .fit()) et se termine avec TabularPredictor saved [...].

Pour évaluer les modèles obtenus sur les données de test il suffit d’indiquer quelles sont les valeurs désirées (à prédire)

y_test = test_data[label]  # valeurs à prédire
test_data_nolabel = test_data.drop(columns=[label])  # pas indispensable, montre simplement qu'AutoGluon ne triche pas
test_data_nolabel.head()

et ensuite celles effectivement prédites, puis de les comparer (appel de .evaluate_predictions()) :

y_pred = predictor.predict(test_data_nolabel)
print("Prédictions :  \n", y_pred)
performance = predictor.evaluate_predictions(y_true=y_test, y_pred=y_pred, auxiliary_metrics=True)

Noter que lors de l’appel .predict() c’est le modèle qui a été trouvé comme étant le meilleur qui est implicitement utilisé.

Dans cet exemple les modèles sont déjà présents en mémoire, mais s’ils ne le sont pas il suffit de les charger avec

predictor = TabularPredictor.load(save_path)

Il est ensuite possible de voir comment se classent les différents modèles construits par AutoGluon à travers leurs performances sur les données de test :

predictor.leaderboard(test_data, silent=True)

Pour prédire sur une donnée en particulier il suffit de l’indiquer, par ex. pour la première de test_data_nolabel :

predictor.predict(test_data_nolabel.iloc[[0]])

Question :

Aux Etats-Unis, la réglementation Equal Credit Opportunity Act (ECOA) interdit l’utilisation des attributs suivants pour prendre la décision d’accorder un crédit : age, marital-status, race, sex et native-country. Relancez l’apprentissage après avoir supprimé, dans les données d’apprentissage et de test, les colonnes correspondant aux variables interdites. Evaluez les nouveaux modèles obtenus et comparez-les aux modèles précédents. Que constatez-vous ? Indication : pour éliminer par ex. les colonnes age et race on peut utiliser .drop : train_data_ECOA = train_data.drop(columns=['age', 'race']). La question de l’équité dans la prise de décision basée sur le traitement automatique de données est abordée de façon un peu plus détaillée dans cette séance de cet autre cours du Cnam.

Correction :

train_data_ECOA = train_data.drop(columns=['age', 'marital-status', 'race', 'sex', 'native-country'])
test_data_ECOA = test_data.drop(columns=['age', 'marital-status', 'race', 'sex', 'native-country'])
train_data_ECOA.head()  # pour vérifier si les variables interdites ont bien été éliminées

Nouvel apprentissage et nouvelle évaluation :

save_path = 'agModelsECOA-predictClass'  # specifies folder to store trained models
predictor_ECOA = TabularPredictor(label=label, path=save_path).fit(train_data_ECOA)
y_pred_ECOA = predictor_ECOA.predict(test_data_ECOA)
print("Prédictions :  \n", y_pred_ECOA)
performance_ECOA = predictor_ECOA.evaluate_predictions(y_true=y_test, y_pred=y_pred_ECOA, auxiliary_metrics=True)

Dans cet exemple précis on constate que la performance (par ex. accuracy) du meilleur modèle diminue peu par rapport à la performance du modèle qui emploie toutes les variables, y compris les variables interdites (0.824 par rapport à 0.837). Cela n’est pas toujours le cas. Mais exclure les variables interdites ne garantit pas le respect de l’équité : on évite simplement l’utilisation intentionnelle d’une variable interdite (ce qui serait considéré comme disparate treatment aux Etats-Unis). Des variables « anodines » peuvent se substituer à une variable interdite si elles sont suffisamment corrélées à celle-ci, avec pour conséquence une inéquité de traitement (aux Etats-Unis on parle de disparate impact).

Utilisation d’AutoGluon sur South German Credit Dataset

Pour voir comment résoudre quelques difficultés qui peuvent apparaître lors du traitement d’autres données tabulaires, considérons les données de South German Credit (update) issues également de la base de l’UCI mais téléchargées directement depuis cette base. Les données brutes sont dans le fichier https://archive.ics.uci.edu/ml/machine-learning-databases/00573/SouthGermanCredit.zip ; le format utilise comme séparateur l’espace (et non la virgule) et les données d’apprentissage ne sont pas séparées de celles de test.

Nous nous servirons donc de la bibliothèque Python pandas pour lire ces données dans un DataFrame et de numpy pour les séparer (avec un tirage aléatoire) en données d’apprentissage et données de test, sachant que les deux ensembles doivent avoir une intersection vide et suivre une même distribution.

Il est nécessaire de télécharger d’abord le fichier https://archive.ics.uci.edu/ml/machine-learning-databases/00573/SouthGermanCredit.zip, d’en extraire le fichier SouthGermanCredit.asc et de le stocker dans un répertoire accessible. A partir du cahier Jupyter, cela peut être fait par les instructions suivantes :

import os
os.system("mkdir -p tpautoml/data")
os.system("wget -nc https://archive.ics.uci.edu/ml/machine-learning-databases/00573/SouthGermanCredit.zip -P tpautoml/data/")
os.system("unzip tpautoml/data/SouthGermanCredit.zip SouthGermanCredit.asc -d tpautoml/data/.")

L’emploi ultérieur du chemin absolu vers ce fichier est préférable, mais avec le chemin relatif il n’y a pas de difficultés en général. Nous obtenons d’abord un DataFrame pandas en précisant que le séparateur est l’espace :

import numpy as np
import pandas as pd
dataset = pd.read_table('tpautoml/data/SouthGermanCredit.asc', sep=' ')
dataset.head()

Les noms des variables en anglais et leurs types peuvent être trouvés sur cette page.

Un échantillon aléatoire de 20% des lignes (observations ou exemples) de ce DataFrame devient l’ensemble de test, le reste des données forme l’ensemble d’apprentissage. Nous avons utilisé pour ce dernier le nom trainval car cet ensemble est utilisé par AutoGluon pour extraire ensuite à la fois des données d’apprentissage et des données de validation.

np.random.seed(10)  # permet de reproduire exactement l'expérience
test_size = 0.2
test_mask = np.random.rand(len(dataset)) < test_size
test = dataset[test_mask]
trainval = dataset[~test_mask]
print('trainval #:', len(trainval), 'test #:', len(test))

from autogluon.tabular import TabularDataset, TabularPredictor
trainval_data = TabularDataset(trainval)
test_data = TabularDataset(test)

Problème de classification

La colonne à prédire est 'kredit', correspondant à la variable nominale qui indique si le contrat de prêt a été respécté (prêt remboursé sans problèmes) ou non.

label = 'kredit'
print("Résumé de la variable de classe : \n", trainval_data[label].describe())

Nous pouvons ensuite lancer l’apprentissage. Par défaut, .fit() est appelée avec la valeur 'medium_quality_faster_train' pour le paramètre presets. Il est possible de demander à AutoGluon d’obtenir les meilleures performances avec la valeur 'best_quality' (dans ce cas il est utile de préciser une valeur plus élevée pour le budget de temps time_limit) ou 'good_quality_faster_inference_only_refit' si c’est le coût de prédiction qui est privilégié, etc.

save_path = 'tpautoml/agModels-predictClassSouthGermanCredit'  # répertoire pour stocker les modèles
predictor = TabularPredictor(label=label, path=save_path).fit(trainval_data, time_limit=200, presets='best_quality')

Après la fin de l’apprentissage (plus long que le précédent) il est possible d’évaluer sur les données de test le meilleur modèle obtenu :

y_test = test_data[label]
test_data_nolabel = test_data.drop(columns=[label])
y_pred = predictor.predict(test_data_nolabel)
performance = predictor.evaluate_predictions(y_true=y_test, y_pred=y_pred, auxiliary_metrics=True)

Plusieurs métriques sont calculées (auxiliary_metrics=True), non seulement le taux de bon classement (accuracy).

Nous pouvons ensuite afficher le classement des différents modèles appris, en précisant éventuellement des métriques supplémentaires grâce au paramètre extra_metrics :

predictor.leaderboard(test_data, extra_metrics=['accuracy', 'balanced_accuracy'], silent=True)

Explications : estimation de l’importance des variables

Après apprentissage d’un modèle, il est possible d’évaluer l’importance de chaque variable explicative (feature) pour le modèle grâce à des permutations aléatoires. Plus précisément, sur un ensemble de données (les données de test en général) il est possible de permuter aléatoirement entre observations les valeurs d’une des variables explicatives et de faire les prédictions sur les observations ainsi modifiées ; si la variable est importante pour la prédiction, les performances de prédiction se dégradent nettement après la permutation, alors que si la variable a peu d’importance les performances changent très peu. Les variables peuvent ainsi être classées par ordre décroissant d’importance (ou de dégradation des prédictions en cas de changement des valeurs).

Il est très facile de réaliser cette évaluation mais elle peut prendre un certain temps s’il y a beaucoup de variables et d’observations :

predictor.feature_importance(test_data)

Il est également possible d’évaluer l’importance de chaque variable localement pour chaque prédiction en utilisant le calcul des valeurs de Shapley, voir les exemples de https://github.com/awslabs/autogluon/tree/master/examples/tabular/interpret.

Problème de régression

Pour illustrer l’utilisation de AutoGluon sur un problème régression (prédiction des valeurs d’une variable quantitative) nous pouvons conserver les données de South German Credit (update). En effet, bien que l’objectif initial est de prédire si un crédit est remboursé sans incidents ou avec des incidents, comme dans ces données il y a des variables quantitatives il est envisageable de chercher à prédire leur valeurs à partir des valeurs des autres variables (incluant la variable kredit). Bien entendu, on ne peut pas espérer une aussi bonne qualité de prédiction car les variables explicatives n’ont pas été identifiées au départ avec cet objectif.

Pour changer de variable à prédire il suffit de préciser le nom de sa colonne dans le TabularDataset. Nous choisissons de chercher à prédire les valeurs de la variable « durée du prêt » (laufzeit, durée en nombre de mois). Nous pouvons aussi avoir des informations sur la distribution de ses valeurs :

duration_column='laufzeit'
print("Résumé de la varable de durée : \n", trainval_data[duration_column].describe())

L’apprentissage peut être lancé facilement, les données d’apprentissage et de test restent ici les mêmes (seule change la variable cible à prédire). Nous pouvons lancer donc un premier apprentissage assez court avec les paramètres par défaut :

predictor_duration = TabularPredictor(label=duration_column, path="tpautoml/agModels-predictDuration").fit(trainval_data)

et ensuite voir si AutoGluon a bien inféré le type de problème (classification binaire, classification multiclasse ou régression) et le type des différentes variables explicatives :

print("AutoGluon infère que le problème est de type: ", predictor_duration.problem_type)
print("AutoGluon a identifié les types suivants de variables:")
print(predictor_duration.feature_metadata)

On constate que les types des variables explicatives peuvent convenir, en revanche le type de problème n’est pas tout à fait le bon : AutoGluon considère qu’il s’agit d’un problème de classification multi-classe (multiclass), la variable cible étant discrète, pourtant avec un nombre assez élevé de valeurs différentes ! Il vaut donc mieux préciser le type de problème avec problem_type='regression' et relancer l’apprentissage avec une limite de temps plus longue et une option presets='best_quality' :

predictor_duration = TabularPredictor(label=duration_column, path="tpautoml/agModels-predictDuration", problem_type='regression').fit(trainval_data, time_limit=200, presets='best_quality')

Après apprentissage il est possible d’évaluer le meilleur modèle obtenu sur les données de test :

performance = predictor_duration.evaluate(test_data)

Il est également possible de classer les modèles obtenus par ordre décroissant de leurs performances sur les données de test. Comme l’erreur quadratique moyenne est une métrique plus difficile à apprécier par un humain (qui dépend, entre autres, de la plage de variation des valeurs de la variable à prédire), nous avons également demandé comme métriques supplémentaires le coefficient de détermination (r2, rapport entre la variance expliquée et la variance totale, entre 0 et 1) et le coefficient de corrélation linéaire (pearsonr, entre -1 et 1), le premier étant égal au carré du second. Plus la valeur de r2 (ou R2) est proche de 1, meilleur est le modèle.

predictor_duration.leaderboard(test_data, extra_metrics=['r2', 'pearsonr'], silent=True)

Question :

En examinant les variables employées sur cette page nous pouvons constater qu’une utilisation de la prédiction de kredit obtenue à partir de toutes ces variables pour décider directement d’accorder ou non un crédit ne respecterait ni la réglementation américaine ECOA, ni la Charte des droits fondamentaux de l’UE (18/12/2000, chap. III, art. 21) car parmi les variables on trouve famges, qui est un mélange entre personal_status et sex, ainsi que alter qui correspond à l’âge. Essayez de prédire une de ces variables à partir des autres, après avoir supprimé la colonne kredit. Que constatez-vous ?

Correction :

Pour famges :

trainval_data_PVI = trainval_data.drop(columns=['kredit'])
test_data_PVI = test_data.drop(columns=['kredit'])
label = 'famges'
print("Résumé de la variable de classe : \n", trainval_data_PVI[label].describe())
predictor_PVI = TabularPredictor(label=label, path=save_path).fit(trainval_data_PVI, time_limit=200, presets='best_quality')
y_test_PVI = test_data_PVI[label]
test_data_PVI_nolabel = test_data_PVI.drop(columns=[label])
y_pred_PVI = predictor_PVI.predict(test_data_PVI_nolabel)
performance_PVI = predictor_PVI.evaluate_predictions(y_true=y_test_PVI, y_pred=y_pred_PVI, auxiliary_metrics=True)

Une accuracy de 0.6 n’est certes pas très bonne mais n’est pas non plus si mauvaise, sachant que nous avons 4 classes pour la variable famges. Cela veut dire que même en supprimant cette variable des variables d’entrée, une partie de l’information qu’elle contient peut être retrouvée dans les autres variables « anodines ».

Pour alter :

trainval_data_PVI = trainval_data.drop(columns=['kredit'])
test_data_PVI = test_data.drop(columns=['kredit'])
label = 'alter'
print("Résumé de la variable à prédire : \n", trainval_data_PVI[label].describe())
predictor_PVI = TabularPredictor(label=label, path=save_path, problem_type='regression') \
                            .fit(trainval_data_PVI, time_limit=200, presets='best_quality')
y_test_PVI = test_data_PVI[label]
test_data_PVI_nolabel = test_data_PVI.drop(columns=[label])
y_pred_PVI = predictor_PVI.predict(test_data_PVI_nolabel)
performance_PVI = predictor_PVI.evaluate_predictions(y_true=y_test_PVI, y_pred=y_pred_PVI, auxiliary_metrics=True)

Comme l’indique le coefficient de détermination r2 qui est de l’ordre de 0.35, la prédiction de l’âge à partir des autres variables n’est pas très bonne (r2 peut être entre 0 et 1, une valeur de 1 indiquant des prédictions parfaites).