Introduction¶
Motivations de l’apprentissage automatique¶
Avant de rentrer dans le vive du sujet et découvrir les différents modèles
d’apprentissage automatique disponibles dans scikit-learn, il advient de
motiver l’usage de telles techniques.
Nous pouvons d’ores et déjà définir ce que nous appelerons un modèle prédictif. Un modèle prédictif correspond à une fonction prenant des données en entrée est capable de renvoyé une prédiction en sortie. Mathématiquement, nous pouvons exprimer un tel modèle comme suit :
ou \(X\) seront nos données d’entrée, \(f()\) est notre modèle prédictif et \(\hat{y}\) seront nos prédictions.
Nous pouvons donner un exemple concret dès maintenant. Nous allons charger un jeu de données minimaliste.
import pandas as pd
donnees = pd.read_csv("../datasets/penguins_regression.csv")
donnees.head()
| Longueur Aileron (mm) | Masse Corporelle (g) | |
|---|---|---|
| 0 | 181.0 | 3750.0 |
| 1 | 186.0 | 3800.0 |
| 2 | 195.0 | 3250.0 |
| 3 | 193.0 | 3450.0 |
| 4 | 190.0 | 3650.0 |
Nous venons de charger en mémoire des données contenant des informations de pingouins. Ce jeu de données contient deux informations : pour chaque pingouin, nous connaissons la longeur de son aileron ainsi que sa masse corporelle.
Nous pouvons formuler le problème de prédiction suivant : à partir de ces données, nous souhaitons apprendre une fonction \(f()\) qui prend en entrée la longueur d’aileron d’un pingouin et qui renvoie sa masse corporelle.
Nous pouvons dès à présent visualiser ces données.
import seaborn as sns
sns.set_context("poster")
ax = donnees.plot.scatter(x=donnees.columns[0], y=donnees.columns[1])
_ = ax.set_title("Peut-on prédire la masse corporelle \nd'un pingouin?")
En oberservant nos données, nous pouvons constater que nous avons un jeu de données contenant à la fois l’entrée et la sortie de notre fonction \(f()\). Typiquement, ceci est le cas lorsque nous sommes dans un contexte d’apprentissage supervisé.
L’idée sera de trouver la fonction \(f()\) optimale à partir de ces données d’entrée et sortie. Par la suite, nous utiliserons cette fonction pour prédire la sortie à partir d’une nouvelle donnée d’entrée.
Dans scikit-learn, les données d’entrée et de sortie sont séparées en deux
tableaux distincts, dénotés X et y dans la documentation.
X = donnees[["Longueur Aileron (mm)"]]
y = donnees["Masse Corporelle (g)"]
Nous allons nous attarder sur la structure de ces données.
X.head()
| Longueur Aileron (mm) | |
|---|---|
| 0 | 181.0 |
| 1 | 186.0 |
| 2 | 195.0 |
| 3 | 193.0 |
| 4 | 190.0 |
X est représenté par une matrice de données en deux dimensions. Chaque
ligne correspond à un échantillon (i.e. un pingouin) et chaque colonne
correspond à une variable (i.e. une mesure physique). Dans notre jeu de
données minimaliste, nous avons une seule variable d’entrée, Longueur Aileron (mm).
y.head()
0 3750.0
1 3800.0
2 3250.0
3 3450.0
4 3650.0
Name: Masse Corporelle (g), dtype: float64
y est représenté par un vecteur de données (i.e. une seul dimension).
Nous avons autant d’éléments que de lignes dans X. Cette variable est
communemment appellée variable cible.
Il important de noter que y est une variable continue : elle peut prendre
n’importe quel valeur entre moins l’infini et plus l’infini ou quasiment
(nous somme limités par la physique puisque le masse corporelle d’un pingouin
ne peut-être négative et trop grande). Lorsque y est une variable
continue nous appelons ce problème de prédiction, un problème de
regression.
Si y est une variable discrète, comme par exemple l’espèce de pinguoin,
nous appelons ce problème, un problème de classification.
Donc à partir de ces données, nous aimerions apprendre une fonction de
prédiction f(). La définition de telles fonctions sont le résultat de
recherche fondamentale dans le domaine de l’apprentissage automatique. Nous
allons présenter ces différentes fonctions dans la suite de ce livre.
Mais afin d’illustrer le principe d’une fonction de prédiction, nous pourrions venir avec notre propre fonction de prédiction, basée sur une intuition. En regardant le graphique représentant notre variable d’entrée et notre variable de sortie, nous pourrions calculer une relation moyenne entre la longueur d’aileron et la masse corporelle.
import numpy as np
relation_X_y = np.mean(X["Longueur Aileron (mm)"] / y)
relation_X_y
0.04898871471403136
Maintenant nous pouvons utiliser cette relation pour prédire la masse : nous sommes entrain de d’utiliser le fameux “produit en croix”. Donc, essayons de prédire la masse du premier pingouin dans notre dataset.
masse_premier_pinguoin = X.iloc[0, 0] / relation_X_y
masse_premier_pinguoin
3694.728491175498
Nous pouvons même estimer la différence avec sa masse réelle.
print(
f"Masse réelle: {y[0]} grammes\n"
f"Masse prédite: {masse_premier_pinguoin:.2f} grammes\n"
f"Différence: {y[0] - masse_premier_pinguoin:.2f} grammes"
)
Masse réelle: 3750.0 grammes
Masse prédite: 3694.73 grammes
Différence: 55.27 grammes
Pour pousser notre exemple plus proche de notre définition mathématique, nous pouvons utiliser une fonction Python pour apprendre la relation et une autre fonction Python pour prédire la masse.
def apprendre_relation(X, y):
"""Apprendre une relation moyenne entre la longueur d'aileron et la masse
corporelle des pinguoins."""
return np.mean(X["Longueur Aileron (mm)"] / y)
def f(X, relation):
"""Function de predisant la masse corporelle d'un pingouin à partir de
la longueur de son aileron."""
return (X["Longueur Aileron (mm)"] / relation).rename("Masse Corporelle (g)")
relation = apprendre_relation(X, y)
predictions = f(X, relation)
predictions
0 3694.728491
1 3796.792814
2 3980.508595
3 3939.682866
4 3878.444273
...
337 4225.462971
338 4123.398648
339 3939.682866
340 4286.701564
341 4041.747189
Name: Masse Corporelle (g), Length: 342, dtype: float64
Nous pouvons même calculer l’erreur (absolue) de nos prédictions.
erreur_absolue_moyenne = np.mean(np.abs(predictions - y))
print(f"Erreur absolue moyenne: {erreur_absolue_moyenne:.2f} grammes")
Erreur absolue moyenne: 467.40 grammes
Notre stratégie d’apprentissage nous permet d’obtenir un modèle prédictif qui commet en moyenne une erreur de 467 grammes sur le même jeu de données. Nous pouvons également représenter graphiquement la relation que nous avons appris.
ax = donnees.plot.scatter(x=donnees.columns[0], y=donnees.columns[1])
ax.plot(
X["Longueur Aileron (mm)"],
predictions,
color="black",
linewidth=4,
label="Notre fonction",
)
ax.legend()
_ = ax.set_title("Notre premier modèle prédictif")
Il est a noté que nous ne devrions pas apprendre et evaluer notre fonction de prédiction sur les mêmes données. Le score obtenu est potentiellement trop optimiste. Nous allons revenir sur ce point à la fin de cette section où nous présenterons les outils et la manière d’évaluer correctement un modèle prédictif.
Introduction à l’interface de programmation de scikit-learn¶
Dans cette section, nous allons présenter succintement l’interface de
programmation de scikit-learn. Nous utiliserons cette interface dans les
sections et chapitres suivants lorsque nous présenterons les différents types
de modèles prédictifs.
Afin de présenter cette interface, nous allons créer nous même un modèle
prédictif qui pourrait être utilisé en conjonction avec tous les outils de
scikit-learn. Nous allons également argumenté les choix d’interface
réalisés par les développeurs de scikit-learn historiquement.
Dans la précédente section, nous avons vu constaté que l’apprentissage automatique était consititué de deux étapes : une étape d’apprentissage pour obtenir la fonction de prédiction optimale et une étape de prédiction ou nous avons réutiliser la relation appris pour prédire la masse corporelle.
Il advient que nous pourrions utiliser une classe Python pour stocker à l’intérieur de cette instance d’objet les informations utilisées pour la prédiction. Nous aurions seulement besoin donc d’exposer une méthode pour effectuer l’apprentissage et une méthode pour prédire.
En résumé, nous pouvons créer la classe suivante :
from sklearn.base import BaseEstimator
class ModelePredictif(BaseEstimator):
def fit(self, X, y):
self.coef_ = np.mean(X["Longueur Aileron (mm)"] / y)
return self
def predict(self, X):
return (X["Longueur Aileron (mm)"] / relation).rename("Masse Corporelle (g)")
Dans scikit-learn, un modèle prédictif (également appelé estimateur)
possède deux méthodes : fit et predict. La première est en charge
d’apprendre les éléments nécessaires à la prédiction. De plus cette méthode
retourne self pour permettre de chainer les appels. La deuxième méthode est
en charge de prédire. Nous pouvons mettre en place notre class de la façon
suivante :
modele = ModelePredictif()
modele.fit(X, y)
predictions = modele.predict(X)
erreur_absolue_moyenne = np.mean(np.abs(predictions - y))
print(f"Erreur absolue moyenne: {erreur_absolue_moyenne:.2f} grammes")
Erreur absolue moyenne: 467.40 grammes
En plus des méthodes fit et predict, scikit-learn expose une méthode
score qui permet de calculer le score de prédiction.
class ModelePredictif(BaseEstimator):
def fit(self, X, y):
self.coef_ = np.mean(X["Longueur Aileron (mm)"] / y)
return self
def predict(self, X):
return (X["Longueur Aileron (mm)"] / relation).rename("Masse Corporelle (g)")
def score(self, X, y):
predictions = self.predict(X)
return np.mean(np.abs(predictions - y))
Nous pourrions donc simplifier notre code précédent de la manière suivante :
print(f"Erreur absolue moyenne: {ModelePredictif().fit(X, y).score(X, y):.2f} grammes")
Erreur absolue moyenne: 467.40 grammes
Evaluation de notre modèle prédictif¶
Dans la section précédente, nous avons calculé l’erreur absolue moyenne sur le même jeu de données utilisé pendant l’apprentissage.
Cependant cette approche est réellement problématique. Nous aurions pu créer un modèle prédictif qui ne prend pas en compte les données d’apprentissage et qui mémorise tous les échantillons d’apprentissage. Nous aurions alors donc obtenu une erreur de prédiction de 0 grammes. En revance, en utilisant le même modèle sur un nouveau dataset pour seulement effectuer de la prédiction, nous aurions alors obtenu une erreur plus importante.
C’est pour cela qu’il est nécessaire d’évaluer un modèle prédictif sur des données qui ne sont pas dans le jeu d’apprentissage. L’erreur obtenu sur le jeu d’apprentissage est appelé l’erreur empirique alors que l’erreur obtenu sur le jeu de test est appelé l’erreur de généralisation. En apprentissage automatique, les méthodes essayent en général de minimiser l’erreur empirique en espérant que l’erreur de généralisation soit minimale également.
Nous pouvons utiliser notre jeu de données original et le séparer en deux
jeux de données afin de calculer les deux types d’erreurs. scikit-learn
fournit une fonction train_test_split qui permet de séparer un jeu de
données.
from sklearn.model_selection import train_test_split
X_apprentissage, X_test, y_apprentissage, y_test = train_test_split(
X,
y,
random_state=0,
test_size=0.2,
shuffle=True,
)
La sélection de ces jeux de données est faite de manière aléatoire. Le
paramètre random_state permet d’obtenir une séparation déterministique même
si un procédure aléatoire est utilisée. De plus, le paramètre test_size
permet de définir la proportion de données qui seront utilisées pour le jeu
de test.
modele = ModelePredictif().fit(X_apprentissage, y_apprentissage)
print(f"Erreur empirique: {modele.score(X_apprentissage, y_apprentissage):.2f} grammes")
print(f"Erreur de généralisation: {modele.score(X_test, y_test):.2f} grammes")
Erreur empirique: 473.92 grammes
Erreur de généralisation: 441.63 grammes
Nous observons que notre modèle performe moins bien sur le jeu de test que sur le jeu d’apprentissage.
La comparaison des erreurs empérique et de généralisation permette de connaître de savoir si un modèle sous-apprend, généralise ou sur-apprend. Le sur-apprentissage est caractérisé par une erreur de géralisation plus élevée que l’erreur empirique. La plage de générasation est définie par lorsque la différence entre l’erreur de généralisation et l’erreur empirique est assez minimale. Un modèle sous-apprentissage est caractérisé par une erreur de généralisation élevée mais également une erreur empirique élevée.
Bien que nous ayons maintenant une idée concernant la capacité de notre modèle à prédire sur un set indépendant de celui d’apprentissage, nous ne pouvons pas réellement savoir si la différence entre les erreurs empirique et de généralisation est suffisante pour déterminer si un modèle est en sous-apprentissage ou en sur-apprentissage ou généralise. En effet, puisque la procédure de séparation des jeux de données est aléatoire, nous devrions réaliser plusieurs essais pour obtenir une distribution des erreurs et evaluer si les différences sont suffisantes.
Ces essais répétés sont dénommés validation croisée. scikit-learn fournit
une fonction cross_validate permettant de réaliser ces essais. Il existe
plusieurs méthodes de validation croisée. Nous allons utiliser la méthode
k-fold qui permet de séparer un jeu de données en \(k\) sous-ensembles. A
chaque itération de la validation croisée, un des \(k\) sous-ensembles est
utilisé pour évaluer le modèle alors que les autres sous-ensembles sont
utilisés pour l’apprentissage.
from sklearn.model_selection import cross_validate
from sklearn.model_selection import KFold
cv = KFold(n_splits=5, shuffle=True, random_state=0)
cv_resultats = cross_validate(
modele, X, y, cv=cv, scoring="neg_mean_absolute_error", return_train_score=True
)
cv_resultats = pd.DataFrame(cv_resultats)
cv_resultats[["train_error", "test_error"]] = -cv_resultats[
["train_score", "test_score"]
]
cv_resultats[["train_error", "test_error"]]
| train_error | test_error | |
|---|---|---|
| 0 | 473.915845 | 441.630137 |
| 1 | 467.033573 | 468.859994 |
| 2 | 449.924529 | 537.826237 |
| 3 | 473.929172 | 441.101644 |
| 4 | 472.229617 | 447.949854 |
Validation croisée nous permet donc d’obtenir plusieurs estimations de nos erreurs. Nous pouvons même répéter l’opération plusieurs fois pour obtenir des distributions d’erreurs que nous pourrons visualiser.
from sklearn.model_selection import RepeatedKFold
cv = RepeatedKFold(n_splits=5, n_repeats=30, random_state=0)
cv_resultats = cross_validate(
modele, X, y, cv=cv, scoring="neg_mean_absolute_error", return_train_score=True
)
cv_resultats = pd.DataFrame(cv_resultats)
cv_resultats[["train_error", "test_error"]] = -cv_resultats[
["train_score", "test_score"]
]
ax = cv_resultats[["train_error", "test_error"]].plot.hist(bins=20, alpha=0.5)
ax.set_xlabel("Erreur moyenne absolue (grammes)")
_ = ax.set_title(
f"Distribution des erreurs\nStd. dev. variable cible: {np.std(y):.2f} (g)"
)
Il est toujours bon de mettre en perspective l’erreur moyenne obtenue avec la variable cible. En effet, notre modèle commet une erreur de 450 grammes sur notre données alors que l’écart-type de la variable cible est de 800 grammes. Notre modèle n’est donc pas un très bon modèle.
Quand on compare les distributions, nous regardons si ces distributions se chevauchent. Si les distributions se chevauchent et que leur moyennes et leur écart-types sont proches, alors le modèle est capable de généraliser. Une différence entre les distributions nous permet de conclure sur le sous- ou sur-apprentissage. En revanche, quand les écart-types sont trop larges, ceci est souvent lié à un manque de données et il sera difficile de tirer des conclusions.