Cours Python · Module 9: Machine learning
Supervised Learning - l'apprentissage supervisé
Dans cette leçon11
Dans la leçon précédente tu as entraîné ton premier modèle et tu l'as évalué en une seule ligne : model.score(X_test, y_test). Imagine qu'elle ait affiché 95 %. Cela sonne comme un résultat digne du journal de l'expédition - jusqu'au moment où tu comptes combien d'images du piège photo montrent réellement un braconnier. Si le braconnier apparaît sur cinq pour cent des clichés, alors un modèle qui ne détecte personne et répond "calme" à chaque image obtient lui aussi 95 % de bonnes réponses. Et il est parfaitement inutile, puisqu'il n'a attrapé aucun braconnier.
Voilà le vrai problème de l'apprentissage supervisé : non pas "comment entraîner un modèle", mais "comment savoir s'il sait quoi que ce soit". Cette leçon répond à deux questions. La première : quel genre de réponse a-t-on le droit d'exiger d'un modèle. La seconde : comment mesurer si ces réponses sont bonnes, et à quoi les comparer.
Le couple : mesure et réponse
Le supervised learning, l'apprentissage supervisé, tu l'as déjà survolé de haut. Le modèle reçoit des couples : un jeu de mesures et la bonne réponse qui va avec. Le mot "supervisé" ne désigne aucune surveillance exercée sur l'algorithme - il signifie que quelqu'un, un naturaliste avec son carnet, a étiqueté chaque spécimen à l'avance. Sans ces étiquettes il n'y a pas d'apprentissage supervisé, car le modèle n'a rien à quoi se comparer.
Deux noms se sont imposés et nous les garderons jusqu'à la fin du module. X, ce sont les caractéristiques (features en anglais), c'est-à-dire le tableau des mesures : une ligne par animal, une colonne par paramètre mesuré. y, c'est l'étiquette (label), la colonne des bonnes réponses - exactement autant de valeurs qu'il y a de lignes dans X. Le grand X et le petit y viennent d'une convention mathématique que scikit-learn a reprise, et tu la retrouveras dans tous les exemples du web.
Toute la différence autour de laquelle tourne cette leçon se cache dans la colonne y. Ci-dessous, les mêmes quatre animaux décrits par les mêmes deux mesures, mais avec deux réponses différentes : une fois le nom de l'espèce, une fois le poids prévu dans un an.
1# Memes mesures, deux reponses differentes
2X = [[190, 58], [55, 110], [175, 60], [48, 115]]
3
4y_species = ["lion", "cheetah", "lion", "cheetah"] # reponse : un nom
5y_weight_next_year = [195, 57, 178, 50] # reponse : un nombreRemarque que X n'a pas changé - c'est le même carnet de terrain dans les deux cas. Le nombre de lignes n'a pas bougé non plus, ni leur forme : une liste de listes, où chaque liste interne représente un animal. Seul le type du contenu de y a changé. Dans le premier cas la réponse est l'un des quelques noms connus d'avance, dans le second c'est un nombre quelconque. Ce seul détail décide de tout ce qui suit : du nom de la tâche, du modèle que tu choisiras et des métriques avec lesquelles tu le jugeras.
La classification - quand la réponse est un nom
Quand y contient des noms tirés d'une liste fermée, la tâche s'appelle une classification et les réponses possibles sont les classes. "Lion ou guépard", c'est de la classification. "Calme ou braconnier", c'est de la classification. "En danger, vulnérable ou non menacé" aussi, avec trois classes cette fois. Les modèles ne comprennent pas les chaînes de caractères, alors nous notons les classes avec des nombres : 0, 1, 2. Ce ne sont que des identifiants et non des quantités - la classe numéro 2 n'est pas "deux fois plus grande" que la classe numéro 1.
Le premier classifieur que nous utiliserons est la régression logistique, LogisticRegression dans le code. Le nom prête à confusion et il vaut mieux le savoir tout de suite : malgré le mot "régression", c'est bel et bien un classifieur, car il renvoie une classe et non un nombre quelconque. Le modèle s'utilise avec deux méthodes. fit(X, y) l'entraîne sur les couples mesure-réponse, et predict(X) renvoie les classes prévues pour de nouvelles mesures. Dans la leçon suivante tu verras que tous les modèles de scikit-learn parlent avec les mêmes verbes - raison de plus pour les découvrir maintenant, sur le jeu de données le plus simple possible.
Rassemblons donc huit animaux : quatre lions et quatre guépards, chacun décrit par son poids en kilogrammes et sa vitesse en kilomètres par heure.
1from sklearn.linear_model import LogisticRegression
2
3# poids en kg, vitesse en km/h
4X_train = [
5 [190, 58], [175, 60], [205, 55], [160, 62],
6 [55, 110], [48, 115], [62, 105], [50, 112],
7]
8y_train = [0, 0, 0, 0, 1, 1, 1, 1] # 0 = lion, 1 = guepard
9
10model = LogisticRegression()
11model.fit(X_train, y_train)
12
13print(model.predict([[52, 108]])) # [1]Le modèle a répondu [1], c'est-à-dire "guépard" - et il a raison, car 52 kilogrammes pour 108 km/h, c'est une silhouette de guépard. Fais attention à deux choses qui n'ont pas changé. D'abord, fit n'a pas renvoyé un nouveau modèle à ranger dans une variable : il a modifié l'objet model sur place, et c'est pourquoi nous l'appelons dans une instruction séparée. Ensuite, X_train et y_train sont restés intacts - le modèle a gardé chez lui les coefficients calculés, et tes données sont là où elles étaient. Remarque aussi que predict reçoit une liste de listes, même pour un seul animal, et renvoie un tableau de réponses plutôt qu'une valeur isolée. Il prédit toujours pour plusieurs spécimens à la fois, même quand ce "plusieurs" vaut un.
Le nom de la classe ne suffit souvent pas. Un pisteur préférerait savoir si le modèle est sûr de lui ou s'il devine. C'est le rôle de predict_proba, qui renvoie des probabilités au lieu d'une classe - un nombre pour chaque classe possible.
1proba = model.predict_proba([[52, 108]])
2
3print(proba.shape) # (1, 2)
4print(round(float(proba.sum()), 2)) # 1.0Le résultat a la forme (1, 2) : une ligne, parce que nous avons interrogé un seul animal, et deux colonnes, parce qu'il y a deux classes. Les colonnes suivent l'ordre croissant des numéros de classe, la première est donc la confiance envers le lion et la seconde envers le guépard. Les deux s'additionnent pour donner un, et c'est exactement ce que vérifie le second affichage. Pour notre guépard le modèle est presque totalement sûr, mais je ne déclare pas ici le nombre exact, car il dépend de la version de la bibliothèque - exécute le code et vois par toi-même. L'usage pratique est le suivant : quand la probabilité la plus haute ne dépasse pas 0,7 par exemple, il vaut mieux marquer l'image pour un examen manuel que de faire comme si le modèle savait. Souviens-toi au passage qu'il s'agit de la confiance du modèle et non de la vérité du monde - un modèle peut être sûr de lui et se tromper en même temps.
La régression - quand la réponse est un nombre
Quand y contient des nombres qu'on ne peut pas énumérer dans une liste fermée, la tâche s'appelle une régression. L'effectif d'un troupeau, le poids d'un animal, les précipitations annuelles de la réserve, le temps qui reste avant la prochaine migration - tout cela relève de la régression. Le mot clé est continu : la réponse peut tomber n'importe où sur l'échelle, et pas seulement sur quelques points listés d'avance. Un poids de 187,4 kg est une réponse aussi sensée que 187,5 kg, et entre les deux il en reste une infinité d'autres. En classification rien d'intermédiaire n'existe - il n'y a pas d'animal à moitié lion et à moitié guépard.
Le modèle de régression le plus simple est LinearRegression, la régression linéaire. Elle cherche une relation simple du type "plus il y a de ceci, plus il y a de cela" et l'écrit sous forme de droite. Tu la pilotes avec les mêmes verbes que le classifieur : fit entraîne, predict prédit. Mettons-la à l'épreuve sur une question qui revient souvent en Safari : combien d'animaux une réserve d'une superficie donnée peut-elle nourrir ?
1from sklearn.linear_model import LinearRegression
2
3X = [[100], [200], [300], [400]] # superficie de la reserve en km2
4y = [150, 280, 420, 550] # troupeau recense
5
6model = LinearRegression()
7model.fit(X, y)
8
9prediction = model.predict([[250]])
10print(f"Effectif prevu : {prediction[0]:.0f}") # 350Le modèle a répondu 350, alors que cette valeur n'apparaissait nulle part dans les données - il l'a calculée à partir de la droite ajustée aux quatre observations. C'est toute l'essence de la régression : la réponse n'a pas à provenir du jeu d'entraînement. Fais attention à deux détails d'écriture. La superficie est une mesure unique, et pourtant chaque réserve forme une liste à part - [[100], [200]] et non [100, 200] - car X est toujours un tableau à lignes, même avec une seule colonne. Et de nouveau predict a renvoyé un tableau, d'où le prediction[0]. Le format :.0f arrondit le résultat à des individus entiers, car un demi-animal ne se promène pas dans la savane.
Ce n'est une question ni de vitesse ni de quantité de données
Avant d'aller plus loin, démontons trois idées qui circulent autour du couple classification-régression, qui sonnent raisonnables et qui sont fausses.
La première : "la classification est plus rapide que la régression". Elle ne l'est pas - et l'inverse non plus. Le temps de calcul dépend du modèle choisi et de la taille des données, pas du fait que la réponse soit un nom ou un nombre. La régression linéaire sur quatre réserves que tu viens d'exécuter s'est calculée instantanément, tandis qu'un classifieur sur un million de photos peut s'entraîner pendant des heures. La distinction entre classification et régression porte sur le type de réponse, pas sur les performances.
La deuxième : "la régression nécessite plus de données". Faux également. Le besoin en données découle du nombre de caractéristiques et de la complexité du phénomène, pas du type de tâche. Notre régression s'est débrouillée avec quatre lignes parce que la relation était simple et linéaire, alors que reconnaître une espèce sur des photos réclame des milliers d'exemples, bien qu'il s'agisse là d'une classification.
La troisième, la plus dangereuse : "il n'y a aucune différence". La différence est fondamentale, car elle traverse tout le reste. Les modèles diffèrent, les métriques diffèrent, et même la question "la réponse est-elle correcte" ne veut pas dire la même chose. En classification la réponse tombe juste ou non, sans nuance. En régression tu ne tombes presque jamais exactement juste - ce qui compte, c'est de combien tu t'es trompé. Retiens cette phrase, tu la reverras dans les métriques d'ici quelques paragraphes : la classification prédit des catégories, la régression prédit des valeurs continues.
Accuracy - le pourcentage de bonnes réponses
Revenons au piège photo du début de la leçon. Pour évaluer un modèle il te faut deux séries de même longueur : y_true, ce qu'il y avait vraiment sur les images, et y_pred, ce qu'a dit le modèle. Ces noms sont un standard non écrit dans scikit-learn et nous nous y tiendrons. Toutes les métriques fonctionnent de la même façon : elles reçoivent ces deux séries et renvoient un seul nombre.
La plus simple d'entre elles est l'accuracy, l'exactitude. Elle mesure exactement une chose : le pourcentage de prédictions correctes, le nombre de bonnes réponses divisé par le nombre total d'essais. C'est la fonction accuracy_score qui la calcule, et tu l'importes du module sklearn.metrics - c'est là que vivent toutes les métriques de cette bibliothèque.
1from sklearn.metrics import accuracy_score
2
3# 12 images du piege photo : 0 = calme, 1 = braconnier
4y_true = [0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0]
5y_pred = [0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0]
6
7print(f"Accuracy: {accuracy_score(y_true, y_pred):.2%}") # 75.00%Le modèle a décrit neuf images sur douze comme le naturaliste, l'accuracy vaut donc 75 %. Le format :.2% multiplie le résultat par cent et colle un signe pourcent, car la fonction elle-même renvoie la fraction 0,75 - c'est du formatage, pas une autre métrique. Et tout de suite une réserve importante : l'accuracy ne mesure rien d'autre. Elle ne dit rien de la vitesse d'entraînement du modèle, car une vitesse se mesure au chronomètre et non avec une métrique. Elle ne dit pas non plus quelle quantité de mémoire le modèle a occupée. Elle n'est pas davantage l'erreur quadratique moyenne - celle-ci, tu le verras dans un instant, appartient à la famille des métriques de régression et mesure une distance à la vérité, pas un nombre de bonnes réponses. L'accuracy, c'est une chose précise et une seule : le pourcentage de prédictions correctes.
Precision et Recall - deux questions différentes sur le même modèle
Ces 75 % ont l'air convenables, mais rappelle-toi le modèle hypothétique du début de la leçon, celui qui disait toujours "calme". Sur nos douze images le braconnier était présent quatre fois, un modèle aussi paresseux tomberait donc juste huit fois sur douze, soit 66,67 %. Notre modèle fait mieux de moins de neuf points - et c'est pourtant lui qui doit garder la réserve. L'accuracy ne le montrera pas, parce qu'elle mélange deux sortes d'erreurs complètement différentes. Appelons-les par leur nom.
Le modèle a donné l'alerte alors que tout était calme : c'est une fausse alerte, false positive en anglais. Quelqu'un ira vérifier un carré de savane vide. Le modèle s'est tu alors que le braconnier était bien là : c'est un oubli, false negative, et il coûte beaucoup plus cher. Une alerte justifiée est un true positive, un silence justifié un true negative.
Deux métriques posent sur ces erreurs deux questions différentes. La precision, la précision, demande : parmi les alertes déclenchées par le modèle, combien étaient vraies ? C'est une évaluation de la crédibilité de l'alerte. Le recall, le rappel, demande : parmi les intrusions réelles, combien le modèle en a-t-il détecté ? C'est une évaluation de l'étanchéité du système. Ce sont precision_score et recall_score qui les calculent, dans ce même module sklearn.metrics.
1from sklearn.metrics import precision_score, recall_score
2
3print(f"Precision: {precision_score(y_true, y_pred):.2%}") # 66.67%
4print(f"Recall: {recall_score(y_true, y_pred):.2%}") # 50.00%Comptons cela à la main, car il vaut la peine de voir d'où sortent ces nombres. Le modèle a donné l'alerte trois fois, dont deux à raison : 2 divisé par 3 donne 66,67 % de précision. Les intrusions réelles étaient au nombre de quatre et le modèle en a repéré deux : 2 sur 4, soit 50 % de rappel. La moitié des braconniers est passée inaperçue. Le plus important est ce qui n'a pas changé : les prédictions sont exactement les mêmes que pour l'accuracy, les données aussi. Seule la question posée a changé - et c'est pour cela qu'un unique jeu de résultats a produit trois nombres différents. Mon conseil : pour chaque classifieur, calcule la precision et le recall en même temps que l'accuracy. Seule, sur un phénomène rare, l'accuracy peut avoir fière allure et ne rien vouloir dire du tout.
La précision et le rappel tirent dans des directions opposées. Un modèle qui alerte au moindre frémissement de l'herbe aura un rappel élevé et une précision catastrophique. Un modèle qui n'alerte qu'avec une certitude de cent pour cent - l'inverse. Pour comparer des modèles avec un seul nombre, on les combine dans la métrique F1, la moyenne harmonique des deux.
1from sklearn.metrics import f1_score
2
3print(f"F1: {f1_score(y_true, y_pred):.2%}") # 57.14%Pourquoi harmonique et non ordinaire ? La moyenne ordinaire de 66,67 % et de 50 % vaut 58,33 %, alors que F1 est sorti à 57,14 % - un peu plus bas, plus près de la moins bonne des deux composantes. C'est voulu. La moyenne harmonique punit le déséquilibre : un modèle avec 100 % de précision et 1 % de rappel obtiendra un F1 proche de zéro, là où la moyenne ordinaire lui accorderait un honorable 50 %. Un F1 élevé n'est donc atteignable que si les deux composantes sont correctes en même temps.
Matrice de confusion - quatre nombres au lieu d'un seul
Chacune des métriques vues jusqu'ici résumait tout le résultat en un seul nombre et perdait au passage l'information sur le type d'erreur le plus fréquent. La matrice de confusion (confusion matrix) ne résume rien : elle montre les quatre cas à la fois. Les lignes portent la vérité, les colonnes la prédiction du modèle, et les classes sont rangées par ordre croissant, donc avec des étiquettes 0 et 1 on trouve d'abord "calme", puis "braconnier". La fonction confusion_matrix s'importe - comme les précédentes - du module sklearn.metrics.
1from sklearn.metrics import confusion_matrix
2
3print(confusion_matrix(y_true, y_pred))
4# [[7 1]
5# [2 2]]Lis ce petit tableau ligne par ligne. La ligne du haut, ce sont les images où le calme régnait vraiment : 7 fois le modèle a dit "calme" lui aussi (silence justifié), 1 fois il a déclenché une fausse alerte. La ligne du bas, ce sont les images avec braconnier : 2 fois le modèle l'a manqué, 2 fois il l'a détecté. La diagonale qui va du coin supérieur gauche au coin inférieur droit rassemble les bonnes réponses, tout ce qui se trouve en dehors est une erreur. À partir de ces quatre nombres tu reconstruis chacune des métriques précédentes : l'accuracy vaut (7 plus 2) sur 12, la precision 2 sur (2 plus 1), le recall 2 sur (2 plus 2). C'est pourquoi il vaut mieux regarder la matrice de confusion en premier - tu en déduis les métriques, alors qu'à partir des métriques tu ne reconstruis pas la matrice.
Quand il y a plus de deux classes, il faut calculer la precision et le recall séparément pour chacune, et cela devient fastidieux. Scikit-learn propose un raccourci tout prêt : classification_report imprime l'ensemble des métriques pour toutes les classes dans un seul tableau. Le paramètre target_names permet de remplacer les numéros de classe par des noms lisibles.
1from sklearn.metrics import classification_report
2
3print(classification_report(y_true, y_pred, target_names=["calme", "braconnier"]))
4# tableau : precision, recall, f1-score et support pour chaque classeDans l'affichage tu verras une ligne par classe et, en colonnes, les familiers precision, recall et f1-score. Une colonne nouvelle s'ajoute : le support, c'est-à-dire le nombre d'occurrences réelles de chaque classe dans y_true. C'est elle qui trahit le déséquilibre - chez nous huit images de calme et seulement quatre avec braconnier. Je ne reproduis pas ici l'aspect exact du tableau, car la largeur des colonnes change d'une version de la bibliothèque à l'autre. Ce qui compte davantage, c'est ce que tu y liras : la classe rare obtient presque toujours de moins bons résultats que la classe fréquente, et c'est justement elle qui t'intéresse d'habitude.
Les métriques de régression - de combien nous sommes-nous trompés
En classification la réponse tombait juste ou non. En régression c'est impossible : un modèle qui a prédit 349 individus au lieu de 350 n'est pas "incorrect", il est proche. C'est pourquoi toutes les métriques de régression mesurent la distance entre les prédictions et la vérité, et non un nombre de bonnes réponses. Nous gardons les noms y_true et y_pred, mais ils recouvrent désormais des données tout à fait différentes : les effectifs réels et prédits des troupeaux de quatre réserves.
La première métrique est le MSE, mean squared error, l'erreur quadratique moyenne. Elle calcule l'écart pour chaque réserve, l'élève au carré et en fait la moyenne. Le carré remplit deux rôles : il supprime le signe, pour qu'une erreur vers le bas n'annule pas une erreur vers le haut, et il amplifie les grosses erreurs. L'ennui, c'est que le résultat s'exprime en individus au carré, ce qui ne s'interprète pas raisonnablement. On calcule donc en général aussi le RMSE, root mean squared error - la racine carrée du MSE, qui ramène à l'unité d'origine. Nous prendrons la racine avec la fonction np.sqrt de NumPy, que tu connais depuis le module huit.
1from sklearn.metrics import mean_squared_error
2import numpy as np
3
4y_true = [150, 280, 420, 550] # effectif reel du troupeau
5y_pred = [140, 300, 400, 560] # predictions du modele
6
7mse = mean_squared_error(y_true, y_pred)
8print(f"MSE: {mse:.2f}") # 250.00
9print(f"RMSE: {np.sqrt(mse):.2f}") # 15.81Le MSE vaut 250, mais ce nombre ne te dit rien, car ce sont "250 individus au carré". Seul le RMSE, 15,81, forme une phrase en français : l'erreur typique du modèle tourne autour de seize animaux. Fais attention à l'ordre des arguments - la vérité d'abord, les prédictions ensuite. Avec le MSE l'inversion ne change justement rien au résultat, puisque l'écart est élevé au carré, mais dans d'autres métriques elle change tout, alors autant prendre l'habitude tout de suite : y_true toujours en premier.
Puisque le carré amplifie les grosses erreurs, tu voudras parfois une métrique qui ne le fait pas. C'est le MAE, mean absolute error, l'erreur absolue moyenne : elle prend la valeur absolue de chaque écart et en fait la moyenne, sans passer par le carré.
1from sklearn.metrics import mean_absolute_error
2
3print(f"MAE: {mean_absolute_error(y_true, y_pred):.2f}") # 15.00Le MAE vaut 15, donc moins que le RMSE qui vaut 15,81. Cet écart n'est pas un hasard et il vaut la peine d'en comprendre l'origine. Nos quatre erreurs sont de 10, 20, 20 et 10 animaux - le MAE en fait simplement la moyenne et obtient 15. Le RMSE, grâce aux carrés, accorde plus de poids à ces vingtaines, il ressort donc plus haut. La règle est simple : le RMSE est toujours supérieur ou égal au MAE, et plus ils s'écartent, plus tes erreurs sont inégales. Quand la différence est grande, va chercher les quelques réserves sur lesquelles le modèle se perd complètement.
Ces deux métriques ont un défaut commun : elles s'expriment en individus, tu ne peux donc pas comparer avec elles un modèle qui compte des troupeaux et un modèle qui prévoit des précipitations. Il faut une mesure sans unité, et c'est le R², le coefficient de détermination. C'est la fonction r2_score qui le calcule, et il répond à la question : de combien mon modèle est-il meilleur que la stratégie la plus simple possible, celle qui consiste à toujours répondre la moyenne des vraies valeurs ?
1from sklearn.metrics import r2_score
2
3print(f"R2: {r2_score(y_true, y_pred):.4f}") # 0.9889Le résultat 0,9889 se lit ainsi : le modèle explique un peu moins de 99 % de la variabilité des effectifs, il est donc très proche de l'ajustement parfait. Un vaudrait un tir au but exact dans chaque réserve. Et il me faut ici corriger une contrevérité répandue, que tu trouveras dans bien des antisèches : le R² ne se situe pas du tout entre zéro et un. La borne supérieure est bien 1, mais il n'existe aucune borne inférieure. Zéro signifie un modèle exactement aussi bon que le fait de répondre la moyenne, et tout ce qui passe sous zéro - un modèle pire que cette moyenne. Voyons ces deux cas sur nos quatre réserves, car c'est seulement là que la chose devient visible.
1# modele qui repond toujours "moyenne" (350)
2print(r2_score([150, 280, 420, 550], [350, 350, 350, 350])) # 0.0
3
4# modele qui a inverse l'ordre des reserves
5print(r2_score([150, 280, 420, 550], [550, 420, 280, 150])) # -3.0Le premier modèle ne regarde pas les données du tout et obtient exactement 0,0 - ce n'est pas une coïncidence, c'est la définition même de cette métrique. Le second a confondu les réserves et tombe à moins trois, car il se trompe bien plus lourdement que la simple devinette de la moyenne. Si tu vois un jour un R² négatif, ne cherche pas de bogue dans la fonction : ton modèle est réellement moins bon qu'un unique nombre constant.
Baseline - contre qui cours-tu au juste
Remarque ce que le R² vient de faire : il a comparé de lui-même le modèle à la stratégie la plus bête qui ait du sens. Un tel point de référence s'appelle une baseline. La régression l'a intégré dans sa métrique, la classification non - et c'est pour cela qu'on peut passer une semaine sur un modèle à 95 % d'accuracy sans savoir que deviner en donne 94 %.
La baseline d'une classification, il faut la calculer soi-même, mais scikit-learn fournit l'outil : DummyClassifier, du module sklearn.dummy. C'est un modèle volontairement stupide - avec la stratégie most_frequent il répond toujours la classe la plus fréquente de l'ensemble d'entraînement et ne regarde pas du tout les caractéristiques. C'est justement pour cela que nous devons lui fournir un X avec le bon nombre de lignes, même si son contenu n'a ici aucune importance.
1from sklearn.dummy import DummyClassifier
2import numpy as np
3
4y_frames = [0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0]
5X_ignored = np.zeros((len(y_frames), 1))
6
7baseline = DummyClassifier(strategy="most_frequent")
8baseline.fit(X_ignored, y_frames)
9
10print(f"Baseline: {baseline.score(X_ignored, y_frames):.2%}") # 66.67%La baseline tombe juste à 66,67 %, car huit des douze images sont calmes et elle répond toujours "calme". Notre modèle en avait 75. Tout l'avantage d'une semaine de travail tient dans un peu plus de huit points de pourcentage - et c'est une information que l'accuracy, seule, ne t'aurait jamais donnée. Remarque que DummyClassifier s'utilise exactement comme un vrai classifieur : fit, puis score. Aucun de tes modèles ni aucune de tes données n'a changé - c'est une mesure de référence à part, faite une seule fois. Mon conseil : calcule la baseline avant d'entraîner quoi que ce soit de sérieux. Si la baseline donne 94 %, tu sais tout de suite qu'il faudra regarder le recall et non l'accuracy.
Le rituel d'évaluation en quatre étapes
Tout ce que nous avons rassemblé jusqu'ici se range dans un ordre fixe. Retiens-le, car tu le répéteras pour chaque modèle de ce module et dans tous tes projets ensuite.
- Entraîne le modèle sur l'ensemble d'entraînement. Le modèle voit les mesures accompagnées des réponses et y ajuste ses nombres internes.
- Fais des prédictions sur l'ensemble de test. C'est seulement maintenant que tu lui montres des données qu'il n'a jamais vues. Des prédictions faites sur les données d'entraînement ne disent rien de la qualité du modèle - c'est vérifier si l'élève se souvient des réponses qu'on lui a soufflées.
- Calcule les métriques. Tu confrontes
y_trueày_predet tu calcules l'accuracy, la precision et le recall, ou pour une régression le MSE, le MAE et le R². - Compare les résultats avec la baseline. C'est seulement cela qui transforme un nombre nu en jugement.
L'ordre n'est pas arbitraire et aucune étape ne se déplace. Tu ne calculeras pas de métriques avant d'avoir fait les prédictions, faute de quoi tu n'aurais rien à confronter à la vérité. La baseline vient en dernier, parce que c'est elle qui donne un sens aux nombres de la troisième étape - exactement comme les 75 % de notre modèle n'ont commencé à signifier quelque chose qu'à côté des 66,67 % du DummyClassifier.
Validation croisée - un seul découpage, c'est une loterie
Il reste dans ce rituel une faiblesse, et elle est sérieuse. L'ensemble de test naît d'un découpage aléatoire des données. Si le hasard a voulu qu'il n'y atterrisse que des cas faciles, tu obtiendras une note gonflée. Rien que des cas difficiles - une note rabaissée. Avec cent animaux et un partage 80 contre 20, tout le verdict repose sur vingt spécimens, et il suffit d'un autre tirage pour que le résultat bondisse de plus de dix points de pourcentage.
Le remède est la validation croisée, cross-validation en anglais. C'est une technique d'évaluation du modèle qui divise les données en plusieurs plis d'entraînement et de validation. Ces parties s'appellent des plis (folds en anglais). Avec cinq plis, les données sont découpées en cinq morceaux égaux, puis la procédure se répète cinq fois : à chaque tour un pli différent joue le rôle d'ensemble de validation et les quatre autres servent à l'entraînement. À la fin tu disposes de cinq notes au lieu d'une, et chaque spécimen a été évalué exactement une fois et utilisé quatre fois pour apprendre.
Précisons tout de suite ce que la validation croisée n'est pas, car le nom prête parfois à confusion. Ce n'est pas un type de réseau de neurones - les réseaux, tu les découvriras dans la leçon sur PyTorch et c'est une tout autre affaire. Ce n'est pas une méthode de nettoyage des données - les valeurs manquantes et les doublons, tu les as supprimés avec Pandas au module précédent, et cela se passe bien avant toute validation. Ce n'est pas non plus un algorithme de tri - la validation croisée ne range rien, elle découpe plusieurs fois et évalue plusieurs fois.
Pour la mettre en pratique il faut plus de huit animaux, nous allons donc générer un jeu artificiel avec la fonction make_classification du module sklearn.datasets. Attention à ses paramètres, l'erreur est vite arrivée : n_features est le nombre total de colonnes, n_informative indique combien d'entre elles portent réellement du signal, et n_redundant combien ne sont que des copies recalculées à partir des autres. Ces trois nombres doivent être arithmétiquement cohérents, et de plus le nombre de classes multiplié par le nombre de grappes par classe (deux par défaut) ne peut pas dépasser deux élevé à la puissance n_informative. Avec trois espèces et un n_informative valant 2 par défaut, l'appel se terminerait par une exception ValueError, c'est pourquoi nous donnons tous ces nombres explicitement.
1from sklearn.datasets import make_classification
2
3X, y = make_classification(
4 n_samples=300,
5 n_features=4,
6 n_informative=4,
7 n_redundant=0,
8 n_classes=3, # 0 = lion, 1 = elephant, 2 = guepard
9 random_state=42,
10)
11
12print(X.shape, y.shape) # (300, 4) (300,)Nous avons obtenu 300 animaux décrits par quatre mesures et 300 étiquettes - les formes sont confirmées par shape, que tu connais depuis NumPy. Le paramètre random_state=42 fixe la graine aléatoire, ce qui fait que chaque exécution génère exactement les mêmes données. Ce n'est pas de la cosmétique mais une condition de reproductibilité : sans lui, chaque lancement du script donnerait d'autres résultats et tu ne distinguerais pas une amélioration du modèle d'un simple coup de chance.
Voici maintenant la fonction la plus importante de cette section. cross_val_score prend un modèle non entraîné, les données et le nombre de plis dans le paramètre cv, puis exécute elle-même tout le cycle : elle découpe, entraîne, évalue et renvoie un tableau de résultats. Tu l'importes du module sklearn.model_selection - le même d'où tu tirais train_test_split à la leçon précédente.
1from sklearn.model_selection import cross_val_score
2from sklearn.linear_model import LogisticRegression
3
4model = LogisticRegression(max_iter=1000)
5scores = cross_val_score(model, X, y, cv=5)
6
7print(len(scores)) # 5
8print(f"Moyenne : {scores.mean():.2%} (+/- {scores.std() * 2:.2%})")Le résultat est constitué de cinq nombres, un par pli - et ils vont réellement différer les uns des autres, souvent de plus de dix points de pourcentage. Je ne donne pas leurs valeurs exactes, car elles dépendent de la version de la bibliothèque, mais c'est la dispersion elle-même qui compte ici plus que les chiffres précis : c'est elle qui montre à quel point un découpage unique pouvait te tromper. La moyenne des cinq notes résume la situation bien plus honnêtement que n'importe laquelle prise isolément, et l'écart type multiplié par deux donne une marge d'incertitude commode. Remarque encore une chose : nous passons le modèle avant l'entraînement et nous n'appelons pas fit nous-mêmes. cross_val_score entraîne cinq copies indépendantes, et ton objet model reste non entraîné une fois tout terminé - après une validation croisée tu devras donc quand même l'entraîner sur les données complètes.
La façon de découper se contrôle elle aussi, mais il vaut d'abord la peine de savoir ce qui se passe par défaut. Quand tu donnes un simple nombre à un classifieur, comme notre cv=5, scikit-learn choisit un découpage stratifié : il veille à ce que les proportions d'espèces soient les mêmes dans chaque pli que dans l'ensemble complet. C'est une bonne décision et elle ne te demande rien. En revanche il ne mélange pas les lignes avant de découper, donc si le carnet est rangé chronologiquement ou par expéditions successives, des spécimens voisins finiront dans le même pli. Pour changer cela, tu passes un objet de découpage à la place du nombre. Le plus simple est KFold, qui accepte le nombre de plis, l'interrupteur shuffle et une graine aléatoire.
1from sklearn.model_selection import KFold
2
3kfold = KFold(n_splits=5, shuffle=True, random_state=42)
4scores = cross_val_score(model, X, y, cv=kfold)
5
6print(len(scores)) # 5Il y a toujours cinq résultats, car n_splits=5 veut dire la même chose que cv=5 - seule la manière de choisir les plis a changé, avec un mélange aléatoire cette fois. Il y a pourtant ici un piège que tu dois connaître : KFold découpe à l'aveugle et ne surveille pas les proportions de classes, tu perds donc en classification la stratification que tu obtenais gratuitement. Ma recommandation est donc la suivante : pour une régression, prends KFold, et pour une classification StratifiedKFold du même module - il a exactement les mêmes paramètres et conserve au passage les proportions d'espèces dans chaque pli. Tu as sans doute vu sur le web des exemples avec une boucle manuelle sur kfold.split(X), où l'on découpe soi-même les indices d'entraînement et de validation. Je déconseille cette écriture pour commencer : elle fait exactement la même chose que la ligne unique ci-dessus et offre quatre occasions supplémentaires de se tromper. Passe KFold comme cv et laisse la bibliothèque tenir la comptabilité - la boucle manuelle, tu l'écriras seulement le jour où tu auras vraiment besoin de faire quelque chose d'inhabituel à l'intérieur de chaque pli.
Retiens une seule phrase de cette leçon : le nombre qu'un modèle affiche après l'entraînement ne signifie rien tant que tu ne sais pas quelle sorte de réponse tu cherchais ni ce qu'aurait obtenu une simple devinette - un bon pisteur ne se fie jamais à une trace qu'il n'a rien pour comparer.
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quelle est la différence entre la classification et la régression ?
2. Que mesure la métrique Accuracy en classification ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri horizontal
Range l'import de accuracy_score depuis sklearn :
- Éditeur de code
Utilise les fonctions de sklearn.metrics pour calculer les métriques de y_true et y_pred.
- Tri vertical
Range les étapes dans le bon ordre :
- Tri horizontal
Range l'import de cross_val_score :
- Éditeur de code
Utilise confusion_matrix de sklearn.metrics pour visualiser les résultats.
- Tri horizontal
Range les éléments :
- Clique dans l'ordre
Clique sur les boutons pour calculer le R² pour la régression :