Machine Learning
Comment une machine ajuste une règle à partir d'exemples.
Machine Learning : les bases
Un modèle qui obtient 99 % de bonnes réponses peut être totalement inutile. Un modèle qui reconnaît parfaitement tous les exemples sur lesquels il a été construit peut être le pire de la sélection. Ces deux affirmations paraissent absurdes, elles sont pourtant le quotidien de l’apprentissage automatique, et les ignorer est la façon la plus courante de livrer un système qui échoue dès sa mise en service.
La raison est que l’apprentissage automatique ne cherche pas à mémoriser des exemples : il cherche à généraliser à des cas jamais vus. Or on ne peut pas mesurer directement cette capacité — par définition, les cas futurs ne sont pas disponibles. Tout l’art consiste donc à organiser une mesure indirecte honnête, puis à choisir le bon indicateur pour la lire.
Cette leçon construit ces garde-fous dans l’ordre où on en a besoin. Nous verrons d’abord ce que signifie concrètement « apprendre » pour une machine, ce qui se ramène à ajuster des nombres. Puis les trois grandes familles de problèmes, selon ce que l’on possède comme information. Ensuite le protocole qui permet de mesurer la généralisation, et le phénomène qu’il révèle : le surapprentissage. Enfin le choix des métriques, là où se jouent la plupart des malentendus entre équipes techniques et métier.
Tous les exemples utilisent scikit-learn et des jeux de données livrés avec la bibliothèque : ils sont exécutables tels quels, sans téléchargement.
Apprendre, c’est ajuster des paramètres
Un modèle est une fonction paramétrée. Apprendre, c’est chercher les valeurs de paramètres qui minimisent l’écart entre les prédictions et les réponses connues. Rien de plus mystérieux que cela.
import numpy as np
from sklearn.linear_model import LinearRegression
# Cinq appartements : surface en m², loyer en euros.
surface = np.array([[30], [45], [60], [75], [90]])
loyer = np.array([620, 780, 1010, 1150, 1400])
modele = LinearRegression().fit(surface, loyer)
print(round(float(modele.coef_[0]), 2)) # 12.87
print(round(float(modele.intercept_), 2)) # 220.0
print(modele.predict([[50]]).round(1)) # [863.3]
Le modèle a trouvé deux nombres. La règle apprise se lit en français : environ 12,87 € par mètre carré, plus 220 € de charge fixe. Elle n’a pas été écrite, elle a été déduite de cinq observations en minimisant la somme des carrés des écarts.
Ce qui compte ici est la nature de la prédiction pour 50 m² : 863,3 €. Aucun appartement de 50 m² ne figure dans les données. Le modèle a interpolé entre le 45 m² et le 60 m². C’est légitime. En revanche, lui demander le loyer d’un 400 m² serait une extrapolation très au-delà du domaine observé : la formule répondrait sans broncher, et il n’y aurait aucune raison de la croire.
Trois vocabulaires à fixer, car ils reviennent partout :
| Terme | Signification | Dans l’exemple |
|---|---|---|
| Variable explicative (feature) | ce que l’on donne en entrée | la surface |
| Cible (target, label) | ce que l’on veut prédire | le loyer |
| Paramètres | les nombres ajustés par l’apprentissage | 12,87 et 220 |
| Hyperparamètres | les réglages fixés par l’humain avant l’apprentissage | ici aucun ; ailleurs la profondeur d’un arbre |
Les trois familles d’apprentissage
La famille dépend de l’information dont on dispose, pas de l’algorithme.
Apprentissage supervisé
On possède des exemples étiquetés : pour chaque entrée, la bonne réponse. C’est le cas de la régression ci-dessus, et de toute classification. C’est de loin le cadre le plus utilisé en production, parce que c’est celui qui se mesure le mieux — et le plus coûteux, parce qu’il faut produire les étiquettes.
Apprentissage non supervisé
On ne possède que les entrées. L’algorithme cherche une structure : des groupes, des axes de variation, des anomalies. L’évaluation devient délicate, faute de vérité de référence.
import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import load_iris
iris = load_iris()
# On n'utilise PAS iris.target : l'algorithme ne voit aucune étiquette.
groupes = KMeans(n_clusters=3, n_init=10, random_state=0).fit_predict(iris.data)
print(np.bincount(groupes)) # [62 50 38]
# Comparaison a posteriori avec les vraies espèces, pour juger du résultat.
import collections
print(collections.Counter(zip(groupes.tolist(), iris.target.tolist())))
Le décompte final montre exactement ce qu’il faut comprendre du non supervisé. Un groupe contient les 50 setosa et rien d’autre : cette espèce est nettement séparée des deux autres. Les deux groupes restants mélangent versicolor et virginica : 14 virginica se retrouvent avec les versicolor, et 2 versicolor avec les virginica. Seize fleurs sur cent cinquante sont mal regroupées, soit un peu plus de 10 %.
L’algorithme n’a pas échoué : il a trouvé les trois groupes les plus compacts au sens de la distance euclidienne. Ces groupes ne coïncident pas parfaitement avec les espèces, parce que rien ne l’y obligeait. C’est la limite intrinsèque du non supervisé : il révèle une structure, pas la structure que vous aviez en tête.
Apprentissage par renforcement
Ni entrées étiquetées, ni simple observation : un agent agit, reçoit une récompense chiffrée, et doit découvrir la stratégie qui maximise le cumul de ces récompenses. La difficulté propre à ce cadre est l’arbitrage entre exploiter ce qui semble bon et explorer ce qui n’a pas été assez essayé.
import numpy as np
# Trois leviers, chacun avec une probabilité de gain inconnue de l'agent.
rng = np.random.default_rng(0)
vraies = np.array([0.2, 0.5, 0.75])
estimations = np.zeros(3)
tirages = np.zeros(3)
for t in range(2000):
if rng.random() < 0.1: # 10 % du temps : on explore
bras = rng.integers(3)
else: # sinon : on exploite le meilleur connu
bras = int(np.argmax(estimations))
recompense = 1.0 if rng.random() < vraies[bras] else 0.0
tirages[bras] += 1
# Moyenne mise à jour de façon incrémentale.
estimations[bras] += (recompense - estimations[bras]) / tirages[bras]
print(estimations.round(2)) # [0.19 0.46 0.75]
print(tirages.astype(int)) # [ 81 72 1847]
Les estimations finales approchent les vraies probabilités, et surtout la répartition des tirages raconte l’apprentissage : 1847 essais sur 2000 ont été consacrés au meilleur levier, contre environ 75 pour chacun des deux autres. L’agent n’a pas seulement estimé, il a concentré son budget. Le prix payé s’appelle le regret : les 153 essais sur les leviers médiocres étaient nécessaires pour être sûr qu’ils étaient médiocres.
Entraînement et test : pourquoi on cache des données
Évaluer un modèle sur les exemples qui ont servi à l’ajuster mesure sa mémoire, pas sa capacité à généraliser. La règle est donc de mettre de côté une partie des données avant tout apprentissage, et de ne la consulter qu’à la fin.
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
donnees = load_breast_cancer() # 569 tumeurs, 30 mesures, 2 classes
X_train, X_test, y_train, y_test = train_test_split(
donnees.data, donnees.target, test_size=0.3, random_state=42
)
print(X_train.shape, X_test.shape) # (398, 30) (171, 30)
for profondeur in (1, 3, None):
arbre = DecisionTreeClassifier(max_depth=profondeur, random_state=0)
arbre.fit(X_train, y_train)
print(profondeur,
round(arbre.score(X_train, y_train), 3),
round(arbre.score(X_test, y_test), 3))
Le paramètre random_state fixe le tirage : sans lui, deux exécutions donneraient deux découpages différents et des chiffres non comparables.
Le surapprentissage, mesuré
Le tableau produit par la boucle précédente est le résultat le plus instructif de cette leçon :
| Profondeur maximale | Exactitude sur l’entraînement | Exactitude sur le test | Écart |
|---|---|---|---|
| 1 | 0,925 | 0,895 | 0,030 |
| 3 | 0,970 | 0,965 | 0,005 |
| illimitée | 1,000 | 0,924 | 0,076 |
L’arbre de profondeur illimitée classe parfaitement les 398 exemples d’entraînement. Il est pourtant moins bon en test que l’arbre de profondeur 3. Il a construit des branches qui décrivent des particularités de ces 398 patients précis — du bruit, pas une régularité. C’est le surapprentissage (overfitting).
À l’autre extrémité, l’arbre de profondeur 1 ne pose qu’une seule question : il est trop rigide pour capter ce qui est réellement présent. C’est le sous-apprentissage (underfitting). Le bon réglage est entre les deux, et il se trouve par mesure, pas par intuition.
erreur
│
│╲ ╱ ← erreur de test
│ ╲ ╱
│ ╲ ╱
│ ╲__________________________╱ ← zone utile
│ ╲
│ ╲___________________ ← erreur d'entraînement
└──────────────────────────────────────► complexité du modèle
sous-apprentissage surapprentissage
Un écart de 0,076 entre entraînement et test est le signal d’alarme à surveiller. Mais un unique découpage reste un tirage au sort : mesurer sur 171 exemples laisse une marge d’incertitude appréciable. La validation croisée répète l’opération sur plusieurs découpages :
from sklearn.model_selection import cross_val_score
scores = cross_val_score(DecisionTreeClassifier(max_depth=3, random_state=0),
donnees.data, donnees.target, cv=5)
print(scores.round(3)) # [0.912 0.895 0.939 0.947 0.894]
print(scores.mean().round(3)) # 0.917
print(scores.std().round(3)) # 0.022
La moyenne (0,917) est une estimation plus sobre que le 0,965 obtenu sur un découpage favorable, et l’écart-type (0,022) chiffre la marge. Annoncer « 96,5 % » sur la base d’un seul découpage n’est pas un mensonge, mais c’est une mesure trop fragile pour engager une décision.
Choisir la métrique
Voici le piège annoncé en ouverture. Sur une maladie qui touche 1 % de la population, un modèle qui répond « en bonne santé » à tout le monde obtient 99 % d’exactitude.
import numpy as np
from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score
# 1000 patients, 10 malades.
y_vrai = np.array([0] * 990 + [1] * 10)
# Modèle paresseux : « personne n'est malade ».
y_paresseux = np.zeros(1000, dtype=int)
print((y_vrai == y_paresseux).mean()) # 0.99
print(recall_score(y_vrai, y_paresseux, zero_division=0)) # 0.0
99 % d’exactitude, 0 % de rappel : le modèle ne détecte aucun malade. Sur un dépistage, c’est un système sans aucune valeur. L’exactitude est trompeuse dès que les classes sont déséquilibrées, ce qui est le cas de presque tous les problèmes intéressants — fraude, panne, maladie rare, défaut de fabrication.
Comparons avec un modèle imparfait mais utile :
y_predit = np.zeros(1000, dtype=int)
y_predit[:20] = 1 # 20 personnes saines signalées à tort
y_predit[990:998] = 1 # 8 malades sur 10 correctement détectés
print(confusion_matrix(y_vrai, y_predit))
# [[970 20]
# [ 2 8]]
print(round(precision_score(y_vrai, y_predit), 3)) # 0.286
print(round(recall_score(y_vrai, y_predit), 3)) # 0.8
print(round(f1_score(y_vrai, y_predit), 3)) # 0.421
La matrice de confusion se lit ainsi : 970 sains correctement classés, 20 fausses alertes, 2 malades manqués, 8 malades détectés.
| Métrique | Définition | Valeur | Question à laquelle elle répond |
|---|---|---|---|
| Exactitude | bonnes réponses / total | 0,978 | Le modèle se trompe-t-il souvent, toutes classes confondues ? |
| Précision | 8 / (8 + 20) | 0,286 | Quand il signale, a-t-il raison ? |
| Rappel | 8 / (8 + 2) | 0,800 | Combien de vrais cas trouve-t-il ? |
| F1 | moyenne harmonique | 0,421 | Compromis unique entre les deux précédentes |
Ce modèle a une exactitude inférieure à celle du modèle paresseux (0,978 contre 0,990) et il est incomparablement plus utile : il trouve 8 malades sur 10. Le prix est 20 fausses alertes.
Ce compromis n’a pas de solution universelle. Il se tranche en dehors des mathématiques :
- Dépistage d’une maladie grave et traitable : privilégier le rappel. Une fausse alerte coûte un examen de contrôle, un cas manqué peut coûter une vie.
- Blocage automatique d’un compte bancaire : privilégier la précision. Chaque faux positif est un client légitime bloqué.
C’est pourquoi la métrique doit être choisie avec le métier avant l’entraînement. Optimiser d’abord et discuter ensuite conduit à refaire le travail.
À retenir
- Apprendre revient à ajuster des paramètres pour minimiser un écart mesuré sur des exemples. Ce qui n’est pas dans les données ne sera pas dans le modèle.
- Les trois familles se distinguent par l’information disponible : étiquettes (supervisé), entrées seules (non supervisé), récompenses (renforcement). Le non supervisé révèle une structure, pas nécessairement celle que vous cherchiez — 16 iris sur 150 mal regroupés le rappellent.
- Un score sur les données d’entraînement ne mesure rien d’utile. L’arbre à 1,000 en entraînement et 0,924 en test est moins bon que celui à 0,970 / 0,965 : c’est du surapprentissage.
- La validation croisée remplace un score fragile par une moyenne et un écart-type. C’est le minimum avant d’annoncer un chiffre à quelqu’un.
- L’exactitude est trompeuse sur des classes déséquilibrées : 99 % en ne détectant aucun malade. Précision et rappel s’arbitrent selon le coût réel d’une fausse alerte et celui d’un cas manqué, décision qui appartient au métier.
Commentaires
Les commentaires sont alimentés par GitHub Discussions
Connectez-vous avec GitHub pour participer à la discussion