John Paul Mueller

Luca Massaron

Le machine learning avec Python

2e édition

Le machine learning avec Python pour les Nuls (2e édition)

Cet ouvrage comporte des extraits de : Machine learning pour les Nuls & Python pour les Nuls

Pour les Nuls est une marque déposée de Wiley Publishing, Inc.

For Dummies est une marque déposée de Wiley Publishing, Inc.

Collection dirigée par Jean-Pierre Cano

Traduction : Daniel Rougé & Dominique Maniez

Mise en page : maged

© Éditions First, un département d’Édi8, 2025.

Édition française publiée en accord avec Wiley Publishing, Inc.

Éditions First, un département d’Édi8

92 avenue de France

75013 Paris - France

Tél. : 01 44 16 09 00

E-mail : firstinfo@efirst.com

Web : www.lisez.com

ISBN : 978-2-412-10556-6

Code éditeur : 10276/01

Dépôt légal : Août 2025

Tous droits réservés. Toute reproduction, même partielle, du contenu, de la couverture ou des icônes, par quelque procédé que ce soit (électronique, photocopie, bande magnétique ou autre) est interdite sans autorisation par écrit de Wiley Publishing, Inc.

Limites de responsabilité et de garantie. L’auteur et l’éditeur de cet ouvrage ont consacré tous leurs efforts à préparer ce livre. Wiley Publishing et l’auteur déclinent toute responsabilité concernant la fiabilité ou l’exhaustivité du contenu de cet ouvrage. Ils n’assument pas de responsabilités pour ses qualités d’adaptation à quelque objectif que ce soit, et ne pourront être en aucun cas tenus responsables pour quelque perte, profit ou autre dommage commercial que ce soit, notamment mais pas exclusivement particulier, accessoire, conséquent, ou autres.

Marques déposées. Toutes les informations connues ont été communiquées sur les marques déposées pour les produits, services et sociétés mentionnés dans cet ouvrage. Wiley Publishing, Inc. et les Éditions First déclinent toute responsabilité quant à l’exhaustivité et à l’interprétation des informations. Tous les autres noms de marque et de produits utilisés dans cet ouvrage sont des marques déposées ou des appellations commerciales de leur propriétaire respectif.

Ce livre numérique a été converti initialement au format EPUB par Isako www.isako.com à partir de l'édition papier du même ouvrage.

Introduction

Le but principal de ce livre divisé en deux livres distincts est de vous aider à comprendre ce que l’apprentissage automatique peut et ne peut pas faire pour vous aujourd’hui, et ce qu’il pourrait faire pour vous dans l’avenir. Vous n’avez pas besoin d’être un informaticien pour utiliser ce livre, même s’il contient de nombreux exemples de codage. En fait, vous pouvez venir de n’importe quelle discipline mettant fortement l’accent sur les mathématiques, car c’est ainsi que ce livre se concentre sur la question de l’apprentissage automatique. Au lieu de traiter d’abstractions, vous voyez les résultats concrets de l’utilisation d’algorithmes spécifiques pour interagir avec le big data de manière particulière afin d’obtenir un résultat certain et utile. L’accent est mis sur ce qui est utile parce que l’apprentissage automatique a le pouvoir d’effectuer un large éventail de tâches d’une manière jamais vue auparavant.

Dans le second livre, vous découvrirez quel est le langage de programmation qui vous permet d’écrire rapidement des applications fonctionnelles et utilisables sur pratiquement n’importe quelle plate-forme ? Vous donnez votre langue au chat ? Oui, c’est Python. Ce qu’il y a de merveilleux avec Python, c’est effectivement que vous pouvez réellement écrire une application sur une plate-forme, et l’utiliser sur n’importe quelle autre plate-forme (à condition bien sûr qu’elle ne soit pas totalement exotique). Contrairement à d’autres langages de programmation, Python ne fait pas que faire des promesses : il les tient. Et, dans ce cas, le résultat vaut la promesse.

Python propose un code lisible et une syntaxe concise qui vous permet d’écrire des applications qui nécessitent moins de code que d’autres langages. De plus, il est utile dans toutes sortes de domaines. Certains le voient uniquement comme un langage de script, mais il vaut en réalité bien plus que cela.

À propos de ce livre

Ce livre utilise quelques conventions :

• Le texte que vous êtes supposé taper au clavier est imprimé en caractères gras. D’accord, lorsque vous devez suivre une liste d’étapes, dont le texte est déjà mis en caractères gras, le texte à taper restera tel quel.

• Si vous voyez des caractères en italiques dans une séquence que vous devez saisir, cela signifie que vous devez les remplacer par quelque chose de plus personnel. Exemple : « Tapez Votre nom et appuyez sur Entrée » . Remplacez alors Votre nom par votre propre nom (ou par un autre, d’ailleurs).

• Les adresses Web apparaissent dans une police particulière. C’est juste pour ne pas les confondre avec autre chose.

En fait, c’est à peu près tout, et donc vous pouvez constater qu’il n’y a rien de particulièrement compliqué dans ces conventions.

Quelques suppositions un peu folles

Nous allons aussi supposer que vous savez un certain nombre de choses sur l’Internet. En fait, vous trouverez dans ce livre de nombreuses références en ligne qui vous aideront à développer vos connaissances. Mais, comme il se doit, ces sources d’information ne vous seront utiles que si vous êtes capable de les trouver. De plus, ces sources sont essentiellement en anglais. Une bonne pratique de la langue de nos amis anglo-saxons est donc un prérequis auquel vous ne pourrez pas échapper.

Icônes utilisées dans ce livre

Cette section décrit les icônes intéressantes (ou pas) que vous allez rencontrer dans ce livre :

Ces trucs et astuces sont utiles pour vous faire gagner du temps ou pour effectuer rapidement certaines tâches. Certaines pointent également vers des ressources que vous devriez essayer pour tirer le maximum de profit de Python.

Je ne veux pas donner l’impression d’être un vieux grincheux, mais vous devriez éviter de faire ce qui est signalé par cette icône. Sinon, il se pourrait bien que votre application n’entraîne que confusion chez vos utilisateurs, qui ne seront plus vos utilisateurs puisqu’ils refuseront de se servir de votre application…

Cette icône signale des informations ou des techniques avancées. Si cela vous ennuie, passez à la suite. Mais vous pouvez peut-être aussi prendre le temps de les lire, pour le cas où cela pourrait vous servir plus tard.

Cette icône est un rappel de ce que vous devriez retenir. Il s’agit généralement d’un processus important, ou encore d’une information que vous devez connaître pour pouvoir écrire des applications Python fonctionnelles et efficaces.

Et maintenant…

Bien entendu, vous pouvez ressaisir tous les exemples manuellement. Mais vous aurez certainement envie de les avoir à portée de main, sans taper une par une toutes les lignes de tous les exemples, avec tout ce que cela comporte de risque d’erreur. C’est pourquoi vous pouvez télécharger les exemples de ce livre depuis l’adresse suivante :

http://www.pourlesnuls.fr/

Cliquez tout en bas de la page sur le lien Téléchargements. Dans la page qui va s’afficher, cliquez sur le bouton Rechercher (la loupe) et saisissez le nom du livre, ou du moins le sujet principal (donc python dans ce cas). Il ne vous reste plus qu’à localiser l’image de couverture de ce livre, puis à cliquer dessus. Dans le page de présentation, cliquez simplement sur le bouton Télécharger. Le fichier contenant le code des exemples est petit. Tout devrait donc aller très vite.

Et maintenant, bonne programmation avec Python !

Livre 1

Machine learning

Partie 1

Découvrir comment les machines apprennent

Dans cette partie…

Découvrir comment l’IA fonctionne réellement et ce qu’elle peut faire pour vous

Comprendre ce que signifie l’expression big data

Comprendre le rôle des statistiques dans l’apprentissage automatique

Définir où l’apprentissage automatique conduira la société dans le futur

DANS CE CHAPITRE

Aller au-delà du battage publicitaire de l’intelligence artificielle (IA)

•

Faire la différence entre le monde réel et le monde imaginaire

•

Comparer l’IA à l’apprentissage automatique

•

Comprendre la partie ingénierie de l’intelligence artificielle et de l’apprentissage automatique

•

Délimiter où s’arrête l’ingénierie et où commence l’art

Chapitre 1

La véritable histoire de l’intelligence artificielle

L’intelligence artificielle (IA), c’est-à-dire l’apparition de l’intelligence dans les machines, est un vaste sujet aujourd’hui, qui ne cesse de prendre de l’ampleur grâce au succès des nouvelles technologies (voir quelques exemples actuels à https://thinkml.ai/top-5-ai-achievements-of-2019/), mais la plupart des gens recherchent des applications courantes, telles que parler à leur smartphone. Parler à son smartphone est à la fois amusant et utile pour trouver des choses comme l’emplacement du meilleur restaurant de sushis de la ville ou pour savoir comment se rendre à la salle de concert. Au fur et à mesure que vous parlez à votre smartphone, celui-ci apprend à mieux connaître votre façon de parler et commet moins d’erreurs dans la compréhension de vos demandes. La capacité de votre smartphone à apprendre et à interpréter votre façon particulière de parler est un exemple d’IA, et l’apprentissage automatique constitue une partie de la technologie utilisée pour y parvenir, c’est-à-dire l’emploi de diverses techniques pour permettre aux algorithmes de mieux fonctionner grâce à l’expérience.

Il est probable que vous fassiez aujourd’hui un usage limité de l’apprentissage automatique et de l’IA sans vraiment y penser. Par exemple, la possibilité de parler à des appareils et de leur demander de faire ce que vous voulez illustre l’exploitation de l’apprentissage automatique. De la même manière, les systèmes de recommandation, comme ceux que l’on trouve sur Amazon, vous aident à faire des achats sur la base de critères tels que les achats précédents ou les produits qui complètent vos choix actuels. L’utilisation de l’IA et de l’apprentissage automatique ne fera que croître avec le temps.

Dans ce chapitre, vous vous pencherez sur l’IA et découvrirez plusieurs points de vue, notamment la manière dont elle vous affecte en tant que consommateur et en tant que scientifique ou ingénieur. Vous verrez également que l’IA n’est pas synonyme d’apprentissage automatique, même si les médias confondent souvent les deux. L’apprentissage automatique est définitivement différent de l’IA, même si les deux sont liés.

Aller au-delà du battage publicitaire

Le développement d’une technologie s’accompagne d’un battage médiatique, et l’IA fait l’objet d’un battage considérable. D’une part, certaines personnes ont décidé de céder à la peur plutôt que de s’engager dans une démarche scientifique. Les robots tueurs, tels que ceux que l’on trouve dans le film Terminator, ne seront assurément pas la prochaine grande nouveauté. Votre première expérience réelle avec une IA androïde se fera probablement sous la forme d’un assistant médical (https://www.robotics.org/blog-article.cfm/The-Future-of-Elder-Care-is-Service-Robots/262) ou peut-être d’un collègue de travail (https://www.computerworld.com/article/2990849/meet-the-virtual-woman-who-may-take-your-job.html). En réalité, vous interagissez déjà avec l’IA et l’apprentissage automatique de manière beaucoup plus banale. Si vous devez lire ce chapitre, c’est en partie pour dépasser le battage médiatique et découvrir ce que l’IA peut faire pour vous aujourd’hui.

Vous avez peut-être aussi entendu parler de l’apprentissage automatique et de l’IA de manière interchangeable. L’IA inclut l’apprentissage automatique, mais l’apprentissage automatique ne définit pas entièrement l’IA. Ce chapitre vous aidera à comprendre la relation entre l’apprentissage automatique et l’IA afin que vous puissiez mieux comprendre comment ce livre vous aide à passer à une technologie qui n’apparaissait auparavant que dans les romans de science-fiction.

L’apprentissage automatique et l’IA ont tous deux une forte composante technique. En d’autres termes, il est possible de quantifier ces deux technologies avec précision en se fondant sur la théorie (explications étayées et testées) plutôt que sur une simple hypothèse (explication suggérée d’un phénomène). En outre, elles ont toutes deux une forte composante scientifique, grâce à laquelle les gens testent les concepts et créent de nouvelles idées sur la manière dont il serait possible d’exprimer le processus de pensée. Enfin, l’apprentissage automatique comporte également une composante artistique, et c’est là qu’un scientifique talentueux peut briller. Dans certains cas, l’IA et l’apprentissage automatique semblent défier la logique, et seul un véritable artiste peut les faire fonctionner comme prévu.

Oui, il existe des armes totalement autonomes

Avant que les gens ne nous envoient leurs dernières dissertations sur les armes entièrement autonomes, oui, quelques âmes bienheureuses travaillent sur de telles technologies. Vous trouverez dans ce livre quelques discussions sur l’éthique de l’IA, mais pour l’essentiel, cet ouvrage se concentre sur les utilisations positives et utiles de l’IA pour aider les êtres humains, plutôt que de les tuer, parce que la plupart des recherches sur l’IA reflètent ces utilisations. Vous pouvez en ligne trouver des articles sur les avantages et les inconvénients de l’IA, tels que l’article de Towards Data Science à https://towardsdatascience.com/advantages-and-disadvantages-of-artificial-intelligence-182a5ef6588c et l’article d’Emerj à https://emerj.com/ai-sector-overviews/autonomous-weapons-in-the-military/.

Si vous voulez vraiment vous faire peur, vous pouvez trouver toutes sortes de sites, tels que https://www.reachingcriticalwill.org/resources/fact-sheets/ critical-issues/7972-fully-autonomous-weapons, qui traitent en profondeur de la question des armes entièrement autonomes. Des sites tels que Campaign to Stop Killer Robots (https://www.stopkillerrobots.org/) peuvent également vous apporter des précisions. Nous vous encourageons à signer la pétition interdisant les armes autonomes à https://futureoflife.org/open-letter-autonomous-weapons/, car elles ne sont vraiment pas une nécessité.

Cependant, il est important de rappeler qu’il existe des interdictions concernant les armes spatiales, les armes chimiques et certaines armes laser. Les pays reconnaissent que ces armes ne résolvent rien et ils interdiront probablement aussi les armes entièrement autonomes, tout simplement parce que les citoyens ne supporteront pas les robots tueurs. En fin de compte, l’objectif de ce livre est de vous aider à comprendre l’apprentissage automatique sous un angle positif.

Le rêve de moutons électriques

Les androïdes (un type de robot spécialisé qui ressemble et agit comme un humain, tel que Data dans Star Trek: The Next Generation) et certains types de robots humanoïdes (un type de robot qui a des caractéristiques humaines mais qui se distingue facilement d’un humain, tel que C-3PO dans Star Wars) sont devenus les enfants vedettes de l’IA (voir les robots dansants à https://www.youtube.com/watch?v=lTckiTBaWkw). Ils présentent les ordinateurs en faisant preuve d’anthropomorphisme, c’est-à-dire en leur donnant des caractéristiques humaines, même s’ils ne sont pas humains. En fait, il est tout à fait possible qu’un jour vous ne puissiez plus distinguer facilement la vie humaine de la vie artificielle. Des auteurs de science-fiction, comme Philip K. Dick, l’ont prédit depuis longtemps, et cela semble tout à fait possible aujourd’hui. L’histoire « Do Androids Dream of Electric Sheep ? » (Les androïdes rêvent-ils de moutons électriques ?) aborde le concept du plus réel que le réel, qui fait partie de l’intrigue du film Blade Runner (https://www.warnerbros.com/movies/blade-runner). Cependant, certaines utilisations des robots aujourd’hui sont tout simplement amusantes, comme dans le spectacle Robot Restaurant à https://www.youtube.com/watch?v=l1vvTtz8hpg. Les sections suivantes vous aideront à comprendre à quel point la technologie actuelle se rapproche des idéaux présentés par les auteurs de science-fiction et les films.

L’état actuel de l’art est réaliste, mais on peut facilement dire que l’on parle à un androïde. Le visionnage de vidéos en ligne peut vous aider à comprendre que les androïdes que l’on ne peut pas distinguer des humains sont loin d’être une réalité aujourd’hui. Regardez les robots japonais sur https://www.youtube.com/watch?v=LyyytwT-BMk et https://www.cnbc.com/2019/10/31/human-like-androids-have-entered-the-workplace-and-may-take-your-job.html. L’un des exemples les plus réalistes est celui du robot Erica (https://www.youtube.com/watch?v=oRlwvLubFxg), qui est conçu pour apparaître dans un film de science-fiction. Son histoire est racontée sur le site HuffPost à https://www.huffpost.com/entry/erica-japanese-robot-science-fiction-film:n_5ef6523dc5b6acab284181c3. Le fait est que la technologie commence à peine à atteindre le point où l’on pourra éventuellement créer des robots et des androïdes plus vrais que nature, mais ils n’existent pas aujourd’hui.

Comprendre l’histoire de l’IA et de l’apprentissage automatique

Il y a une raison, autre que l’anthropomorphisation, pour laquelle les humains considèrent que l’IA ultime est celle qui est contenue dans un type d’androïde. Depuis les Grecs de l’Antiquité, les humains ont évoqué la possibilité de placer un esprit à l’intérieur d’un corps mécanique. Parmi ces mythes, on peut citer celui d’un homme mécanique appelé Talos (http://www.ancient-wisdom.com/greekautomata.htm). Le fait que les Grecs de l’Antiquité disposaient de dispositifs mécaniques complexes, dont un seul existe encore (voir le mécanisme d’Antikythera à http://www.ancient-wisdom.com/antikythera.htm), établit qu’il est fort probable que leurs rêves n’étaient pas seulement le fruit de leur imagination. Tout au long des siècles, on a parlé de personnes mécaniques capables de penser (comme le Golem du rabbin Judah Loew, https://www.nytimes.com/2009/05/11/world/europe/11golem.html).

L’IA repose sur l’hypothèse qu’il est possible de mécaniser la pensée. Au cours du premier millénaire, les philosophes grecs, indiens et chinois ont tous travaillé sur les moyens d’accomplir cette tâche. Dès le XVIIe siècle, Gottfried Leibniz, Thomas Hobbes et René Descartes ont évoqué la possibilité de rationaliser toute la pensée sous la forme de simples symboles mathématiques. Bien entendu, la complexité du problème leur échappait (et nous échappe encore aujourd’hui, malgré les progrès que vous découvrirez dans la troisième partie de ce livre). En fait, la vision de l’IA existe depuis très longtemps, mais sa mise en œuvre est relativement récente.

La véritable naissance de l’IA telle que nous la connaissons aujourd’hui a commencé avec la publication par Alan Turing de « Computing Machinery and Intelligence » en 1950 (https://www.csee.umbc.edu/courses/471/papers/turing.pdf). Dans cet article, Turing explore l’idée de déterminer si les machines peuvent penser. Bien entendu, cet article a débouché sur le jeu appelé Imitation Game, qui implique trois joueurs. Le joueur A est un ordinateur et le joueur B est un être humain. Chacun doit convaincre le joueur C (un humain qui ne peut voir ni le joueur A ni le joueur B) qu’il est humain. Si le joueur C ne peut pas déterminer qui est humain et qui ne l’est pas de manière cohérente, l’ordinateur gagne.

L’un des problèmes persistants de l’IA est l’excès d’optimisme. Le problème que les scientifiques tentent de résoudre avec l’IA est incroyablement complexe. Cependant, l’optimisme des années 1950 et 1960 a conduit les scientifiques à croire que le monde produirait des machines intelligentes dans une vingtaine d’années seulement. Après tout, les machines faisaient toutes sortes de choses étonnantes, comme jouer à des jeux complexes. L’IA connaît actuellement son plus grand succès dans des domaines tels que la logistique, l’exploration de données et le diagnostic médical.

Explorer ce que l’apprentissage automatique peut faire pour l’IA

L’apprentissage automatique s’appuie sur des algorithmes pour analyser d’énormes ensembles de données. À l’heure actuelle, l’apprentissage automatique n’est pas en mesure de fournir le type d’IA que les films présentent. Même les meilleurs algorithmes ne peuvent pas penser, ressentir, présenter une quelconque forme de conscience de soi ou exercer leur libre arbitre. En revanche, ce que l’apprentissage automatique peut faire, c’est réaliser des analyses prédictives bien plus rapidement que n’importe quel être humain. Par conséquent, l’apprentissage automatique peut aider les humains à travailler plus efficacement. Actuellement, l’IA permet donc une analyse, mais les humains doivent toujours considérer les implications de cette analyse en prenant les décisions morales et éthiques qui s’imposent. La section « Relations entre IA et apprentissage automatique » de ce chapitre examine plus en détail la manière dont l’apprentissage automatique contribue à l’IA dans son ensemble. L’essentiel est que l’apprentissage automatique ne constitue que la partie apprentissage de l’IA, et cette partie est loin d’être prête à créer une IA du type de celles que l’on voit dans les films.

La confusion principale entre apprentissage et intelligence est que les gens supposent que, simplement parce qu’une machine s’améliore dans son travail (l’apprentissage), elle est aussi consciente (l’intelligence). Rien ne vient étayer cette vision de l’apprentissage automatique. Le même phénomène se produit lorsque les gens supposent qu’un ordinateur leur cause délibérément des problèmes. L’ordinateur ne sait rien des émotions, et il n’agit donc qu’en fonction de l’entrée fournie et des instructions contenues dans une application pour traiter cette entrée. Une véritable IA finira par émerger lorsque les ordinateurs pourront enfin émuler la combinaison intelligente utilisée par la nature :

• Génétique : apprentissage lent d’une génération à la suivante.

• Enseignement : apprentissage rapide à partir de sources organisées.

• Exploration : apprentissage spontané via des médias et des interactions avec les autres.

Objectifs de l’apprentissage automatique

Actuellement, l’IA est basée sur l’apprentissage automatique qui diffère par essence des statistiques. Certes, l’apprentissage automatique repose sur une base statistique, mais il se fonde sur des hypothèses différentes de celles des statistiques, car les objectifs sont différents. Le Tableau 1.1 énumère certaines caractéristiques à prendre en compte lorsque l’on compare l’IA et l’apprentissage automatique aux statistiques.

Tableau 1.1

Comparaison entre l’apprentissage automatique et les statistiques.

Technique | Apprentissage automatique | Statistiques
Traitement des données | Travaille avec des données volumineuses (le big data) sous forme de réseaux et de graphiques ; les données brutes provenant de capteurs ou du texte Web sont divisées en données d’apprentissage et de test. | Les modèles sont utilisés pour créer un pouvoir prédictif sur de petits échantillons.
Entrée des données | Les données sont échantillonnées, randomisées et transformées pour maximiser la précision de la prédiction d’exemples hors échantillon (ou complètement nouveaux). | Les paramètres interprètent les phénomènes du monde réel et mettent l’accent sur l’ampleur.
Résultat | La probabilité est prise en compte pour comparer ce qui pourrait être la meilleure estimation ou décision. | Les résultats reflètent la variabilité et l’incertitude des paramètres.
Hypothèses | Le scientifique apprend à partir des données. | Le scientifique suppose un certain résultat et tente de le prouver.
Distribution | La distribution est inconnue ou ignorée avant l’apprentissage à partir des données. | Le scientifique suppose une distribution bien définie.
Adaptation | Le scientifique crée un modèle le mieux adapté, mais généralisable. | Le résultat est adapté à la distribution des données actuelles.

Définir les limites de l’apprentissage automatique en fonction du matériel

Les grands ensembles de données nécessitent d’énormes quantités de mémoire. Malheureusement, les exigences ne s’arrêtent pas là. Lorsque vous disposez d’énormes quantités de données et de mémoire, vous devez également disposer de processeurs dotés de plusieurs cœurs et de vitesses élevées. L’un des problèmes que les scientifiques s’efforcent de résoudre est de savoir comment utiliser plus efficacement le matériel existant. Dans certains cas, il n’est tout simplement pas possible d’attendre des jours pour obtenir un résultat à un problème d’apprentissage automatique. Les scientifiques qui veulent connaître la réponse ont besoin de l’obtenir rapidement, même si le résultat n’est pas tout à fait exact. Dans cette optique, les investissements dans un meilleur matériel nécessitent également des investissements dans l’amélioration de la science. Ce livre aborde certaines des questions suivantes dans le cadre de l’amélioration de votre expérience de l’apprentissage automatique :

• Obtenir un résultat utile : au fil du livre, vous découvrirez qu’il faut d’abord obtenir un résultat utile avant de pouvoir l’affiner. En outre, il arrive que la mise au point d’un algorithme aille trop loin et que le résultat devienne assez fragile (et éventuellement inutile en dehors d’un ensemble de données spécifique).

• Poser la bonne question : de nombreuses personnes se sentent frustrées lorsqu’elles tentent d’obtenir une réponse à partir de l’apprentissage automatique parce qu’elles ne cessent d’ajuster leur algorithme sans poser une question différente. Pour utiliser efficacement le matériel, il faut parfois prendre du recul et revoir la question que l’on pose. La question peut être erronée, ce qui signifie que même le meilleur matériel ne trouvera jamais la réponse.

• S’en remettre trop à l’intuition : toutes les questions relatives à l’apprentissage automatique commencent par une hypothèse. Un scientifique utilise son intuition pour créer un point de départ afin de découvrir la réponse à une question. L’échec est plus fréquent que la réussite lorsque l’on travaille sur une expérience d’apprentissage automatique. Votre intuition ajoute de l’art à l’expérience d’apprentissage automatique, mais il arrive que l’intuition soit erronée et que vous deviez revoir vos hypothèses.

Lorsque vous commencez à réaliser l’importance de l’environnement pour l’apprentissage automatique, vous pouvez également commencer à comprendre la nécessité d’avoir le bon matériel et le bon équilibre pour obtenir le résultat souhaité. Les systèmes de pointe actuels s’appuient sur des unités de traitement graphique (GPU) pour effectuer les tâches d’apprentissage automatique. L’utilisation des GPU accélère considérablement le processus d’apprentissage automatique. Une discussion complète sur l’utilisation des GPU sort du cadre de cet ouvrage, mais vous pouvez en apprendre plus sur le sujet à https://devblogs.nvidia.com/parallelforall/bidmach-machine-learning-limit-gpus/ et https://towardsdatascience.com/what-is-a-gpu-and-do-you-need-one-in-deep-learning-718b9597aa0d.

Dépasser les fantasmes de l’IA

Comme beaucoup d’autres technologies, l’IA et l’apprentissage automatique ont tous deux leurs utilisations fantaisistes ou à la mode. Par exemple, certaines personnes utilisent l’apprentissage automatique pour créer des œuvres d’art de type Picasso à partir de photos en utilisant des produits comme NightCafé (https://creator.nightcafe.studio/), qui soutient les personnes qui aiment vraiment cette forme d’art. Pour en savoir plus sur l’utilisation de l’apprentissage automatique pour créer des œuvres d’art, consultez le site https://www.washingtonpost.com/news/innovations/wp/2015/08/31/this-algorithm-cancreate-a-new-van-gogh-or-picasso-in-just-an-hour/. Bien entendu, les problèmes liés à cette utilisation sont nombreux. Tout d’abord, la plupart des gens ne voudraient pas d’un Picasso créé de cette manière, si ce n’est en tant qu’objet à la mode (parce que personne ne l’a fait auparavant). L’intérêt de l’art n’est pas de créer une interprétation intéressante d’une représentation particulière du monde réel, mais plutôt de voir comment l’artiste l’a interprétée. La fin de l’article souligne qu’à ce stade, l’ordinateur ne peut que copier un style existant, et non pas créer un style entièrement nouveau. Les sections suivantes traitent de diverses sortes de fantasmes en matière d’IA et d’apprentissage automatique.

Découvrir les utilisations à la mode de l’IA et de l’apprentissage automatique

L’IA entre dans une ère d’innovation dont on n’entendait parler que dans la science-fiction. Il peut être difficile de déterminer si une utilisation particulière de l’IA est réelle ou s’il s’agit simplement de l’incarnation du rêve d’un scientifique particulier. Par exemple, L’homme qui valait trois milliards (https://fr.wikipedia.org/wiki/L%27Homme_qui_valait_trois_milliards) est une série télévisée qui a semblé fantaisiste à l’époque de sa sortie, et personne ne pensait que des créatures bioniques verraient le jour. Cependant, Hugh Herr (https://www.smithsonianmag.com/innovation/future-robotic-legs-180953040/) et d’autres chercheurs (https://www.fiercebiotech.com/medtech/using-onboard-ai-to-power-quicker-more-complex-prosthetic-hands) ont été encore plus loin, car des jambes et des bras bioniques sont réellement possibles aujourd’hui. Bien sûr, ils ne sont pas encore disponibles pour tout le monde, et la technologie commence seulement à devenir utile. Le film The Six Billion Dollar Man, basé en partie sur la série télévisée L’homme qui valait trois milliards (https://www.cinemablend.com/new/Mark-Wahlberg-Six-Billion-Dollar-Man-Just-Made-Big-Change-91947. html), qui a subi des retards pour diverses raisons (https://screenrant.com/mark-wahlberg-six-billion-dollar-man-delays-updates/), vient brouiller les pistes. Le fait est que l’IA et l’apprentissage automatique offriront des possibilités de créer des technologies étonnantes et que nous en sommes déjà au stade de la création de ces technologies, mais il faut toujours prendre ce que l’on entend avec des pincettes.

L’une des utilisations les plus intéressantes de l’apprentissage automatique à des fins de divertissement est le film B (https://www.cinemablend.com/news/2548939/one-sci-fi-movie-will-be-able-to-film-during-the-pandemic-thanks-to-casting-an-ai-robot-as-its-lead), qui met en scène un androïde nommé Erica. Les inventeurs d’Erica, Hiroshi Ishiguro et Kohei Ogawa, ont passé beaucoup de temps à essayer de la rendre réaliste en tentant de mettre en œuvre les qualités humaines de l’intention et du désir (https://www.yoichimatsuyama.com/conversation-with-evolving-robotic-species-interview-with-hiroshi-ishiguro/). Le résultat est quelque chose qui empiète sur la vallée de l’étrange (uncanny valley ; voir https://fr.wikipedia.org/wiki/Vall%C3%A9e_de_l%27%C3%A9trange et https://www.scientificameri-can.com/article/why-uncanny-valley-human-look-alikes-put-us-on-edge/) d’une nouvelle manière. L’intrigue de ce film est du même ordre que celle d’Ex Machina (https://www.indiewire.com/2020/06/ex-machina-real-robot-erica-science-fiction-movie-1234569484/).

Pour que les utilisations futures de l’IA et de l’apprentissage automatique correspondent aux concepts que la science-fiction a présentés au fil des ans, les programmeurs du monde réel, les scientifiques des données et les autres parties prenantes doivent créer des outils. Rien ne se fait par magie, même si cela peut sembler magique quand on ne sait pas ce qui se passe en coulisses. Pour que les utilisations à la mode de l’IA et de l’apprentissage automatique deviennent des utilisations réelles, les développeurs, les scientifiques des données et les autres parties prenantes doivent continuer à créer des outils concrets qui peuvent être difficiles à imaginer à l’heure actuelle.

Envisager les véritables utilisations de l’IA et de l’apprentissage automatique

L’IA et l’apprentissage automatique sont employés dans un grand nombre d’applications aujourd’hui. Le seul problème est que la technologie fonctionne si bien que vous ne savez même pas qu’elle existe. En fait, vous pourriez être surpris de constater que de nombreux appareils dans votre maison utilisent déjà ces deux technologies. Les deux technologies sont certainement présentes dans votre voiture et, plus particulièrement, sur votre lieu de travail. En fait, les utilisations de l’IA et de l’apprentissage automatique se comptent par millions, et sont souvent bien cachées, même lorsqu’elles sont de nature très spectaculaire. Voici quelques-unes des utilisations possibles de l’IA :

• Détection de fraude : vous recevez un appel ou un message de votre banque vous demandant si vous avez fait un achat particulier. La banque n’est pas curieuse, mais elle vous avertit simplement du fait que quelqu’un d’autre pourrait avoir fait un achat avec votre carte. L’IA intégrée au code de la société émettrice de la carte de crédit a détecté une dépense tout à fait inhabituelle et l’a signalée à quelqu’un.

• Planification des ressources : de nombreuses organisations doivent planifier l’utilisation de leurs ressources de manière efficace. Par exemple, un hôpital peut avoir à déterminer où placer un patient en fonction de ses besoins, de la disponibilité d’experts qualifiés et de la durée d’hospitalisation estimée par le médecin.

• Analyse complexe : les humains ont souvent besoin d’aide pour des analyses complexes parce qu’il y a littéralement trop de facteurs à prendre en considération. Par exemple, un même ensemble de symptômes peut indiquer plusieurs problèmes. Un médecin ou un autre expert peut avoir besoin d’aide pour poser un diagnostic en temps utile afin de sauver la vie d’un patient.

• Automatisation : toute forme d’automatisation peut bénéficier de l’ajout de l’intelligence artificielle pour gérer les changements ou les événements imprévus. Un problème avec certains types d’automatisation aujourd’hui est qu’un événement inattendu, tel qu’un objet qui se trouve au mauvais endroit, peut en fait entraîner l’arrêt de l’automatisation. L’ajout de l’intelligence artificielle à l’automatisation peut permettre à celle-ci de gérer les événements inattendus et de continuer comme si de rien n’était.

• Service à la clientèle : un numéro d’appel pour un service à la clientèle n’a peut-être même pas un être humain au bout du fil. L’automatisation est suffisamment performante pour suivre des scripts et utiliser diverses ressources afin de répondre à la grande majorité de vos questions. Avec une bonne inflexion de la voix (fournie également par l’IA), vous ne serez peut-être même pas capable de vous rendre compte que vous parlez avec un ordinateur.

• Systèmes de sécurité : bon nombre des systèmes de sécurité que l’on trouve aujourd’hui dans les véhicules de toutes sortes s’appuient sur l’IA pour prendre en charge ceux-ci en cas de problème. Par exemple, de nombreux systèmes de freinage automatique s’appuient sur l’IA pour arrêter la voiture en fonction de toutes les données qu’un véhicule peut fournir, comme la direction d’un dérapage.

• Efficacité des machines : l’IA peut aider à contrôler une machine de manière à obtenir une efficacité maximale. L’IA contrôle l’utilisation des ressources afin que le système ne dépasse pas la vitesse autorisée ou d’autres objectifs. Chaque parcelle d’énergie est utilisée avec précision pour fournir les services souhaités.

Cette liste n’est pas exhaustive. L’IA est employée de bien d’autres façons. Cependant, il est également utile de voir les utilisations de l’apprentissage automatique en dehors du domaine courant que beaucoup considèrent comme relevant de l’IA. Voici quelques utilisations de l’apprentissage automatique que vous n’associez peut-être pas à l’intelligence artificielle :

• Contrôles d’accès : dans de nombreux cas, le contrôle d’accès est une proposition de type oui/non. La carte à puce d’un employé donne accès à une ressource de la même manière que les gens utilisent les clés depuis des siècles. Certaines serrures offrent la possibilité de régler les jours et les heures auxquels l’accès est autorisé, mais un contrôle classique, générique, ne répond pas vraiment à tous les besoins. En utilisant l’apprentissage automatique, vous pouvez déterminer si un employé devrait pouvoir accéder à une certaine ressource en fonction de son rôle et de ses besoins. Par exemple, un employé peut avoir accès à une salle de formation lorsque cette formation le concerne directement.

• Protection des animaux : l’océan peut sembler assez grand pour permettre aux animaux et aux navires de cohabiter sans problème. Malheureusement, de nombreux animaux sont heurtés par les navires chaque année. Un algorithme d’apprentissage automatique pourrait permettre aux navires d’éviter les animaux marins en apprenant les sons et les caractéristiques de ces animaux et du bateau.

• Prévision des délais d’attente : la plupart des gens n’aiment pas attendre quand ils n’ont aucune idée du temps que cela va prendre. L’apprentissage automatique permet à une application de déterminer les temps d’attente en fonction des effectifs, de leur charge de travail, de la complexité des problèmes qu’ils tentent de résoudre, de la disponibilité des ressources, etc.

Être utile, être banal

Même si les films donnent l’impression que l’IA va faire un tabac, et que l’on voit parfois des utilisations incroyables de l’IA dans la vie réelle, le fait est que la plupart des utilisations de l’IA sont banales, voire ennuyeuses. Par exemple, un article explique comment Verizon utilise le langage R pour analyser les données relatives aux failles de sécurité (https://www.computerworld.com/article/1630116/how-verizon-analyzes-security-breach-data-with-r-2.html et https://softwarestrategiesblog.com/category/verizons-2020-data-breach-investigations-report-dbir/). La Partie 5 de ce livre fournit des exemples concrets de ce même type d’analyse. La réalisation de cette analyse n’est pas extraordinaire par rapport à d’autres types d’activités d’IA, mais les avantages sont que Verizon économise de l’argent en réalisant l’analyse à l’aide du langage R, et que les résultats sont également meilleurs.

En outre, les développeurs Python (voir les Chapitres 4 et 5 pour plus de détails sur le langage Python) disposent d’une vaste gamme de bibliothèques qui facilitent l’apprentissage automatique. En fait, Kaggle (https://www.kaggle.com/competitions) organise des concours pour permettre aux développeurs Python et aux praticiens R d’affiner leurs compétences en matière d’apprentissage automatique en créant des applications pratiques. Les résultats de ces concours apparaissent souvent plus tard dans des produits que les gens utilisent réellement. Bien que R bénéficie toujours d’un soutien important de la part de la communauté statistique dans la recherche universitaire, la communauté des développeurs Python est particulièrement active dans la création de nouvelles bibliothèques qui ont pour objectif de faciliter le développement d’applications complexes de science des données et d’apprentissage automatique (voir https://www.globalsqa.com/top-20-opensource-python-libraries/ pour les 20 bibliothèques Python les plus utilisées aujourd’hui).

Relations entre IA et apprentissage automatique

L’apprentissage automatique n’est qu’une partie de ce dont un système a besoin pour devenir une IA. C’est l’apprentissage automatique qui permet à l’IA d’accomplir ces tâches :

• S’adapter à de nouvelles circonstances que le développeur initial n’avait pas envisagées.

• Détecter des modèles dans toutes sortes de sources de données.

• Créer de nouveaux comportements basés sur les modèles reconnus.

• Prendre des décisions en fonction de la réussite ou de l’échec de ces comportements.

L’emploi d’algorithmes pour manipuler des données est la pièce maîtresse de l’apprentissage automatique. Pour réussir, une session d’apprentissage automatique doit utiliser un algorithme approprié pour obtenir le résultat souhaité. En outre, les données doivent se prêter à l’analyse à l’aide de l’algorithme souhaité, ou bien elles doivent être soigneusement préparées par les scientifiques.

L’IA englobe de nombreuses autres disciplines afin de simuler avec succès le processus de pensée. Outre l’apprentissage automatique, l’IA comprend normalement :

• Traitement du langage naturel : il s’agit de l’action d’accepter une entrée linguistique et de la mettre sous une forme utilisable par un ordinateur.

• Compréhension du langage naturel : c’est l’acte de déchiffrer la langue afin d’agir sur le sens qu’elle fournit.

• Représentation des connaissances : il s’agit de la capacité de stocker les informations sous une forme qui permet un accès rapide.

• Planification (sous forme de recherche d’objectifs) : c’est la capacité d’utiliser l’information stockée pour tirer des conclusions en temps quasi réel (presque au moment où cela se produit, mais avec un léger retard, parfois si court qu’un être humain ne le remarquera pas, mais que l’ordinateur remarquerait).

• Robotique : il s’agit de la capacité de répondre aux requêtes d’un utilisateur sous une forme physique quelconque.

En fait, vous serez peut-être surpris de constater que le nombre de disciplines nécessaires à la création d’une IA est énorme. Par conséquent, ce livre ne vous expose qu’une partie de ce que contient une IA. Cependant, même la partie consacrée à l’apprentissage automatique peut devenir complexe, car comprendre le monde à travers les entrées de données qu’un ordinateur reçoit est une tâche complexe. Il suffit de penser à toutes les décisions que vous prenez constamment sans y penser. Par exemple, le simple fait de voir quelque chose et de savoir si vous pouvez interagir avec cette chose peut devenir une tâche complexe.

Spécifications de l’IA et de l’apprentissage automatique

Au fur et à mesure que les scientifiques continuent à travailler sur une technologie et à transformer les hypothèses en théories, la technologie s’apparente davantage à l’ingénierie (où les théories sont mises en œuvre) qu’à la science (où les théories sont créées). Au fur et à mesure que les règles régissant une technologie deviennent plus claires, des groupes d’experts travaillent ensemble pour définir ces règles sous forme écrite. Il en résulte des spécifications (un ensemble de règles sur lesquelles tout le monde s’accorde).

En fin de compte, les mises en œuvre des spécifications deviennent des normes gérées par un organe directeur, tel que l’IEEE (Institute of Electrical and Electronics Engineers) ou une combinaison de l’ISO et de la CEI (Organisation internationale de normalisation/Commission électrotechnique internationale). L’IA et l’apprentissage automatique existent depuis suffisamment longtemps pour créer des spécifications, mais vous ne trouverez actuellement aucune norme pour l’une ou l’autre technologie. Cependant, on peut pouvez trouver des projets pour de telles normes dans des organisations comme le National Institute of Standards and Technology (NIST) à https://www.nist.gov/topics/artificial-intelligence/ai-standards.

L’apprentissage automatique repose sur les mathématiques. Les algorithmes déterminent comment interpréter les grands jeux de données (le big data) de manière spécifique. Les bases mathématiques de l’apprentissage automatique sont présentées dans la Partie 3 de ce livre. Vous découvrirez que les algorithmes traitent les données d’entrée de manière spécifique et créent des résultats prévisibles basés sur les modèles de données. Ce qui n’est pas prévisible, ce sont les données elles-mêmes. La raison pour laquelle vous avez besoin de l’IA et de l’apprentissage automatique, c’est de déchiffrer les données de manière à pouvoir visualiser les modèles qu’elles contiennent et leur donner un sens.

Les spécifications détaillées dans la Partie 4 se présentent sous la forme d’algorithmes employés pour effectuer des tâches spécifiques. Lorsque vous arrivez à la Partie 5, vous commencerez à comprendre la raison pour laquelle tout le monde accepte des ensembles de règles spécifiques régissant l’utilisation d’algorithmes pour effectuer des tâches. Il s’agit d’employer un algorithme qui conviendra le mieux aux données dont vous disposez pour atteindre les objectifs spécifiques que vous avez créés. Les professionnels mettent en œuvre des algorithmes en utilisant les langages qui conviennent le mieux à la tâche. L’apprentissage automatique repose sur Python et R, et dans une certaine mesure sur MATLAB, Java, Julia et C++. Pour plus de détails, voir la discussion à https://www.quora.com/What-is-the-best-language-to-use-while-learning-machine-learning-for-the-first-time.

Définir la frontière entre science et ingénierie

Si l’IA et l’apprentissage automatique sont des sciences et non pas des disciplines d’ingénierie, c’est parce qu’ils requièrent tous deux un certain niveau scientifique pour obtenir de bons résultats. L’élément scientifique de l’apprentissage automatique prend de nombreuses formes. Par exemple, il faut tenir compte de la manière dont les données sont utilisées. Certaines données servent de référence pour entraîner un algorithme à obtenir des résultats spécifiques. Les données restantes fournissent les résultats employés pour comprendre les modèles sous-jacents. Il n’existe pas de règles spécifiques régissant l’équilibrage des données ; les scientifiques qui travaillent avec les données doivent découvrir si un équilibre spécifique produit un résultat optimal.

Le nettoyage des données confère également une certaine qualité scientifique au résultat. La manière dont un scientifique prépare les données en vue de leur utilisation est importante. Certaines tâches, telles que la suppression des enregistrements en double, sont effectuées régulièrement. Toutefois, un scientifique peut également choisir de filtrer les données d’une certaine manière ou de n’examiner qu’un sous-ensemble de données. Par conséquent, l’ensemble de données nettoyées utilisé par un scientifique pour des tâches d’apprentissage automatique peut ne pas correspondre précisément à l’ensemble des données nettoyées utilisé par un autre.

Vous pouvez également ajuster les algorithmes d’une certaine manière, ou affiner le fonctionnement d’un algorithme. Là encore, l’idée est de créer un résultat qui expose réellement les schémas souhaités afin que vous puissiez donner un sens aux données. Par exemple, lorsqu’il regarde une image, un robot peut avoir à déterminer les éléments de l’image avec lesquels il peut interagir et ceux avec lesquels il ne peut pas interagir. La réponse à cette question est importante si le robot doit éviter certains éléments pour rester sur la bonne voie ou pour atteindre des objectifs spécifiques.

Lorsque vous travaillez dans un environnement d’apprentissage automatique, vous devez également prendre en compte le problème des données d’entrée. Par exemple, le microphone d’un smartphone ne produira pas exactement les mêmes données d’entrée que le microphone d’un autre smartphone. Les caractéristiques des microphones diffèrent, mais le résultat de l’interprétation des commandes vocales fournies par l’utilisateur doit rester le même. De la même manière, le bruit ambiant modifie la qualité d’entrée de la commande vocale, et le smartphone peut subir certaines formes d’interférences électromagnétiques. Il est clair que les variables auxquelles un concepteur est confronté lorsqu’il crée un environnement d’apprentissage automatique sont à la fois vastes et complexes.

L’art qui se cache derrière l’ingénierie est une partie essentielle de l’apprentissage automatique. L’expérience qu’un scientifique acquiert en travaillant sur des problèmes de données est essentielle, car elle lui permet d’ajouter des valeurs qui améliorent le fonctionnement de l’algorithme. Un algorithme finement réglé peut faire la différence entre un robot qui réussit à se frayer un chemin à travers les obstacles et celui qui les heurte tous.

Prévoir le prochain hiver de l’IA

Le développement de l’apprentissage automatique et de l’IA est lent pour un certain nombre de raisons, telles que le manque de matériel puissant, le manque de données appropriées pour alimenter les algorithmes et l’incapacité des gens à comprendre leurs propres processus de pensée. Les entreprises, cependant, cherchent des moyens de générer rapidement des liquidités en s’appuyant sur les nouvelles technologies. Il est évident qu’un développement lent n’est pas compatible avec un retour sur investissement rapide. Les développeurs-entrepreneurs exacerbent le problème en survendant les technologies. Ils indiquent que l’état de l’art est plus avancé qu’il ne l’est réellement, souvent pour profiter de bénéfices exceptionnels, gagner en puissance et faire avancer leur carrière. En raison de la différence entre le calendrier et les attentes, l’apprentissage automatique et l’IA ont tous deux connu des hivers de l’IA, c’est-à-dire une période pendant laquelle les entreprises montrent peu, voire pas d’intérêt du tout pour le développement de nouveaux processus, de nouvelles technologies ou de nouvelles stratégies.

Le premier hiver de l’IA s’est produit à la suite d’attentes insatisfaites résultant de la survente de la technologie et de difficultés imprévues. Au cours de l’été 1956, plusieurs scientifiques ont participé à un atelier organisé sur le campus du Dartmouth College en vue de créer des machines artificiellement intelligentes. Ils prévoyaient que les machines capables de raisonner aussi efficacement que les humains ne verraient le jour qu’après une génération tout au plus. Ils se sont trompés, et ce n’est qu’aujourd’hui que nous avons réalisé des machines capables d’effectuer des raisonnements mathématiques et logiques aussi efficacement qu’un être humain. Pour parvenir à une véritable compréhension humaine, une IA devrait également faire preuve d’intelligence dans les domaines visuel-spatial, corporel-kinesthésique, créatif, interpersonnel, intrapersonnel et linguistique. Le problème énoncé lors du séminaire au Dartmouth College, ainsi que d’autres projets de l’époque, concernent le matériel, c’est-à-dire la capacité de traitement permettant d’effectuer des calculs suffisamment rapidement pour créer une simulation. Cependant, ce n’est pas vraiment le problème. En effet, le matériel entre en ligne de compte, mais on ne peut pas simuler des processus que l’on ne comprend pas, surtout si l’on ne dispose pas des données nécessaires. Malgré tout, si l’IA est aujourd’hui assez efficace, c’est parce que le matériel est enfin devenu suffisamment puissant pour prendre en charge le nombre de calculs requis.

Quiconque a passé beaucoup de temps à analyser les domaines de l’apprentissage automatique et de l’IA sait que la technologie actuelle a atteint une sorte de plateau. La technologie continue de progresser petit à petit, mais il n’y a pas de nouvelles utilisations de l’apprentissage automatique ou de l’IA à l’heure actuelle. En revanche, les entreprises utilisent efficacement l’apprentissage automatique et l’IA pour générer des bénéfices. Ainsi, certains pensent que l’apprentissage automatique et l’IA se dirigent vers un nouvel hiver de l’IA en raison d’attentes insatisfaites et de ventes excessives (pensez à la voiture autonome), tandis que d’autres estiment que les entreprises sont satisfaites des progrès réalisés actuellement (pensez à l’utilisation de systèmes de recommandation sur des sites tels qu’Amazon.com).

Des sites tels que https://www.thinkautomation.com/bots-and-ai/the-ai-winter-is-coming/ prévoient un hiver de l’IA dans un avenir proche, en partie parce que les termes « apprentissage automatique » , « apprentissage profond » (deep learning) et « IA » sont devenus galvaudés et mal définis. Ces mêmes sites examinent la manière dont les entreprises utilisent réellement l’apprentissage automatique et l’IA aujourd’hui. Dans la plupart des cas, ces sources indiquent que ces technologies sont employées pour les processus d’arrière-plan, et non pas pour les interactions directes avec les clients. L’idée est que l’automatisation utilisée pour les processus directs n’est pas réellement de l’apprentissage automatique ou de l’IA, et que les entreprises finiront par considérer que l’automatisation est distincte de l’apprentissage automatique et de l’IA. En conséquence, elles cesseront à nouveau d’investir dans l’une ou l’autre de ces technologies. Dans de nombreux cas, ceux qui pensent qu’un hiver de l’IA est proche affirment que les scientifiques devraient se concentrer sur l’incroyable éventail de tâches que l’apprentissage automatique et l’IA peuvent accomplir aujourd’hui, plutôt que de continuer à vanter les mérites de tâches futures très hypothétiques.

Avant d’adopter le point de vue que tout le monde s’attend à un nouvel hiver de l’IA, il faut examiner les arguments de ceux qui sont d’un avis contraire. Des sites tels que https://towardsdatascience.com/the-ai-winter-is-over-heres-why-1001e6b7bb0 affirment que l’apprentissage automatique et l’IA sont si profondément ancrés qu’un hiver de l’IA n’est plus vraiment possible. En règle générale, les articles que vous lisez affirment sans détour que l’apprentissage automatique et l’IA n’ont pas atteint certains objectifs, comme la création de véhicules autonomes. Même si ces objectifs ne sont pas réalisables aujourd’hui, il est possible de les atteindre à l’avenir, lorsque les scientifiques auront mené des recherches plus approfondies. En outre, grâce aux recherches menées et aux applications créées, l’apprentissage automatique et l’IA sont devenus rentables, et les entreprises continueront par conséquent à les soutenir. L’article du site Towards Data Science est intéressant parce qu’il met en évidence un grand nombre de fournisseurs qui emploient l’apprentissage automatique dans des applications commerciales majeures qui génèrent d’énormes profits.

Lorsque l’on réfléchit à l’avenir de l’apprentissage automatique et de l’IA, il est probablement préférable d’adopter une approche plus modérée, comme celle que l’on trouve à https://mindmatters.ai/2020/01/so-is-an-ai-winter-really-coming-this-time/. À ce stade, les scientifiques des données et les autres chercheurs doivent prendre du recul et envisager le niveau suivant. Les technologies actuelles ne peuvent pas nous mener plus loin. Elles sont certes rentables, mais elles ne peuvent pas produire une voiture autonome et elles ne peuvent certainement pas produire un robot aussi intelligent que celui que l’on voit dans le film Ex Machina. Par conséquent, s’il y a un hiver de l’IA, il risque d’être doux, car des entreprises comme Amazon et Google ne vont pas mettre leurs technologies à la poubelle sous le prétexte que quelques journalistes pensent qu’elles devraient le faire. En résumé, les concepts, les idées et les technologies que vous découvrirez dans ce livre restent viables et permettent des débouchés professionnels.

DANS CE CHAPITRE

Comprendre et identifier le big data

•

Considérer comment les statistiques et le big data fonctionnent conjointement dans l’apprentissage automatique

•

Définir le rôle des algorithmes dans l’apprentissage automatique

•

Déterminer le fonctionnement de l’entraînement avec des algorithmes dans l’apprentissage automatique

Chapitre 2

L’apprentissage à l’ère du big data

Ce chapitre, qui présente l’essentiel de ce qu’il faut savoir pour effectuer des tâches d’apprentissage automatique, n’entre pas dans les détails, mais il offre plutôt une vue d’ensemble qui vous aidera à comprendre les informations des chapitres suivants. Bien entendu, l’apprentissage commence par des données, et c’est la raison pour laquelle la première partie de ce chapitre traite des données, des très grandes quantités de données, c’est-à-dire le big data. Si un être humain apprend mieux avec davantage de données, il en va de même pour les applications d’apprentissage automatique.

Vous devez disposer d’un moyen d’organiser et d’analyser toutes ces données. De la même manière que vous organisez les informations pour en faciliter l’accès et en dégager plus facilement les tendances, l’ordinateur doit organiser les données et les analyser à l’aide des statistiques, qui est une méthode d’interprétation et de présentation mathématique des tendances des données. La deuxième partie de ce chapitre traite des statistiques appliquées à l’apprentissage automatique.

Une fois que vous disposez de vos données et qu’elles sont présentées dans un ordre utile et compréhensible, vous pouvez commencer à les transmettre à des algorithmes qui les manipulent d’une certaine manière afin de produire un résultat. Le résultat indique quelque chose que vous auriez pu déduire par vous-même. La troisième partie de ce chapitre examine la relation entre les algorithmes et l’apprentissage automatique.

Les algorithmes d’apprentissage automatique ne sont utiles que lorsqu’ils sont entraînés, car l’entraînement permet à l’ordinateur d’exploiter des analyses antérieures afin de travailler avec de nouvelles données qu’il n’a jamais vues auparavant. La quatrième partie de ce chapitre permet de commencer à comprendre l’entraînement des algorithmes.

Aspects fondamentaux de l’apprentissage automatique

Les ordinateurs gèrent les données par le biais d’applications qui effectuent des tâches à l’aide de différentes sortes d’algorithmes. Si l’on veut définir simplement ce qu’est un algorithme, on peut dire qu’il s’agit d’un ensemble systématique d’opérations à effectuer sur un jeu de données, ce qui peut s’assimiler à une procédure. Les quatre opérations de base sur les données sont la création, la lecture, la mise à jour et la suppression (que l’on désigne en anglais par l’acronyme CRUD, soit Create, Read, Update, Delete). Toutes ces opérations peuvent vous paraître simples, mais l’exécution de ces tâches essentielles constitue la base de tout ce que vous faites avec un ordinateur.

Au fur et à mesure que le jeu de données s’agrandit, l’ordinateur peut utiliser les algorithmes trouvés dans une application pour effectuer davantage de travail. L’utilisation d’immenses jeux de données, connus sous le nom de big data, permet à un ordinateur d’effectuer un travail basé sur la reconnaissance de modèles d’une manière non déterministe. Les algorithmes déterminent la manière dont une machine interprète le big data. L’algorithme utilisé pour effectuer l’apprentissage automatique affecte le résultat du processus d’apprentissage et, par conséquent, les résultats que vous obtenez. En bref, pour créer une configuration informatique capable d’apprendre, il faut un jeu de données suffisamment important pour que les algorithmes puissent le gérer d’une manière qui permette la reconnaissance de modèles, et cette reconnaissance doit utiliser un sous-ensemble simple pour faire des prédictions (analyse statistique) de l’ensemble de l’ensemble des données.

Le big data existe aujourd’hui sous de nombreuses formes. Parmi les sources évidentes, on peut citer les bases de données en ligne, telles que celles qui sont créées par les vendeurs pour suivre les achats des consommateurs. Cependant, on trouve également de nombreuses autres sources de données qui sont moins évidentes, et ce sont souvent celles qui fournissent les meilleures ressources pour réaliser quelque chose d’intéressant. Trouver les bonnes sources dans le big data permet de créer des scénarios d’apprentissage automatique dans lesquels une machine peut apprendre d’une manière spécifique et produire un résultat souhaité.

Les statistiques, l’une des méthodes d’apprentissage automatique étudiées dans ce livre, sont une méthode de description des problèmes à l’aide des mathématiques. En combinant le big data et les statistiques, vous pouvez créer un environnement d’apprentissage automatique dans lequel la machine prend en compte la probabilité d’un événement donné. Cependant, il est faux de dire que les statistiques sont la seule méthode d’apprentissage automatique. Ce chapitre présente également les autres formes d’apprentissage automatique qui sont exploitées actuellement.

Le big data est présent aujourd’hui dans de nombreux endroits. Des sources évidentes sont les bases de données en ligne, telles que celles créées par les fournisseurs pour suivre les achats des consommateurs. Cependant, vous trouvez aussi de nombreuses sources de données non évidentes, et ce sont souvent elles qui fournissent les ressources les plus intéressantes. Trouver des sources appropriées de big data vous permet de créer des scénarios d’apprentissage automatique dans lesquels une machine peut apprendre d’une manière spécifique et produire un résultat souhaité.

Avant qu’un algorithme puisse faire la moindre chose en matière d’apprentissage automatique, vous devez l’entraîner. Le processus d’entraînement modifie la façon dont l’algorithme voit le big data. La dernière section de ce chapitre vous aide à comprendre que l’entraînement utilise en fait un sous-ensemble de données comme méthode pour créer les modèles dont l’algorithme a besoin pour reconnaître des cas spécifiques à partir des cas plus généraux que vous fournissez dans le cadre de l’entraînement.

Définir le big data

Le « big data » ne se résume pas à une simple base de données volumineuse. Certes, le big data implique beaucoup de données, mais il inclut également l’idée de complexité et de profondeur. Une source de big data décrit quelque chose de manière suffisamment détaillée pour que vous puissiez commencer à exploiter ces données afin de résoudre des problèmes pour lesquels la programmation généraliste s’avère inadéquate.

Pour illustrer la complexité du big data, prenons l’exemple des voitures autonomes de Google (https://waymo.com/). La voiture doit tenir compte non seulement de la mécanique du matériel et de la position de la voiture dans l’espace, mais aussi des effets des décisions humaines, de l’état de la route, des conditions environnementales et des autres véhicules sur la route, ce qui explique pourquoi on ne croise pas encore beaucoup de ces voitures (voir https://www.vox.com/future-perfect/2020/2/14/21063487/self-driving-cars-autonomous-vehicles-waymo-cruise-uber). Il n’est pas difficile d’imaginer certains des problèmes spécifiques à l’homme que les voitures autonomes devront résoudre, tels que les personnes qui font une sieste alors qu’elles devraient surveiller la route, même si la voiture autonome est aux commandes (https://robbreport.com/motors/cars/canadian-police-arrest-sleeping-driver-tesla-autopilot-1234570071/).

La source de données d’une voiture autonome (ou de toute autre entreprise complexe) contient de nombreuses variables, qui affectent toutes le véhicule d’une manière ou d’une autre. La programmation traditionnelle peut être en mesure d’analyser toutes les données, mais pas en temps réel. Il n’est pas souhaitable que la voiture s’écrase contre un mur et que l’ordinateur décide finalement cinq minutes plus tard que la voiture va s’écraser contre un mur. Le traitement doit se faire en temps voulu pour que la voiture puisse éviter le mur.

L’acquisition de données volumineuses peut également s’avérer décourageante. Le volume de l’ensemble de données n’est pas le seul problème à prendre en compte, car il est également essentiel de réfléchir à la manière dont le jeu de données est stocké et transféré pour que le système puisse le traiter. Dans la plupart des cas, les développeurs essaient de stocker l’ensemble de données en mémoire pour permettre un traitement rapide. L’utilisation d’un disque dur pour stocker les données s’avérerait trop coûteuse en termes de temps.

Lorsque l’on pense au big data, on pense aussi à l’anonymat. Le big data pose des problèmes de protection de la vie privée. Toutefois, en raison de la manière dont fonctionne l’apprentissage automatique, il n’est pas particulièrement utile de connaître des informations précises sur les individus. L’apprentissage automatique consiste à déterminer des modèles, c’est-à-dire à analyser des données d’entraînement de manière à ce que l’algorithme ainsi entraîné puisse effectuer des tâches pour lesquelles le développeur ne l’avait pas programmé à l’origine. Les données personnelles n’ont pas leur place dans un tel environnement.

Enfin, le big data concerne de tels volumes de données que les humains ne peuvent pas raisonnablement visualiser celles-ci sans aide. Le big data est partiellement défini par le fait qu’un être humain peut en tirer quelque chose, mais l’ampleur même du jeu de données rend la reconnaissance des modèles impossible (ou prendrait trop de temps à réaliser). L’apprentissage automatique aide les humains à comprendre et à utiliser les données volumineuses.

Grand jusqu’où ?

Les données volumineuses peuvent vraiment devenir très volumineuses. Supposons par exemple que votre voiture autonome Google dispose de quelques caméras HD et de quelques centaines de capteurs qui fournissent des informations à un débit de 100 mesures/s. Vous pourriez vous retrouver avec un ensemble de données brutes dont l’entrée dépasse les 100 Mbit/s. Le traitement d’une telle quantité de données est incroyablement difficile.

Une partie du problème actuel consiste à déterminer comment contrôler les données volumineuses. Actuellement, on tente de tout enregistrer, ce qui produit un ensemble de données massives et détaillées. En revanche, ce jeu de données n’est pas bien formaté, ce qui le rend encore une fois difficile à utiliser. Au fil de ce livre, vous découvrirez des techniques qui permettent de contrôler à la fois la taille et l’organisation du big data, de sorte que les données deviennent utiles pour faire des prédictions.

Sources du big data

Avant de pouvoir utiliser le big data pour une application d’apprentissage automatique, il faut disposer d’une source de big data. Bien sûr, la première chose à laquelle pensent la plupart des développeurs est l’énorme base de données de l’entreprise, qui peut contenir des informations intéressantes, mais ce n’est qu’une source parmi d’autres. En réalité, les bases de données de votre entreprise ne contiennent peut-être même pas de données particulièrement utiles pour un besoin spécifique. Les sections suivantes décrivent les emplacements que vous pouvez utiliser pour obtenir des sources de big data supplémentaires.

Créer une nouvelle source de données

Pour créer des sources viables de big data répondant à des besoins spécifiques, vous devrez peut-être créer une nouvelle source de données. Dans de nombreux cas, les développeurs ont conçu les sources de données existantes en fonction des besoins de l’architecture client-serveur, et ces sources peuvent ne pas fonctionner correctement pour les scénarios d’apprentissage automatique parce qu’elles n’ont pas la profondeur requise (car leur optimisation pour économiser de l’espace sur les disques durs présente des inconvénients). En outre, au fur et à mesure que vous vous familiariserez avec l’apprentissage automatique, vous vous apercevrez que vous posez des questions auxquelles les bases de données d’entreprise standard ne peuvent pas répondre. Dans cette optique, les sections suivantes décrivent quelques nouvelles sources intéressantes de big data.

Obtenir des données de sources publiques

Les gouvernements, les universités, les organisations à but non lucratif et d’autres entités gèrent souvent des bases de données accessibles au public que vous pouvez utiliser seules ou en combinaison avec d’autres bases de données pour créer des sources de big data pour l’apprentissage automatique. Par exemple, vous pouvez combiner plusieurs systèmes d’information géographique (SIG) pour créer le big data nécessaire à la prise de décisions concernant, par exemple, l’emplacement de nouveaux magasins ou de nouvelles usines. L’algorithme d’apprentissage automatique peut prendre en compte toutes sortes d’informations, depuis le montant des taxes à payer jusqu’à l’altitude du terrain (qui peut contribuer à rendre votre magasin plus visible).

Une source officielle de données publiques en France (ce que l’on appelle l’open data) est le site gouvernemental https://www.data.gouv.fr/fr/.

L’avantage d’utiliser des données publiques est qu’elles sont généralement gratuites, même pour un usage commercial (éventuellement vous allez payer une redevance nominale). En outre, de nombreuses organisations qui les ont créées maintiennent ces sources dans un état presque parfait parce qu’elles ont un mandat, qu’elles utilisent les données pour générer des revenus ou qu’elles les utilisent en interne. Lorsque vous obtenez des données de source publique, vous devez prendre en compte un certain nombre de questions pour vous assurer que vous obtenez quelque chose d’utile. Voici quelques-uns des critères à prendre en compte avant de prendre une décision :

• le coût, s’il y a lieu, de l’utilisation de la source de données ;

• le formatage de la source de données ;

• l’accès à la source de données (ce qui signifie avoir l’infrastructure appropriée en place, par exemple une connexion Internet permanente lors de l’utilisation de données de X (ex-Twitter) ;

• l’autorisation d’utiliser la source de données (certaines sources de données sont protégées par le droit d’auteur) ;

• les problèmes potentiels de nettoyage des données pour les rendre utiles dans un projet d’apprentissage automatique ;

• les problèmes de sécurité potentiels liés à l’accès aux données, à leur ajout à d’autres sources de données et à leur gestion au niveau local ;

• s’assurer que les données sont les données originales, plutôt que des données qui prétendent être originales mais qui ont été biaisées ou modifiées d’une autre manière qui changerait les résultats de leur utilisation ;

• déterminer que les données ne contiennent pas d’informations personnellement identifiables que l’auteur de la source de données n’a peut-être pas le droit d’utiliser.

Obtenir des données de sources privées

Vous pouvez obtenir des données auprès d’organisations privées telles qu’Amazon (voir Open Data, https://aws.amazon.com/opendata/) et Google (voir Public Data Explorer, https://www.google.com/public-data/directory), qui gèrent toutes deux d’immenses bases de données contenant toutes sortes d’informations utiles. Dans certains cas, à l’exception des sources de données partagées publiquement, vous devez vous attendre à payer pour accéder aux données, en particulier lorsqu’elles sont utilisées dans un cadre commercial. Il se peut que vous ne soyez pas autorisé à télécharger les données sur vos serveurs personnels, et cette restriction peut affecter la façon dont vous utilisez les données dans un environnement d’apprentissage automatique. Par exemple, certains algorithmes fonctionnent plus lentement avec des données auxquelles ils doivent accéder par petits morceaux.

Le principal avantage de l’utilisation de données provenant d’une source privée est que vous pouvez vous attendre à une meilleure cohérence. Les données sont probablement plus propres que celles provenant d’une source publique. En outre, vous avez généralement accès à une base de données plus importante, avec une plus grande variété de types de données. Bien entendu, tout dépend de l’emplacement où vous obtenez les données.

Créer de nouvelles données à partir de données existantes

Vos données existantes ne sont peut-être pas adaptées aux scénarios d’apprentissage automatique, mais cela ne vous empêche pas de créer une nouvelle source de données en utilisant les anciennes données comme point de départ. Par exemple, vous pouvez disposer d’une base de données clients qui contient toutes les commandes des clients, mais les données ne sont pas utiles pour l’apprentissage automatique parce qu’elles ne comportent pas les balises nécessaires pour regrouper les données en types spécifiques. On peut s’attendre à voir apparaître de nouveaux types d’emploi, notamment des personnes qui sont chargées d’optimiser les données pour les rendre plus adaptées à l’apprentissage automatique (y compris l’ajout de types d’informations spécifiques tels que les balises).

L’apprentissage automatique aura un effet significatif sur votre entreprise. L’article publié à https://www.computerworld.com/article/1635873/how-machine-learning-will-affect-your-business.html décrit quelques-unes des façons dont l’apprentissage automatique va modifier l’exercice de votre activité. L’un des points abordés dans cet article est que l’apprentissage automatique fonctionne généralement sur 80 % des données. Dans 20 % des cas, il faut encore que des humains prennent le relais pour décider de la manière de réagir aux données et d’agir en conséquence. L’apprentissage automatique permet d’économiser de l’argent en prenant en charge des tâches répétitives que les humains ne veulent pas vraiment faire (ce qui les rend inefficaces). Cependant, l’apprentissage automatique n’élimine pas complètement le besoin d’êtres humains, et il crée le besoin de nouveaux types d’emplois qui sont un peu plus intéressants que ceux que l’apprentissage automatique a pris en charge. Il est également important de prendre en compte le fait que vous aurez besoin de plus d’humains au début, jusqu’à ce que les modifications qu’ils apportent entraînent l’algorithme à comprendre quels types de changements apporter aux données.

Utiliser des sources de données existantes

Votre organisation possède des données cachées dans toutes sortes d’endroits. Le problème est de reconnaître les données en tant que telles. Par exemple, vous pouvez avoir des capteurs sur une chaîne de montage qui suivent la façon dont les produits se déplacent dans le processus d’assemblage et garantissent que la chaîne de montage reste efficace. Ces mêmes capteurs peuvent potentiellement fournir des informations dans un scénario d’apprentissage automatique, car ils pourraient indiquer comment le mouvement des produits affecte la satisfaction des clients ou le prix que vous payez pour l’affranchissement. L’idée est de découvrir comment créer des mashups (des mélanges de données) qui présentent les données existantes sous la forme d’un nouveau type de données, ce qui vous permettrait d’améliorer le fonctionnement de votre organisation.

Le big data peuvent provenir de n’importe quelle source, même de votre courrier électronique. L’article à https://www.semrush.com/blog/deep-learning-an-upcoming-gmail-feature-that-will-answer-your-emails-for-you/ explique comment Google utilise votre courrier électronique pour créer une liste de réponses potentielles à de nouveaux courriels. Pour en savoir plus sur la procédure suivie par l’utilisateur, consultez le site https://www.lifewire.com/how-to-send-canned-replies-automatically-in-gmail-1172080. Au lieu de devoir répondre à chaque courriel individuellement, vous pouvez simplement sélectionner une réponse standard au bas de la page. Ce type d’automatisation n’est pas possible sans la source de données d’origine des courriels. En cherchant des sources de big data dans des endroits spécifiques, vous ne verrez pas le big data qui se trouve dans des endroits courants que la plupart des gens ne considèrent pas comme des sources de données. Les applications de demain s’appuieront sur ces sources de données alternatives, mais pour créer ces applications, vous devez commencer à voir les données cachées dès aujourd’hui.

Certaines de ces applications existent déjà, même si vous n’en êtes pas du tout conscient. La vidéo à https://research.microsoft.com/apps/video/default.aspx?id=256288 met en évidence la présence de ces types d’applications. Lorsque vous aurez terminé la vidéo, vous commencerez à comprendre que de nombreuses utilisations de l’apprentissage automatique sont déjà en place et que les utilisateurs les considèrent déjà comme allant de soi (ou n’ont même pas idée de l’existence de l’application). De nombreux développeurs considèrent la quête d’une expérience ultime d’apprentissage automatique comme l’algorithme maître ; c’est d’ailleurs le sujet d’un livre de Pedro Domingos intitulé The Master Algorithm (https://www.amazon.com/exec/obidos/ASIN/0465094279/datacservip0f-20/).

Localiser des sources de données de test

Au fur et à mesure que vous avancez dans ce livre, vous découvrez la nécessité d’apprendre à l’algorithme que vous utilisez (ne vous préoccupez pas des algorithmes spécifiques, car vous en verrez un certain nombre plus loin dans l’ouvrage) à reconnaître différents types de données et à en faire quelque chose d’intéressant. Ce processus d’entraînement garantit que l’algorithme réagit correctement aux données qu’il reçoit une fois l’entraînement achevé. Bien entendu, vous devez également tester l’algorithme pour déterminer si l’entraînement a été un succès. Dans de nombreux cas, le livre vous aide à découvrir des moyens de diviser une source de données en composants de données d’entraînement et de test afin d’obtenir le résultat souhaité. Ensuite, après l’entraînement et les tests, l’algorithme peut travailler avec de nouvelles données en temps réel pour effectuer les tâches dont vous avez vérifié la capacité.

Dans certains cas, il se peut que vous ne disposiez pas de suffisamment de données au départ pour l’entraînement (le test initial essentiel) et les tests. Dans ce cas, vous devrez peut-être créer une configuration de test pour générer davantage de données, vous appuyer sur des données générées en temps réel ou créer artificiellement la source de données de test. Vous pouvez également utiliser des données similaires provenant de sources existantes, telles qu’une base de données publique ou privée. Le fait est que vous avez besoin de données d’entraînement et de test qui produiront un résultat connu avant de lancer votre algorithme dans le monde réel avec des données incertaines.

Rôle des statistiques dans l’apprentissage automatique

Certains sites en ligne voudraient vous faire croire que les statistiques et l’apprentissage automatique sont deux technologies complètement différentes. Par exemple, lorsque vous lisez « Statistics vs. Machine Learning, fight ! » (http://brenocon.com/blog/2008/12/statistics-vs-machine-learning-fight/), on a l’impression que les deux technologies ne sont pas seulement différentes, mais carrément hostiles l’une à l’égard de l’autre (les mises à jour ultérieures de l’article sont importantes, car elles montrent le processus d’apprentissage que l’auteur, et beaucoup d’entre nous, traversent en essayant de comprendre ces technologies). Le fait est que les statistiques et l’apprentissage automatique ont beaucoup en commun et que les statistiques représentent l’une des cinq tribus (écoles de pensée) qui rendent l’apprentissage automatique possible. Voici la liste de ces cinq tribus :

• Symbolistes : l’origine de cette tribu se trouve dans la logique et la philosophie. Ce groupe s’appuie sur la déduction inverse pour résoudre les problèmes.

• Connexionnistes : l’origine de cette tribu se trouve dans les neurosciences. Ce groupe s’appuie sur la rétropropagation pour résoudre les problèmes.

• Évolutionnistes : l’origine de cette tribu se trouve dans la biologie de l’évolution. Ce groupe s’appuie sur la programmation génétique pour résoudre les problèmes.

• Bayésiens : l’origine de cette tribu se trouve dans les statistiques. Ce groupe s’appuie sur l’inférence probabiliste pour résoudre les problèmes.

• Analogiseurs : l’origine de cette tribu se trouve dans la psychologie. Ce groupe s’appuie sur les machines à noyaux pour résoudre les problèmes.

L’objectif ultime de l’apprentissage automatique est de combiner les technologies et les stratégies adoptées par les cinq tribus pour créer un algorithme unique (l’algorithme maître) capable d’apprendre n’importe quoi (Figure 2.1). Bien entendu, cet objectif est encore loin d’être atteint. Néanmoins, des scientifiques tels que Pedro Domingos (http://homes.cs.washington.edu/∼pedrod/) travaillent actuellement à la réalisation de cet objectif.

[picture]

Figure 2.1 Les cinq tribus combineront leurs efforts en vue de l’algorithme maître.

Ce livre suit en grande partie la stratégie de la tribu bayésienne, en ce sens que la plupart des problèmes sont résolus à l’aide d’une forme d’analyse statistique. Des stratégies adoptées par d’autres tribus sont décrites, mais la principale raison pour laquelle vous commencez par les statistiques est que la technologie est déjà bien établie et comprise. En fait, de nombreux éléments des statistiques relèvent davantage de l’ingénierie (dans laquelle les théories sont mises en œuvre) que de la science (dans laquelle les théories sont créées). La section suivante du chapitre approfondit les cinq tribus en examinant les types d’algorithmes utilisés par chacune d’entre elles. Il est essentiel de comprendre le rôle des algorithmes dans l’apprentissage automatique pour en définir le fonctionnement.

Comprendre le rôle des algorithmes

Dans le domaine de l’apprentissage automatique, tout tourne autour des algorithmes. Un algorithme est une procédure ou une formule utilisée pour résoudre un problème. Le domaine du problème influe sur le type d’algorithme nécessaire, mais le principe de base est toujours le même : résoudre un type de problème, comme conduire une voiture ou jouer aux dominos. Dans le premier cas, les problèmes sont complexes et nombreux, mais le problème ultime consiste à transporter un passager d’un endroit à un autre sans que la voiture ait d’accident. De la même manière, l’objectif d’un jeu de dominos est de gagner. Les sections suivantes abordent les algorithmes de manière plus détaillée.

Définir ce que font les algorithmes

Un algorithme est une sorte de conteneur. Il fournit une boîte pour stocker une méthode permettant de résoudre un type particulier de problème. Les algorithmes traitent les données à travers une série d’états bien définis. Ces états ne sont pas nécessairement déterministes, mais ils sont néanmoins définis. L’objectif est de créer un résultat qui résout un problème. Dans certains cas, l’algorithme reçoit des données qui aident à définir la sortie, mais l’accent est toujours mis sur la sortie.

Les algorithmes doivent exprimer les transitions entre les états à l’aide d’un langage bien défini et formel que l’ordinateur peut comprendre. En traitant les données et en résolvant le problème, l’algorithme définit, affine et exécute une fonction. La fonction est toujours spécifique au type de problème traité par l’algorithme.

Les cinq techniques principales

Comme décrit dans la section précédente, chacune des cinq tribus possède une technique et une stratégie différentes pour résoudre les problèmes, ce qui se traduit par des algorithmes uniques. La combinaison de ces algorithmes devrait permettre d’aboutir à l’algorithme maître capable de résoudre n’importe quel problème donné. Les sections suivantes donnent un aperçu des cinq principales techniques algorithmiques.

Raisonnement symbolique

Le terme déduction inverse est couramment utilisé pour désigner l’induction. Dans le raisonnement symbolique, la déduction étend le domaine de la connaissance humaine, tandis que l’induction élève le niveau de la connaissance humaine. L’induction ouvre généralement de nouveaux champs d’exploration, tandis que la déduction explore ces champs. Cependant, le plus important est que l’induction est la partie scientifique de ce type de raisonnement, tandis que la déduction est l’ingénierie. Les deux stratégies travaillent main dans la main pour résoudre les problèmes en ouvrant d’abord un champ d’exploration potentiel pour résoudre le problème, puis en explorant ce champ afin de déterminer s’il permet effectivement de le résoudre.

En guise d’exemple de cette stratégie, la déduction dirait que si un arbre est vert et que les arbres verts sont vivants, l’arbre doit être vivant. Dans le cadre de l’induction, on dira que l’arbre est vert et que l’arbre est également vivant ; par conséquent, les arbres verts sont vivants. L’induction fournit la réponse à la question de savoir ce qu’il manque à une connaissance étant donné une entrée et une sortie connues.

Connexions calquées sur les neurones du cerveau

Les connexionnistes sont peut-être la plus célèbre des cinq tribus. Cette tribu s’efforce de reproduire les fonctions du cerveau en utilisant du silicium au lieu de neurones. Essentiellement, chacun des neurones (créés sous la forme d’un algorithme qui modélise l’homologue du monde réel) résout une petite partie du problème, et l’utilisation de nombreux neurones en parallèle permet de résoudre le problème dans son ensemble.

L’utilisation de la rétropropagation, ou propagation à rebours des erreurs, vise à déterminer les conditions dans lesquelles les erreurs sont éliminées des réseaux construits pour ressembler aux neurones humains en modifiant les poids (l’importance d’une entrée particulière dans le résultat) et les biais (les caractéristiques sélectionnées) du réseau. L’objectif est de continuer à modifier les poids et les biais jusqu’à ce que la sortie réelle corresponde à la sortie cible. À ce stade, le neurone artificiel se déclenche et transmet sa solution au neurone suivant. La solution créée par un seul neurone n’est qu’une partie de la solution globale. Chaque neurone transmet des informations au neurone suivant jusqu’à ce que le groupe de neurones crée une sortie finale.

Algorithmes évolutionnistes testant les variations

Les évolutionnistes s’appuient sur les principes de l’évolution pour résoudre les problèmes. En d’autres termes, cette stratégie est basée sur la survie des plus aptes (en éliminant toutes les solutions qui ne correspondent pas au résultat souhaité). Une fonction d’adaptation (fitness function) détermine la viabilité de chaque fonction dans la résolution d’un problème.

En utilisant une structure arborescente, la méthode de résolution recherche la meilleure solution sur la base de la sortie de la fonction. Le gagnant de chaque niveau d’évolution se voit confier la construction des fonctions du niveau suivant. L’idée est que le niveau suivant se rapprochera de la résolution du problème, mais ne le résoudra peut-être pas complètement, ce qui signifie qu’un autre niveau est nécessaire. Cette tribu particulière s’appuie fortement sur la récursivité et sur des langages qui prennent bien en charge la récursivité pour résoudre les problèmes. Les algorithmes évolutifs sont un résultat intéressant de cette stratégie, car une génération d’algorithmes construit en fait la génération suivante.

Inférence bayésienne

Les bayésiens utilisent diverses méthodes statistiques pour résoudre les problèmes. Étant donné que les méthodes statistiques peuvent créer plus d’une solution apparemment correcte, le choix d’une fonction consiste à déterminer celle qui a la plus grande probabilité de réussite. Par exemple, lorsque vous utilisez ces techniques, vous pouvez accepter un ensemble de symptômes comme données d’entrée et déterminer la probabilité qu’une maladie particulière résulte de ces symptômes comme données de sortie. Étant donné que plusieurs maladies présentent les mêmes symptômes, la probabilité est importante car l’utilisateur verra dans certains cas qu’une sortie à faible probabilité est en fait la sortie correcte pour une circonstance donnée.

En fin de compte, cette tribu soutient l’idée qu’il ne faut jamais faire entièrement confiance à une hypothèse (un résultat que quelqu’un vous a donné) sans avoir vu les preuves utilisées pour l’établir (les données que l’autre personne a utilisées pour établir l’hypothèse). L’analyse des preuves permet de prouver ou d’infirmer l’hypothèse qu’elles soutiennent. Par conséquent, il n’est pas possible de déterminer de quelle maladie souffre une personne avant d’en avoir testé tous les symptômes. L’un des résultats les plus probants de cette tribu est le filtre antispam.

Systèmes qui apprennent par analogie

Les analogiseurs utilisent des machines à noyau pour reconnaître des modèles dans les données. En reconnaissant le modèle d’un ensemble d’entrées et en le comparant au modèle d’une sortie connue, vous pouvez créer une solution au problème. L’objectif est d’utiliser la similarité pour déterminer la meilleure solution à un problème. C’est le type de raisonnement qui détermine que l’utilisation d’une solution particulière a fonctionné dans une circonstance donnée à un moment donné ; par conséquent, l’utilisation de cette solution pour un ensemble de circonstances similaires devrait également fonctionner. Les systèmes de recommandation sont l’un des résultats les plus connus de cette tribu. Par exemple, lorsque vous achetez un produit sur Amazon, le système de recommandation propose d’autres produits connexes que vous pourriez également vouloir acheter.

Définir la signification de l’entraînement

Beaucoup de gens sont plutôt habitués à l’idée que les applications commencent par une fonction, acceptent des données en entrée et fournissent ensuite un résultat. Par exemple, un programmeur peut créer une fonction appelée Add() qui accepte deux valeurs en entrée, telles que 1 et 2. Le résultat de Add() est 3. La sortie de ce processus est une valeur. Autrefois, écrire un programme signifiait comprendre la fonction utilisée pour manipuler les données afin de créer un résultat donné avec certaines entrées.

L’apprentissage automatique inverse ce processus. Dans ce cas, vous savez que vous disposez d’entrées, telles que 1 et 2, et que le résultat souhaité est 3. Cependant, vous ne savez pas quelle fonction appliquer pour créer le résultat voulu. L’entraînement fournit à l’algorithme de l’apprenant toutes sortes d’exemples des entrées souhaitées et des résultats attendus de ces entrées. L’apprenant utilise ensuite ces données pour créer une fonction. En d’autres termes, l’entraînement est le processus par lequel l’algorithme d’apprentissage fait correspondre une fonction flexible aux données. La sortie est généralement la probabilité d’une certaine classe ou une valeur numérique.

Un seul algorithme d’apprentissage peut apprendre beaucoup de choses différentes, mais tous les algorithmes ne sont pas adaptés à certaines tâches. Certains algorithmes sont suffisamment généraux pour jouer aux échecs, reconnaître des visages sur Facebook et diagnostiquer le cancer chez les patients. Un algorithme réduit les entrées de données et les résultats attendus de ces entrées à une fonction dans tous les cas, mais la fonction est spécifique au type de tâche que vous voulez que l’algorithme accomplisse.

Le secret de l’apprentissage automatique est la généralisation. L’objectif est de généraliser la fonction de sortie de manière à ce qu’elle fonctionne sur des données autres que celles du jeu d’entraînement. Prenons l’exemple d’un filtre antispam. Votre dictionnaire contient 100 000 mots (en fait, il s’agit d’un petit dictionnaire). Un ensemble de données d’entraînement limité à 4 000 ou 5 000 combinaisons de mots doit créer une fonction généralisée capable de trouver les spams dans les 2¹⁰⁰⁰⁰⁰ combinaisons que la fonction verra lorsqu’elle travaillera avec des données réelles.

De ce point de vue, l’entraînement peut sembler impossible et l’apprentissage encore pire. Cependant, pour créer cette fonction généralisée, l’algorithme d’apprentissage ne repose que sur trois composants :

• Représentation : l’algorithme d’apprentissage crée un modèle, c’est-à-dire une fonction qui produira un résultat donné pour des entrées spécifiques. La représentation est un ensemble de modèles qu’un algorithme d’apprentissage peut apprendre. En d’autres termes, l’algorithme d’apprentissage doit créer un modèle qui produira les résultats souhaités à partir des données d’entrée. Si l’algorithme d’apprentissage ne peut pas accomplir cette tâche, il ne peut pas apprendre à partir des données et les données sont en dehors de l’espace d’hypothèses de l’algorithme d’apprentissage. Une partie de la représentation consiste à découvrir les caractéristiques (éléments de données dans la source de données) à utiliser pour le processus d’apprentissage.

• Évaluation : l’apprenant peut créer plus d’un modèle. Cependant, il ne sait pas faire la différence entre les bons et les mauvais modèles. Une fonction d’évaluation détermine lequel des modèles fonctionne le mieux pour créer un résultat souhaité à partir d’un ensemble d’entrées. La fonction d’évaluation attribue une note aux modèles, car plusieurs modèles peuvent fournir les résultats souhaités.

• Optimisation : à un moment donné, le processus d’entraînement produit un ensemble de modèles qui peuvent généralement donner le bon résultat pour un ensemble donné d’entrées. À ce stade, le processus d’entraînement examine ces modèles pour déterminer celui qui fonctionne le mieux. Le meilleur modèle constitue le résultat du processus d’entraînement.

Une grande partie de ce livre est consacrée à la représentation. Par exemple, au Chapitre 10, vous découvrirez comment créer un détecteur de spam fonctionnel à l’aide de l’algorithme du bayésien naïf, basé sur une représentation probabiliste du problème. Cependant, le processus d’entraînement ne se limite pas au simple choix d’une représentation. Les trois étapes entrent en jeu lors du processus d’entraînement. Heureusement, vous pouvez commencer par vous concentrer sur la représentation et laisser les différentes bibliothèques présentées dans le livre faire le reste du travail à votre place.

DANS CE CHAPITRE

Découvrir comment l’apprentissage automatique va contribuer à créer des technologies d’avenir utiles

•

Développer de nouveaux types de métiers grâce à l’apprentissage automatique

•

Réfléchir à la manière dont l’apprentissage automatique peut créer des problèmes

Chapitre 3

Un regard sur l’avenir

La technologie de l’apprentissage automatique est présente dans de nombreux produits aujourd’hui, mais elle n’est pas encore tout à fait utilisable. Les algorithmes employés aujourd’hui pour l’apprentissage automatique sont encore relativement basiques par rapport à ce que les scientifiques prévoient de fournir dans le futur. En outre, les sources de données utilisées aujourd’hui pour l’apprentissage automatique sont plus réduites que les jeux de données prévus pour une utilisation future. En bref, l’apprentissage automatique n’en est qu’à ses débuts. Cependant, il permet déjà d’effectuer un nombre considérable de tâches de manière étonnamment efficace. Ce chapitre examine ce qui pourrait être possible à l’avenir. Il vous aide à comprendre la direction que prend l’apprentissage automatique et comment cette direction pourrait contribuer à ancrer l’apprentissage automatique dans tous les aspects de la vie quotidienne.

Une nouvelle technologie telle que l’apprentissage automatique fait craindre que les gens perdent leur travail. En fait, c’est tout le contraire : l’apprentissage automatique ouvrira de nouvelles professions que les gens devraient trouver plus excitantes que de travailler sur une chaîne de montage ou de retourner des hamburgers dans un restaurant. L’un des objectifs est de proposer des emplois créatifs et intéressants. Bien entendu, ces nouveaux emplois nécessiteront des formations plus nombreuses et d’un type nouveau avant que les gens puissent les exercer correctement.

Toute nouvelle technologie comporte également des pièges. C’est un cliché, mais il est vrai qu’il est plus facile de détruire que de créer. Les pièges potentiels de l’apprentissage automatique doivent être pris au sérieux. Cette technologie n’en étant qu’à ses débuts, il est temps d’envisager les pièges potentiels et d’y remédier avant qu’ils ne se matérialisent. La dernière section du chapitre ne traite pas des problèmes qui se produiront, mais plutôt des problèmes qui pourraient se produire et que les gens peuvent certainement éviter en utilisant la technologie correctement.

Créer des technologies utiles pour le futur

Pour survivre, une technologie doit s’avérer utile. En fait, elle doit se révéler plus qu’utile, car elle doit répondre à des besoins perçus d’une manière telle que les autres technologies existantes ne répondent pas aussi bien à ces besoins, fournissant ainsi une justification monétaire pour continuer à investir dans cette technologie. Par exemple, l’Apple Lisa était une technologie intéressante et utile qui a démontré l’utilité de l’interface graphique pour des utilisateurs professionnels qui n’en avaient jamais vue auparavant. Il a répondu au besoin de rendre les ordinateurs conviviaux. Cependant, il a échoué parce qu’il n’a pas réussi à créer une base d’adeptes. L’ordinateur n’a tout simplement pas été à la hauteur du battage médiatique qui l’entourait (voir https://www.inexhibit.com/case-studies/different-fate-apples-lisa-macintosh-design-matters/ pour plus de détails). Le système suivant construit par Apple, le Macintosh, a été un peu plus à la hauteur de l’engouement qu’il suscitait, mais il reposait sur la même technologie que le Lisa. La différence est que le Macintosh a développé un nombre considérable d’adeptes purs et durs.

L’apprentissage automatique résout un nombre considérable de problèmes d’une manière que d’autres technologies auraient du mal à imiter. Toutefois, pour devenir la technologie incontournable dans laquelle tout le monde veut investir, l’apprentissage automatique doit se constituer un groupe d’adeptes inconditionnels. L’histogramme visible à https://www.grandviewresearch.com/industry-analysis/machine-learning-market montre que l’apprentissage automatique connaît une croissance significative, les services représentant la plus grande partie. Le rapport téléchargeable gratuitement qui est associé à ce graphique (vous devez remplir une demande pour l’obtenir) détaille l’ampleur de la croissance de l’apprentissage automatique au cours des dernières années. Les sections suivantes examinent quelques-unes des façons dont l’apprentissage automatique vous affecte déjà personnellement et comment cette utilisation augmentera à l’avenir, ce qui en fera une technologie indispensable.

Rôle de l’apprentissage automatique dans la robotique

Aujourd’hui, l’un des objectifs de l’apprentissage automatique est de créer des robots domestiques utiles. Vous pensez peut-être à Rosie, le robot domestique que l’on trouve dans la série américaine The Jetsons (voir https://thejetsons.fandom.com/wiki/Rosey pour une description rapide). Cependant, les robots du monde réel doivent résoudre des problèmes pratiques et importants pour attirer l’attention. Pour devenir viable et attirer des financements, une technologie doit également rassembler un groupe d’adeptes, et pour ce faire, elle doit offrir à la fois une interaction et une appropriation. Selon MarketsAndMarkets (voir https://www.marketsandmarkets.com/Market-Reports/household-robot-market-253781130.html), le marché des robots domestiques est d’environ 10,3 milliards de dollars en 2023 et passera à 24,5 milliards de dollars en 2028.

Parmi les exemples de robots domestiques réussis, on peut citer le Roomba d’iRobot (https://www.amazon.fr/iRobot-Roomba-Combo-Max-lenvironnement/dp/B0D96MYJGD). Il est possible d’acheter un Roomba aujourd’hui, il est utile et il a attiré suffisamment d’attention pour devenir une technologie viable. Le Roomba montre également ce qu’il est possible de faire aujourd’hui au niveau commercial, domestique et autonome. Oui, le Roomba est un aspirateur sophistiqué, doté d’une intelligence intégrée basée sur des algorithmes simples, mais très efficaces. Le Roomba peut naviguer avec succès dans une maison, ce qui est beaucoup plus difficile à réaliser qu’on ne le pense. Il peut également passer plus de temps dans les zones les plus sales de la maison. Toutefois, vous devez toujours vider le Roomba lorsqu’il est plein ; la technologie robotique actuelle n’est pas très performante. (Au cas où cela vous intéresserait, vous pouvez également trouver le robot tondeuse WORX sur Amazon à https://www.amazon.fr/WORX-LANDROID-Installation-Configurable-Intelligente/dp/B07LFPKYC1).

Vous pouvez trouver d’autres véritables robots que les gens utilisent pour effectuer des tâches spécialisées, mais vous ne les trouverez pas chez vous. La vidéo à https://www.youtube.com/watch?v=PfvXKXSAsUM présente 15 robots humanoïdes intéressants, mais pas particulièrement utiles aujourd’hui (il faut tout de même admettre qu’ils sont agréables à regarder). D’autres sites, tels que https://shareably.net/70-robots-that-actually-exist/ et https://www.cnn.com/2020/09/23/asia/japan-gundam-robot-test-scli-intl-scn/index.html, présentent d’autres robots, mais aucun d’entre eux n’a d’utilité générale. Avant que les robots puissent entrer dans une maison et travailler comme des aides généralisées, l’apprentissage automatique doit résoudre une multitude de problèmes, et les algorithmes doivent devenir à la fois plus généralistes et plus profonds. Les robots feront bien partie de la vie quotidienne à l’avenir, mais ce n’est pas encore pour tout de suite.

Utiliser l’apprentissage automatique dans le domaine de la santé

La question des soins aux personnes âgées fait l’objet d’une grande attention. Les gens vivent plus longtemps et un EHPAD ou une maison de retraite médicalisée ne semblent pas être une solution idéale pour la fin de vie. Les robots permettront aux personnes de rester chez elles tout en restant en sécurité. Certains pays sont également confrontés à une grave pénurie de personnel de santé, comme le Japon. C’est pourquoi le pays consacre des ressources considérables à la résolution des problèmes posés par la robotique. (Pour plus de détails, voir l’article à https://www.bbc.com/worklife/article/20200205-what-the-world-can-learn-from-japans-robots).

La technologie la plus proche de l’idée que l’on se fait d’un robot infirmier à domicile est représentée par le robot de téléprésence, que l’on trouve également dans les hôpitaux (voir https://spectrum.ieee.org/automaton/robotics/medical-robots/telepresence-robots-are-helping-take-pressure-off-hospital-staff pour plus de détails). Dans ce cas, le robot est une extension d’un médecin ou d’une infirmière, et il n’est donc pas du tout proche de ce que les Japonais espèrent créer à l’avenir.

Créer des systèmes intelligents pour différents besoins

Un grand nombre de solutions faisant appel à l’apprentissage automatique seront des assistants pour les êtres humains. Ils accomplissent très bien diverses tâches, mais celles-ci sont banales et répétitives par nature. Par exemple, vous pourriez avoir besoin de trouver un restaurant pour satisfaire les besoins d’un client qui ne connaît pas votre ville. Vous pouvez perdre du temps à chercher vous-même un restaurant approprié, ou vous pouvez faire appel à une IA en beaucoup moins de temps, avec plus de précision et d’efficacité. Siri (https://www.apple.com/siri/) est l’une des solutions les plus populaires et les plus connues. Nara, une autre solution de ce type (http://www.news.com.au/technology/innovation/meet-your-artificial-brain-the-algorithm-redefining-the-web/news-story/6a9eb73df016254a65d96426e7dd59b4), est une IA expérimentale qui apprend à connaître ce que vous aimez et ce que vous n’aimez pas au fur et à mesure que vous passez du temps avec elle. Contrairement à Siri, qui peut répondre à des questions simples, Nara va plus loin et fait des recommandations.

Utiliser l’apprentissage automatique en milieu industriel

L’apprentissage automatique joue déjà un rôle important dans les environnements industriels où l’accent est mis sur l’efficacité. Faire les choses plus rapidement, avec plus de précision et moins de ressources contribue à la rentabilité et rend une organisation plus flexible avec une marge bénéficiaire plus élevée. La diminution du nombre d’erreurs est également bénéfique pour les personnes qui travaillent dans une organisation, car elle réduit le niveau de frustration. Vous pouvez actuellement voir l’apprentissage automatique à l’œuvre dans les domaines suivants :

• diagnostic médical ;

• fouille de données ;

• bio-informatique ;

• reconnaissance de la parole et de l’écriture manuscrite ;

• catégorisation des produits ;

• centrale à inertie (IMU, pour Inertial Measurement Unit en anglais), telle que la technologie de capture de mouvement ;

• recherche d’informations.

Cette liste ne fait qu’effleurer la surface. L’apprentissage automatique est aujourd’hui beaucoup utilisé dans l’industrie, et le nombre d’applications continuera d’augmenter à mesure que des algorithmes avancés permettront d’atteindre des niveaux d’apprentissage plus élevés. À l’heure actuelle, l’apprentissage automatique accomplit des tâches dans un certain nombre de domaines, dont les suivants :

• Analyse : déterminer ce qu’un utilisateur veut et pourquoi, et quels types de modèles (comportements, associations, réponses, etc.) l’utilisateur présente lorsqu’il l’obtient.

• Enrichissement : ajout d’annonces, de widgets et d’autres fonctionnalités à un environnement afin que l’utilisateur et l’organisation puissent obtenir des avantages supplémentaires, tels qu’une productivité accrue ou une amélioration des ventes.

• Adaptation : modifier une présentation pour qu’elle reflète les goûts de l’utilisateur et ses choix en termes d’enrichissement. Chaque utilisateur se retrouve avec une expérience personnalisée qui réduit la frustration et améliore la productivité.

• Optimisation : modifier l’environnement pour que la présentation consomme moins de ressources sans diminuer l’expérience utilisateur.

• Contrôle : orienter l’utilisateur vers un plan d’action particulier en fonction des entrées et de la probabilité de succès la plus élevée.

Il est intéressant d’avoir une vision théorique de ce que l’apprentissage automatique fait dans l’industrie, mais il est important de voir comment cela fonctionne dans le monde réel. L’apprentissage automatique est utilisé de manière relativement banale, mais son emploi est d’une grande importance. Par exemple, l’apprentissage automatique joue un rôle dans l’automatisation de l’accès des employés, la protection des animaux, la prévision des temps d’attente aux urgences, l’identification de l’insuffisance cardiaque, la prévision des AVC et des crises cardiaques, et la prévision des réadmissions à l’hôpital (voir l’article à https://www.forbes.com/sites/85broads/2014/01/06/six-novel-machine-learning-applications/ qui fournit des détails sur chacune de ces utilisations).

Comprendre le rôle des nouveaux processeurs et d’autres matériels de pointe

La section « Rôle des statistiques dans l’apprentissage automatique » du Chapitre 2 présente les cinq écoles de pensée (tribus) liées à l’apprentissage automatique. Chacune de ces écoles de pensée énonce que le matériel informatique actuel n’est pas tout à fait à la hauteur de la tâche qui consiste à faire fonctionner correctement l’apprentissage automatique. Par exemple, vous pouvez vous adresser à une tribu dont les membres vous parlent de la nécessité d’augmenter la mémoire du système et d’utiliser des GPU pour accélérer les calculs. Une autre tribu pourrait prôner la création de nouveaux types de processeurs. Les processeurs d’apprentissage, qui imitent le cerveau humain, font fureur chez les connexionnistes. Pour en savoir plus sur les processeurs conçus pour accélérer les tâches d’IA et d’apprentissage automatique, consultez le site https://www.eetimes.eu/top-10-processors-for-ai-acceleration-at-the-endpoint/. Le fait est que tout le monde s’accorde à dire qu’un nouveau matériel facilitera l’apprentissage automatique, mais la forme précise que prendra ce matériel reste encore à déterminer.

Nouvelles possibilités en matière d’emploi grâce à l’apprentissage automatique

Dans le passé, il était facile de trouver des articles décrivant la perte totale d’opportunités d’emploi pour les êtres humains à cause des robots. Les robots effectuent déjà un certain nombre de tâches qui employaient des humains, et cette utilisation augmentera avec le temps. Cependant, maintenant que les entreprises ont acquis plus d’expérience, les articles parlent souvent d’augmentation du nombre d’emplois, ce qui implique que le robot et l’homme travaillent côte à côte pour effectuer des tâches. La section précédente de ce chapitre vous a aidé à comprendre certaines des utilisations pratiques et réelles de l’apprentissage automatique aujourd’hui et vous a aidé à découvrir où ces utilisations sont susceptibles de se développer à l’avenir. En lisant cette section, vous avez dû également réfléchir à la manière dont ces nouvelles utilisations pourraient potentiellement vous mettre au chômage ou l’un de vos proches. L’article publié sur le site https://aithority.com/guest-authors/the-future-of-artificial-intelligence-is-job-augmentation-not-elimination/ remet les pendules à l’heure en soulignant que les robots ne peuvent pas réellement remplacer les humains dans de nombreux scénarios, si ce n’est la plupart des scénarios.

Le fait est qu’il est difficile de déterminer comment l’apprentissage automatique affectera l’environnement de travail, tout comme il nous a été difficile de voir où la révolution industrielle nous mènerait en matière de production de masse de biens pour le grand public (voir https://www.history.com/topics/industrial-revolution pour plus de détails). Tout comme ces travailleurs ont dû trouver de nouveaux emplois, les personnes confrontées aujourd’hui à la perte de leur emploi en raison de l’apprentissage automatique devront trouver de nouvelles opportunités ou découvrir comment accomplir leurs tâches d’une nouvelle manière.

Travailler pour une machine

Il est tout à fait possible que vous vous retrouviez un jour à travailler pour une machine. En fait, vous travaillez peut-être déjà pour une machine sans le savoir. Certaines entreprises utilisent déjà l’apprentissage automatique pour analyser les processus opérationnels et les rendre plus efficaces. Par exemple, Hitachi utilise actuellement une telle configuration dans l’encadrement intermédiaire (voir l’article à http://www.hitachi.com/New/cnews/month/2015/09/150904.html). Dans ce cas, l’IA émet les ordres de travail sur la base de son analyse du flux de travail, tout comme le ferait un cadre intermédiaire humain. La différence est que l’IA est en fait 8 % plus efficace que les humains qu’elle remplace.

Cependant, il y a aussi une opportunité d’emploi. Les travailleurs soumis à l’IA exécutent les tâches que l’IA leur demande de faire, mais ils peuvent faire appel à leur expérience et à leur créativité pour déterminer la manière d’exécuter la tâche. L’IA analyse les processus utilisés par les travailleurs humains et mesure les résultats obtenus. Tout processus réussi est ajouté à la base de données des techniques que les travailleurs peuvent appliquer pour accomplir des tâches. En d’autres termes, les humains enseignent à l’IA de nouvelles techniques pour rendre l’environnement de travail encore plus efficace.

Enseigner de nouvelles techniques à l’IA est un exemple de la manière dont l’apprentissage automatique peut libérer les êtres humains de la pénibilité de l’environnement de travail. Lorsque l’on fait appel à des cadres intermédiaires humains, les nouveaux processus sont souvent noyés dans une bureaucratie de règles tacites et de batailles d’ego. Le cadre intermédiaire de l’IA est conçu pour apprendre de nouvelles techniques sans parti pris, de sorte que les humains sont encouragés à exercer leur créativité, et tout le monde en profite. En bref, l’IA, qui n’a pas d’ego à froisser, est le cadre accessible que de nombreux travailleurs ont toujours souhaité lorsqu’il s’agit de mettre en œuvre de nouvelles idées qui rendent tout le monde plus productif.

Malheureusement, l’inconvénient de cette augmentation de la productivité est que le manager non humain rend souvent l’environnement de travail stressant et dangereux, si l’on en croit l’article publié à https://www.theverge.com/2020/2/27/21155254/automation-robots-unemployment-jobs-vs-human-google-amazon. Afin de créer une intégration harmonieuse, les algorithmes d’apprentissage automatique doivent à l’avenir également prendre en compte la sécurité et le bonheur des travailleurs, ce que seul un gestionnaire humain sait faire à l’heure actuelle.

Travailler avec des machines

Les gens travaillent déjà régulièrement avec des machines, mais ils ne s’en rendent peut-être pas compte. Par exemple, lorsque vous parlez à votre smartphone et que celui-ci reconnaît ce que vous dites, vous travaillez avec une machine pour atteindre un objectif précis. La plupart des gens reconnaissent que l’interaction vocale fournie par un smartphone s’améliore avec le temps : plus vous l’utilisez, plus il reconnaît votre voix. Au fur et à mesure que l’algorithme d’apprentissage se perfectionne, il devient plus efficace pour reconnaître votre voix et obtenir le résultat souhaité. Cette tendance va se poursuivre.

Cependant, l’apprentissage automatique est utilisé de toutes sortes de façons qui ne vous viendraient pas forcément à l’esprit. Lorsque vous pointez un appareil photo sur un sujet et que l’appareil peut placer un cadre autour du visage (pour aider à cibler la photo), vous voyez le résultat de l’apprentissage automatique. L’appareil photo vous aide à prendre une photo avec beaucoup plus d’efficacité. En outre, l’appareil photo supprime automatiquement au moins une partie des effets de tremblement et de mauvais éclairage. Les appareils photo sont devenus très performants pour aider les humains à accomplir des tâches facilement.

L’utilisation de langages déclaratifs, tels que SQL (Structured Query Language), va également s’accentuer, car l’apprentissage automatique va faciliter les progrès. À certains égards, un langage déclaratif permet simplement de décrire ce que vous voulez et non pas la manière de l’obtenir. Cependant, l’utilisation du langage SQL nécessite toujours un informaticien, un scientifique des données, un administrateur de base de données ou un autre professionnel. Les futurs langages n’auront pas cette limitation. À terme, une personne formée à l’exécution d’une tâche particulière dira simplement à l’assistant robotique ce qu’il doit faire et cet assistant découvrira les moyens d’y parvenir. Les humains feront preuve de créativité pour découvrir ce qu’il faut faire et les détails (le comment) deviendront le domaine des machines.

Réparer des machines

La majeure partie de ce chapitre traite de la technologie actuelle, de son évolution future et des raisons pour lesquelles les choses fonctionnent comme elles le font. Cependant, vous remarquerez que la discussion est toujours axée sur l’exécution d’une tâche grâce à la technologie. En effet, avant que la technologie ne puisse faire quoi que ce soit d’autre, elle doit accomplir une tâche pratique qui attirera l’attention et profitera aux êtres humains d’une manière telle qu’ils voudront posséder la technologie en question. Quelle que soit la technologie, elle finira par tomber en panne. Rendre utile une technologie reste la considération primordiale aujourd’hui, et l’aboutissement de tous les rêves de ce que la technologie fera un jour s’étend sur une grande période dans le futur, de sorte que des choses banales comme la réparation des machines retomberont toujours sur les épaules des humains. Même si l’homme n’est pas directement impliqué dans la réparation physique, l’intelligence humaine dirigera l’opération de réparation.

Certains articles que vous lisez en ligne pourraient vous faire croire que les robots qui se réparent tout seuls sont déjà une réalité. Par exemple, les robots de la station spatiale internationale, Dextre et Canadarm, ont réparé une caméra défectueuse (voir l’article à https://gizmodo.com/a-self-repairing-space-robot-on-the-international-space-1580869685). En revanche, ce que les articles ne disent pas, c’est qu’un humain a décidé de la manière d’effectuer la tâche et a dirigé les robots pour qu’ils effectuent le travail physique. L’autoréparation devient possible. Les articles https://spectrum.ieee.org/automaton/robotics/robotics-hardware/japanese-researchers-teaching-robots-to-repair-themselves et https://www.robotics.org/blog-article.cfm/How-Self-Healing-Robots-Repair-Themselves/219 décrivent l’état d’avancement de la technologie

Créer de nouvelles tâches d’apprentissage automatique

Les algorithmes d’apprentissage automatique ne sont pas créatifs, ce qui signifie que les humains doivent faire preuve de créativité pour améliorer l’apprentissage automatique. Même les algorithmes qui construisent d’autres algorithmes ne font qu’améliorer l’efficacité et la précision des résultats obtenus par l’algorithme, car ils ne savent pas créer des algorithmes qui exécutent de nouveaux types de tâches. Les humains doivent fournir les données nécessaires pour définir ces tâches et les processus nécessaires pour commencer à les résoudre.

Vous pensez peut-être que seuls les experts en apprentissage automatique créent de nouvelles tâches d’apprentissage automatique. Cependant, l’histoire du cadre intermédiaire d’Hitachi, évoquée dans la section « Travailler pour une machine » , plus haut dans ce chapitre, devrait vous montrer que les choses se passent différemment. Oui, des experts aideront à définir la manière de résoudre la tâche, mais la création effective des tâches viendra des personnes qui connaissent le mieux un secteur particulier. L’histoire d’Hitachi sert de base pour comprendre à la fois que l’avenir verra des personnes de tous horizons contribuer aux scénarios d’apprentissage automatique et qu’une formation spécifique ne sera peut-être même pas utile pour définir de nouvelles tâches.

Concevoir de nouveaux environnements d’apprentissage automatique

À l’heure actuelle, la conception de nouveaux environnements d’apprentissage automatique est l’apanage des entreprises de recherche et de développement. Un groupe de spécialistes hautement qualifiés doit créer les paramètres d’un nouvel environnement. Par exemple, la NASA a besoin de robots pour explorer Mars. Dans ce cas, la NASA s’appuie sur les compétences de personnes du MIT et de Northeastern pour réaliser cette tâche (voir l’article à https://www.computerworld.com/article/1636209/nasa-needs-robotic-upgrades-for-work-on-mars.html). Étant donné que le robot devra effectuer des tâches de manière autonome, les algorithmes d’apprentissage automatique deviendront assez complexes et comprendront plusieurs niveaux de résolution de problèmes.

Ce qui est encore plus étonnant, c’est qu’une IA émotionnellement intelligente pourrait aider les astronautes à se rendre sur Mars, selon https://www.technologyreview.com/2020/01/14/64990/an-emotionally-intelligent-ai-could-support-astronauts-on-a-trip-to-mars/. Curieusement, un astronaute pourrait avoir besoin du soutien d’une entité qui n’est pas émotionnellement impliquée dans les activités exténuantes consistant à travailler dans des espaces restreints avec les mêmes personnes pendant une longue période. Pensez-y : l’IA est incapable de s’énerver.

Un jour, quelqu’un pourra décrire un problème de manière suffisamment détaillée pour qu’un programme spécialisé puisse créer l’algorithme nécessaire à l’aide d’un langage approprié. En d’autres termes, les gens ordinaires finiront par créer de nouveaux environnements d’apprentissage automatique sur la base des idées qu’ils ont et qu’ils veulent essayer. Comme pour la création de tâches d’apprentissage automatique, les personnes qui créeront les futurs environnements seront des experts dans leur domaine particulier, plutôt que des informaticiens ou des spécialistes des données. La science de l’apprentissage automatique se transformera finalement en un exercice d’ingénierie qui donnera à toute personne ayant une bonne idée les outils dont elle a besoin.

Éviter les pièges potentiels des technologies du futur

Toute nouvelle technologie comporte des pièges potentiels. Plus les attentes à l’égard de cette technologie sont élevées, plus les écueils sont importants. Les attentes irréalistes sont à l’origine de toutes sortes de problèmes liés à l’apprentissage automatique, car les gens pensent que ce qu’ils voient dans les films correspond à ce qu’ils obtiendront dans le monde réel. Il est essentiel de se rappeler les concepts de base présentés au Chapitre 1, à savoir que les algorithmes d’apprentissage automatique ne peuvent actuellement ni ressentir, ni penser de manière indépendante, ni créer quoi que ce soit. Contrairement aux IA des films, un algorithme d’apprentissage automatique fait précisément ce que vous attendez de lui, rien de plus. Bien sûr, certains résultats sont étonnants, mais il est important de garder les attentes du public en phase avec ce que la technologie réelle peut faire. Sinon, vous allez promettre quelque chose que la technologie ne pourra jamais réaliser, et les soutiens que vous attendiez iront voir ailleurs.

En fait, les utilisations de l’apprentissage automatique sont aujourd’hui assez limitées. Comme le décrit l’article à https://www.linkedin.com/pulse/machine-learning-its-hard-problems-valuable-to-by-coppel, l’IA restreinte, telle que l’utilisation commerciale de l’IA pour obtenir des informations sur d’énormes jeux de données, s’appuie sur des techniques bien comprises que les entreprises ont commencé à utiliser au cours de la dernière décennie. La machine ne peut rien déduire, ce qui limite son utilisation à la tâche pour laquelle le développeur ou le scientifique des données l’a conçue. En fait, une bonne analogie pour les algorithmes d’aujourd’hui est qu’ils sont comme une chemise sur mesure (voir l’article à https://www.computerworld.com/article/1634867/why-microsofts-data-chief-thinks-machine-learning-tools-are-like-tailored-shirts.html pour plus de détails). Il faut des compétences spécialisées pour créer un algorithme adapté à des besoins spécifiques aujourd’hui, mais l’avenir pourrait voir des algorithmes capables de s’attaquer à presque n’importe quelle tâche. Les entreprises qui s’appuient sur l’IA faible (narrow IA, voir https://www.free-work.com/fr/tech-it/blog/actualites-informatiques/ia-faible-en-quoi-ca-consiste) doivent faire preuve de prudence dans la manière dont elles développent leurs produits ou leurs services. Un changement dans l’offre de produits ou de services pourrait placer les données utilisées pour l’environnement d’apprentissage automatique en dehors du domaine de l’algorithme d’apprentissage automatique, ce qui réduirait le résultat de l’algorithme d’apprentissage automatique à du charabia (ou du moins le rendrait peu fiable).

L’utilisation de l’apprentissage automatique au sein d’une organisation nécessite également de recruter des personnes possédant les compétences adéquates et de créer une équipe. L’apprentissage automatique dans l’environnement de l’entreprise, où les résultats signifient une amélioration de la rentabilité, est relativement nouveau. Les entreprises sont confrontées à des difficultés lorsqu’il s’agit de réunir la bonne équipe, de définir un ensemble d’objectifs raisonnables et d’atteindre ces objectifs. Pour attirer une équipe de classe internationale, votre entreprise doit proposer un problème suffisamment passionnant pour attirer les personnes recherchées dans d’autres organisations. Ce n’est pas une tâche facile, et vous devez y réfléchir dans le cadre de la définition des objectifs de création d’un environnement d’apprentissage automatique.

Partie 2

Préparer vos outils pour l’apprentissage automatique

Dans cette partie…

Créer une configuration pour Python

Effectuer des tâches Python de base

Utiliser Google Colab

DANS CE CHAPITRE

Déterminer la distribution Python à utiliser pour l’apprentissage automatique

•

Effectuer une installation Linux, Mac OS X et Windows

•

Récupérer les jeux de données et le code des exemples

Chapitre 4

Installer une distribution Python

Avant de pouvoir exploiter Python ou de l’utiliser pour résoudre des problèmes d’apprentissage automatique, vous devez disposer d’une installation fonctionnelle. En outre, vous devez avoir accès aux jeux de données et au code utilisés dans cet ouvrage. Ce chapitre vous explique comment effectuer les configurations nécessaires et les téléchargements afin d’exploiter Python. Le téléchargement des exemples de code (qui se trouvent sur www.lisez.com, voir l’introduction de ce livre) et leur installation sur votre système sont le meilleur moyen de tirer le meilleur parti de cet ouvrage. Ce chapitre vous aide à configurer votre système afin que vous puissiez facilement suivre les exemples dans le reste du livre.

L’utilisation du code source téléchargeable ne vous empêche pas de saisir les exemples par vous-même, de les suivre à l’aide d’un débogueur, de les améliorer ou de les tester dans tous les sens. Le code source téléchargeable est là pour vous aider à prendre un bon départ dans votre expérience de l’apprentissage automatique et de Python. Après avoir vu comment le code fonctionne lorsqu’il est correctement saisi et configuré, vous pouvez essayer de créer les exemples par vous-même. Si vous faites une erreur, vous pouvez comparer ce que vous avez saisi avec le code source téléchargeable et découvrir précisément où se trouve l’erreur. Vous trouverez le code source téléchargeable de ce chapitre dans les fichiers ML4D2E ; 04 ; Sample.ipynb et ML4D2E ; 04 ; Dataset Load.ipynb. L’introduction vous indique où télécharger le code source de ce livre.

La source téléchargeable permet également d’accéder aux exemples sous forme de variantes R. Bien que le code Python apparaisse dans le dossier ML4D2E (qui est l’abréviation de Machine Learning For Dummies, 2e édition) de la source téléchargeable, le code R apparaît dans le dossier ML4D2ER (pour Machine Learning For Dummies, 2e édition, code R). Les exemples R ne suivent pas toujours précisément les exemples Python en raison des différences entre les deux langages, mais les exemples R comprennent de nombreuses annotations afin que vous puissiez les suivre. L’utilisation de l’un ou l’autre langage vous permettra d’atteindre le résultat souhaité.

Utiliser Anaconda pour l’apprentissage automatique

Vous pouvez employer un certain nombre de packages pour effectuer des tâches d’apprentissage automatique. En fait, il en existe trop pour qu’un seul chapitre soit complet sur le sujet. Pour vous permettre de vous concentrer sur l’apprentissage automatique plutôt que sur un progiciel, la première section qui suit explique comment obtenir une copie d’Anaconda. Ce livre utilise Anaconda pour un certain nombre de raisons, comme nous l’expliquons dans la section suivante. Cependant, si vous utilisez une plateforme où le processus d’installation ne fonctionne pas bien (voire pas du tout), vous pouvez également suivre les exemples de code du livre en employant Google Colab, qui est décrit au Chapitre 6. Les sections suivantes vous donnent un bref aperçu d’Anaconda.

Obtenir Anaconda

Le package de base Anaconda est un téléchargement gratuit que vous pouvez obtenir à l’adresse https://repo.anaconda.com/archive/. Dans ce répertoire, vous avez accès aux différents installateurs qui dépendent du système d’exploitation de votre ordinateur. Anaconda est compatible avec les plateformes suivantes :

• Windows 32 bits et 64 bits (le programme d’installation peut vous proposer uniquement la version 64 bits ou 32 bits, en fonction de la version de Windows qu’il détecte) ;

• Linux 64 bits (installateurs x86 et PowerPC 8/9) ;

• macOS 64 bits (installateur graphique et en ligne de commande).

Les versions d’Anaconda évoluent au fil du temps, mais pour faire tourner les exemples de cet ouvrage, il est impératif que vous installiez la version que les auteurs du livre ont employée. Il s’agit de la version Anaconda3-2020.07 que vous pouvez obtenir à l’adresse https://repo.anaconda.com/archive/. Il est par conséquent fortement déconseillé d’installer la toute dernière version d’Anaconda.

L’installation fonctionne mieux si vous supprimez d’abord les versions précédentes d’Anaconda de votre système. Sinon, une version du produit peut interférer avec d’autres versions. Anaconda fournit un programme de désinstallation distinct dans le dossier de l’exécutable Anaconda sur votre système, dont l’emplacement peut varier. Par exemple, pour désinstaller une version antérieure d’Anaconda 3 sur un système Windows, recherchez Uninstall-Anaconda3.exe dans le dossier C : \Utilisateurs\<NomUtilisateur>\Anaconda3 de votre système. Exécutez ce fichier pour désinstaller le produit. En outre, ce livre ne prend pas en charge l’utilisation du programme d’installation de Miniconda décrit à https://docs.conda.io/en/latest/miniconda.html.

La version Anaconda3-2020.07 installe Python 3.8. Windows et Mac OS X fournissent tous deux des installateurs graphiques. Lorsque vous utilisez Linux, l’installation exploite l’utilitaire bash.

Comprendre pourquoi Anaconda est utilisé dans ce livre

Anaconda n’est pas un environnement de développement intégré (IDE) comme beaucoup d’autres produits. Il s’agit plutôt d’une méthode centralisée d’accès à un certain nombre de paquets. Ce livre utilise Jupyter Notebook comme IDE parce qu’il prend en charge les techniques de programmation littérale. Cependant, vous pourriez tout aussi bien utiliser Spyder pour le développement, et vous seriez peut-être plus satisfait, car il fournit une interface plus traditionnelle. Vous pouvez voir une comparaison entre ces deux produits à https://www.slant.co/versus/1246/15716/∼spyder_vs_jupyter. Anaconda a l’avantage de vous aider à gérer les deux IDE, ainsi qu’un grand nombre d’autres paquets. En outre, vous pouvez créer des environnements pour utiliser les IDE de manière spécifique. Par exemple, vous pouvez avoir un environnement pour utiliser Jupyter Notebook pour Python et un environnement complètement différent pour employer Jupyter Notebook pour R.

Il est donc important de savoir pourquoi cette section met l’accent sur Jupyter Notebook alors qu’Anaconda donne accès à un certain nombre d’autres IDE. La plupart des IDE ressemblent à des éditeurs de texte spécialisés, et c’est précisément ce qu’ils sont. Vous bénéficiez de toutes sortes de fonctionnalités intelligentes, d’astuces, de conseils, de coloration syntaxique du code, etc., et il n’y a aucun mal à employer des éditeurs de texte. Cependant, étant donné que les développeurs Python se concentrent souvent sur des applications scientifiques qui nécessitent quelque chose de mieux qu’une présentation purement textuelle, l’emploi de notebooks peut s’avérer utile.

Un notebook diffère d’un éditeur de texte en ce sens qu’il se concentre sur une technique appelée programmation littérale, mise au point par l’informaticien de Stanford Donald Knuth. La programmation littérale permet de créer une sorte de présentation du code, des notes, des équations mathématiques et des graphiques. En bref, vous vous retrouvez avec un carnet de notes de scientifique rempli de tout ce qui est nécessaire à la compréhension complète du code. Les techniques de programmation littérale sont couramment utilisées dans des progiciels coûteux tels que Mathematica et MATLAB. Le développement à l’aide de notebooks excelle dans les domaines suivants :

• démonstration ;

• collaboration ;

• recherche ;

• objectifs pédagogiques ;

• présentation.

Ce livre utilise la collection d’outils Anaconda parce qu’elle vous offre une excellente expérience de codage en Python, mais aussi parce qu’elle vous aide à découvrir l’énorme potentiel des techniques de programmation littérale. Si vous passez beaucoup de temps à effectuer des tâches scientifiques, Anaconda et les produits similaires sont essentiels. En outre, Anaconda est gratuit, ce qui permet de bénéficier des avantages du style de programmation littérale sans avoir à payer le coût d’autres logiciels.

Pour plus d’informations sur Anaconda et sur les modifications apportées aux éditions précédentes, consultez les notes de mise à jour à https://docs.anaconda.com/anacondaorg/release-notes/. La plupart des changements que vous trouverez concernent des corrections de bogues et des mises à jour.

Installation d’Anaconda sous Linux

L’installation d’Anaconda sous Linux s’effectue en ligne de commande, car il n’existe pas d’option d’installation graphique. La procédure suivante devrait fonctionner correctement sur n’importe quel système Linux, que vous utilisiez la version Intel ou PowerPC d’Anaconda :

❶ Ouvrez une copie de Terminal.

La fenêtre Terminal s’affiche.

❷ Changez de répertoire pour accéder à la copie téléchargée d’Anaconda sur votre système.

Le nom de ce fichier varie, mais il apparaît normalement sous la forme Anaconda3-2020.07-Linux-x86_64.sh pour les systèmes Intel et Anaconda3-2020.07-Linux-ppc64le.sh pour les systèmes PowerPC. Le numéro de version est intégré au nom du fichier. Dans ce cas, le nom de fichier fait référence à la version 3.2020.07, qui est la version utilisée pour ce livre. Si vous utilisez une autre version, vous risquez de rencontrer des problèmes avec le code source et de devoir procéder à des ajustements.

❸ Saisissez Anaconda3-2020.07-Linux-x86_64.sh (pour la version Intel) ou bash Anaconda3-2020.07-Linux-ppc64le.sh (pour la version PowerPC) et appuyez sur Entrée.

Un assistant d’installation démarre et vous demande d’accepter les conditions de licence pour l’utilisation d’Anaconda.

❹ Lisez l’accord de licence et acceptez les conditions en utilisant la méthode requise pour votre version de Linux.

L’assistant vous demande de fournir un emplacement d’installation pour Anaconda. Le livre suppose que vous utilisez l’emplacement par défaut de votre plateforme. Si vous choisissez un autre emplacement, vous devrez peut-être modifier certaines procédures plus loin dans le livre pour les adapter à votre configuration.

❺ Indiquez un emplacement d’installation (si nécessaire) et appuyez sur Entrée (ou cliquez sur Next).

Le processus d’extraction de l’application commence. Le programme d’installation vous demande si vous souhaitez initialiser Anaconda3 à l’aide de la commande conda init.

❻ Saisissez yes et appuyez sur Entrée ou cliquez sur Yes.

Une fois l’extraction terminée, un message d’achèvement de l’opération s’affiche.

❼ Ajoutez le chemin d’installation à votre déclaration PATH en utilisant la méthode requise pour votre version de Linux.

Vous êtes prêt à utiliser Anaconda.

Installation d’Anaconda sur Mac OS X

L’installation de Mac OS X n’existe que sous la forme 64 bits. Les étapes suivantes vous aideront à installer Anaconda 64 bits sur un système Mac en utilisant la méthode avec une interface graphique :

❶ Localisez la copie téléchargée d’Anaconda sur votre système.

Le nom de ce fichier varie, mais il apparaît normalement sous la forme Anaconda3-2020.07-MacOSX-x86_64.pkg. Le numéro de version est intégré dans le nom du fichier. Dans ce cas, le nom de fichier fait référence à la version 3.2020.07, qui est la version utilisée pour ce livre. Si vous utilisez une autre version, vous risquez de rencontrer des problèmes avec le code source et de devoir procéder à des ajustements.

❷ Faites un double-clic sur le fichier d’installation.

Une boîte de dialogue d’introduction apparaît.

❸ Cliquez sur Continuer.

L’assistant vous demande si vous souhaitez consulter les documents « Lisez-moi » . Vous pourrez lire ces documents ultérieurement. Pour l’instant, vous pouvez ignorer ces informations en toute sécurité.

❹ Cliquez sur Continuer.

L’assistant affiche un accord de licence. Veillez à lire le contrat de licence afin de connaître les conditions d’utilisation.

❺ Cliquez sur Accepter si vous acceptez l’accord de licence.

L’assistant vous demande d’indiquer une destination pour l’installation. La destination détermine si l’installation est destinée à un utilisateur individuel ou à un groupe.

❻ Cliquez sur Continuer.

Cet ouvrage part du principe que vous conservez les paramètres par défaut.

❼ Cliquez sur Installer.

L’installation commence. Une barre de progression indique l’état d’avancement du processus d’installation. Lorsque l’installation est terminée, une boîte de dialogue d’achèvement du processus s’affiche.

❽ Cliquez sur Continuer.

Vous êtes prêt à utiliser Anaconda.

Installation d’Anaconda sous Windows

Anaconda est livré avec une application d’installation graphique pour Windows, de sorte que pour obtenir une bonne installation, il faut utiliser un assistant, comme vous le feriez pour toute autre installation. Si vous avez déjà installé une version d’Anaconda sur votre ordinateur différente de celle de l’ouvrage, n’oubliez pas de la désinstaller avant d’accomplir la procédure suivante qui devrait fonctionner correctement sur n’importe quel système Windows, que vous utilisiez la version 32 bits ou 64 bits d’Anaconda :

❶ Localisez la copie téléchargée d’Anaconda sur votre système.

Le nom de ce fichier varie, mais il apparaît normalement sous la forme Anaconda3-2020.07-Windows-x86.exe pour les systèmes 32 bits et Anaconda3-2020.07-Windows-x86_64.exe pour les systèmes 64 bits. Le numéro de version est intégré dans le nom du fichier. Dans ce cas, le nom de fichier fait référence à la version 3.2020.07, qui est la version utilisée pour ce livre. Si vous employez une autre version, vous risquez de rencontrer des problèmes avec le code source et de devoir procéder à des ajustements.

❷ Faites un double-clic sur le fichier d’installation.

Une boîte de dialogue de bienvenue s’affiche et indique la version d’Anaconda dont vous disposez (32 bits ou 64 bits). Assurez-vous que vous avez la bonne version.

❸ Cliquez sur Next.

L’assistant affiche un accord de licence. Veillez à lire le contrat de licence afin de connaître les conditions d’utilisation.

❹ Cliquez sur I Agree si vous acceptez l’accord de licence.

Il vous est demandé quel type d’installation vous souhaitez effectuer, comme le montre la Figure 4.1. Dans la plupart des cas, il est préférable d’installer le produit pour soi-même (Option Just Me).

❺ Choisissez l’un des types d’installation et cliquez sur Next.

L’assistant demande où installer Anaconda sur le disque. Ce livre part du principe que vous utilisez l’emplacement par défaut.

[picture]

Figure 4.1 Indiquez à l’assistant comment installer Anaconda sur votre système.

Si vous choisissez un autre emplacement, vous devrez peut-être modifier certaines procédures plus loin dans le livre pour les adapter à votre configuration.

❻ Choisissez un emplacement d’installation (si nécessaire), puis cliquez sur Next.

Les options d’installation avancées s’affichent, comme le montre la Figure 4.2. Ces options sont sélectionnées par défaut, et il n’y a aucune raison de les modifier dans la plupart des cas. Ce livre suppose que vous avez configuré Anaconda en utilisant les options par défaut.

❼ Modifiez les options d’installation avancées (si nécessaire), puis cliquez sur Install.

Une boîte de dialogue d’installation s’affiche avec une barre de progression. Le processus d’installation peut prendre quelques minutes, si bien que vous avez le temps de prendre une tasse de café ou de lire une bande dessinée. Lorsque le processus d’installation est terminé, le bouton Next est activé.

[picture]

Figure 4.2 Configuration des options d’installation avancées.

Un mot sur les captures d’écran

Au fur et à mesure que vous avancez dans le livre, vous utilisez l’IDE de votre choix pour ouvrir les fichiers Python et les notebooks contenant le code source du livre. Toutes les captures d’écran contenant des informations spécifiques à l’IDE reposent sur Anaconda, car Anaconda fonctionne sur les trois plateformes prises en charge par cet ouvrage. L’emploi d’Anaconda n’implique pas qu’il s’agisse du meilleur IDE ou que les auteurs le recommandent d’une manière ou d’une autre ; Anaconda fonctionne simplement bien en tant que produit de démonstration.

Lorsque vous travaillez avec Anaconda, le nom de l’IDE, Jupyter Notebook, est exactement le même sur les trois plateformes, et vous ne verrez même pas de différence significative dans la présentation. Les différences que vous voyez sont mineures, et vous devriez les ignorer au fur et à mesure que vous avancez dans le livre.

❽ Cliquez sur Next.

Si vous voyez une page avec un lien pour PyCharm, https://www.jetbrains.com/pycharm/, cliquez à nouveau sur Next. L’assistant indique que l’installation est terminée. Cette page comprend des options pour le didacticiel Anaconda et pour en savoir plus sur Anaconda. Si vous laissez les deux cases cochées, les pages correspondantes seront chargées dans votre navigateur.

❾ Cliquez sur Finish.

Téléchargement des jeux de données et des exemples de code

Ce livre traite de l’utilisation de Python pour effectuer des tâches d’apprentissage automatique. Bien entendu, vous pouvez passer tout votre temps à créer le code des exemples à partir de zéro, à le déboguer et à découvrir ensuite comment il a un rapport avec l’apprentissage automatique, mais vous pouvez aussi opter pour la facilité et télécharger le code à partir de www.lisez.com. Recherchez le titre du livre, puis une fois sur la page de ce dernier, cliquez sur Ressources et Télécharger en regard de Fichiers additionnels.

(Rubrique Downloads, puis Python Source Code) afin de pouvoir vous mettre directement au travail. De la même manière, la création de jeux de données suffisamment importants pour l’apprentissage automatique prendrait beaucoup de temps. Heureusement, vous pouvez accéder à des jeux de données standardisés et déjà créés assez facilement en utilisant les fonctionnalités fournies dans certaines bibliothèques de science des données (qui fonctionnent également très bien pour l’apprentissage automatique). Les sections suivantes vous aident à télécharger et à utiliser le code des exemples et des jeux de données afin que vous puissiez gagner du temps et vous atteler directement aux tâches spécifiques à la science des données.

Utiliser Jupyter Notebook

Pour faciliter le travail avec le code relativement complexe de ce livre, vous allez vous servir de Jupyter Notebook. Cette interface permet de créer facilement des fichiers sous la forme de notebooks Python pouvant contenir un nombre illimité d’exemples, chacun pouvant être exécuté individuellement. Le programme s’exécute dans votre navigateur, de sorte que la plateforme que vous utilisez pour le développement n’a pas d’importance ; tant qu’elle dispose d’un navigateur, tout devrait bien se passer.

Démarrer Jupyter Notebook

Vous accédez à Jupyter Notebook à partir d’Anaconda Navigator, qui fournit également un accès centralisé aux diverses applications prises en charge par un environnement particulier. L’environnement ou canal par défaut, base (root), prend en charge directement le développement Python. Le fichier Setup.readme fourni dans le dossier ML4D2ER indique comment configurer un canal R distinct. La Figure 4.3 montre le contenu du canal base (root).

[picture]

Figure 4.3 Anaconda Navigator offre un accès centralisé à tous les besoins de développement.

Il arrive parfois que le lancement d’Anaconda Navigator se fige et que le message « Loading applications… » s’affiche à l’écran indéfiniment. Si cela se produit, supprimez le processus Python (dans le Gestionnaire des tâches, faites un clic droit sur le processus Python et cliquez sur Fin de tâche). Localisez le répertoire C : \Users\<nom d’utilisateur>\ anaconda3\Lib\site-packages\anaconda_navigator\api et à l’aide d’un éditeur de texte (par exemple, Notepad++), ouvrez le fichier conda_ api.py. Modifiez la ligne numérotée 1391 en transformant data = yaml. full_load(f) en data = yaml.safeload(f).

Vous noterez que chaque entrée indique la version du produit à laquelle vous accéderez. Pour ce livre, vous utilisez Jupyter Notebook 6.0.3. Si vous ne voyez pas cette version, cliquez sur l’icône en forme d’engrenage dans le coin supérieur droit de l’entrée Jupyter Notebook et choisissez Install specific version → 6.0.3 dans le menu. Anaconda Navigator vous guidera tout au long du processus d’installation de la version correcte.

Pour démarrer Jupyter Notebook, il suffit de cliquer sur Launch dans l’entrée Jupyter Notebook. La Figure 4.4 montre l’apparence de l’interface lorsqu’elle est affichée dans Chrome. Il s’agit de la page d’accueil, sur laquelle vous pouvez effectuer des opérations telles que la création d’un nouveau dossier pour stocker un projet. L’apparence exacte sur votre système dépend du navigateur que vous utilisez et du type de plateforme que vous avez installé.

[picture]

Figure 4.4 Jupyter Notebook offre une méthode simple pour créer des exemples d’apprentissage automatique.

Arrêt du serveur Jupyter Notebook

Quelle que soit la manière dont vous démarrez Jupyter Notebook (que nous désignerons simplement par Notebook dans suite de ce livre), vous devez cliquer sur Quit dans le coin supérieur droit de la fenêtre client pour quitter Notebook. Sinon, le serveur Notebook continue de fonctionner en arrière-plan et vous laissez votre environnement dans un état incertain, ce qui peut entraîner une perte de données dans certaines situations. Après avoir cliqué sur Quit, une boîte de message Server Stopped s’affiche, que vous pouvez fermer en cliquant sur la croix dans le coin supérieur droit. Fermez la fenêtre du navigateur.

Si vous avez fini de travailler avec Anaconda, il faut choisir File → Quit dans la fenêtre Anaconda Navigator. Lors du premier arrêt (et éventuellement des suivants), une boîte de dialogue Close running applications s’affiche, dans laquelle vous devez cocher la case notebook, puis cliquer sur Quit pour mettre fin à Anaconda Navigator. Si vous ne voulez plus voir cette boîte de dialogue s’afficher, vous pouvez la supprimer en sélectionnant Don’t Show Again (Ne plus l’afficher) avant de cliquer sur Quit.

Il arrive que le processus d’arrêt du serveur prenne plus de temps que prévu. Dans ce cas, Anaconda Navigator affiche un message vous indiquant qu’il travaille toujours en arrière-plan. Laissez-le terminer les processus dont il a besoin avant de quitter. Vous pouvez également voir ce message si vous n’avez pas quitté Jupyter Notebook correctement. Dans ce cas, vous devez terminer Anaconda Navigator car le serveur ne s’arrêtera jamais.

Définir le référentiel de code

Le code que vous créez et utilisez dans ce livre résidera dans un référentiel (parfois appelé également dépôt) sur votre disque dur. Pensez à un référentiel comme à une sorte de classeur où vous mettez votre code. Notebook ouvre un tiroir, sort le dossier et vous montre le code. Vous pouvez le modifier, exécuter des exemples individuels dans le dossier, ajouter de nouveaux exemples, et simplement interagir avec votre code de manière naturelle. Les sections suivantes vous permettent de démarrer avec Notebook afin que vous puissiez voir comment fonctionne ce concept de référentiel.

Définir le dossier du livre

Il est utile d’organiser vos fichiers de manière à pouvoir y accéder plus facilement par la suite. Dans ce livre, les fichiers Python sont conservés dans les dossiers ML4D2E et ML4D2ER. Cependant, comme le livre se concentre sur le code Python, les étapes suivantes montrent comment créer le dossier Python dans Notebook.

❶ Après avoir lancé Notebook, choisissez New → New Folder.

Notebook crée un nouveau dossier nommé Untitled Folder, comme le montre la Figure 4.5. Le fichier apparaîtra dans l’ordre alphanumérique, il est donc possible que vous ne le voyiez pas au départ. Vous devez faire défiler l’écran vers le bas pour trouver l’emplacement correct.

[picture]

Figure 4.5 Les nouveaux dossiers apparaissent sous le nom Untitled Folder.

❷ Cochez la case située à côté de l’entrée Untitled Folder (Dossier sans titre).

❸ Cliquez sur Rename en haut de la page.

L’entrée Untitled Folder passe en surbrillance.

❹ Saisissez ML4D2E et validez par l’appui sur la touche Entrée.

Notebook change le nom du dossier.

❺ Cliquez sur la nouvelle entrée ML4D2E dans la liste.

Notebook change l’emplacement du dossier ML4D2E dans lequel vous allez effectuer les tâches liées aux exercices de ce livre.

Création d’un nouveau notebook

Chaque nouveau notebook peut s’assimiler à un dossier. Vous pouvez y placer des exemples individuels, comme vous le feriez avec des feuilles de papier dans un dossier physique. Chaque exemple apparaît dans une cellule. Vous pouvez également placer d’autres types d’éléments dans le dossier, mais vous verrez comment ces éléments fonctionnent au fur et à mesure de la progression dans ce livre. Suivez ces étapes pour créer un nouveau notebook :

❶ Cliquez sur New → Python 3.

Un nouvel onglet s’ouvre dans le navigateur avec le nouveau notebook, comme le montre la Figure 4.6. Vous noterez que le notebook contient une cellule et que Notebook l’a mise en surbrillance pour que vous puissiez commencer à y saisir du code. Pour l’instant, le titre du notebook est Untitled (Sans titre). Comme ce n’est pas un titre particulièrement utile, vous devez le modifier.

❷ Cliquez sur Untitled sur la page.

[picture]

Figure 4.6 Un notebook contient des cellules que vous utilisez pour stocker du code.

Notebook vous demande ce que vous voulez utiliser comme nouveau nom.

❸ Saisissez ML4D2E; 04; Exemple et cliquez sur Rename.

Ce nouveau nom indique qu’il s’agit d’un fichier pour la deuxième édition de Machine Learning For Dummies, Chapitre 4, Exemple.ipynb. L’utilisation de cette convention de nommage vous permettra de différencier facilement ces fichiers des autres fichiers de votre référentiel.

Bien entendu, le notebook Exemple ne contient encore rien. Placez le curseur dans la cellule et saisissez le code suivant :

import sys
print('Python Version:\n', sys.version)
import os
result = os.popen('conda list anaconda$').read()
print('\nAnaconda Version:\n', result)

La première instruction print() affiche le numéro de version de Python de votre installation. La seconde instruction print() affiche le numéro de version Anaconda de votre installation. Ces deux sorties dépendent de l’utilisation de code externe qui se fait à l’aide de l’instruction import. Le deuxième appel fonctionne directement avec un utilitaire de ligne de commande appelé conda que vous verrez utilisé plusieurs fois dans ce livre.

Cliquez sur le bouton Exécuter, situé en haut dans la barre d’outils. Vous obtenez le résultat illustré à la Figure 4.7. Le numéro de version de votre installation doit correspondre à celui indiqué à la Figure 4.7. La sortie fait partie de la même cellule que le code, mais Notebook sépare visuellement la sortie du code afin que vous puissiez les distinguer. Notebook crée automatiquement une nouvelle cellule pour vous.

[picture]

Figure 4.7 Le notebook utilise des cellules pour stocker votre code.

Lorsque vous avez fini de travailler avec un notebook, il est important de le fermer. Pour fermer un notebook, choisissez File → Close and Halt. Vous revenez à la page d’accueil, où vous pouvez voir que le notebook que vous venez de créer a été ajouté à la liste.

Exportation d’un notebook

Créer des notebooks et les garder pour soi n’est pas très amusant. À un moment donné, vous allez souhaiter les partager avec d’autres personnes. Pour ce faire, vous devez exporter votre notebook du référentiel vers un fichier. Vous pouvez ensuite envoyer ce fichier à quelqu’un d’autre, qui l’importera dans son propre référentiel.

La section précédente vous a montré comment créer un notebook nommé ML4D2E ; 04 ; Exemple. Vous pouvez ouvrir ce notebook en cliquant sur son entrée dans la liste du référentiel. Le fichier s’ouvre à nouveau pour que vous puissiez voir votre code. Pour exporter ce code, choisissez File → Download as → Notebook (.ipynb). Ce que vous voyez ensuite dépend de votre navigateur, mais c’est généralement une sorte de boîte de dialogue pour enregistrer le notebook sous forme de fichier. Utilisez la même méthode pour enregistrer le fichier IPython Notebook que celle que vous utilisez pour tout autre fichier que vous enregistrez à l’aide de votre navigateur

Suppression d’un notebook

Parfois, les notebooks deviennent obsolètes ou vous n’en avez tout simplement plus besoin. Plutôt que de laisser votre référentiel encombré avec de fichiers dont vous n’avez pas l’utilité, vous pouvez supprimer ces notebooks indésirables de la liste. Accomplissez les étapes suivantes pour supprimer le fichier :

❶ Cochez la case située à côté de l’entrée ML4D2E; 04; Exemple. ipynb.

❷ Cliquez sur l’icône de la corbeille (qui s’affiche sur un fond de couleur rouge) en haut de la page.

Un message d’avertissement s’affiche.

❸ Cliquez sur Supprimer.

Le fichier est supprimé de la liste.

Importation d’un notebook

Pour utiliser le code source de ce livre, vous devez importer les fichiers téléchargés dans votre référentiel. Le code source se présente sous la forme d’un fichier d’archive que vous devez extraire vers un emplacement de votre disque dur. L’archive contient une liste de fichiers .ipynb (Notebook) contenant le code source de ce livre (voir l’introduction pour plus de détails sur le téléchargement du code source). Les étapes suivantes expliquent comment importer ces fichiers dans votre référentiel :

❶ Cliquez sur Upload en haut à droite de la page.

Ce que vous voyez dépend de votre navigateur. Dans la plupart des cas, vous voyez une boîte de dialogue de téléchargement ou d’ouverture de fichier qui vous permet d’accéder aux fichiers de votre disque dur.

❷ Naviguez jusqu’au répertoire contenant les fichiers que vous souhaitez importer dans Notebook.

❸ Mettez en surbrillance un ou plusieurs fichiers à importer et cliquez sur le bouton Ouvrir (ou autre bouton similaire) pour lancer le processus de téléchargement.

Le fichier est ajouté à une liste de téléchargement, comme le montre la Figure 4.8. Le fichier ne fait pas encore partie du référentiel, vous l’avez simplement sélectionné pour le télécharger.

❹ Cliquez sur le bouton Téléverser.

Notebook place le fichier dans le référentiel afin que vous puissiez commencer à l’utiliser.

[picture]

Figure 4.8 Les exemples de code peuvent être importés dans le référentiel.

Découvrir les jeux de données utilisés dans ce livre

Outre les jeux de données proposés par Scikit-learn (https://scikitlearn.org/stable/datasets.html), ce livre utilise un certain nombre de jeux de données auxquels vous pouvez accéder à l’adresse https://github.com/lmassaron/datasets. Ces jeux de données illustrent différentes façons d’interagir avec les données et vous allez les employer dans les exemples pour effectuer une grande variété de tâches. La liste suivante donne un aperçu rapide de chacun des jeux de données que vous allez pouvoir importer dans votre code Python :

• Passagers aériens (https://www.kaggle.com/rakannimer/air-passengers) : fichier .csv contenant le nombre de passagers d’une compagnie aérienne donnée en exemple, par mois, pendant 12 ans à partir de 1949.

• IMDB 50K (https://www.kaggle.com/lakshmi25npathi/imdb-data-set-of-50k-movie-reviews) : jeu de données pour la classification binaire des opinions contenant un ensemble de 25 000 critiques de films très populaires pour l’entraînement et 25 000 pour les tests.

• Fashion MNIST (https://github.com/zalandoresearch/fashion-mnist) : jeu de données d’images d’articles de Zalando (https://jobs.zalando.com/en/tech/). Il se compose d’un jeu d’entraînement de 60 000 exemples et d’un jeu de test de 10 000 exemples, chacun d’entre eux étant étiqueté dans l’une des dix catégories proposées.

• Manchots de Palmer (https://github.com/allisonhorst/palmer-penguins) : il s’agit d’un package contenant deux jeux de données mis à la disposition par le Docteur Kristen Gorman et la station Palmer, Antarctica LTER, membre du Long Term Ecological Research Network.

• Shakespeare (https://www.kaggle.com/kingburrito666/shakespeare-plays) : liste de toutes les pièces de Shakespeare, avec le texte de ces pièces.

• Collection de spams reçus par SMS (https://www.kaggle.com/uciml/sms-spam-collection-dataset) : ensemble de messages SMS étiquetés et collectés pour la recherche sur le spam par SMS. Il contient un ensemble de SMS en anglais de 5 574 messages, étiquetés selon que le message est légitime ou du spam.

• Tennis (https://www.kaggle.com/datasets/ehallmar/a-large-tennis-dataset-for-atp-and-itf-betting) : jeu de données contenant des statistiques pour un grand nombre de matchs de tennis des ligues ATP et ITF.

• Titanic (https://www.openml.org/d/40945) : jeu de données décrivant l’état de survie des passagers du Titanic. Les données du Titanic ne contiennent pas d’informations sur l’équipage, mais elles contiennent l’âge réel de la moitié des passagers.

• Vin (https://www.kaggle.com/sgus1318/winedata) : contient des statistiques relatives à la qualité du vin.

La technique de chargement de chacun de ces jeux de données peut varier en fonction de la source. L’exemple suivant montre comment charger le jeu de données sur les passagers aériens. Vous trouverez le code dans le notebook ML4D2E ; 04 ; Dataset Load.ipynb. Les jeux de données téléchargeables sont archivés au format Apache Arrow-based Feather File Format (https://arrow.apache.org/docs/python/feather.html). Pour rendre ce format de fichier accessible dans Notebook, il faut ouvrir un prompt Anaconda (en lançant l’exécutable Anaconda Prompt) et saisir la commande suivante :

conda install conda-forge::feather-format

La commande met plusieurs minutes à s’exécuter, car elle recueille les informations sur le paquet et résout l’environnement (c’est-à-dire détermine ce qu’il faut faire pour effectuer l’installation). À un moment donné, vous devrez saisir y et appuyer sur Entrée pour terminer l’installation. Pour vérifier que l’installation est correcte, utilisez la commande suivante :

conda list feather-format

Après quelques instants, vous obtenez un résultat similaire à celui-ci :

# packages in environment at C:\Users\John\anaconda3:
#
# Name          Version         Build   Channel
feather-format   0.4.1    pyh9f0ad1d_0   conda-forge

Maintenant que vous disposez de la bibliothèque requise, vous pouvez charger un jeu de données parmi ceux fournis sur le site des jeux de données du livre. Pour commencer, téléchargez le fichier air_passen-gers.feather à partir de https://github.com/lmassaron/datasets et placez-le dans le dossier que vous avez créé pour ce livre (utilisez le bouton Upload pour ce faire). Dans les chapitres suivants, vous verrez comment télécharger les fichiers .feather directement à partir du site des jeux de données du livre, mais le fait d’effectuer le téléchargement maintenant permet de simplifier les choses. Voici un exemple du code que vous allez employer pour charger le jeu de données Air Passengers en tant que dataframe.

import pyarrow.feather as feather
read_df = feather.read_feather('air_passengers.feather')
print(read_df)

Le résultat est un dataframe de 144 lignes contenant le nombre de passagers par mois. La Figure 4.9 illustre un résultat typique.

[picture]

Figure 4.9 L’objet read_df contient le jeu de données chargé sous la forme d’un dataframe.

DANS CE CHAPITRE

Effectuer des tâches de programmation essentielles en Python

•

Gérer les données à l’aide d’ensembles, de listes et de tuples

•

Accéder à des éléments individuels à l’aide d’itérateurs

•

Faciliter l’accès aux données grâce au stockage indexé

Chapitre 5

Introduction au codage en Python

Le Chapitre 4 vous a aidé à créer une installation Python que vous pouvez utiliser pour différentes finalités, notamment pour l’apprentissage automatique et la science des données. L’objectif de ce chapitre est de fournir une bonne vue d’ensemble du fonctionnement de Python en tant que langage et de la résolution des problèmes d’apprentissage automatique. Si vous connaissez déjà Python, vous pouvez ignorer ce chapitre et passer au suivant. Cependant, il serait préférable de parcourir au moins ce chapitre et de tester certains des exemples, afin de vous assurer que votre installation est correcte.

Si vous ne connaissez pas du tout Python, ce livre ne vous servira pas à apprendre Python à partir de zéro ; pour cela, vous avez besoin d’un livre tel que Python pour les Nuls (4e édition), qui a été écrit par l’un des auteurs de ce livre, John Mueller, ou vous avez besoin d’un tutoriel tel que celui qui se trouve à https://docs.python.org/fr/3.8/tutorial/. Ce chapitre suppose que vous avez travaillé avec d’autres langages de programmation et que vous avez au moins une idée du fonctionnement de Python. Cette limite mise à part, vous pouvez exploiter ce chapitre pour vous rafraîchir la mémoire sur la façon dont les choses fonctionnent en Python, ce qui est l’essentiel de ce dont la majorité des gens ont besoin. Vous découvrirez les bases du travail avec les types de données Python, les structures de codage et les stratégies de manipulation des données.

Il n’est pas nécessaire de saisir manuellement le code source de ce chapitre. En fait, il est beaucoup plus facile d’utiliser la source téléchargeable. Vous trouverez le code source de ce chapitre dans le dossier ML4D2E du référentiel téléchargeable (www.lisez.com ; recherchez le titre du livre, puis une fois sur la page de ce dernier, cliquez sur Ressources et Télécharger en regard de Fichiers additionnels). Les fichiers d’exemple de ce chapitre commencent par ML4D2E ; 05 ; .

Concepts de base de Python que vous devez connaître

Tout langage possède des caractéristiques de base, et Python ne fait pas exception à cette règle. Par exemple, les langages fournissent divers types de données, opérateurs, méthodes d’interaction avec les données, etc. En outre, Python prend en charge les fonctions et les modules. Vous utiliserez ces connaissances lorsque vous travaillerez sur les exemples de ce livre. Les sections suivantes présentent un bref rappel de ces notions de base.

Bases de Python

Python est un langage riche qui offre une grande flexibilité ainsi qu’une grande facilité d’utilisation. Cette section présente une brève vue d’ensemble des bases de Python. Si vous avez besoin de plus d’aide et que la référence au tutoriel Python standard fournie dans l’introduction du chapitre ne soit pas suffisante, vous pouvez également exploiter les tutoriels à l’adresse https://www.w3schools.com/python/ et https://www.tutorialspoint.com/python/index.htm. Le Tableau 5.1 présente une vue d’ensemble des types de données numériques courants de Python.

Tableau 5.1

Types de données numériques Python.

Type | Plage | Exemple
int ou long | -9,223,372,036,854,775,808 et 9,223,372,036,854,775,807 (défini par sys.maxsize pour une plateforme particulière) | 1
float | ±2.2250738585072014 × 10-308 et ±1.7976931348623157 × 10308 (définis par sys.float_info.max et sys.float_info.min pour une plateforme particulière) | 1.0
complex | N/D | 3 + 4j
bool | True ou False | True

Pour créer des variables utilisées pour stocker des données, vous réalisez des affectations. Le Tableau 5.2 contient une liste d’opérateurs d’affectation Python (on suppose que MyVar contient au départ la valeur 5 dans chaque exemple).

Tableau 5.2

Opérateurs d’affectation Python.

Opérateur | Description | Exemple
= | Affecte la valeur trouvée dans l’opérande de droite à l’opérande de gauche. | MyVar = 5 a pour résultat que MyVar contient 5.
+= | Ajoute la valeur trouvée dans l’opérande de droite à la valeur trouvée dans l’opérande de gauche et place le résultat dans l’opérande de gauche. | MyVar += 2 a pour résultat que MyVar contient 7.
−= | Soustrait la valeur trouvée dans l’opérande de droite de la valeur trouvée dans l’opérande de gauche et place le résultat dans l’opérande de gauche. | MyVar −= 2 a pour résultat que MyVar contient 3.
*= | Multiplie la valeur trouvée dans l’opérande de droite par la valeur trouvée dans l’opérande de gauche et place le résultat dans l’opérande de gauche. | MyVar *= 2 a pour résultat que MyVar contient 10.
/= | Divise la valeur trouvée dans l’opérande de gauche par la valeur trouvée dans l’opérande de droite et place le résultat dans l’opérande de gauche. | MyVar /= 2 a pour résultat que MyVar contient 2.5.
%= | Divise la valeur trouvée dans l’opérande de gauche par la valeur trouvée dans l’opérande de droite et place le reste dans l’opérande de gauche. | MyVar %= 2 a pour résultat que MyVar contient 1.
**= | Détermine la valeur exponentielle trouvée dans l’opérande de gauche lorsqu’elle est élevée à la puissance de la valeur trouvée dans l’opérande de droite et place le résultat dans l’opérande de gauche. | MyVar **= 2 a pour résultat que MyVar contient 25.
//= | Divise la valeur trouvée dans l’opérande de gauche par la valeur trouvée dans l’opérande de droite et place le résultat entier (nombre entier) dans l’opérande de gauche. | MyVar //= 2 a pour résultat que MyVar contient 2.

Les opérateurs arithmétiques, unaires et binaires permettent de manipuler les données de différentes manières. Le Tableau 5.3 présente ces opérateurs.

Tableau 5.3

Opérateurs arithmétiques, unaires et binaires de Python.

Opérateur | Description | Exemple
+ | Additionne deux valeurs. | 5+2=7
− | Soustrait l’opérande de droite de l’opérande de gauche. | 5−2=3
* | Multiplie l’opérande de droite par l’opérande de gauche. | 5 * 2 = 10
/ | Divise l’opérande gauche par l’opérande droit. | 5 / 2 = 2.5
% | Divise l’opérande gauche par l’opérande droit et renvoie le reste. | 5%2=1
** | Calcule la valeur exponentielle de l’opérande de droite par l’opérande de gauche. | 5 ** 2 = 25
// | Effectue une division entière, dans laquelle l’opérande de gauche est divisé par l’opérande de droite et seul le nombre entier est retourné. | 5 // 2 = 2
∼ | Inverse les bits d’un nombre de sorte que tous les bits 0 deviennent des bits 1 et vice versa. | ∼4 donne une valeur de −5.
− | Réalise une négation de la valeur d’origine de sorte que le positif devient négatif et vice versa. | − (−4) donne 4 et −4 donne −4.
+ | Cet opérateur renvoie la même valeur que celle que vous avez fournie en entrée. | +4 donne une valeur de 4.
& (et) | Détermine si les deux bits individuels de deux opérateurs sont vrais et met le bit résultant à True si c’est le cas. | 0b1100 & 0b0110 = 0b0100
| (ou) | Détermine si l’un ou l’autre des bits individuels de deux opérateurs est vrai et met le bit résultant à True lorsque c’est le cas. | 0b1100 | 0b0110 = 0b1110
^ (ou exclusif) | Détermine si un seul des bits individuels de deux opérateurs est vrai et met le bit résultant à True si c’est le cas. Lorsque les deux bits sont vrais ou que les deux bits sont faux, le résultat est False. | 0b1100 ^ 0b0110 = 0b1010
∼ (complément à un) | Calcule la valeur en complément à un d’un nombre. | ∼0b1100 = −0b1101 ∼0b0110 = −0b0111
<< (décalage vers la gauche) | Décale les bits de l’opérande de gauche vers la gauche de la valeur de l’opérande de droite. Tous les nouveaux bits sont mis à 0 et tous les bits qui sortent sont perdus. | 0b00110011 << 2 = 0b11001100
>> (décalage vers la droite) | Décale les bits de l’opérande de gauche vers la droite de la valeur de l’opérande de droite. Tous les nouveaux bits sont mis à 0 et tous les bits qui sortent sont perdus. | 0b00110011 >> 2 = 0b00001100

Il est parfois nécessaire de vérifier la relation entre deux variables ou de les comparer d’une manière ou d’une autre. Le Tableau 5.4 fournit une liste des opérateurs relationnels et logiques de Python.

Tableau 5.4

Opérateurs relationnels et logiques de Python.

Opérateur | Description | Exemple
== | Détermine si deux valeurs sont égales. Vous noterez que l’opérateur relationnel utilise deux signes égal (=). Une erreur commise par de nombreux développeurs consiste à n’utiliser qu’un seul signe égal, ce qui a pour effet d’affecter une valeur à une autre. | 1 == 2 renvoie False.
!= | Détermine si deux valeurs ne sont pas égales. Certaines anciennes versions de Python permettaient d’utiliser l’opérateur <> à la place de l’opérateur ! =. L’utilisation de l’opérateur <> entraîne une erreur dans les versions actuelles de Python. | 1 ! = 2 renvoie True.
> | Vérifie que la valeur de l’opérande gauche est supérieure à la valeur de l’opérande droit. | 1 > 2 renvoie False.
< | Vérifie que la valeur de l’opérande gauche est inférieure à la valeur de l’opérande droit. | 1 < 2 renvoie True.
>= | Vérifie que la valeur de l’opérande gauche est supérieure ou égale à la valeur de l’opérande droit. | 1 >= 2 renvoie False.
<= | Vérifie que la valeur de l’opérande gauche est inférieure ou égale à la valeur de l’opérande droit. | 1 <= 2 renvoie True.

and

Détermine si les deux opérandes sont vrais.

True and True renvoie True.

True and False renvoie False.

False and True renvoie False.

False and False renvoie False.

or

Détermine si l’un des deux opérandes est vrai.

True or True renvoie True.

True or False renvoie True.

False or True renvoie True.

False or False renvoie False.

not | Négation de la valeur de vérité d’un seul opérande. Une valeur vraie devient fausse et une valeur fausse devient vraie. | not True renvoie False. not False renvoie True.

Comme pour les nombres, vous pouvez utiliser certains opérateurs spéciaux avec des objets, tels que les chaînes de caractères, afin d’en savoir plus sur eux. Les opérateurs d’appartenance permettent de déterminer si une chaîne de caractères contient un contenu spécifique. Les opérateurs d’identité permettent de déterminer le type de données contenues dans les variables. Le Tableau 5.5 présente ces opérateurs.

Tableau 5.5

Opérateurs d’appartenance et d’identité Python.

Opérateur | Description | Exemple
in | Détermine si la valeur de l’opérande de gauche apparaît dans la séquence trouvée dans l’opérande de droite. | "Hello" in "Hello Goodbye" renvoie True.
not in | Détermine si la valeur de l’opérande de gauche est absente de la séquence trouvée dans l’opérande de droite. | "Hello" not in "Hello Goodbye" renvoie False.
is | Est évalué à True lorsque le type de la valeur ou de l’expression dans l’opérande de droite pointe vers le même type dans l’opérande de gauche. | type(2) is int renvoie True.
is not | Est évalué à True lorsque le type de la valeur ou de l’expression dans l’opérande de droite pointe vers un type différent de celui de la valeur ou de l’expression dans l’opérande de gauche. | type(2) is not int renvoie False.

Les ordinateurs instaurent une priorité dans les comparaisons en donnant plus d’importance à certains opérateurs qu’à d’autres. L’ordre des opérateurs constitue ce que l’on appelle la priorité des opérateurs. Le Tableau 5.6 indique la priorité de tous les opérateurs courants de Python, y compris certains que vous n’avez pas encore vus. Lorsque vous effectuez des comparaisons, tenez toujours compte de la priorité des opérateurs ; sinon, les hypothèses que vous faites sur le résultat d’une comparaison seront probablement erronées.

Tableau 5.6

Priorité des opérateurs Python.

Opérateur | Description
() | Les parenthèses permettent de regrouper des expressions et d’ignorer la priorité par défaut, de manière à ce qu’une opération de moindre priorité (telle que l’addition) soit prioritaire sur une opération de plus grande priorité (telle que la multiplication).
** | L’exponentiation élève la valeur de l’opérande de gauche à la puissance de l’opérande de droite.
∼+− | Les opérateurs unaires interagissent avec une seule variable ou expression.
* / % // | Multiplier, diviser, modulo et division entière.
+− | Addition et soustraction.
>> << | Décalage des bits vers la droite et vers la gauche.
& | AND au niveau binaire.
^| | OR exclusif au niveau binaire et OR standard.
<= < > >= | Opérateurs de comparaison.
== != | Opérateurs d’égalité.
= −= += *= **= | Opérateurs d’affectation.
is |
is not | Opérateurs d’identité.
in |
not in | Opérateurs d’appartenance.
not or not | Opérateurs logiques.

Travailler avec des fonctions

Pour gérer correctement l’information, vous devez organiser les outils employés pour effectuer les tâches requises. Chaque ligne de code que vous créez exécute une tâche spécifique et on combine ces lignes de code pour obtenir le résultat souhaité. Parfois, vous devez répéter les instructions avec des données différentes et, dans certains cas, votre code devient si long qu’il est difficile de garder une trace de ce que fait chaque partie. Les fonctions sont des outils d’organisation qui permettent à votre code de rester propre et ordonné. En outre, les fonctions permettent de réutiliser facilement les instructions que vous avez créées avec des données différentes. Cette section du chapitre vous explique tout sur les fonctions. Plus important encore, dans cette section, vous commencez à créer vos premières applications sérieuses de la même manière que les développeurs professionnels.

Créer des fonctions réutilisables

Vous allez dans votre armoire, vous sortez un pantalon et une chemise, vous enlevez les étiquettes et vous les mettez. À la fin de la journée, vous enlevez tous vos vêtements et les jetez à la poubelle. Heureusement, ce n’est pas vraiment ce que font la plupart des gens, car ils enlèvent les vêtements, les lavent et les remettent dans le placard pour les réutiliser. Les fonctions sont également réutilisables. Personne ne veut répéter sans cesse la même tâche, car cela devient monotone et ennuyeux. Lorsque vous créez une fonction, vous définissez un ensemble de codes que vous pouvez utiliser à l’infini pour effectuer la même tâche. Il vous suffit de demander à l’ordinateur d’effectuer une tâche spécifique en lui indiquant la fonction à utiliser. L’ordinateur exécute fidèlement chaque instruction de la fonction chaque fois que vous le lui demandez.

Lorsque vous travaillez avec des fonctions, le code qui a besoin des services de la fonction se nomme appelant, et il fait appel à la fonction pour effectuer des tâches à sa place. La plupart des informations sur les fonctions font référence à l’appelant. L’appelant doit fournir des informations à la fonction, et la fonction renvoie des informations à l’appelant.

Il fut un temps où les programmes informatiques n’incluaient pas le concept de réutilisation du code. Par conséquent, les développeurs devaient sans cesse réinventer le même code. Très vite, quelqu’un a eu l’idée des fonctions, et le concept a évolué au fil des ans jusqu’à ce que les fonctions deviennent très flexibles. Vous pouvez faire faire aux fonctions tout ce que vous voulez. La réutilisation du code est une partie nécessaire des applications pour :

• réduire le temps de développement ;

• réduire les erreurs de programmation ;

• augmenter la fiabilité des applications ;

• permettre à des communautés de développeurs de bénéficier du travail d’un seul programmeur ;

• faciliter la compréhension du code ;

• améliorer l’efficacité des applications.

En parcourant les exemples de ce livre, vous verrez comment la réutilisation facilite considérablement la vie. Sans la réutilisation, vous seriez encore en train de programmer en introduisant manuellement des 0 et des 1 dans l’ordinateur.

La création d’une fonction ne demande pas beaucoup de travail. Pour voir comment marchent les fonctions, ouvrez l’invite Anaconda (Anaconda Prompt), saisissez python et appuyez sur Entrée pour démarrer une copie de Python. Saisissez le code suivant (en appuyant sur Entrée à la fin de chaque ligne) :

def SayHello():
    return "Bonjour !"

Pour terminer la fonction, vous devez appuyer sur Entrée une deuxième fois après la dernière ligne. Une fonction commence par le mot-clé def (pour define, qui signifie définir en anglais). On fournit un nom de fonction, des parenthèses qui peuvent contenir des arguments de fonction (données utilisées dans la fonction) et le caractère deux-points. L’éditeur met automatiquement en retrait la ligne suivante. Python s’appuie sur les espaces pour définir les blocs de code (instructions regroupées dans une fonction).

Vous pouvez maintenant utiliser la fonction. Il suffit de saisir SayHello() et d’appuyer sur Entrée. Les parenthèses qui suivent le nom de la fonction sont importantes, car elles indiquent à Python d’exécuter la fonction, plutôt que de vous indiquer que vous accédez à une fonction en tant qu’objet (pour déterminer ce qu’elle est). La sortie de cette fonction est Bonjour ! .

Appeler des fonctions

Les fonctions peuvent accepter des arguments (données supplémentaires) et des valeurs de retour. La possibilité d’échanger des données rend les fonctions beaucoup plus utiles qu’elles ne le seraient autrement. Les sections suivantes décrivent comment appeler des fonctions de différentes manières pour envoyer et recevoir des données.

Envoi des arguments requis

Une fonction peut exiger de l’appelant qu’il lui fournisse des arguments. Un argument requis est une variable qui doit contenir des données pour que la fonction marche. Ouvrez l’invite Anaconda, saisissez python et appuyez sur Entrée, puis saisissez le code suivant :

def DoSum(Valeur1, Valeur2):
   return Valeur1 + Valeur2

Vous disposez à présent d’une nouvelle fonction, DoSum(). Cette fonction nécessite que vous fournissiez deux arguments pour être utilisée. Pour voir comment cela fonctionne, saisissez DoSum(1, 2) et appuyez sur Entrée. Vous obtenez la valeur 3 en sortie. Notez que DoSum() renvoie toujours la valeur 3 en sortie lorsque vous fournissez 1 et 2 en entrée. L’instruction return fournit la valeur de retour. Chaque fois que vous voyez l’instruction return dans une fonction, vous savez que la fonction fournit une valeur de retour.

Envoi d’arguments avec des mots-clés

Au fur et à mesure que vos fonctions se complexifient et que les méthodes qui les utilisent se complexifient également, il se peut que vous souhaitiez contrôler un peu plus précisément la manière dont vous appelez la fonction et dont vous lui fournissez des arguments. Jusqu’à présent, vous avez utilisé des arguments positionnels, ce qui signifie que vous avez fourni des valeurs dans l’ordre dans lequel elles apparaissent dans la liste des arguments de la définition de la fonction. Cependant, Python dispose également d’une méthode permettant d’envoyer des arguments nommés. Dans ce cas, vous fournissez le nom de l’argument suivi d’un signe égal (=) et de la valeur de l’argument. Pour voir comment cela fonctionne, ouvrez l’invite Anaconda, saisissez python et appuyez sur Entrée, puis saisissez le code suivant :

def DisplaySum(Valeur1, Valeur2):
    print(str(Valeur1) + ' + ' + str(Valeur2) + ' = ' +
    str((Valeur1 + Valeur2)))

Notez que l’argument de la fonction print() comprend une liste d’éléments à afficher et que ces éléments sont séparés par des signes plus (+). En outre, les arguments étant de types différents, vous devez les convertir à l’aide de la fonction str(). Python permet de mélanger facilement les arguments de cette manière. Cette fonction introduit également le concept de continuation de ligne automatique. La fonction print() apparaît en fait sur deux lignes, et Python poursuit automatiquement la fonction de la première ligne sur la seconde.

Ensuite, il est temps de tester DisplaySum(). Bien sûr, vous voulez d’abord essayer la fonction en utilisant des arguments positionnels, alors saisissez DisplaySum(2, 3) et appuyez sur Entrée. La sortie attendue est 2 + 3 = 5. Saisissez maintenant DisplaySum(Valeur2 = 3, Valeur1 = 2) et appuyez sur Entrée. Vous obtenez à nouveau le résultat 2 + 3 = 5 bien que la position des arguments ait été inversée.

Attribuer une valeur par défaut aux arguments des fonctions

Que vous fassiez l’appel en utilisant des arguments positionnels ou des arguments nommés, les fonctions ont jusqu’à présent exigé que vous fournissiez une valeur. Parfois, une fonction peut utiliser des valeurs par défaut lorsqu’une valeur est couramment employée. Pour créer une valeur par défaut, il suffit de faire suivre le nom de l’argument d’un signe égal et de la valeur par défaut. Pour voir comment cela fonctionne, ouvrez l’invite Anaconda, saisissez python et appuyez sur Entrée, puis saisissez le code suivant :

def SayHello(Greeting = "Aucune valeur fournie"):
    print(Greeting)

La fonction SayHello() fournit une valeur automatique pour Greeting lorsque l’appelant n’en fournit pas. Lorsque quelqu’un essaie d’appeler SayHello() sans argument, Python ne lève pas d’erreur, mais à la place, il affiche Aucune valeur fournie. Saisissez SayHello() et appuyez sur Entrée pour vous en convaincre. Vous voyez le message par défaut. Saisissez SayHello("Bonjour ! ") pour voir une réponse habituelle.

Créer des fonctions avec un nombre variable d’arguments

Dans la plupart des cas, vous savez précisément combien d’arguments fournir à votre fonction. Heureusement, Python propose une technique pour envoyer un nombre variable d’arguments à une fonction. Il suffit de créer un argument précédé d’un astérisque, tel que *VarArgs. La technique habituelle consiste à fournir un second argument qui contient le nombre d’arguments passés en entrée. Pour voir comment cela fonctionne, ouvrez l’invite Anaconda, saisissez python et appuyez sur Entrée, puis saisissez le code suivant :

def DisplayMulti(ArgCount = 0, *VarArgs):
   print('Vous avez passé ' + str(ArgCount) + ' arguments.',
   VarArgs)

Vous noterez que la fonction print() affiche une chaîne de caractères puis la liste des arguments. En raison de la conception de cette fonction, vous pouvez saisir DisplayMulti() et appuyer sur Entrée pour voir qu’il est possible de ne passer aucun argument. Pour voir les arguments multiples à l’œuvre, saisissez DisplayMulti(3, 'Hello', 1, True) et appuyez sur Entrée. La sortie de Vous avez passé 3 arguments. ('Hello', 1, True) montre qu’il n’est pas nécessaire de passer des valeurs d’un type particulier.

Travailler avec des modules

Pour utiliser un module, vous devez l’importer. Python place le code du module en ligne avec le reste de votre application en mémoire, ce qui revient à avoir créé un énorme fichier. Aucun des deux fichiers n’est modifié sur le disque ; ils sont toujours séparés, mais la façon dont Python voit le code est différente.

Vous avez deux façons d’importer des modules. Chaque technique est utilisée dans des circonstances spécifiques :

• import : l’instruction import est utilisée pour importer un module en entier. Il s’agit de la méthode la plus courante utilisée par les développeurs pour importer des modules, car elle permet de gagner du temps et ne nécessite qu’une seule ligne de code. Cependant, cette approche utilise également plus de ressources mémoire que l’approche consistant à importer de manière sélective les attributs dont vous avez besoin, méthode qui est décrite dans le paragraphe suivant.

• from...import : on emploie l’instruction from...import lorsque l’on veut importer de manière sélective des attributs de modules individuels. Cette méthode permet d’économiser des ressources, mais au prix d’une certaine complexité. En outre, si vous essayez d’utiliser un attribut que vous n’avez pas importé, Python enregistre une erreur. Oui, le module contient toujours l’attribut, mais Python ne peut pas le voir parce que vous ne l’avez pas importé.

Pour utiliser le code d’un module, Python doit pouvoir localiser le module et le charger en mémoire. Les informations relatives à l’emplacement sont stockées sous forme de chemins d’accès dans Python. Lorsque vous demandez à Python d’importer un module, Python consulte tous les fichiers de sa liste de chemins d’accès pour le trouver. Les informations sur les chemins proviennent de trois sources différentes :

• Variables d’environnement : une variable d’environnement est une variable au niveau du système d’exploitation spécifique à une plateforme que vous pouvez utiliser pour stocker des informations sur une application. Les variables d’environnement peuvent être définies de différentes manières, en fonction de la plateforme. Par exemple, lorsque vous travaillez avec Windows, vous pouvez définir les variables d’environnement dans l’invite de commande ou à l’aide de la boîte de dialogue Variables d’environnement (voir http://blog.johnmuellerbooks.com/2014/02/17/adding-a-location-to-the-windows-path/ pour plus de détails). PYTHONPATH est l’une des variables d’environnement les plus importantes pour les développeurs Python ; elle indique l’emplacement par défaut pour la recherche de fichiers sur votre disque.

• Répertoire courant : Python utilise le répertoire courant pour rechercher les fichiers dont il a besoin. Le code suivant permet de connaître le répertoire courant de Python :

import os
os.getcwd()

Si l’on veut changer le répertoire courant pour que Python regarde à un autre emplacement, on utilise le code suivant :

import os
os.chdir('nouveau_chemin')

où nouveau_chemin est l’emplacement que vous souhaitez utiliser.

• Répertoires par défaut : même si vous ne définissez aucune variable d’environnement et que le répertoire courant ne contienne aucun module utilisable, Python peut toujours trouver ses propres bibliothèques dans l’ensemble des répertoires par défaut qui sont inclus dans ses propres informations de chemin d’accès.

Il est important de savoir où Python cherche les modules. Parfois, vous pouvez rencontrer une erreur lorsque Python ne peut pas trouver un module, même si vous savez que le module existe sur le disque. Le code suivant montre comment déterminer où Python cherche les modules sur le disque :

import os
os.environ['PYTHONPATH']

Si vous recevez un message d’erreur KeyError : 'PYTHONPATH', cela signifie que la variable d’environnement PYTHONPATH n’est pas définie. Python peut toujours trouver des fichiers, mais il n’a pas PYTHONPATH comme ressource.

Stockage de données à l’aide d’ensembles, de listes et de tuples

Python propose une multitude de méthodes pour stocker des données en mémoire. Chaque méthode présente des avantages et des inconvénients. Il est important de choisir la méthode la plus appropriée à vos besoins particuliers. Les sections suivantes présentent trois techniques courantes utilisées pour stocker des données dans le cadre de l’apprentissage automatique.

Création d’ensembles

La plupart des gens ont utilisé des ensembles à un moment ou à un autre de leur scolarité pour créer des listes d’éléments qui nécessitent d’être regroupés. Ces listes ont ensuite été manipulées à l’aide d’opérations mathématiques telles que l’intersection, l’union, la différence et la différence symétrique. Les ensembles sont la meilleure option à choisir lorsque vous devez effectuer des tests d’appartenance et supprimer les doublons d’une liste. Les ensembles ne permettent pas d’effectuer des tâches liées aux séquences, telles que l’indexation ou le découpage (slicing). Pour voir comment vous pouvez travailler avec des ensembles, ouvrez l’invite Anaconda, saisissez python et appuyez sur Entrée, puis saisissez le code suivant :

SetA = set(['Rouge', 'Bleu', 'Vert', 'Noir'])
SetB = set(['Noir', 'Vert', 'Jaune', 'Orange'])

Le terme anglais pour ensemble est set. Il s’agit aussi d’un verbe en anglais qui signifie définir ou paramétrer.

Effectuer des opérations sur des ensembles

En supposant que vous ayez créé les ensembles présentés dans la section précédente, vous avez peut-être remarqué que chacun d’entre eux présente des éléments communs. Pour voir en quoi ces ensembles sont similaires, créez de nouveaux ensembles qui s’appuient sur des opérations mathématiques à des fins de comparaison, comme le montre le code suivant :

SetX = SetA.union(SetB)
SetY = SetA.intersection(SetB)
SetZ = SetA.difference(SetB)

Pour voir les résultats de chaque opération mathématique, saisissez print('{0}\n{1}\n{2}'.format(SetX, SetY, SetZ)) et appuyez sur Entrée. Un ensemble est affiché sur chaque ligne, comme ceci :

{'Vert', 'Bleu', 'Orange', 'Rouge', 'Noir', 'Jaune'}
{'Noir', 'Vert'}
{'Bleu', 'Rouge'}

Les sorties montrent les résultats des opérations mathématiques : union(), intersection() et difference(). Le formatage amélioré de Python peut s’avérer utile pour travailler avec des collections telles que des ensembles. La fonction format() indique à Python les objets à placer dans chacun des espaces réservés de la chaîne. Un espace réservé est un ensemble de parenthèses ({}) contenant un nombre facultatif. Le caractère d’échappement (essentiellement une sorte de caractère de contrôle ou de caractère spécial), /n, fournit un caractère de retour à la ligne entre les entrées. Pour en savoir plus sur le formatage amélioré, consultez le site https://docs.python.org/fr/3.8/tutorial/inputoutput.html.

Vous pouvez également tester les relations entre les différents ensembles. Par exemple, saisissez SetA.issuperset(SetY) et appuyez sur Entrée. La valeur de sortie True indique que SetA est un sur-ensemble de SetY. De la même manière, si vous saisissez SetA.issubset(SetX) et appuyez sur Entrée, vous constaterez que SetA est un sous-ensemble de SetX.

Il est important de comprendre que les ensembles sont soit mutables (on emploie aussi l’adjectif muable), soit immuables (c’est-à-dire qu’ils ne peuvent pas être modifiés). Tous les ensembles de cet exemple sont mutables, ce qui signifie que vous pouvez y ajouter ou en retirer des éléments. Par exemple, si vous saisissez SetA.add('Violet') et appuyez sur Entrée, SetA reçoit un nouvel élément. Si vous saisissez SetA. issubset(SetX) et appuyez sur Entrée, vous constaterez que SetA n’est plus un sous-ensemble de SetX, car SetA contient l’élément 'Violet'.

Utiliser des listes

Python définit une liste comme une sorte de séquence. Les séquences fournissent simplement un moyen de permettre à plusieurs éléments de données d’exister ensemble dans une seule unité de stockage, mais en tant qu’entités distinctes. Pensez à l’un de ces grands casiers à courrier que l’on trouve dans les immeubles d’habitation. Un seul porte-courrier contient un certain nombre de petites boîtes aux lettres, chacune pouvant contenir du courrier. Python prend également en charge d’autres types de séquences :

• Tuples : un tuple (la documentation de Python emploie parfois le terme n-uplet pour désigner un tuple) est une collection employée pour créer des séquences complexes de type liste. L’un des avantages des tuples est qu’il est possible d’imbriquer leur contenu. Cette caractéristique permet de créer des structures pouvant contenir des enregistrements de salariés ou des paires de coordonnées x-y.

• Dictionnaires : comme pour les vrais dictionnaires, vous créez des paires clé/valeur lorsque vous utilisez la collection dictionnaires (pensez à un mot et à la définition qui lui est associée). Un dictionnaire offre des temps de recherche incroyablement rapides et facilite considérablement le classement des données.

• Les piles : la plupart des langages de programmation prennent directement en charge les piles. Cependant, Python ne prend pas en charge les piles, bien qu’il existe une solution de contournement. Une pile est une séquence dernier entré/premier sorti (LIFO). Imaginez une pile de crêpes : vous pouvez ajouter de nouvelles crêpes au sommet et en retirer. Une pile est une collection importante que vous pouvez simuler en Python à l’aide d’une liste.

• Files : une file est une collection de type premier entré/premier sorti (FIFO). Elle sert à suivre les éléments qui doivent être traités d’une manière ou d’une autre. Imaginez une file comme une file d’attente à la banque. Vous faites la queue, vous attendez votre tour et vous êtes finalement appelé pour parler à un employé.

• Deques : une file à double extrémité (deque se prononce deck) est une structure de type file qui permet d’ajouter ou de supprimer des éléments à l’une ou l’autre extrémité, mais pas au milieu. Vous pouvez utiliser un deque comme une file, une pile ou tout autre type de collection à laquelle vous ajoutez et dont vous retirez des éléments de manière ordonnée (contrairement aux listes, aux tuples et aux dictionnaires, qui permettent un accès et une gestion aléatoires).

De toutes les séquences, les listes sont les plus faciles à comprendre et sont plus en rapport avec les objets du monde réel. Travailler avec des listes permet de mieux travailler avec d’autres types de séquences qui offrent une plus grande fonctionnalité et une plus grande flexibilité. Le fait est que les données sont stockées dans une liste de la même manière que vous les écririez sur une feuille de papier : un élément vient après l’autre. Les sections suivantes vous aideront à mieux comprendre les listes.

Définition d’une liste

La liste a un début, un milieu et une fin. Les éléments sont numérotés (même si vous ne les numérotez pas dans la vie réelle, Python numérote toujours les éléments à votre place). Pour voir comment vous pouvez travailler avec des listes, ouvrez l’invite Anaconda, saisissez python et appuyez sur Entrée, puis saisissez le code suivant :

ListeA = [0, 1, 2, 3]
ListeB = [4, 5, 6, 7]
ListeA.extend(ListeB)
ListeA

Lorsque vous saisissez la dernière ligne de code, vous obtenez [0, 1, 2, 3, 4, 5, 6, 7]. La fonction extend() ajoute les membres de ListeB à ListeA. Outre l’extension des listes, vous pouvez également les compléter à l’aide de la fonction append(). Saisissez ListeA.append(−5) et appuyez sur Entrée. Lorsque vous saisissez ListeA et que vous appuyez à nouveau sur Entrée, vous voyez que Python a ajouté -5 à la fin de la liste. Il se peut que vous deviez à nouveau supprimer des éléments, ce que vous ferez à l’aide de la fonction remove(). Par exemple, saisissez ListeA.remove(−5) et appuyez sur Entrée. Lorsque vous listez à nouveau ListeA, vous constatez que l’entrée ajoutée a disparu.

Combiner des listes à l’aide de la concaténation

Les listes permettent la concaténation à l’aide du signe plus (+). Par exemple, si vous saisissez ListeX = ListeA + ListeB et que vous appuyiez sur Entrée, vous constaterez que la nouvelle liste créée ListeX contient à la fois ListeA et ListeB, les éléments de ListeA venant en premier.

Construire des listes à l’aide de compréhensions

Les compréhensions de liste constituent une méthode simple et élégante pour créer des listes au contenu spécifique. Une compréhension apparaît entre crochets après l’affectation du nom de la liste et comprend trois éléments : une expression, un élément individuel sur lequel agir et une liste d’éléments avec lesquels travailler. Par exemple, la compréhension de liste suivante place les lettres individuelles de la chaîne "Bonjour" dans la liste Lettres après les avoir converties en majuscules :

Lettres = [lettre.upper() for lettre in "Bonjour"]
print(Lettres)

La sortie contiendrait ['B', 'O', 'N', 'J', 'O', 'U', 'R']. Normalement, pour créer une liste comme celle-ci, il faudrait une boucle for et plusieurs lignes de code. La compréhension de liste réalise cela en une seule ligne, et le code est facile à lire.

Découpage de listes

Vous avez accès à un large éventail de méthodes de liste et de techniques permettant d’agir sur une partie seulement d’une liste. Ces opérations de découpage sont souvent désignées par leur nom anglais, slicing. La liste suivante présente quelques-unes des techniques les plus courantes pour interagir avec les listes. Vous trouverez un certain nombre d’autres méthodes dans les exemples du livre.

MaListe.append(x) : ajoute un élément à la fin de la liste.

• MaListe.insert(index, valeur) : ajoute un élément, valeur, à l’emplacement spécifié par index dans la liste.

Le premier élément d’une liste est numéroté 0, et non pas 1, comme on pourrait le penser. Ainsi, pour ajouter le chiffre 4 au début de MaListe, on utilise MaListe.insert(0, 4).

• MaListe.remove(x) : supprime la première occurrence de x de la liste.

• MaListe.extend(ListeB) : ajoute les éléments de ListeB à la fin de MaListe.

• MaListe[a : b] : renvoie uniquement la partie de la liste exprimée par le point de départ, a, et le point d’arrivée, b. Vous pouvez éventuellement ajouter un troisième élément, un pas (step), qui indique le nombre d’éléments à sauter entre les sorties.

• MaListe.index(valeur) : renvoie l’indice de la première occurrence de la valeur dans la liste.

• MaListe.sort() : trie la liste dans l’ordre. Vous pouvez spécifier reverse=True pour trier la liste dans l’ordre inverse. En outre, vous pouvez fournir une fonction spéciale pour définir l’ordre de tri.

• len(MaListe) : affiche la longueur de la liste.

• max(MaListe) : obtient la valeur maximale de la liste.

• min(MaListe) : obtient la valeur minimale de la liste.

Créer et utiliser des tuples

Un tuple est une collection utilisée pour créer des listes complexes, dans lesquelles vous pouvez intégrer un tuple à l’intérieur d’un autre. Cette imbrication permet de créer des hiérarchies avec des tuples. Une hiérarchie peut être quelque chose d’aussi simple que la liste des répertoires de votre disque dur ou l’organigramme de votre entreprise. L’idée est de pouvoir créer des structures de données complexes à l’aide d’un tuple.

Les tuples sont immuables, ce qui signifie que vous ne pouvez pas les modifier. Vous pouvez créer un nouveau tuple portant le même nom et le modifier d’une manière ou d’une autre, mais vous ne pouvez pas modifier un tuple existant. Les listes sont mutables, ce qui signifie que vous pouvez les modifier. Ainsi, un tuple peut sembler à première vue moins avantageux, mais l’immutabilité présente toutes sortes d’avantages, tels qu’une plus grande sécurité et une plus grande rapidité. En outre, les objets immuables sont plus faciles à utiliser avec plusieurs processeurs. Pour voir comment vous pouvez travailler avec des tuples, ouvrez l’invite Anaconda, saisissez python et appuyez sur Entrée, puis saisissez le code suivant :

MonTuple = (1, 2, 3, (4, 5, 6, (7, 8, 9)))

MonTuple est imbriqué à trois niveaux. Le premier niveau est constitué des valeurs 1, 2, 3 et d’un tuple. Le deuxième niveau est constitué des valeurs 4, 5, 6 et d’un autre tuple. Le troisième niveau est constitué des valeurs 7, 8 et 9. Pour voir comment fonctionne l’imbrication, saisissez le code suivant dans la console python :

for Valeur1 in MonTuple:
   if type(Valeur1) == int:
       print(Valeur1)
   else:
        for Valeur2 in Valeur1:
            if type(Valeur2) == int:
                print("\t", Valeur2)
            else:
                 for Valeur3 in Valeur2:
                     print("\t\t", Valeur3)

Lorsque vous exécutez ce code, vous constatez que les valeurs se situent réellement à trois niveaux différents. Vous pouvez voir les indentations qui indiquent le niveau :

1
2
3
        4
        5
        6
                7
                8
                9

Vous pouvez effectuer des tâches telles que l’ajout de nouvelles valeurs, mais vous devez le faire en ajoutant les entrées originales et les nouvelles valeurs à un nouveau tuple. En outre, vous ne pouvez ajouter des tuples qu’à un tuple existant. Pour voir comment ajouter des tuples, saisissez MonNouveauTuple = MonTuple.__add__((10, 11, 12, (13, 14, 15))) et appuyez sur Entrée. MonNouveauTuple contient de nouvelles entrées au premier et au deuxième niveau, comme ceci : (1, 2, 3, (4, 5, 6, (7, 8, 9)), 10, 11, 12, (13, 14, 15)).

Définition d’itérateurs utiles

Les chapitres suivants utilisent toutes sortes de techniques pour accéder à des valeurs individuelles dans divers types de structures de données. Pour cette section, vous utiliserez deux listes simples, définies de la manière suivante :

ListeA = ['Orange', 'Jaune', 'Vert', 'Marron']
ListeB = [1, 2, 3, 4]

La méthode la plus simple pour accéder à une valeur particulière consiste à utiliser un indice. Par exemple, si vous saisissez ListeA[1] et que vous appuyiez sur Entrée, vous obtiendrez 'Jaune'. Tous les indices en Python démarrent à partir de zéro, ce qui signifie que la première entrée est 0 et non pas 1.

Travailler avec des plages

Les plages constituent une autre méthode simple d’accès aux valeurs. Par exemple, si vous saisissez ListeB[1:3] et que vous appuyiez sur Entrée, vous obtiendrez [2, 3]. Vous pouvez utiliser la plage comme entrée d’une boucle for, telle que

for Valeur in ListeB[1 : 3] :
print Valeur

Au lieu de la liste entière, vous voyez seulement 2 et 3 comme sorties, affichées sur des lignes séparées. La plage comporte deux valeurs séparées par le caractère deux-points. Toutefois, les valeurs sont facultatives. Par exemple, ListeB[ : 3] produirait [1, 2, 3]. Lorsque vous omettez une valeur, la plage commence au début ou à la fin de la liste, selon le cas.

Itération de plusieurs listes à l’aide de zip

Il est parfois nécessaire de traiter deux listes en parallèle. La méthode la plus simple consiste à utiliser la fonction zip(). Voici un exemple de la fonction zip() en action :

for Valeur1, Valeur2 in zip(ListeA, ListeB) :
print(Valeur1, '\t', Valeur2)

Ce code traite en même temps ListeA et ListeB. Le traitement se termine lorsque la boucle for atteint la fin de la plus courte des deux listes. Dans cet exemple, vous voyez la sortie suivante :

Orange    1
Jaune    2
Vert     3
Marron    4

Cette utilisation des itérateurs n’est que la partie émergée de l’iceberg. Vous verrez une multitude de types d’itérateurs utilisés tout au long du livre. L’idée est de vous permettre d’énumérer uniquement les éléments que vous souhaitez, plutôt que tous les éléments d’une liste ou d’une autre structure de données. Certains des itérateurs utilisés dans les chapitres suivants sont un peu plus compliqués que ce que vous voyez ici, mais vous avez déjà de bonnes bases.

Travailler avec des générateurs en utilisant yield

Vous pouvez utiliser un générateur de plusieurs façons, mais essentiellement pour vous aider à parcourir les objets. Considérez-le comme une sorte de boucle for flexible que vous n’avez pas à recréer et pour laquelle vous pouvez définir un certain nombre d’actions. Pour l’instant, nous ne voulons pas compliquer les choses en créant un générateur extrêmement simple. Pour voir comment vous pouvez travailler avec des générateurs en utilisant yield, ouvrez l’invite Anaconda, saisissez python et appuyez sur Entrée, puis saisissez le code suivant :

def MaLettre():
   for Lettre in Lettres:
       yield Lettre

Cette fonction va itérer à travers le contenu d’une liste nommée Lettres. L’utilisation de yield signifie que l’état du générateur est sauvegardé chaque fois que vous l’appelez. Par conséquent, vous obtiendrez la première lettre lors du premier appel, la deuxième lettre lors du deuxième appel, et ainsi de suite. Saisissez Lettres = [lettre.upper() for lettre in "Bonjour"] pour créer Lettres. Maintenant, utilisez ce code pour tester le générateur :

ListIt = MaLettre()
next(ListIt)
next(ListIt)
next(ListIt)
next(ListIt)
next(ListIt)
next(ListIt)
next(ListIt)

Chaque appel à next() produit une nouvelle lettre jusqu’à ce que Lettres n’ait plus de lettres à fournir. Si vous deviez appeler next() à nouveau, Python afficherait un message d’erreur.

Indexation des données à l’aide de dictionnaires

Un dictionnaire est un type particulier de séquence qui utilise une paire nom/valeur. L’utilisation d’un nom permet d’accéder facilement à des valeurs particulières à l’aide d’un indice qui ne soit pas numérique. Les sections suivantes vous permettront d’en savoir plus sur les dictionnaires.

Création de dictionnaires

Pour créer un dictionnaire, vous devez placer les paires de noms et de valeurs entre crochets. Créez un dictionnaire de test en saisissant MonDict = {'Orange':1, 'Bleu':2, 'Rose':3} et en appuyant sur Entrée.

Stocker et extraire des données de dictionnaires

Pour accéder à une valeur particulière, on utilise le nom comme indice. Par exemple, saisissez MonDict['Rose'] et appuyez sur Entrée pour obtenir la valeur de sortie 3. L’utilisation de dictionnaires comme structures de données vous permet d’accéder facilement à des ensembles de données incroyablement complexes en utilisant des termes compréhensibles par tous. À bien d’autres égards, travailler avec un dictionnaire revient à travailler avec n’importe quelle autre séquence.

Les dictionnaires ont quelques caractéristiques particulières. Par exemple, saisissez MonDict.keys() et appuyez sur Entrée pour obtenir la liste des clés. Vous pouvez utiliser la fonction values() pour afficher la liste des valeurs du dictionnaire.

Les clés du dictionnaire font la différence entre les majuscules et les minuscules. Par conséquent, 'Rose', 'ROSE' et 'rose' sont des clés différentes.

DANS CE CHAPITRE

Comprendre Google Colab

•

Accéder à Google et Colab

•

Exécuter des tâches essentielles dans Colab

•

Obtenir des informations complémentaires

Chapitre 6

Travailler avec Google Colab

Colaboratory (https://colab.research.google.com/notebooks/welcome.ipynb), que l’on abrège en Colab, est un service Google qui réplique Jupyter Notebook dans le cloud. Vous n’avez pas besoin d’installer quoi que ce soit sur votre système pour l’utiliser. À bien des égards, vous utilisez Colab comme vous le feriez avec une installation de Jupyter Notebook (également appelé Notebook tout au long du livre). Ce livre inclut ce chapitre principalement pour les lecteurs qui utilisent autre chose qu’une installation de bureau standard pour travailler sur les exemples.

Étant donné que vous n’utilisez peut-être pas les mêmes versions de produits que celles présentées dans ce livre, le code source des exemples de ce livre peut ne pas fonctionner exactement comme cela est décrit dans le texte lorsque vous utilisez Colab. De la même manière, lorsque vous employez Colab, il se peut que vous n’obteniez pas les résultats présentés dans ce livre en raison des différences de matériel entre les plateformes. Les premières sections de ce chapitre présentent Colab de manière plus détaillée et vous aident à comprendre ce que vous pouvez en attendre. Cependant, la chose la plus importante à retenir est que Colab ne remplace pas Jupyter Notebook et que les exemples ne sont pas testés pour fonctionner spécifiquement avec Colab, mais vous pouvez l’essayer avec votre ordinateur si vous le souhaitez pour suivre les exemples.

Pour utiliser Colab, vous devez disposer d’un compte Google et accéder à Colab à l’aide de votre compte. Dans le cas contraire, la plupart des fonctionnalités de Colab ne marcheront pas. La section suivante du chapitre permet de démarrer avec Google et d’accéder à Colab.

Comme avec Notebook, vous pouvez utiliser Colab pour effectuer des tâches spécifiques dans un modèle orienté cellule. Les sections suivantes du chapitre abordent une série de sujets liés à des tâches qui commencent par l’utilisation de notebooks. Si vous avez utilisé Notebook dans les chapitres précédents, vous noterez une forte ressemblance entre Notebook et Colab. Bien entendu, vous pourrez également effectuer d’autres types de tâches, telles que la création de différents types de cellules et leur emploi pour créer des notebooks qui ressemblent à ceux que vous avez créés avec Notebook.

Enfin, comme ce chapitre ne peut pas aborder tous les aspects de Colab, la dernière section du chapitre constitue une ressource pratique pour trouver les informations les plus fiables sur Colab.

Qu’est-ce que Google Colab ?

Google Colab est la version cloud de Notebook. La page d’accueil en témoigne d’ailleurs. Le site utilise même des fichiers IPython (l’ancien nom de Jupyter) Notebook (.ipynb). En effet, vous visualisez un notebook dans votre navigateur. Bien que les deux applications soient similaires et qu’elles emploient toutes les deux des fichiers .ipynb, elles présentent certaines différences qu’il convient de connaître et que les sections suivantes vous aideront à comprendre.

Comprendre ce que fait Google Colab

Vous pouvez utiliser Colab pour effectuer de nombreuses tâches, mais dans le cadre de ce livre, vous allez l’employer pour écrire et exécuter du code, créer la documentation associée et afficher des graphiques, tout comme vous le faites avec Notebook. Les techniques dont vous vous servirez sont en fait similaires à celles du Notebook, mais plus loin dans le chapitre, vous découvrirez les petites différences entre les deux. Malgré cela, le code source téléchargeable de ce livre devrait fonctionner sans trop d’efforts de votre part.

Notebook est une application qui utilise des ressources locales. Vous pourriez éventuellement utiliser d’autres sources, mais cela pourrait s’avérer peu pratique ou impossible dans certains cas. Par exemple, selon https://help.github.com/articles/working-with-jupyter-notebook-files-on-github/, vos fichiers Notebook apparaîtront comme des pages HTML statiques lorsque vous utilisez un dépôt GitHub. En fait, certaines fonctionnalités ne marcheront pas du tout. Colab permet d’interagir pleinement avec vos fichiers Notebook en utilisant GitHub comme dépôt. En fait, Colab prend en charge un certain nombre d’options de stockage en ligne, de sorte que vous pouvez considérer Colab comme votre partenaire en ligne dans la création de code Python.

L’autre raison pour laquelle vous devez vraiment connaître Colab, c’est qu’il est possible de l’utiliser avec un terminal alternatif. Au cours du processus d’écriture de ce livre, une partie du code des exemples a été testée sur une tablette Android (une ASUS ZenPad 3S 10). Sur cette tablette était installé Chrome et le code s’exécutait suffisamment bien pour suivre les exemples. Cela dit, il n’est guère souhaitable de tenter d’écrire du code sur une tablette de cette taille, car d’une part le texte est incroyablement petit, et d’autre part l’absence de clavier pourrait également être un problème. Le fait est que vous n’êtes pas obligé d’avoir un système Windows, Linux ou OS X pour essayer le code, mais les alternatives pourraient ne pas offrir les performances que vous attendez.

Google Colab ne fonctionne généralement pas avec les navigateurs autres que Chrome, Firefox ou Safari. Dans la plupart des cas, un message d’erreur tel que « Ce site peut ne pas fonctionner dans votre navigateur. Veuillez utiliser un navigateur pris en charge. » et aucun autre message ne s’affiche si vous essayez de lancer Colab dans un navigateur qui n’est pas pris en charge. Le lien « Plus d’infos » vous mènera à https://research.google.com/colaboratory/faq.html#browsers, où vous pouvez obtenir plus d’informations. Votre version de Firefox peut également nécessiter une certaine configuration pour fonctionner correctement (voir la section « Utiliser le support d’exécution local » , plus loin dans ce chapitre, pour plus de détails). La lourdeur du travail de configuration que vous aurez à effectuer dépend des fonctionnalités de Colab que vous choisirez d’utiliser. De nombreux exemples fonctionnent parfaitement dans Firefox sans aucune modification.

Tenir compte des différences du codage en ligne

Pour l’essentiel, Colab s’utilise comme Notebook. Toutefois, certaines fonctionnalités marchent différemment. Par exemple, pour exécuter le code d’une cellule, on la sélectionne et on clique sur le bouton d’exécution (flèche orientée vers la droite) de cette cellule. La cellule actuelle reste sélectionnée, ce qui signifie que vous devez initier la sélection de la cellule suivante en tant qu’action distincte. Un bloc situé à côté de la sortie permet d’effacer uniquement cette sortie sans affecter aucune autre cellule. Le passage de la souris sur le bloc indique quand quelqu’un a exécuté le contenu. Sur le côté droit de la cellule, on voit une ellipse verticale sur laquelle on peut cliquer pour afficher un menu d’options. Le résultat est le même que lorsque l’on utilise Notebook, mais le processus pour y parvenir est différent.

Quelques bizarreries de Firefox

Même avec l’aide en ligne, il se peut que votre copie de Firefox affiche un message d’erreur SecurityError : L’opération n’est pas sécurisée. La boîte de dialogue d’erreur initiale indique un problème sans rapport, tel que les cookies, mais vous voyez ce message d’erreur lorsque vous cliquez sur Détails. Le simple fait d’ignorer la boîte de dialogue en cliquant sur OK donnera l’impression que Colab fonctionne parce qu’il affiche votre code, mais vous ne verrez pas les résultats de l’exécution du code.

Pour commencer à résoudre ce problème, assurez-vous que votre copie de Firefox est à jour ; les versions plus anciennes ne fourniront pas le support nécessaire. Après avoir mis à jour votre copie, le réglage de la préférence network.websocket.al-lowInsecureFromHTTPS à l’aide de l’onglet about : preferences sur Vrai devrait résoudre le problème, mais parfois ce n’est pas le cas. Dans ce cas, vérifiez que Firefox autorise bien les cookies tiers en sélectionnant à la fois l’option Toujours pour accepter les cookies tiers et les données du site et l’option Conserver l’historique dans la section Historique de l’onglet Vie privée et sécurité de la boîte de dialogue Paramètres. Redémarrez Firefox après chaque modification et réessayez Colab. Si aucune de ces solutions ne fonctionne, vous devez utiliser Chrome pour travailler avec Colab sur votre système.

Le processus de travail avec le code diffère également de celui de Notebook. Certes, vous saisissez le code comme vous l’avez toujours fait et le code s’exécute sans problème dans Colab. La différence réside dans la manière dont vous pouvez gérer le code. Vous pouvez télécharger le code depuis votre disque local comme vous le souhaitez, puis l’enregistrer sur Google Drive ou GitHub. Le code devient alors accessible depuis n’importe quel appareil en accédant à ces mêmes sources. Il suffit de charger Colab pour y accéder.

Si vous utilisez Chrome lorsque vous travaillez avec Colab et que vous choisissiez de synchroniser votre copie de Chrome entre plusieurs appareils, tout votre code devient disponible sur n’importe quel appareil avec lequel vous travaillerez. La synchronisation transfère vos choix à tous vos appareils, à condition que ces derniers soient également configurés pour synchroniser leurs paramètres. Par conséquent, vous pouvez écrire du code sur votre ordinateur de bureau, le tester sur votre tablette, puis le réviser sur votre smartphone. Il s’agit du même code, du même référentiel et de la même configuration Chrome, mais sur un appareil différent.

Cependant, vous constaterez peut-être que toute cette flexibilité se fait au prix de la rapidité et de l’ergonomie. En examinant les différentes options, nous avons constaté qu’une copie locale de Notebook exécute le code de ce livre plus rapidement qu’une copie de Colab employant n’importe laquelle des configurations disponibles (même en travaillant avec une copie locale du fichier .ipynb). Ainsi, vous échangez la vitesse contre la flexibilité lorsque vous travaillez avec Colab. En outre, il est difficile de visualiser le code source sur une tablette, et presque impossible de le faire sur un smartphone. Si le texte est suffisamment grand pour être vu, il n’est pas possible de voir suffisamment le code pour pouvoir l’éditer raisonnablement. Au mieux, vous pouvez examiner le code ligne par ligne pour déterminer comment il fonctionne.

L’utilisation de Notebook présente également d’autres avantages. Par exemple, lorsque vous travaillez avec Colab, vous avez la possibilité de télécharger vos fichiers source uniquement sous forme de fichiers .ipynb ou .py. Colab n’inclut pas toutes les autres options de téléchargement, notamment (mais sans s’y limiter) HTML, LaTeX et PDF. Par conséquent, vos options de création de présentations à partir du contenu en ligne sont également limitées à certains égards. En résumé, l’utilisation de Colab et de Notebook offre, dans une certaine mesure, des expériences de codage différentes. Cependant, ces deux applications ne s’excluent pas mutuellement, car elles partagent des formats de fichiers. En théorie, il est possible de passer de l’un à l’autre en fonction des besoins.

Lors de l’utilisation de Notebook et de Colab, il faut prendre en compte le fait que les deux produits utilisent pratiquement la même terminologie et de nombreuses fonctionnalités, mais ils ne sont pas complètement identiques. Les méthodes utilisées pour effectuer des tâches diffèrent légèrement, de même que la terminologie. Par exemple, une cellule Markdown dans Notebook est appelée cellule Texte dans Colab. La section « Exécuter des tâches courantes » , plus loin dans ce chapitre, vous présente d’autres différences à prendre en compte.

Utiliser le support d’exécution local

Le seul cas où vous avez réellement besoin d’un support d’exécution local, c’est lorsque vous souhaitez travailler dans un environnement d’équipe et que vous avez besoin de la vitesse ou de l’avantage d’accès aux ressources offert par un runtime local. L’utilisation d’un moteur d’exécution local permet normalement d’obtenir une vitesse supérieure à celle que vous obtenez en vous appuyant sur le nuage. En outre, un moteur d’exécution local vous permet d’accéder à des fichiers sur votre machine. Un runtime local vous permet également de contrôler la version de Notebook utilisée pour exécuter le code. Vous pouvez en savoir plus sur la prise en charge de l’exécution locale à l’adresse https://research.google.com/colaboratory/local-runtimes.html.

Vous devez prendre en compte plusieurs éléments pour déterminer si vous avez besoin d’un support d’exécution local. Le plus évident est que vous avez besoin d’un runtime local, ce qui signifie que cette option ne fonctionnera pas avec votre notebook ou votre tablette, à moins que votre notebook ne soit équipé de Windows, Linux ou OS X et de la version appropriée de Notebook. Votre notebook ou votre tablette aura également besoin d’un navigateur approprié ; Internet Explorer posera très certainement des problèmes, à supposer qu’il fonctionne.

Cependant, la considération la plus importante à prendre en compte lors de l’utilisation d’un runtime local, c’est que le code de Notebook ouvre maintenant potentiellement la porte à une infection. Vous devez faire confiance à la partie qui fournit le code. L’option d’exécution locale n’ouvre cependant pas votre machine aux autres personnes avec lesquelles vous partagez du code, car elles doivent soit utiliser leurs propres exécutions locales, soit s’appuyer sur le cloud pour exécuter le code.

Lorsque vous travaillez avec Colab en utilisant le support d’exécution local et Firefox, vous devez effectuer certaines configurations spéciales. Veillez à lire la section Configurations spécifiques aux navigateurs sur la page Exécutions locales pour vous assurer que Firefox est correctement configuré. Vérifiez toujours votre configuration. Firefox peut sembler fonctionner correctement avec Colab, mais un problème de configuration survient lorsque vous effectuez certaines tâches, et Colab affiche des messages d’erreur indiquant que le code ne s’est pas exécuté.

Travailler avec les fonctionnalités de Google Colab

Google Colab permet d’accéder à un certain nombre de fonctionnalités via le système de menus. Parmi celles-ci, il y a l’accélération matérielle, qui est décrite dans la section « Utilisation de l’accélération matérielle » , plus loin dans ce chapitre. Les fonctionnalités de cette section apparaissent toutes dans le menu Outils.

L’option Outils → Palette de commandes affiche une liste de commandes que vous pouvez exécuter, comme le montre la Figure 6.1. Vous noterez que certaines de ces commandes disposent également de touches de raccourci, comme Ctrl+Alt+P pour afficher le volet des extraits de code. Toutes ces commandes vous aident à effectuer des tâches associées au contenu du notebook, telles que l’ajout de formulaires.

[picture]

Figure 6.1 L‘utilisation des commandes Colab facilite la configuration de votre notebook.

L’option Outils → Paramètres permet d’afficher la boîte de dialogue Paramètres, illustrée à la Figure 6.2. Les six onglets de paramètres permettent d’effectuer les tâches suivantes :

Site : configure le fonctionnement du site. Le paramètre le plus intéressant est le thème. En sélectionnant Adaptive, Colab choisit les couleurs de l’interface en fonction des conditions d’éclairage. Cet onglet permet également de configurer les paramètres d’affichage et d’accès.

Éditeur : détermine la manière dont le texte apparaît à l’écran et le fonctionnement de l’interface. Par exemple, vous pouvez configurer les raccourcis clavier pour qu’ils fonctionnent comme ceux de Vim, si vous le souhaitez. Vous pouvez également sélectionner la taille de la police, les espaces pour chaque niveau d’indentation et une pléthore d’autres paramètres.

[picture]

Figure 6.2 La boîte de dialogue Paramètres permet de configurer l’environnement de développement de Colab.

Assistance IA : vous pouvez autoriser ou masquer les fonctionnalités d’IA générative. Notez toutefois que dans Colab, l'IA générative est une technologie expérimentale qui peut par conséquent afficher parfois des informations inexactes.

Colab Pro : présente une publicité pour Colab Pro, qui offre des avantages significatifs tels que des GPU plus rapides, des durées d’exécution plus longues et plus de mémoire, ce qui permet d’effectuer plus de travail en moins de temps.

GitHub : vous pouvez lier Colab à votre compte GitHub et accéder à des dépôts privés.

Divers : contient des paramètres amusants. Vous pouvez choisir parmi trois effets visuels : ajouter du tonnerre et des éclairs en utilisant le réglage du niveau de puissance ; vous pouvez laisser un Corgi courir en haut de l’écran, un chaton (Mode Kitty) ou un crabe. Vous pouvez choisir n’importe quelle combinaison de ces effets visuels.

Il se peut que les raccourcis clavier par défaut ne vous conviennent pas. Lorsque vous choisissez Outils → Raccourcis clavier, la boîte de dialogue Préférences du clavier, illustrée à la Figure 6.3, s’affiche et permet de personnaliser les raccourcis clavier en fonction de vos besoins. Si vous voyez le bouton Définir un raccourci, cela signifie que la commande n’a pas encore de raccourci, et vous pouvez donc en ajouter un si vous le souhaitez. Voici comment utiliser les raccourcis :

Pour ajouter ou modifier un raccourci, placez le curseur dans la case située à côté de la commande et appuyez sur la touche de raccourci que vous souhaitez utiliser pour cette commande.

[picture]

Figure 6.3 Vous pouvez personnaliser les touches de raccourci pour accélérer l’accès aux commandes.

Pour supprimer un raccourci, cliquez sur le raccourci et appuyez sur la touche Supprimer.

Vous avez parfois besoin de comparer deux fichiers pour voir en quoi ils diffèrent. Lorsque vous sélectionnez Outils → Diff. entre notebooks, Colab ouvre un nouvel onglet de navigateur et affiche deux notebooks côte à côte, comme le montre la Figure 6-4. Il s’agit de fichiers sélectionnés au hasard dans votre Google Drive. Pour sélectionner les fichiers avec lesquels vous souhaitez réellement travailler, cliquez sur la flèche vers le bas dans chaque volet. Les différences apparaissent à l’écran.

[picture]

Figure 6.4 Colab permet de comparer deux fichiers pour voir en quoi ils diffèrent.

Création d’un compte Google

Avant de pouvoir effectuer des tâches importantes avec Colab, vous devez disposer d’un compte Google. Vous pouvez utiliser le même compte pour toutes sortes d’activités, et pas seulement pour le développement. Par exemple, un compte Google vous donne accès à Google Docs (https://workspace.google.com/products/docs/), un système de gestion de documents en ligne similaire à Office 365.

Création du compte

Pour créer un compte Google, il suffit de se rendre sur le site https://www.google.com/account/about/?hl=fr et de cliquer sur le lien Créer un compte. Cette page contient également de nombreuses informations sur les possibilités offertes par un compte Google. Lorsque vous cliquez sur Créer un compte Google, la page illustrée à la Figure 6.5 s’affiche. Le processus de création de compte se déroule sur plusieurs pages. Il suffit de fournir les informations requises dans chaque page et de cliquer sur Suivant.

[picture]

Figure 6.5 Suivez les consignes pour créer votre compte Google.

Connexion

Après avoir créé et vérifié votre compte, vous pouvez vous y connecter. Avant de pouvoir utiliser Colab de manière efficace, vous devez vous connecter à votre compte. En effet, Colab utilise votre Google Drive pour certaines tâches. Vous pouvez également stocker des notebooks à d’autres endroits, tels que GitHub, mais le fait de disposer d’un compte Google garantit que tout fonctionne comme prévu. Pour vous connecter à votre compte, accédez à https://accounts.google.com/ ; la page de connexion illustrée à la Figure 6.6 s’affiche. Saisissez votre adresse électronique et cliquez sur Suivant.

[picture]

Figure 6.6 La page de connexion permet d’accéder à toutes les fonctions générales, y compris Google Drive.

Travailler avec des notebooks

Comme pour Jupyter Notebook, le notebook constitue la base des interactions avec Colab. En fait, Colab est construit sur des notebooks, comme nous l’avons indiqué précédemment. Lorsque vous placez la souris sur certaines parties de la page d’accueil à l’adresse https://colab.research.google.com/notebooks/welcome.ipynb, vous avez la possibilité d’interagir avec la page en ajoutant du code ou des entrées de texte (que vous pouvez utiliser pour prendre des notes si nécessaire). Ces entrées sont actives et vous pouvez donc interagir avec elles. Vous pouvez également déplacer les cellules et copier le résultat sur votre Google Drive. Bien entendu, même si l’interaction avec la page d’accueil est à la fois inattendue et amusante, le véritable objectif de ce chapitre est de vous montrer comment interagir avec les notebooks Colab. Les sections suivantes décrivent comment effectuer des tâches de base liées aux notebooks avec Colab.

Création d’un nouveau notebook

Pour créer un nouveau notebook, choisissez Fichier → Nouveau notebook dans Drive. Un nouveau notebook Python 3 s’affiche, tel que celui illustré à la Figure 6.7. Le nouveau notebook ressemble à ceux que l’on trouve dans Notebook, mais ce n’est pas exactement le même. Cependant, toutes les fonctionnalités sont identiques. Vous noterez que la dernière version de Colab ne prend pas en charge Python 2, si bien que vous ne pouvez l’utiliser.

[picture]

Figure 6.7 Création d’un nouveau notebook Python 3 en utilisant les mêmes techniques que d’habitude.

Le notebook illustré à la Figure 6.7 permet de modifier le nom du fichier en cliquant dessus, comme vous le faites lorsque vous travaillez dans Notebook. Certaines fonctions marchent différemment mais donnent les mêmes résultats. Par exemple, pour exécuter le code d’une cellule particulière, on clique sur la flèche pointant vers la droite à gauche de cette cellule. Contrairement à ce qui se passe dans Notebook, le focus de la cellule ne passe pas à la cellule suivante, et vous devez donc la sélectionner directement.

Ouverture de notebooks existants

Vous pouvez ouvrir des notebooks existants qui se trouvent dans le stockage local, sur Google Drive ou sur GitHub. Vous pouvez également ouvrir n’importe quel exemple Colab ou télécharger des fichiers à partir de sources auxquelles vous pouvez accéder, telles qu’un lecteur réseau sur votre système. Dans tous les cas, vous commencez par choisir Fichier → Ouvrir le notebook. La boîte de dialogue illustrée à la Figure 6.8 s’affiche.

[picture]

Figure 6.8 Cette boîte de dialogue permet d’ouvrir des notebooks existants.

L’affichage par défaut montre tous les fichiers que vous avez ouverts récemment, quel que soit leur emplacement. Les fichiers apparaissent dans l’ordre chronologique d’ouverture. Vous pouvez filtrer le nombre d’éléments affichés en saisissant une chaîne de caractères dans le champ Rechercher des notebooks. D’autres options d’ouverture des notebooks sont proposées en haut de l’écran.

Même si vous n’êtes pas connecté, vous pouvez toujours accéder aux projets d’exemple de Colab. Ces projets vous aident à comprendre Colab, mais ne vous permettent pas de faire quoi que ce soit avec vos propres projets. Vous pouvez néanmoins expérimenter Colab sans vous connecter à Google. Les sections suivantes présentent ces options plus en détail.

Utilisation de Google Drive pour les notebooks existants

Google Drive est l’emplacement par défaut pour de nombreuses opérations dans Colab, et vous pouvez toujours le choisir comme destination. Lorsque vous travaillez avec Drive, vous voyez une liste de fichiers similaire à celle illustrée à la Figure 6.8. Pour ouvrir un fichier particulier, on clique sur son lien dans la boîte de dialogue. Le fichier s’ouvre dans l’onglet actuel de votre navigateur.

Utilisation de GitHub pour les notebooks existants

Lorsque vous travaillez avec GitHub, vous devez d’abord indiquer l’emplacement du code source en ligne, comme le montre la Figure 6.9. L’emplacement doit pointer vers un projet public et vous ne pouvez donc pas utiliser Colab pour accéder à des projets privés.

[picture]

Figure 6.9 Lorsque vous utilisez GitHub, vous devez indiquer l’emplacement du code source.

Une fois la connexion à GitHub établie, deux listes s’affichent : les dépôts, qui sont des conteneurs pour le code lié à un projet particulier, et les branches, une implémentation particulière du code. La sélection d’un dépôt et d’une branche affiche une liste de fichiers notebook que vous pouvez charger dans Colab. Il suffit de cliquer sur le lien requis pour qu’il se charge comme si vous utilisiez un Google Drive.

Utilisation du stockage local pour les notebooks existants

Si vous souhaitez utiliser la source téléchargeable de ce livre, ou toute autre source locale d’ailleurs, vous devez sélectionner l’onglet Importer de la boîte de dialogue Ouvrir le notebook. Au centre se trouve un seul bouton, Parcourir. En cliquant sur ce bouton, vous ouvrez la boîte de dialogue d’ouverture de fichier de votre navigateur. Vous localisez le fichier que vous souhaitez télécharger, comme vous le feriez normalement pour ouvrir n’importe quel fichier.

Le fait de sélectionner un fichier et de cliquer sur Ouvrir charge le fichier sur Google Drive. Si vous modifiez le fichier, ces modifications apparaissent sur Google Drive, et non pas sur votre disque local. Selon votre navigateur, une nouvelle fenêtre s’ouvre généralement avec le code chargé. Dans ce cas, vous devez ouvrir le fichier en utilisant la même technique que lorsque vous utilisez Google Drive. Dans certains cas, votre navigateur vous demande si vous souhaitez quitter la page en cours. Vous devez demander au navigateur de le faire.

La commande Fichier → Importer le notebook permet également de charger un fichier sur Google Drive. En fait, le chargement d’un notebook fonctionne comme le chargement de n’importe quel autre type de fichier, et vous voyez la même boîte de dialogue. Si vous souhaitez charger d’autres types de fichiers, l’utilisation de la commande Fichier → Importer le notebook est probablement plus rapide.

Chargement d’un notebook

On utilise la technique décrite dans la section précédente de ce chapitre, « Utilisation du stockage local pour les notebooks existants » , pour charger un notebook. La principale raison pour laquelle vous allez effectuer cette tâche dans le cadre de ce livre, c’est le chargement de la source téléchargeable sur votre Google Drive. L’introduction du livre vous indique où trouver ces fichiers et nous vous recommandons vivement d’utiliser la source téléchargeable lorsque vous travaillez sur les exemples.

Certains exemples fonctionneront différemment dans Colab en raison du support qu’il fournit. Par exemple, si vous téléchargez le fichier ML4D2E; 04; Sample.ipynb du Chapitre 4, vous constaterez que les résultats diffèrent de ceux d’une installation Anaconda de bureau, comme cela est illustré à la Figure 6.10.

[picture]

Figure 6.10 Votre résultat peut différer de celui du livre lorsque vous utilisez Colab.

Vous noterez que la version de Python est la 3.10.12 à la Figure 6.10 au lieu de la 3.8.3 utilisée dans le livre. La différence de version pourrait affecter le résultat de certains codes. De plus, il n’y a pas d’affichage de la version d’Anaconda car vous n’utilisez pas Anaconda. Cependant, la tentative d’exécution de la partie Anaconda du code ne génère pas d’erreur, Conda arrivant apparemment à ignorer cette partie du code.

Sauvegarde des notebooks

Colab propose un grand nombre d’options pour sauvegarder vos notebooks. Cependant, aucune de ces options ne fonctionne avec votre disque local. Après avoir chargé le contenu de votre disque local vers Google Drive ou GitHub, Colab gère le contenu dans le cloud et non pas sur votre disque local. Pour enregistrer les mises à jour sur votre disque local, vous devez télécharger le fichier à l’aide des techniques décrites dans la section « Télécharger des notebooks » , plus loin dans ce chapitre. Les sections suivantes passent en revue les options basées sur le cloud pour l’enregistrement des notebooks.

Utiliser Drive pour sauvegarder des notebooks

L’emplacement par défaut pour le stockage de vos données est Google Drive. Lorsque vous choisissez Fichier → Enregistrer, le contenu que vous créez va dans le répertoire racine de votre Google Drive. Si vous souhaitez enregistrer le contenu dans un autre dossier, vous devez sélectionner ce dossier dans Google Drive (https://drive.google.com/).

Colab suit les versions de votre projet au fur et à mesure que vous effectuez des sauvegardes. Cependant, au fur et à mesure que ces révisions deviennent obsolètes, Colab les supprime. Pour sauvegarder une version que vous souhaitez conserver, utilisez la commande Fichier → Enregistrer et épingler cette version. Pour voir les révisions de votre projet, choisissez Fichier → Historique des versions. Vous obtenez le résultat illustré à la Figure 6.11. Remarquez que les entrées sont épinglées. Vous pouvez également épingler des entrées en les sélectionnant dans la liste Historique. L’historique des révisions indique également la date de modification et l’auteur de la révision. Vous pouvez utiliser cette liste pour restaurer une version précédente ou télécharger la révision sur votre disque local.

[picture]

Figure 6.11 Colab conserve un historique des révisions de votre projet.

Vous pouvez également enregistrer une copie de votre projet en choisissant Fichier → Enregistrer une copie dans Drive. La copie est préfixée avec les mots Copie de. Bien entendu, vous pouvez la renommer ultérieurement. Colab stocke la copie dans le dossier Google Drive actuel.

Utiliser GitHub pour sauvegarder des notebooks

GitHub est une alternative à Google Drive pour l’enregistrement des contenus. Il offre une méthode organisée de partage de code à des fins de discussion, de révision et de distribution. GitHub est accessible à https://github.com/.

Vous ne pouvez utiliser que des référentiels publics lorsque vous travaillez avec GitHub depuis Colab, même si GitHub prend également en charge les référentiels privés. Pour enregistrer un fichier sur GitHub, choisissez Fichier → Enregistrer une copie dans GitHub. Si vous n’êtes pas déjà connecté à GitHub, Colab affiche une fenêtre qui demande vos informations de connexion. Une fois que vous vous êtes connecté, une boîte de dialogue similaire à celle illustrée à la Figure 6.12 s’affiche.

[picture]

Figure 6.12 En utilisant GitHub, vous stockez vos données dans un référentiel.

Si votre compte n’a pas encore de dépôt, vous devez soit créer un nouveau dépôt, soit choisir un dépôt existant dans lequel stocker vos données. Après avoir enregistré le fichier, celui-ci apparaîtra dans le dépôt GitHub de votre choix. Le dépôt comprendra un lien permettant d’ouvrir par défaut les données dans Colab, à moins que vous ne choisissiez de ne pas inclure cette fonctionnalité.

Utiliser GitHub Gist pour sauvegarder des notebooks

On utilise les gists de GitHub pour partager des fichiers ou d’autres ressources avec d’autres personnes. Certaines personnes les utilisent également pour des projets complets, mais on les emploie à la base quand on a un concept que l’on veut partager (quelque chose qui n’est pas tout à fait abouti et qui n’est pas encore une application utilisable). Pour en savoir plus sur les gists, consultez le site https://docs.gi-thub.com/fr/get-started/writing-on-github/editing-and-sharing-content-with-gists/creating-gists.

Comme pour GitHub, les gists sont disponibles sous forme publique et secrète. Vous pouvez accéder aux gists publics et secrets à partir de Colab, mais Colab garde automatiquement vos fichiers secrets. Pour enregistrer votre projet actuel en tant que gist, il faut choisir la commande Fichier → Enregistrer une copie en tant que Gist GitHub. Contrairement à GitHub, vous n’avez pas besoin de créer un référentiel ou de faire quoi que ce soit de sophistiqué dans ce cas. Le fichier s’enregistre en tant que gist sans aucun effort supplémentaire. L’entrée résultante contient toujours un bouton Open in Colab, comme le montre la Figure 6.13.

[picture]

Figure 6.13 Utilisez les gists pour stocker des fichiers individuels ou d’autres ressources.

Télécharger des notebooks

Colab propose deux méthodes pour télécharger les notebooks sur votre disque local : les fichiers .ipynb (en utilisant Fichier → Télécharger le fichier .ipynb) et les fichiers .py (en utilisant Fichier → Télécharger le code .py). Dans les deux cas, le fichier apparaît dans le répertoire de téléchargement par défaut de votre navigateur ; Colab ne propose pas de méthode pour télécharger le fichier dans un répertoire spécifique.

Exécuter des tâches courantes

La plupart des tâches dans Colab fonctionnent de la même manière que dans Notebook. Par exemple, vous pouvez créer des cellules de code comme vous le faites dans Notebook en utilisant les options du menu Insérer. Les cellules qui ne contiennent pas de code se présentent sous trois formes différentes :

Texte

En-tête de section

Champ de formulaire, qui se décline en plusieurs types :

Liste déroulante

Entrée

Curseur

Markdown

Les cellules ne contenant pas de code fonctionnent un peu différemment des cellules Markdown que l’on trouve dans Notebook, mais l’idée est la même. Il y a deux ajouts intéressants que l’on ne trouve pas dans Notebook : les cellules de code bloc-notes, qui permettent d’expérimenter du code en temps réel, et les extraits de code, qui sont du code prêt à l’emploi pour effectuer des tâches spécifiques (il suffit de les insérer là où c’est nécessaire).

Vous pouvez également modifier et déplacer des cellules, comme vous le faites avec Notebook. Il y a cependant une différence importante, car vous ne pouvez pas changer le type de cellule. Une cellule créée en tant qu’en-tête de section ne peut pas se transformer tout à coup en cellule de code. Les sections suivantes donnent un bref aperçu des différentes fonctionnalités.

Création de cellules de code

La première cellule créée par Colab est une cellule de code. Le code utilise toutes les mêmes fonctions que celles que vous trouvez dans Notebook. Cependant, sur le côté droit de la cellule, on voit un menu d’options supplémentaires qui ne sont pas présentes dans Notebook, comme le montre la Figure 6.14.

[picture]

Figure 6.14 Les cellules de code Colab contiennent quelques éléments supplémentaires que l’on ne trouve pas dans Notebook.

Les icônes illustrées à la Figure 6.14 permettent de simplifier le codage dans Colab. La liste suivante fournit une brève description de ces fonctions (par ordre d’apparition, de gauche à droite, dans la figure) :

• Déplacer la cellule vers le haut : déplace la cellule d’une position vers le haut dans la hiérarchie des cellules.

• Déplacer la cellule vers le bas : déplace la cellule vers le bas d’une position dans la hiérarchie des cellules.

• Copier le lien dans la cellule : copie dans le presse-papiers un lien que vous pouvez utiliser pour accéder à une cellule spécifique du notebook. Vous pouvez intégrer ce lien n’importe où sur une page Web ou dans un notebook afin de permettre à quelqu’un d’accéder à cette cellule spécifique. La personne voit toujours l’ensemble du notebook, mais n’a pas besoin de rechercher la cellule dont vous voulez parler.

• Ajouter un commentaire (en supposant que vous ayez le droit de le faire) : crée une bulle de commentaire à droite de la cellule. Il ne s’agit pas de la même chose qu’un commentaire de code, qui existe en ligne dans le code mais qui affecte l’ensemble de la cellule. Vous pouvez modifier, supprimer ou résoudre les commentaires. Un commentaire résolu est un commentaire qui a fait l’objet d’une attention particulière et qui n’est plus applicable.

• Ouvrir les paramètres de l’éditeur : ouvre la même boîte de dialogue que celle illustrée à la Figure 6.2 et décrite dans la section « Travailler avec les fonctionnalités de Google Colab » , plus haut dans ce chapitre. Vous devez sélectionner du code avant que cette option n’apparaisse.

• Mettre en miroir la cellule dans un onglet : cela met en miroir la cellule sélectionnée dans un volet Cellule qui apparaît sur le côté droit de la fenêtre (voir la Figure 6.15). Vous pouvez faire défiler le code où vous le souhaitez dans le volet gauche tout en gardant le code accessible dans le volet droit. La flèche pointant vers la droite permet d’exécuter la cellule à tout moment après avoir apporté des modifications au code du volet gauche. Une paire de flèches à double pointe permet de ramener le focus sur le code sélectionné dans le volet gauche d’un simple clic. Vous pouvez également déplacer le code de la cellule vers une cellule bloc-notes, où vous pouvez tester votre code sans modifier le code d’origine. Il est possible d’avoir plusieurs volets Cellule. Il suffit de sélectionner celui que vous voulez et de passer de l’un à l’autre selon vos besoins, ce qui vous permet de vous déplacer facilement d’un endroit à l’autre dans votre code. Pour fermer un volet Cellule, cliquez sur la croix X située à côté du mot Cellule.

• Supprimer la cellule : supprime la cellule du notebook.

• Menu vertical (point de suspension) : contient un certain nombre de fonctions supplémentaires dans un menu :

• Sélectionner une cellule : sélectionne l’intégralité de la cellule (code et résultat).

[picture]

Figure 6.15 Utilisez les volets de cellules pour que les cellules importantes soient facilement accessibles en cas de besoin.

• Copier la cellule : copie le contenu de la cellule sélectionnée dans le presse-papiers.

• Couper la cellule : supprime le contenu de la cellule sélectionnée et le place dans le presse-papiers.

• Expliquer le code : l’IA entre ici en scène et ouvre un volet où l’on vous explique votre code. Chaque ligne de code est décortiquée (en anglais) afin de savoir ce qu’elle fait, et à la fin l’objectif du programme est résumé.

• Supprimer l’élément de sortie : supprime la sortie de la cellule. Vous devez exécuter le code à nouveau pour régénérer la sortie.

• Afficher les éléments de sortie en mode plein écran : affiche la sortie (et non pas la totalité de la cellule ou toute autre partie du notebook) en mode plein écran sur le périphérique hôte. Cette option est utile pour afficher une quantité importante d’informations ou lorsqu’une vue détaillée des graphiques permet d’expliquer un sujet. Appuyez sur Echap pour quitter le mode plein écran.

• Ajouter un formulaire : insère un formulaire dans la cellule à droite du code. Les formulaires servent à fournir une entrée graphique pour les paramètres. Les formulaires n’apparaissent pas dans Notebook, mais en raison de la façon dont vous les créez, ils ne vous empêcheront pas d’exécuter le code dans Notebook. Pour en savoir plus sur les formulaires, consultez le site https://colab.research.google.com/notebooks/forms.ipynb.

[picture]

Figure 6.16 L’IA de Google est désormais capable de générer automatiquement des explications sur le code d’une cellule.

En mai 2023, Google a annoncé que Colab allait ajouter des fonctionnalités de codage IA telles que les complétions de code, la génération de code en langage naturel et même un chatbot d’aide au codage. Colab utilise Codey, une famille de modèles de code construits sur PaLM 2. Les versions de Codey dont se sert Colab ont été personnalisées spécialement pour Python et pour les utilisations spécifiques à Colab.

Même si Google nous avertit que les fonctionnalités d’IA générative de Colab peuvent parfois afficher des informations inexactes ou choquantes qui ne représentent pas l’opinion de Google, et qu’il convient par conséquent d’utiliser le code avec précaution, il faut reconnaître que les nouvelles possibilités de l’IA générative sont bluffantes.

Création de cellules de texte

Les cellules de texte fonctionnent de la même manière que les cellules au format Markdown dans Notebook. Cependant, la Figure 6.17 montre que vous bénéficiez d’une aide supplémentaire pour formater le texte à l’aide d’une interface graphique. Le balisage est le même, mais l’interface graphique peut vous guider dans la création du balisage. Par exemple, pour créer le signe # pour un titre, on clique sur l’icône en forme de double T qui apparaît en premier dans la liste. En cliquant à nouveau sur cette icône, on augmente le niveau de l’en-tête. À droite, vous voyez comment le texte apparaîtra dans le notebook.

[picture]

Figure 6.17 Utilisez l’interface graphique pour faciliter la mise en forme de votre texte.

Vous noterez la présence du menu en haut à droite de la cellule de texte. Ce menu contient un grand nombre d’options identiques à celles d’une cellule de code. Par exemple, vous pouvez créer une liste de liens pour aider les gens à accéder à des parties spécifiques de votre notebook par le biais d’un index. Contrairement à Notebook, vous ne pouvez pas exécuter les cellules de texte pour résoudre les balises qu’elles contiennent.

Création de cellules spéciales

Les cellules spéciales fournies par Colab sont des variantes de la cellule de texte. Ces cellules, auxquelles on accède à l’aide de l’option de menu Insérer, accélèrent leur création. Les sections suivantes décrivent chacun de ces types de cellules spéciales.

Travailler avec des titres

Lorsque vous choisissez Insérer → Cellule d’en-tête de section, une nouvelle cellule qui contient l’entrée d’en-tête de niveau 1 est créée sous la cellule actuellement sélectionnée. Vous pouvez augmenter le niveau de l’en-tête en cliquant sur l’icône en forme de double T. L’interface graphique est identique à celle de la Figure 6.17, de sorte que vous disposez de toutes les fonctions de formatage standard pour votre texte.

Travailler avec une table des matières

La génération automatique d’une table des matières pour votre notebook est une fonctionnalité intéressante de Colab. Pour l’exploiter, cliquez sur l’icône Sommaire dans la partie gauche de la fenêtre. La Figure 6.18 montre le résultat pour cet exemple particulier.

La table des matières contient un titre pour chaque titre que vous fournissez dans votre code. Les titres sont automatiquement organisés par niveau, ce qui permet de voir la hiérarchie de votre code. En cliquant sur un en-tête, vous accédez automatiquement à l’emplacement correspondant dans votre code.

[picture]

Figure 6.18 La table des matières rend les informations du notebook plus accessibles.

Modification des cellules

Colab et Notebook disposent tous deux de menus d’édition contenant les options attendues, telles que la possibilité de couper, copier et coller des cellules. Les deux produits présentent également des différences intéressantes. Par exemple, Notebook permet de diviser et de fusionner des cellules. Colab contient une option, sous la forme d’une bascule, qui permet d’afficher ou de masquer le code. Ces différences font que chaque produit a un style particulier, mais elles ne changent pas fondamentalement votre capacité de créer et de modifier du code Python.

Déplacement de cellules

La technique utilisée pour déplacer des cellules dans Notebook fonctionne également avec Colab. La seule différence est que Colab s’appuie exclusivement sur les boutons de la barre d’outils ; Notebook dispose également d’options de déplacement de cellules dans le menu d’édition.

Utilisation de l’accélération matérielle

Votre code Colab est exécuté sur un serveur Google, votre ordinateur se contentant d’héberger un navigateur qui affiche le code et ses résultats. Par conséquent, tout matériel spécial sur votre ordinateur est ignoré, à moins que vous ne choisissiez d’exécuter le code localement.

Heureusement, vous disposez d’une autre option lorsque vous travaillez avec Colab. Choisissez Modifier → Paramètres du notebook pour afficher la boîte de dialogue Paramètres du notebook, illustrée à la Figure 6.19. Cette boîte de dialogue permet de choisir le niveau d’accélération matérielle de votre code en ajoutant une unité de traitement graphique (GPU) ou une unité de traitement tensoriel (TPU). L’article à https://medium.com/deep-learning-turkey/google-colab-free-gpu-tutorial-e113627b9f5d fournit des détails supplémentaires sur le fonctionnement de cette fonctionnalité.

[picture]

Figure 6.19 L’accélération matérielle accélère l’exécution du code.

La disponibilité d’un GPU ou d’une TPU n’est pas une invitation à effectuer de gros calculs à l’aide de Colab. Le contenu du site https://research.google.com/colaboratory/faq.html#gpu-availability vous indique les limites de l’accélération matérielle de Colab (y compris le fait qu’elle peut ne pas être disponible lorsque vous en avez besoin).

La boîte de dialogue Paramètres du notebook permet également de choisir d’inclure ou non la sortie des cellules lors de l’enregistrement du notebook. Étant donné que, dans la plupart des cas, vous stockez votre notebook dans le cloud et que le chargement de fichiers volumineux dans votre navigateur peut prendre du temps, la suppression de la sortie des cellules permet de redémarrer une session plus rapidement. Bien évidemment, la contrepartie est que vous devez ensuite régénérer toutes les sorties dont vous avez besoin.

Visualisation des notebooks

Un notebook présente une série de quatre icônes dans sa marge gauche, comme celles illustrées dans la Figure 6.7. En cliquant sur l’une de ces icônes, vous affichez un volet contenant des onglets qui présentent différents types d’informations sur votre notebook. Par exemple, en cliquant sur la première icône, vous affichez un volet Sommaire (table des matières). Vous pouvez également choisir des informations spécifiques à afficher à partir du menu Affichage. Pour fermer ce volet, cliquez sur le X dans le coin supérieur droit du volet. Les sections suivantes décrivent chacun de ces éléments d’information.

Affichage de la table des matières

Choisissez Affichage → Sommaire pour afficher la table des matières de votre notebook, comme cela est illustré à la Figure 6.18. En cliquant sur l’une des entrées, vous accédez à la section correspondante du notebook. En bas du volet se trouve un bouton + Section. Cliquez sur ce bouton pour créer une nouvelle cellule d’en-tête sous la cellule sélectionnée.

Obtenir des informations sur le notebook

Lorsque vous choisissez Affichage → Informations sur le notebook, une boîte de dialogue s’ouvre dans le navigateur, comme le montre la Figure 6.20. Ce volet contient la taille du notebook, les paramètres et éventuellement le propriétaire. Vous noterez que l’affichage indique également la taille maximale du notebook.

La boîte de dialogue Informations sur le notebook comprend également un lien Ouvrir les paramètres du notebook qui affiche la boîte de dialogue Paramètres du notebook, dans laquelle vous pouvez choisir le type d’exécution et si le notebook exploite l’accélération matérielle, comme nous l’avons décrit dans la section « Utilisation de l’accélération matérielle » , plus haut dans ce chapitre.

Vérification de l’exécution du code

Colab garde une trace de votre code au fur et à mesure que vous l’exécutez. Choisissez Affichage → Historique d’exécution pour afficher l’onglet Exécutions dans le volet situé à droite de la fenêtre (Figure 6.21). Vous noterez que le numéro associé aux entrées de l’onglet Code exécuté peut ne pas correspondre aux numéros associés aux cellules correspondantes. En outre, chaque exécution unique de code reçoit un numéro distinct.

[picture]

Figure 6.20 Les informations relatives au notebook comprennent à la fois la taille et les paramètres.

[picture]

Figure 6.21 Colab suit le code que vous exécutez et l’ordre dans lequel il a été exécuté.

Exécution du code

Pour que votre code soit utile, il faut qu’il soit exécuté. Les sections précédentes ont mentionné la flèche pointant vers la droite qui apparaît dans la cellule courante. En cliquant sur cette flèche, vous n’exécutez que la cellule en cours. Bien sûr, vous avez d’autres options que de cliquer sur la flèche pointant vers la droite, et toutes ces options apparaissent dans le menu Exécution. La liste suivante résume ces options :

• Exécuter la cellule courante : en plus de cliquer sur la flèche pointant vers la droite, vous pouvez également choisir Exécution → Exécuter la cellule sélectionnée pour exécuter le code dans la cellule actuelle.

• Exécuter d’autres cellules : Colab propose des options dans le menu Exécution pour exécuter le code dans la cellule suivante, la cellule précédente ou une sélection de cellules. Il suffit de choisir l’option qui correspond à la cellule ou à l’ensemble de cellules que vous souhaitez exécuter.

• Exécuter toutes les cellules : dans certains cas, vous souhaitez exécuter tout le code d’un notebook. Dans ce cas, sélectionnez Exécution → Tout exécuter. L’exécution commence en haut du notebook, dans la première cellule contenant du code, et se poursuit jusqu’à la dernière cellule contenant du code. Vous pouvez arrêter l’exécution à tout moment en choisissant Exécution → Interrompre l’exécution.

Choisir Exécution → Gérer les sessions affiche une boîte de dialogue contenant une liste de toutes les sessions qui sont actuellement en cours d’exécution pour votre compte sur Colab. Vous pouvez utiliser cette boîte de dialogue pour déterminer la date de la dernière exécution du code dans ce notebook et la quantité de mémoire consommée par le notebook. Cliquez sur l’icône en forme de poubelle pour mettre fin à l’exécution d’un notebook particulier.

Partager son notebook

Vous pouvez partager vos notebooks Colab de plusieurs façons. Par exemple, vous pouvez les enregistrer sur GitHub ou GitHub Gist. Cependant, voici les deux méthodes les plus directes :

Créer un message de partage et l’envoyer au destinataire.

Obtenir un lien vers le code et l’envoyer au destinataire.

Dans les deux cas, on clique sur le bouton Partager en haut à droite de la fenêtre Colab. La boîte de dialogue illustrée à la Figure 6.22 s’ouvre.

Lorsque vous saisissez un ou plusieurs noms dans le champ Ajouter des personnes, un champ supplémentaire s’ouvre pour ajouter un message de partage. Vous pouvez saisir un message et cliquer sur Envoyer pour envoyer le lien immédiatement. Si vous cliquez plutôt sur l’icône en forme de roue dentée, une autre boîte de dialogue s’affiche, dans laquelle vous pouvez définir les modalités de partage du notebook.

[picture]

Figure 6.22 Vous pouvez partager vos notebooks.

Cliquez sur le bouton Copier le lien de la boîte de dialogue pour placer l’URL dans le presse-papiers. Vous pouvez ensuite le coller dans des messages ou d’autres formes de communication avec d’autres personnes.

Obtenir de l’aide

L’endroit le plus évident pour obtenir de l’aide avec Colab est le menu Aide. Ce menu ne comporte pas de lien d’aide générale, mais vous le trouverez à https://colab.research.google.com/notebooks/welcome.ipynb (ce qui nécessite que vous vous connectiez au site Colab). Ce menu contient toutes les entrées habituelles :

• Questions fréquentes : permet d’accéder à une page contenant des questions posées par d’autres personnes.

• Rechercher des extraits de code : ouvre un volet présentant des tâches courantes, telles que l’utilisation d’un appareil photo, dans lequel vous pouvez rechercher un exemple de code qui pourrait répondre à vos besoins moyennant quelques modifications. En cliquant sur le bouton Insérer, vous insérez le code à l’emplacement actuel du curseur dans la cellule qui a le focus. Chacune des entrées présente également un exemple de code.

• Signaler un bug : permet d’accéder à une page où vous pouvez signaler les erreurs de Colab.

• Donner votre avis : affiche une boîte de dialogue contenant des liens vers des sites où vous pouvez obtenir des informations supplémentaires.

Partie 3

Débuter avec les bases mathématiques de l’apprentissage automatique

Dans cette partie…

Comprendre les mathématiques utilisées dans l’apprentissage automatique

Découvrir les processus de l’apprentissage automatique

S’assurer d’obtenir les bons résultats

Effectuer des tâches avec des apprenants simples

DANS CE CHAPITRE

Explorer les matrices et tirer parti du calcul matriciel

•

Comprendre le fonctionnement des probabilités

•

Expliquer le point de vue bayésien sur les probabilités

•

Décrire les observations à l’aide de mesures statistiques

Chapitre 7

Démystifier les mathématiques derrière l’apprentissage automatique

Si vous souhaitez mettre en œuvre des algorithmes d’apprentissage automatique à partir de zéro ou en concevoir de nouveaux, vous devez avoir une connaissance approfondie des probabilités, de l’algèbre linéaire, de la programmation linéaire et du calcul à plusieurs variables. Vous devez également savoir comment traduire les mathématiques en code fonctionnel, ce qui implique des compétences informatiques sophistiquées. Ce chapitre commence par vous aider à comprendre les mécanismes mathématiques de l’apprentissage automatique et décrit comment traduire les bases mathématiques en un code utilisable.

Si vous souhaitez appliquer l’apprentissage automatique existant à des fins pratiques, vous pouvez exploiter les bibliothèques logicielles Python existantes en utilisant des connaissances de base en mathématiques et en statistiques. En fin de compte, vous devez posséder certaines de ces compétences, car l’apprentissage automatique est fortement ancré dans les mathématiques et les statistiques, mais vous n’avez pas besoin d’en faire trop. Après avoir acquis quelques bases mathématiques, ce chapitre montre comment des principes bayésiens simples peuvent vous aider à réaliser des tâches intéressantes en matière d’apprentissage automatique.

Même si ce livre d’introduction se concentre sur des expériences d’apprentissage automatique utilisant Python, vous trouverez dans le texte de nombreuses références à des vecteurs, des matrices, des variables, des probabilités et leurs distributions. Le livre utilise également parfois des statistiques descriptives. Par conséquent, il est utile de savoir ce que sont une moyenne, une médiane et un écart-type afin de comprendre ce qui se passe sous le capot du logiciel que vous utilisez. Cette connaissance permet d’apprendre plus facilement à mieux utiliser le logiciel. La dernière partie du chapitre montre comment l’apprentissage automatique peut vous aider à faire de meilleures prédictions, même lorsque vous ne disposez pas de toutes les informations nécessaires.

Il n’est pas nécessaire de saisir manuellement le code source de ce chapitre. En fait, il est beaucoup plus facile d’utiliser le code source téléchargeable. Vous trouverez le code source de ce chapitre dans le dossier ML4D2E de la source téléchargeable pour le code Python et dans le dossier ML4D2ER de la source téléchargeable pour le code R. Les fichiers d’exemple de ce chapitre commencent par ML4D2E; 07; (pour Python) ou ML4D2E_R ; 07 ; (pour R). L’introduction fournit plus de détails sur la manière de trouver ces fichiers source.

Travailler avec des données

Si l’apprentissage automatique est tellement attrayant, c’est parce qu’il permet aux machines d’apprendre à partir d’exemples du monde réel (tels que des enregistrements de ventes, des signaux de capteurs ou encore des flux de données textuelles provenant d’Internet) et de déterminer ce que ces données impliquent. Les résultats courants d’un algorithme d’apprentissage automatique sont des prédictions, des prescriptions pour agir maintenant, ou de nouvelles connaissances en termes d’échantillons catégorisés par groupes. De nombreuses applications utiles sont déjà devenues une réalité en s’appuyant sur ces résultats :

• diagnostiquer des maladies difficiles à trouver ;

• découvrir des comportements criminels et détecter des criminels en action ;

• recommander le bon produit à la bonne personne ;

• filtrer et classifier des données provenant d’Internet à une échelle énorme ;

• conduire un véhicule de façon autonome.

Les bases mathématiques et statistiques de l’apprentissage automatique permettent d’obtenir de tels résultats pratiques. Autrement dit, utiliser de cette façon les mathématiques et les statistiques permet aux algorithmes de comprendre tout ce qui a une base numérique. Les sections suivantes vous aideront à comprendre la manipulation de données de base à l’aide de scalaires, de vecteurs et de matrices :

• Scalaire : donnée de base unique. Par exemple, le nombre 2 affiché seul est un scalaire.

• Vecteur : tableau unidimensionnel (essentiellement une liste) d’éléments de données. Par exemple, un tableau contenant les nombres 2, 3, 4 et 5 est un vecteur. On accède aux éléments d’un vecteur à l’aide d’un indice qui démarre à zéro, et qui constitue un pointeur sur l’élément souhaité. L’élément ayant l’indice 0 est le premier élément du vecteur, qui est 2 dans cet exemple.

• Matrice : tableau à plusieurs dimensions (essentiellement une table) d’éléments de données. Par exemple, un tableau contenant les nombres 2, 3, 4 et 5 dans la première ligne et 6, 7, 8 et 9 dans la deuxième ligne est une matrice. On accède aux éléments d’une matrice à l’aide d’un indice de ligne et de colonne démarrant à zéro. L’élément situé à la ligne 0 et à la colonne 0 est le premier élément de la matrice, soit 2 dans le cas présent.

Parmi les autres types de structures de données que vous rencontrerez, citons les tableaux multidimensionnels (trois dimensions ou plus) et les tenseurs (une matrice généraliste qui peut avoir n’importe quel nombre de dimensions). Ce chapitre ne couvre pas ces structures de données spécialisées qui sont davantage employées pour le traitement des données, mais rarement pour l’apprentissage automatique, car la plupart des algorithmes s’attendent à travailler avec une matrice de données numériques.

Apprentissage de la terminologie

Pour débuter le processus, vous représentez la solution du problème sous la forme d’un nombre. Par exemple, si votre objectif est de diagnostiquer une maladie à l’aide d’un algorithme d’apprentissage automatique, vous pouvez choisir un 1 ou un 0 (donc une réponse binaire) pour indiquer si la personne est malade ou non (1 indique simplement que la personne est malade, et 0 qu’elle n’est pas malade). Vous pouvez également prendre un nombre compris entre 0 et 1 pour donner une réponse moins précise. La valeur décimale représentera par exemple la probabilité pour que la personne soit malade, 0 indiquant que la personne n’est pas malade et 1 indiquant que la personne est certainement atteinte de la maladie.

Un algorithme d’apprentissage automatique peut fournir une réponse (des prédictions) lorsqu’il s’appuie sur les informations requises (données d’échantillons) et une réponse associée (exemples de prédictions que vous voulez pouvoir deviner). L’information peut comprendre des faits, des événements, des observations, des dénombrements, des mesures, etc. Toute information utilisée en entrée est une caractéristique (feature) ou une variable (un terme tiré des statistiques). Ce livre utilise le terme caractéristique pour faciliter la lecture et pour séparer les variables de code des caractéristiques de données. Les caractéristiques efficaces décrivent les valeurs qui se rapportent à la réponse, et aident l’algorithme à deviner une réponse à l’aide de la fonction qu’il crée à partir d’informations similaires recueillies dans d’autres circonstances.

Il existe deux types de caractéristiques : quantitatives et qualitatives. Les caractéristiques quantitatives sont parfaites pour l’apprentissage automatique, car elles définissent les valeurs en tant que nombres (entiers, flottants, dénombrements, classements ou autres mesures). Les caractéristiques qualitatives sont généralement des étiquettes ou des symboles qui transmettent des informations utiles de manière non numérique, quelque chose donc que vous pouvez définir comme étant plus « humain » (des mots, des descriptions ou des concepts).

Vous trouverez un exemple classique de caractéristiques qualitatives dans l’article « Induction of Decision Trees » de John Ross Quinlan (https://dl.acm.org/doi/10.1023/A:1022643204877), un informaticien qui a contribué de manière fondamentale à l’élaboration des arbres de décision. Les arbres de décision sont l’un des algorithmes d’apprentissage automatique les plus populaires à ce jour. Dans son article, Quinlan décrit un ensemble d’informations utiles pour décider s’il faut jouer au tennis à l’extérieur ou non, ce qu’une machine peut apprendre en utilisant la bonne technique. Le jeu de caractéristiques décrit par Quinlan est le suivant :

• Ciel : ensoleillé, couvert ou pluvieux.

• Température : fraîche, douce, chaude.

• Humidité : forte ou normale.

• Venteux : vrai ou faux.

Un algorithme d’apprentissage automatique ne peut pas vraiment digérer des informations sous cette forme. Vous devez d’abord les transformer en nombres. Il existe de nombreuses façons de le faire, mais la plus simple est ce que l’on appelle l’encodage one-hot (ou encodage 1 parmi n, ou encore encodage à un bit non nul discriminant), qui transforme chaque caractéristique en un nouvel ensemble de caractéristiques binaires (valeurs de 0 ou 1) pour toutes ses valeurs symboliques. Par exemple, considérons la variable appelée ci-dessus Ciel. Elle va servir à former trois nouvelles caractéristiques : ciel : soleil, ciel : couvert, et ciel : pluvieux. Chacune aura une valeur numérique de 1 ou 0 selon que la condition sous-jacente est présente ou non. Ainsi, lorsque la journée est ensoleillée, ciel : soleil aura une valeur de 1, tandis que ciel : couvert et ciel : pluvieux auront une valeur de 0.

En plus de l’encodage one-hot, vous pouvez utiliser quelques autres techniques pour transformer des éléments qualitatifs en nombres, surtout lorsqu’un tel élément est composé de mots, comme un tweet sur X (anciennement Twitter), un morceau de texte provenant d’une évaluation en ligne, ou un fil d’actualité. Dans la dernière partie du livre, vous aurez l’occasion de discuter d’autres méthodes permettant de transformer efficacement des mots et des concepts en nombres significatifs, qui soient compréhensibles par un algorithme d’apprentissage automatique dans le contexte d’une analyse textuelle.

Quelle que soit la nature de l’information, pour qu’un algorithme d’apprentissage automatique la traite correctement, l’information doit toujours être transformée en un nombre.

Comprendre les opérations scalaires et vectorielles

La bibliothèque NumPy fournit des fonctionnalités essentielles pour le calcul scientifique en Python. Pour utiliser numpy, vous devez l’importer à l’aide d’une commande telle que import numpy as np. Vous pouvez maintenant accéder à NumPy en utilisant l’abréviation courante à deux lettres np.

Python ne permet d’accéder qu’à un seul type de données dans une catégorie donnée. Par exemple, si vous devez créer une variable qui représente un nombre sans partie décimale, vous utilisez le type de données integer. L’utilisation d’une désignation générique comme celle-ci est utile, car elle simplifie le code et la vie du développeur. Cependant, dans les calculs scientifiques, vous avez souvent besoin d’un meilleur contrôle sur la façon dont les données apparaissent en mémoire, ce qui implique d’avoir plus de types de données, et c’est exactement la possibilité que numpy vous offre. Par exemple, vous pouvez avoir besoin de définir un scalaire particulier comme un short (une valeur de 16 bits). En utilisant numpy, vous pourriez le définir comme myShort = np.short(15). Vous pouvez définir une variable de la même taille en utilisant la fonction np.int16. La bibliothèque NumPy permet d’accéder à un large éventail de types de données décrits à https://numpy.org/doc/stable/reference/arrays.scalars.html.

Utilisez la fonction numpy array() pour créer un vecteur. Par exemple, myVect = np.array([1, 2, 3, 4]) crée un vecteur à quatre éléments. Dans cet exemple, le vecteur contient des entiers Python standard. Vous pouvez également utiliser la fonction arange pour produire des vecteurs, comme myVect = np.arange(1, 10, 2), qui affecte array([1, 3, 5, 7, 9]) à myVect. Le premier argument indique le point de départ, le deuxième le point d’arrêt et le troisième le pas entre chaque nombre. Un quatrième argument permet de définir le type de données du vecteur. Vous pouvez également créer un vecteur avec un type de données spécifique. Il suffit de spécifier le type de données de la manière suivante : myVect = np.array(np.int16([1, 2, 3, 4])) pour affecter à myVect un vecteur comme ceci : array([1, 2, 3, 4], dtype=int16).

Dans certains cas, vous avez besoin de fonctions numpy spéciales pour créer un vecteur (ou une matrice) d’un type spécifique. Par exemple, certaines tâches mathématiques requièrent que vous remplissiez le vecteur avec des uns. Dans ce cas, on utilise la fonction ones() comme ceci : myVect = np.ones(4, dtype=np.int16) pour affecter à myVect des 1 d’un type de données spécifique comme ceci : array([1, 1, 1, 1], dtype=int16). Vous pouvez également utiliser la fonction zeros() pour remplir un vecteur avec des zéros.

Vous pouvez exécuter des fonctions mathématiques de base sur les vecteurs, ce qui rend numpy très utile ; il est d’autre part moins sujet aux erreurs qui peuvent survenir lors de l’utilisation de constructions de programmation telles que les boucles pour effectuer la même tâche. Prenons les exemples suivants où myVect contient array([1, 2, 3, 4], dtype=int16) : vous pouvez maintenant effectuer quelques calculs ; par exemple, myVect + 1 produit array([2, 3, 4, 5]) lorsque l’on travaille avec des entiers Python standard. Si vous choisissez de travailler avec le type de données numpy int16, myVect + 1 produit array([2, 3, 4, 5], dtype=int16). Vous noterez que la sortie indique précisément quel type de données est utilisé. Comme vous pouvez vous y attendre, myVect - 1 produit array([0, 1, 2, 3]). Vous pouvez même utiliser des vecteurs dans des scénarios mathématiques plus complexes, tels que 2 ** myVect, où la sortie est array([ 2, 4, 8, 16], dtype=int16). Cependant, lorsqu’il est utilisé de cette manière, numpy attribue souvent un type spécifique à la sortie, même si vous définissez un vecteur en utilisant des entiers Python standard.

Pour conclure sur les opérations scalaires et vectorielles, vous pouvez également effectuer des tâches logiques et de comparaison. Par exemple, le code suivant effectue des opérations de comparaison sur deux tableaux :

a = np.array([1, 2, 3, 4])
b = np.array([2, 2, 4, 4])

print(a == b)
print(a < b)

À partir de deux vecteurs, a et b, le code vérifie si les éléments individuels de a sont égaux à ceux de b. Dans cet exemple, a[0] n’est pas égal à b[0]. Cependant, a[1] est égal à b[1]. La sortie est un vecteur de type bool qui contient des valeurs vraies ou fausses basées sur les comparaisons individuelles. De la même manière, vous pouvez rechercher les cas où a < b et produire un autre vecteur contenant les valeurs de vérité dans ce cas. Voici le résultat de cet exemple :

[False True False True]
[True False True False]

Les opérations logiques reposent sur des fonctions spéciales. On vérifie la sortie logique des opérateurs booléens AND, OR, XOR et NOT. Voici un exemple de fonctions logiques :

a = np.array([True, False, True, False])
b = np.array([True, True, False, False])

print(np.logical_or(a, b))
print(np.logical_and(a, b))
print(np.logical_not(a))
print(np.logical_xor(a, b))

Voici le résultat de cet exemple :

[ True True True False]
[ True False False False]
[False True False True]
[False True True False]

Vous pouvez également utiliser une entrée numérique pour ces fonctions. Lors de l’utilisation d’une entrée numérique, un 0 est faux et un 1 est vrai. Comme pour les comparaisons, les fonctions opèrent élément par élément, même si vous n’effectuez qu’un seul appel. Pour en savoir plus sur les fonctions logiques, consultez https://numpy.org/doc/stable/reference/routines.logic.html.

Effectuer une multiplication vectorielle

L’addition, la soustraction ou la division de vecteurs s’effectue élément par élément, comme nous l’avons décrit dans la section précédente. Cependant, lorsqu’il s’agit de la multiplication, les choses se compliquent. En fait, selon ce que vous voulez vraiment faire, les choses peuvent même devenir assez étranges. Prenons l’exemple de la multiplication décrite dans la section précédente. myVect * myVect et np.multiply(myVect, myVect) produisent tous deux une sortie élément par élément de array([ 1, 4, 9, 16]).

Malheureusement, une multiplication élément par élément peut produire des résultats incorrects lorsque l’on travaille avec des algorithmes. Dans de nombreux cas, ce dont vous avez réellement besoin est un produit scalaire (dot product), qui est la somme des produits de deux séquences de nombres. Lorsque vous travaillez avec des vecteurs, le produit scalaire est toujours la somme des multiplications élément par élément, et il donne un seul nombre. Par exemple, myVect.dot(myVect) donne un résultat de 30. Si vous additionnez les valeurs issues de la multiplication élément par élément, vous constatez que la somme est bien de 30. La discussion à https://www.mathsisfun.com/algebra/vectors-dot-product.html vous renseigne sur les produits scalaires et vous aide à comprendre leur place dans les algorithmes. Vous pouvez en apprendre plus sur les fonctions de manipulation de l’algèbre linéaire dans numpy à https://numpy.org/doc/stable/reference/routines.linalg.html.

Créer une matrice

Après avoir rendu toutes les données numériques, l’algorithme d’apprentissage automatique exige que vous transformiez les caractéristiques individuelles en une matrice de caractéristiques, et les réponses individuelles en un vecteur ou une matrice (lorsqu’il y a plusieurs réponses). Une matrice est une collection de nombres, disposés en rangées et en colonnes, tout comme les cases d’un échiquier. Cependant, contrairement à un échiquier, qui est toujours carré, les matrices peuvent avoir un nombre différent de lignes et de colonnes.

Par convention, une matrice utilisée pour l’apprentissage automatique repose sur des lignes pour représenter les exemples et des colonnes pour représenter les caractéristiques. Ainsi, pour reprendre l’exemple qui part des conditions météorologiques pour déterminer où jouer au tennis, vous devez construire une matrice qui utilise une nouvelle ligne pour chaque journée et des colonnes contenant les différentes valeurs pour le ciel, la température, l’humidité et le vent. En règle générale, on représente une matrice sous la forme d’une série de nombres entourés de parenthèses ou de crochets droits, comme illustré ci-dessous :

X=\begin{bmatrix}1.1 & 1 & 545 & 1 \\4.6 & 0 & 345 & 2 \\7.2 & 1
& 754 & 3\end{bmatrix}

Dans cet exemple, la matrice appelée X contient trois lignes et quatre colonnes ; on peut donc dire que la matrice a des dimensions de 3 par 4 (également écrites 3 × 4). Pour indiquer le nombre de lignes dans une formule, on utilise généralement la lettre n, et la lettre m pour le nombre de colonnes. Connaître les dimensions d’une matrice est fondamental pour pouvoir l’utiliser correctement.

Pour opérer sur une matrice, il faut également pouvoir récupérer une de ses valeurs (ou coefficients), ou encore une de ses parties pour effectuer des calculs spécifiques. On utilise des indices, c’est-à-dire des nombres qui indiquent la position d’un élément dans une matrice pour effectuer cette tâche. Les indices spécifient le numéro de ligne et de colonne qui déterminent l’emplacement d’une certaine valeur. Habituellement, on spécifie l’indice de ligne avec la lettre i, et l’indice de la colonne par la lettre j. Les indices i et j commencent à dénombrer les lignes et les colonnes en commençant par le chiffre 0 (base 0) ou 1 (base 1). Les matrices Python sont indicées à partir de 0. L’utilisation de différents points de départ d’indices peut s’avérer déroutante, et c’est la raison pour laquelle vous devez connaître la manière dont le langage de programmation que vous employez fonctionne.

Lors de l’affichage de notre matrice d’exemple, l’élément 2,3 est l’élément situé dans la deuxième ligne, à l’intersection avec la troisième colonne, c’est-à-dire 345 (en supposant que la matrice est indicée à partir de 1). Par conséquent, si vous avez besoin d’exprimer trois éléments différents de la matrice X, vous pouvez utiliser la notation suivante :

X1,1=1.1, X2,3=345, X3,4=3

Parfois, plusieurs matrices sont empilées comme des tranches dans une structure de données plus complexe appelée tableau (array). Dans ce contexte, un tableau est une collection de données numériques ayant plus de deux dimensions. Python ne prend pas réellement en charge un type tableau, mais les développeurs utilisent le terme tableau de diverses manières, comme lorsqu’ils se réfèrent à un vecteur et à une matrice plus tôt dans le chapitre. À titre d’exemple de cette utilisation, vous pouvez avoir des tableaux à trois dimensions où chaque matrice représente une période différente, et les matrices sont ensuite empilées comme les tranches d’un gâteau. Ce type de tableau peut être utilisé lorsque vous enregistrez en continu des données médicales, par exemple à partir d’un scanner enregistrant des fonctions corporelles telles que l’activité cérébrale. Dans ce cas, les lignes sont toujours des exemples et les colonnes des caractéristiques, la troisième dimension représentant le temps.

Une matrice comportant une seule caractéristique est un cas particulier appelé vecteur, comme indiqué dans la section « Comprendre les opérations scalaires et vectorielles » , plus haut dans ce chapitre. On utilise principalement des vecteurs lorsque l’on parle de valeurs de réponse (vecteur de réponse) ou lorsque l’on traite des coefficients internes de certains algorithmes. Dans ce cas, on parle de vecteur de coefficients.

Dans l’apprentissage automatique, la matrice de caractéristiques apparaît généralement notée sous la forme X et le vecteur de réponse correspondant sous la forme y. Plus généralement, les matrices utilisent habituellement une lettre majuscule, et les vecteurs une lettre minuscule pour leur identification. En outre, on utilise des lettres minuscules pour les constantes. Vous devez donc faire attention lorsque vous déterminez si une lettre est un vecteur ou une constante, car l’ensemble des opérations possibles est très différent.

Les matrices sont souvent utilisées dans l’apprentissage automatique, car elles permettent d’organiser, d’indexer et d’extraire rapidement de grandes quantités de données d’une manière uniforme et significative. Pour chaque exemple i dans la matrice de caractéristiques X, vous pouvez alors déterminer la i-ième ligne de la matrice exprimant ses caractéristiques et le i-ième élément du vecteur de réponse vous indiquant les résultats qu’un ensemble spécifique de caractéristiques implique. Cette stratégie permet à l’algorithme de rechercher des données et de produire rapidement des prédictions.

La notation matricielle permet également d’effectuer rapidement des opérations systématiques sur l’ensemble de la matrice ou sur des parties de celle-ci. Les matrices sont également utiles pour écrire et exécuter des programmes rapidement, car vous pouvez utiliser des commandes informatiques pour exécuter des opérations matricielles.

Comprendre les opérations de base

Les opérations matricielles de base sont l’addition, la soustraction et la multiplication scalaire. Elles ne sont possibles que lorsque vous avez deux matrices de même taille et que le résultat est une nouvelle matrice de même dimension. Si vous avez deux matrices de même forme, vous appliquez l’opération à chaque position correspondante dans les deux matrices. Par conséquent, pour effectuer une addition, on commence par additionner les valeurs de la première ligne et de la première colonne des deux matrices source et on place la valeur obtenue à la même position dans la matrice résultante. On continue le processus pour chaque élément apparié dans les deux matrices jusqu’à ce que l’on ait terminé toutes les opérations. Le même processus s’applique à la soustraction, comme le montre l’exemple suivant :

A = np.array([[1, 1], [1, 0]])
B = np.array([[1, 0], [0, 1]])
C = A - B

print(C)

Dans cet exemple, le résultat est le suivant :

[[ 0 1]
 [ 1 -1]]

Dans la multiplication scalaire, on prend une seule valeur numérique (le scalaire) et on la multiplie pour chaque élément de la matrice. Si votre valeur est fractionnaire, comme ½ ou ¼, votre multiplication se transforme en division. Dans l’exemple précédent, vous pouvez multiplier la matrice résultante par −2 en utilisant print(C * -2) avec un résultat de :

[[ 0 -2]
 [-2 2]]

Vous pouvez également effectuer des additions et des soustractions scalaires. Dans ce cas, on ajoute ou soustrait une valeur unique à tous les éléments d’une matrice.

Effectuer une multiplication matricielle

En utilisant des indices et des opérations matricielles de base, vous pouvez exprimer un certain nombre d’opérations de manière compacte. La combinaison des indices et des opérations vous permet de :

• découper une partie d’une matrice ;

• masquer une partie de la matrice, en la réduisant à zéro ;

• centrer les coefficients d’une matrice en supprimant une valeur de tous ses éléments ;

• rééchantillonner les valeurs d’une matrice, en changeant leur plage de valeurs.

Toutefois, vous ne pouvez réaliser le plus grand nombre d’opérations en une seule fois que si vous multipliez une matrice par un vecteur ou une autre matrice. Ce sont des tâches courantes en apprentissage automatique, et la multiplication d’une matrice par un vecteur est une opération fréquente. De nombreux algorithmes d’apprentissage automatique reposent sur la recherche d’un vecteur de coefficients qui, multiplié par la matrice des caractéristiques, peut aboutir à une approximation du vecteur des valeurs de réponse. Dans de tels modèles, vous avez des formulations comme celle-ci :

y = Xb

où y est le vecteur de réponse, X la matrice des caractéristiques et b un vecteur de coefficients. Souvent, l’algorithme inclut aussi un scalaire appelé a qui est ajouté au résultat. Dans cet exemple, vous pouvez l’imaginer comme valant zéro, si bien qu’il n’est pas présent. Le vecteur y est constitué de trois éléments :

[ 2 -2 3]

Dans cette optique, on peut exprimer la multiplication entre X et b de la manière suivante :

X = np.array([[4, 5], [2, 4], [3, 3]])
b = [3, -2]

print(X @ b)

Ensuite, vous devez savoir comment X multiplié par b peut donner y. Pour vérifier que vous pouvez effectuer la multiplication, la matrice et le vecteur impliqués dans la multiplication doivent avoir des tailles compatibles. En fait, le nombre de colonnes de la matrice doit être égal au nombre de lignes du vecteur. Dans ce cas, il y a correspondance, car X a une taille de 3 par 2 et b de 2 par 1. Connaissant les formes des termes, vous pouvez déterminer à l’avance la forme de la matrice résultante, qui est donnée par les lignes de la matrice et les colonnes du vecteur, soit 3 par 1.

La multiplication vectorielle matricielle fonctionne comme une série de multiplications vecteur-vecteur additionnées. La multiplication traite chaque ligne de la matrice X comme un vecteur et la multiplie par le vecteur b. Le résultat devient l’élément de ligne correspondant du vecteur résultant. Par exemple, la première ligne [4,5] est multipliée par [3,−2], ce qui donne un vecteur [12,−10], dont les éléments additionnés donnent une valeur de 2. Cette première multiplication additionnée correspond à la première ligne du vecteur résultant, puis tous les autres calculs s’enchaînent de la même manière :

 sum([4*3, 5*-2]) = 2
 sum([2*3, 4*-2]) = -2
 sum([3*3, 3*-2]) = 3

Le vecteur résultant est [2, −2, 3]. Les choses deviennent un peu plus délicates lorsque l’on multiplie deux matrices, mais vous pouvez effectuer l’opération comme une série de multiplications matrice-vecteur, comme dans l’exemple précédent, en considérant la deuxième matrice comme une série de vecteurs d’éléments. En multipliant la première matrice par les m vecteurs, vous obtenez une seule colonne de la matrice résultante pour chaque multiplication.

Un exemple peut clarifier les étapes de l’obtention d’une matrice par multiplication matricielle. L’exemple suivant multiplie X par B, qui est une matrice carrée 2 × 2 :

B = np.array([[3, -2], [-2, 5]])

print(X @ B)

Vous pouvez diviser l’opération en deux matrices distinctes par multiplications vectorielles en divisant la matrice B en vecteurs-colonnes.

\begin{bmatrix}4 & 5 \\2 & 4 \\3 & 3\end{bmatrix}\begin{bmatrix}3
\\ -2\end{bmatrix} = \begin{bmatrix}2 \\-2 \\3 \end{bmatrix}

\begin{bmatrix}4 & 5 \\2 & 4 \\3 & 3\end{bmatrix}\begin{bma-
trix}-2 \\5 \end{bmatrix} = \begin{bmatrix}17 \\16 \\9 \end{bma-
trix}

Il ne vous reste plus qu’à prendre les vecteurs-colonnes obtenus et à les employer pour reconstruire la matrice de sortie en utilisant la multiplication du premier vecteur-colonne comme première colonne de la nouvelle matrice, et ainsi de suite.

XB = \begin{bmatrix}4 & 5 \\2 & 4 \\3 & 3\end{bmatrix}\begin{bma-
trix}3 & -2 \\-2 & 5 \end{bmatrix} = \begin{bmatrix}2 & 17 \\-2 &
16 \\3 & 9\end{bmatrix}

Dans la multiplication matricielle, en raison des formes de la matrice, l’ordre est important. Par conséquent, vous ne pouvez pas inverser les termes, comme vous le feriez dans une multiplication de nombres scalaires. Multiplier 5*2 ou 2*5 revient au même, en raison de la propriété commutative de la multiplication scalaire, mais XB n’est pas la même chose que BX parce que parfois la multiplication n’est pas possible (en raison de l’incompatibilité des formes des matrices) ou, pire, elle produit un résultat différent. Lorsque vous avez une série de multiplications matricielles, comme ABC, l’ordre des opérations n’a pas d’importance ; que vous fassiez AB en premier ou BC en premier, vous obtenez le même résultat car, comme les scalaires, la multiplication matricielle est associative.

Un mot sur les opérations matricielles avancées

Vous pouvez rencontrer deux opérations matricielles importantes dans certaines formulations d’algorithmes. Il s’agit de la transposition et de l’inverse d’une matrice. La transposition se produit lorsqu’une matrice de forme n × m est transformée en une matrice m × n en échangeant les lignes avec les colonnes. La plupart des tests indiquent cette opération à l’aide de l’exposant T, comme dans AT. Cette opération est le plus souvent utilisée pour la multiplication, afin d’obtenir les bonnes dimensions. Lorsque vous travaillez avec Python, vous utilisez un code comme celui-ci pour effectuer une transposition :

A = np.array([[1, 2, 3], [2, 3, 5], [7, 11, 13]])

print(A.transpose())

La sortie montre la transposition suivante :

[[ 1 2  7]
 [ 2 3 11]
 [ 3 5 13]]

L’inversion s’applique à des matrices de forme m × m, qui sont donc des matrices carrées ayant le même nombre de lignes et de colonnes. Cette opération est très importante, car elle permet la résolution immédiate d’équations impliquant une multiplication matricielle, comme y = bX, où il faut découvrir les valeurs du vecteur b. Partant du fait que la plupart des valeurs scalaires (à l’exception de zéro) ont un inverse dont la multiplication avec elles donne une valeur de 1, l’idée est de trouver une matrice inverse dont la multiplication donnera une matrice spéciale appelée matrice identité, dont tous les éléments sont nuls, sauf ceux de la diagonale principale (ceux dont l’indice de ligne i est égal à l’indice de colonne j). Trouver l’inverse d’un scalaire est assez facile (le scalaire n a pour inverse n⁻¹, soit 1/n). Pour une matrice, c’est une autre histoire. L’inversion matricielle implique un assez grand nombre de calculs, de sorte qu’il existe pour cela des fonctions mathématiques spéciales en Python. L’inverse d’une matrice A est notée sous la forme A⁻¹. On utilise le code suivant pour inverser une matrice :

print(np.linalg.inv(A))

La sortie montre la matrice inversée :

[[-3.2  1.4  0.2]
 [ 1.8 -1.6  0.2]
 [ 0.2  0.6 -0.2]]

Parfois, il est impossible de trouver l’inverse d’une matrice. Lorsqu’une matrice ne peut pas être inversée, on l’appelle matrice singulière ou dégénérée. Les matrices singulières ne sont pas la norme et sont par conséquent assez rares.

Utiliser efficacement la vectorisation

Si l’exécution d’opérations matricielles, comme la multiplication d’une matrice par un vecteur, semble un peu difficile, considérez que votre ordinateur se charge de tout le travail pénible. Tout ce que vous avez à faire, c’est de déterminer dans une formulation théorique ce qui arrive aux nombres au fur et à mesure que vous les placez dans des matrices, des vecteurs et des constantes, puis vous les additionnez, les soustrayez, les divisez ou les multipliez.

Comprendre ce qui se passe dans un algorithme d’apprentissage automatique vous donnera un avantage dans le cadre de son utilisation, parce que vous comprendrez en même temps comment il digère et traite les données. Pour obtenir un résultat correct, vous devez fournir les bonnes données au bon algorithme, en fonction de la manière dont il travaille avec les données.

En Python, le package NumPy offre toutes les fonctionnalités nécessaires pour créer et manipuler des matrices. Les objets ndarray permettent la création rapide d’un tableau, tel qu’une matrice multidimensionnelle, en partant des données mises en file d’attente dans les listes.

Le terme ndarray signifie « tableau à n dimensions » , ce qui implique que vous pouvez créer des tableaux de dimensions multiples, pas seulement des matrices de type « ligne par colonne » . En utilisant une simple liste, ndarray peut rapidement créer un tableau 1D, comme le montre l’exemple Python ci-dessous :

import numpy as np
y = np.array([44,21,37])
print (y)
print (y.shape)

[44 21 37]

(3,)

La méthode shape peut vous informer rapidement de la forme d’une matrice. Ici, elle rapporte qu’il y a trois lignes et aucune colonne, ce qui signifie que l’objet est un tableau 1D.

Pour créer des matrices composées de lignes et de colonnes, on peut utiliser une liste de listes. Le contenu des listes à l’intérieur de la liste principale constitue les lignes de votre matrice.

X = np.array([[1.1, 1, 545, 1],[4.6, 0, 345, 2],
 [7.2, 1, 754, 3]])
print (X)

[[ 1.1   1.  545.   1. ]
 [ 4.6   0.  345.   2. ]
 [ 7.2   1.  754.   3. ]]

Vous pouvez également obtenir le même résultat en utilisant une seule liste, qui crée un tableau 1D que vous pouvez redimensionner selon le nombre de lignes et de colonnes souhaité. Les nombres sont remplis ligne par ligne dans la nouvelle matrice, en commençant par l’élément (0,0) et en allant jusqu’à la dernière ligne.

X = np.array([1.1, 1, 545, 1, 4.6, 0, 345, 2,
 7.2, 1, 754, 3]).reshape(3,4)

Explorer le monde des probabilités

La probabilité indique la vraisemblance d’un événement et s’exprime sous la forme d’un nombre. La probabilité d’un événement est mesurée sur une échelle allant de 0 (aucune probabilité qu’un événement se produise) à 1 (certitude qu’un événement se produise). Les valeurs intermédiaires, telles que 0,25, 0,5 et 0,75, indiquent que l’événement se produira avec une certaine fréquence s’il est testé suffisamment de fois. Les sections suivantes décrivent les probabilités plus en détail.

Vue d’ensemble des probabilités

Si vous multipliez la probabilité par un nombre entier représentant le nombre d’essais que vous allez tenter, vous obtiendrez une estimation du nombre de fois qu’un événement devrait se produire en moyenne si tous les essais sont tentés. Par exemple, si un événement se produit avec une probabilité p=0,25 et que vous faites 100 essais, il est probable que cet événement se produise 0,25 * 100 = 25 fois. C’est, par exemple, la probabilité de choisir une certaine couleur lorsqu’on choisit une carte au hasard dans un jeu de cartes. Les cartes à jouer constituent un exemple classique d’explication des probabilités. Le jeu contient 52 cartes également réparties en quatre couleurs : trèfle et pique, qui sont noirs, et carreau et cœur, qui sont rouges. Si vous voulez déterminer la probabilité de choisir un as, vous devez donc considérer qu’il y a quatre as de couleurs différentes. La réponse en termes de probabilité est p=4/52=0,077.

Les probabilités sont comprises entre 0 et 1. Aucune probabilité ne peut dépasser ces limites. Vous définissez les probabilités de manière empirique à partir d’observations. Comptez simplement le nombre de fois qu’un événement spécifique se produit par rapport à tous les événements qui vous intéressent. Supposons, par exemple, que vous souhaitiez calculer la probabilité pour que des opérations bancaires soient frauduleuses, ou pour que les gens contractent une certaine maladie dans un pays donné. Après avoir été témoin de l’événement, vous pouvez estimer la probabilité correspondante en comptant le nombre de fois que l’événement se produit et en divisant par le nombre total d’événements.

Vous pouvez compter le nombre de fois qu’une certaine fraude ou une certaine maladie se produit à l’aide de données enregistrées (la plupart provenant de bases de données), puis diviser cette quantité par le nombre total d’événements génériques ou d’observations disponibles. Par conséquent, vous divisez le nombre de fraudes par le nombre de transactions dans une année, ou bien vous comptez le nombre de personnes qui sont tombées malades au cours de l’année par rapport à la population d’une certaine région. Le résultat est un nombre allant de 0 à 1, que vous pouvez utiliser comme probabilité de base pour un certain événement compte tenu de certaines circonstances.

Il n’est pas toujours possible de compter toutes les occurrences d’un événement, et c’est la raison pour laquelle vous devez connaître l’échantillonnage. L’échantillonnage, qui est un acte fondé sur certaines probabilités attendues, permet d’observer une petite partie d’un ensemble plus large d’événements ou d’objets, tout en étant capable de déduire des probabilités correctes pour un événement, ainsi que des mesures exactes comme des mesures quantitatives ou des classes qualitatives associées à un ensemble d’objets.

Par exemple, si vous voulez suivre les ventes de voitures aux États-Unis au cours du dernier mois, vous n’avez pas besoin de suivre toutes les ventes dans le pays. À l’aide d’un échantillon basé sur les ventes de quelques vendeurs de voitures dans le pays, vous pouvez déterminer des mesures quantitatives, comme le prix moyen d’une voiture vendue, ou qualitatives, comme le modèle de voiture le plus vendu.

Travailler avec les probabilités

Les opérations sur les probabilités sont évidemment un peu différentes des opérations numériques classiques. Comme elles doivent toujours se situer dans un intervalle compris entre 0 et 1, vous devez vous fier à des règles particulières pour que l’opération ait un sens. Par exemple, les sommations entre probabilités sont possibles si les événements sont mutuellement exclusifs (ils ne peuvent se produire simultanément). Supposons que vous vouliez connaître la probabilité de tirer un pique ou un carreau dans un jeu de cartes. Vous pouvez additionner les deux probabilités correspondantes de cette façon : p = 0,25 + 0,25 = 0,5.

On utilise la soustraction (différence) pour déterminer la probabilité d’événements qui sont différents de la probabilité d’un événement qui a déjà été calculée. Par exemple, pour déterminer la probabilité de tirer dans le jeu une carte qui n’est pas un carreau, il suffit de soustraire de la probabilité de tirer n’importe quel type de carte, soit p = 1, la probabilité de tirer un carreau, comme ceci : p = 1 − 0,25 = 0,75. Lorsque vous soustrayez une probabilité de 1, vous obtenez son complément.

La multiplication permet de calculer l’intersection d’événements indépendants. Les événements indépendants sont des événements qui ne s’influencent pas mutuellement. Par exemple, si vous jouez à un jeu de dés et que vous lanciez deux dés, la probabilité d’obtenir deux six est de 1/6 (la probabilité d’obtenir un six avec le premier dé) multiplié par 1/6 (la probabilité d’obtenir un six avec le deuxième dé), soit p = 1/6 * 1/6 = 0,028. Cela signifie que si vous lancez les dés cent fois, vous pouvez vous attendre à ce que deux six ne sortent que deux ou trois fois. Notez que vous pouvez utiliser des mathématiques simples en Python pour effectuer ce type de calculs (assurez-vous simplement d’utiliser des parenthèses pour garantir l’ordre de calcul si nécessaire) : Six = (1/6) * (1/6).

En utilisant la sommation, la différence et la multiplication, vous pouvez obtenir une probabilité pour la plupart des situations les plus complexes liées à des événements. Par exemple, vous pouvez maintenant calculer la probabilité d’obtenir au moins un six sur deux dés lancés, qui est une sommation d’événements mutuellement exclusifs :

• La probabilité d’avoir deux six : Six = (1/6) * (1/6)

• La probabilité d’avoir un six sur le premier dé et autre chose qu’un six sur le second : SixEtAutre = (1/6) * (1 – (1/6))

• La probabilité d’avoir un six sur le deuxième dé et autre chose qu’un six sur le premier : AutreEtSix = (1/6) * (1–(1/6))

Votre probabilité d’obtenir au moins un six à partir de deux dés lancés est UnSix = (1/6) * [1/6 + (1 - 1/6) + (1 - 1/6)] = (1/6) * [1/6 + 5/6 + 5/6 ] = (1/6)[11/6] = 11/36. Dans ce cas, la probabilité d’obtenir un six avec le premier dé est de 1/6 et la probabilité d’obtenir un six avec le deuxième dé est de 1/6. Comme ces deux événements sont indépendants, on utilise l’addition pour trouver la probabilité.

Améliorer le hasard avec le théorème de Bayes

La probabilité a un sens en termes de temps et d’espace, mais d’autres conditions influencent également la probabilité que vous mesurez. Le contexte est important. Lorsque vous estimez la probabilité d’un événement, vous pouvez (parfois à tort) avoir tendance à croire que vous pouvez appliquer la probabilité calculée à chaque situation possible. Le terme pour exprimer cette croyance est celui de probabilité a priori, c’est-à-dire la probabilité générale d’un événement.

Par exemple, lorsque vous jouez à pile ou face, si la pièce est bien équilibrée, la « probabilité a priori » de tomber sur face est de 50 %. Peu importe le nombre de fois où vous jetez la pièce : pour chaque nouveau lancement, la probabilité de tomber sur face est toujours de 50 %.

Le jeu de pile ou face est un exemple de situation dans laquelle la véritable probabilité n’est pas calculée. Si le jeu est vraiment équitable, il y a une chance, bien qu’incroyablement faible, que la pièce tombe sur sa tranche, c’est-à-dire ni pile ni face. Lorsque vous effectuez un calcul de probabilité, vous devez vous demander si vous devez inclure des conditions de ce type pour obtenir une réponse plus précise.

Cependant, il y a d’autres situations dans lesquelles, si vous changez le contexte, la probabilité a priori n’est plus valable parce que quelque chose de subtil est arrivé et a modifié la situation. Dans ce cas, vous pouvez exprimer cette nouvelle estimation comme étant une « probabilité a posteriori » , qui est la probabilité a priori modifiée par quelque chose qui est arrivé et qui change la donne. Par exemple, la probabilité a priori qu’une personne soit une femme est d’environ 50 %. Cependant, cette probabilité peut différer considérablement si l’on considère seulement des tranches d’âge spécifiques, parce que les femmes vivent en moyenne plus longtemps, et donc, après un certain âge, il y a plus de femmes que d’hommes. Dans le même registre, si vous examinez la présence des femmes dans certaines universités, vous constatez qu’elles sont en proportion inférieure à celle des hommes dans les facultés à dominante scientifique. Compte tenu de ces deux contextes, la probabilité a posteriori est différente de la probabilité a priori attendue. En termes de répartition par sexe, les conditions de vie et la culture peuvent toutes deux créer une probabilité a posteriori différente.

Vous pouvez considérer de tels cas comme étant des probabilités conditionnelles, et les exprimer sous la forme p(y|x), qui se lit comme étant la probabilité que l’événement y se produise étant donné que x s’est produit. Les probabilités conditionnelles sont un outil très puissant pour l’apprentissage automatique. En fait, si la probabilité a priori peut changer autant en raison de certaines circonstances, connaître justement les circonstances possibles peut augmenter vos chances de prédire correctement un événement en observant des échantillons – exactement ce que l’apprentissage automatique est censé faire. Par exemple, comme nous venons de le voir, on s’attend généralement à ce qu’une personne prise au hasard soit un homme ou une femme dans une proportion de 50 %. Mais que faire si vous ajoutez comme critère supplémentaire que les cheveux de la personne sont longs ou bien courts ? Vous pouvez estimer la probabilité d’avoir les cheveux longs à 35 % de la population. Cependant, si vous observez uniquement la population féminine, la probabilité monte à 60 %. Si le pourcentage est si élevé dans la population féminine, contrairement à la probabilité a priori, un algorithme d’apprentissage automatique peut donc bénéficier du fait de savoir si les cheveux de la personne sont longs ou courts.

En fait, l’algorithme bayésien naïf (ou naïf bayésien, selon les sources) peut augmenter les chances de faire une prédiction correcte en connaissant les circonstances entourant la prédiction, comme l’explique le Chapitre 10, qui couvre les premiers apprenants, les plus simples. Tout commence avec le révérend Bayes et son théorème révolutionnaire sur les probabilités. En fait, l’une des tribus d’apprentissage automatique (voir la section « Rôle des statistiques dans l’apprentissage automatique » du Chapitre 2) porte son nom. En outre, le développement d’algorithmes avancés basés sur les probabilités bayésiennes suscite de grands espoirs ; le magazine Technology Review du MIT a mentionné l’apprentissage automatique bayésien comme une technologie émergente qui changera notre monde (https://web.archive.org/web/20220407005958/http://www2.technologyreview.com/news/402435/10-emerging-technologies-that-will-change-your/). Pourtant, les fondements du théorème ne sont pas si compliqués que cela (bien qu’ils puissent être un peu contre-intuitifs si vous ne considérez normalement que les probabilités a priori sans tenir compte des probabilités a posteriori).

Le révérend Thomas Bayes était un statisticien et un philosophe qui a formulé son théorème au cours de la première moitié du XVIIIe siècle. Le théorème n’a été publié de son vivant que de manière posthume (sans doute vers 1763). En revanche, il a révolutionné la théorie des probabilités en introduisant l’idée de probabilité conditionnelle qui vient d’être mentionnée.

Grâce au théorème de Bayes, il est plus facile de prédire la probabilité qu’une personne soit un homme ou une femme s’il est prouvé que la personne a les cheveux longs. La formule utilisée par Thomas Bayes est très utile pour cela :

P(B|E) = P(E|B)*P(B) / P(E)

La lecture de la formule à l’aide de l’exemple précédent peut permettre de mieux la comprendre (même si elle n’est pas réellement intuitive) :

• P(B|E) : la probabilité d’une certaine « conviction » (B) à partir d’un ensemble de preuves (E) (probabilité dite postérieure). Lisez le mot « conviction » (ou encore « croyance » ) comme une autre façon d’exprimer une hypothèse. Dans ce cas, l’hypothèse est qu’une personne est une femme et que les preuves sont la longueur des cheveux. Connaître la probabilité d’une telle croyance peut aider à prédire le sexe de la personne avec une certaine confiance.

• P(E|B) : la probabilité d’avoir les cheveux longs lorsque la personne est une femme. Ce terme fait référence à la probabilité des éléments prouvés dans le sous-groupe, qui est elle-même une probabilité conditionnelle. Dans ce cas, l’estimation est de 60 %, ce qui correspond à une valeur de 0,6 dans la formule (probabilité dite antérieure).

• P(B) : la probabilité générale d’être une femme, c’est-à-dire la probabilité a priori de la croyance. Ici, cette probabilité est de 50 %, soit une valeur de 0,5 (vraisemblance).

• P(E) : la probabilité générale d’avoir les cheveux longs. Ici, il s’agit d’une autre probabilité a priori, cette fois-ci liée à la preuve observée. Dans cette formule, il s’agit d’une probabilité de 35 %, ce qui correspond à une valeur de 0,35 (preuve).

Si vous résolvez le problème précédent de la détermination du sexe en utilisant la formule de Bayes et les valeurs que vous avez sélectionnées, le résultat est Femme = 0,6 * 0,5 / 0,35 = 0,857. Il s’agit d’un pourcentage élevé de vraisemblance, ce qui vous amène à affirmer que, compte tenu de ces éléments, la personne est probablement une femme.

Un autre exemple courant, qui peut faire froncer les sourcils mais que l’on trouve régulièrement dans les manuels scolaires et les revues scientifiques, est celui du test médical positif. Il est très intéressant pour mieux comprendre comment les probabilités a priori et a posteriori peuvent effectivement provoquer beaucoup de variations dans des circonstances différentes.

Supposons que vous vous inquiétiez du fait que 1 % de la population soit atteinte d’une maladie rare. Vous passez le test et les résultats sont positifs. Mais les tests médicaux ne sont jamais parfaitement précis et le laboratoire vous informe que, si vous êtes malade, le test est positif dans 99 % des cas, alors que, si vous êtes en bonne santé, le test sera négatif dans 99 % des cas.

À partir de ces chiffres, vous croyez immédiatement que vous êtes certainement malade, étant donné le pourcentage élevé de tests positifs indiquant qu’une personne est malade (99 %). Cependant, la réalité est tout autre. Dans ce cas, les chiffres à communiquer au théorème de Bayes sont les suivants :

• 0,99 pour P(E|B)

• 0,01 pour P(B)

• 0,01 * 0,99 + 0,99 * 0,01 = 0,0198 pour P(E)

Les calculs sont donc les suivants : EstMalade = 0,99 * 0,01 / ((0,01 * 0,99) + (0,99 * 0,01)) = 0,50, ce qui correspond seulement à une probabilité de 50 % que vous soyez malade. En fin de compte, vos chances de ne pas être malade sont plus élevées que vous ne le pensiez. Vous vous demandez peut-être comment cela est possible. Le fait est que le nombre de personnes qui obtiennent une réponse positive au test est le suivant :

• Qui est malade et obtient la réponse correcte au test : ce groupe est celui des vrais positifs, et il représente 99 % du 1 % de la population qui contracte la maladie.

• Qui n’est pas malade et obtient la mauvaise réponse au test : ce groupe représente 1 % des 99 % de la population qui obtiennent une réponse positive même s’ils ne sont pas malades. Encore une fois, il s’agit de multiplier 99 % par 1 %. Ce groupe correspond aux faux positifs.

Si vous examinez le problème dans cette perspective, il devient évident qu’en limitant le contexte aux personnes qui obtiennent une réponse positive au test, la probabilité d’être dans le groupe des vrais positifs est la même que celle d’être dans les faux positifs.

Utilisation des statistiques

En guise de conclusion sur la question des probabilités, il est important de passer en revue certains concepts statistiques de base liés aux probabilités et aux statistiques, et de comprendre comment ils peuvent vous aider à mieux décrire l’information utilisée par les algorithmes d’apprentissage automatique. Les sections précédentes traitent des probabilités d’une manière qui peut s’avérer utile parce que l’échantillonnage, les distributions statistiques et les mesures descriptives statistiques sont tous, d’une façon ou d’une autre, fondés sur les concepts de probabilité.

Ici, il ne s’agit pas simplement de savoir comment décrire un événement en comptant ses occurrences ; il s’agit de décrire un événement sans compter toutes les fois où il se produit, mais de manière fiable. Par exemple, si vous voulez qu’un algorithme apprenne à détecter une maladie ou une intention criminelle, vous devez faire face au fait que vous ne pouvez pas créer une matrice comprenant toutes les occurrences de la maladie ou du crime, de sorte que l’information que vous allez élaborer sera nécessairement partielle. De plus, si vous mesurez quelque chose dans le monde réel, souvent, vous n’obtenez pas les données exactes à cause d’une erreur dans la procédure, d’une imprécision dans l’instrument que vous utilisez, ou simplement à cause d’une nuisance aléatoire qui perturbe le processus d’enregistrement de ces données. Une mesure simple comme votre poids, par exemple, différera chaque fois que vous montez sur la balance, en oscillant légèrement autour de ce que vous croyez être votre poids réel. Si vous preniez cette mesure à plus grande échelle, par exemple en pesant toutes les personnes qui vivent dans une très grande ville sur une échelle énorme, vous pourriez vous faire une idée de la difficulté de réaliser des mesures avec précision (parce qu’il y a des erreurs) et de manière complète (parce qu’il est difficile de tout mesurer).

Avoir des informations partielles, surtout si ce que vous voulez décrire est assez complexe et varié, n’est pas une condition complètement négative, car vous pouvez utiliser des matrices plus petites, ce qui implique moins de calculs. Parfois, vous ne pouvez même pas obtenir un échantillon de ce que vous voulez décrire et apprendre pour certains problèmes, parce que l’événement est complexe et a une grande variété de caractéristiques. Comme autre exemple, demandez-vous comment apprendre à déterminer les sentiments (au sens large) à partir de textes tirés d’Internet, comme des messages (tweets) postés sur X. En dehors des retweets, il est peu probable que vous voyiez jamais deux tweets identiques (exprimant le même sentiment en utilisant exactement les mêmes mots pour exactement le même sujet) postés par deux personnes différentes. Il se peut que vous voyiez quelque chose de similaire, mais jamais d’identique. Il est donc impossible de connaître à l’avance tous les tweets possibles qui associent certains mots aux sentiments. En bref, vous devez utiliser un échantillon et dériver des règles générales à partir d’un ensemble partiel.

Même avec de telles restrictions pratiques et l’impossibilité d’obtenir toutes les données possibles, vous pouvez toujours saisir ce que vous voulez décrire et en tirer des leçons. L’échantillonnage fait partie de la pratique statistique. Lorsque vous utilisez des échantillons, vous choisissez vos exemples en fonction de certains critères. En procédant soigneusement, vous obtenez une certaine probabilité pour que votre vue partielle ressemble bien à la vue globale.

En statistique, la population fait référence à tous les événements et objets que vous voulez mesurer. Un échantillon fait partie de cette population et il est choisi selon certains critères. L’utilisation de l’échantillonnage aléatoire, qui consiste à choisir les événements ou les objets à représenter au hasard, aide à créer un ensemble d’exemples pour l’apprentissage automatique qui lui servent à apprendre comme il le ferait avec tous les échantillons possibles. Cette technique fonctionne parce que les distributions de valeurs dans l’échantillonnage sont semblables à celles de la population, ce qui est suffisant.

L’échantillonnage aléatoire n’est pas la seule approche possible. Vous pouvez également appliquer l’échantillonnage stratifié, par lequel vous pouvez contrôler certains aspects de l’échantillon aléatoire afin d’éviter de choisir trop ou trop peu d’événements d’un certain type. Après tout, le hasard est aléatoire, et vous n’avez aucune assurance absolue de toujours reproduire la répartition exacte de la population.

Une distribution est une formulation statistique qui décrit comment observer un événement ou une mesure en vous indiquant la probabilité d’observer une certaine valeur. Les distributions sont décrites dans une formule mathématique (un sujet qui n’est pas abordé dans ce livre) et peuvent être représentées visuellement à l’aide de graphiques tels que des histogrammes ou des tracés de distribution. L’information que vous placez dans votre matrice a une distribution, et vous pouvez constater que les distributions de différentes caractéristiques sont liées. Une distribution implique naturellement une variation et, lorsqu’il s’agit de valeurs numériques, il est important de déterminer un centre de variation, qui est souvent la moyenne statistique, calculée en additionnant toutes vos valeurs et en divisant le total par le nombre de valeurs considérées.

La moyenne est une mesure descriptive indiquant la valeur à laquelle vous devez vous attendre le plus, compte tenu de tous les cas possibles. La moyenne convient mieux à une distribution symétrique et en forme de cloche (de sorte que lorsque les valeurs sont supérieures à la moyenne, la distribution est de forme similaire à celle des valeurs inférieures). Une distribution célèbre, la distribution normale ou gaussienne, a ce type de forme, mais, dans le monde réel, vous pouvez aussi trouver de nombreuses distributions asymétriques qui n’ont des valeurs extrêmes que d’un côté de la distribution, ce qui influence donc trop la moyenne.

La médiane est une mesure qui prend la valeur du milieu après avoir ordonné toutes vos observations de la plus petite à la plus grande. Étant basée sur l’ordre des valeurs, la médiane est insensible aux valeurs de la distribution et peut représenter dans certains cas un descripteur plus juste que la moyenne. La signification de la moyenne et de la médiane est qu’elles décrivent une valeur dans la distribution autour de laquelle il y a une certaine variation, et les algorithmes d’apprentissage automatique se soucient d’une telle variation. La plupart des gens appellent cette variation une variance. Comme la variance est un nombre au carré, il existe également un équivalent racine, appelé écart-type. L’apprentissage automatique prend en compte la variance de chaque variable particulière (distributions univariées) et de l’ensemble des caractéristiques (distributions multivariées) pour déterminer comment cette variation influe sur la réponse.

En d’autres termes, les statistiques sont importantes dans l’apprentissage automatique parce qu’elles véhiculent l’idée que les caractéristiques ont une distribution. Une distribution implique une variation, et la variation est comme une quantification de l’information – plus il y a de variance dans vos caractéristiques, plus l’information peut être adaptée à la réponse afin d’établir une règle entre certains types d’informations et certaines réponses. Vous pouvez ensuite utiliser les statistiques pour évaluer la qualité de votre matrice de caractéristiques, et même vous appuyer sur des mesures statistiques pour construire des algorithmes d’apprentissage automatique efficaces, comme nous le verrons plus loin, où les opérations matricielles, l’échantillonnage des distributions, les statistiques et les probabilités contribuent tous aux solutions et permettent aux ordinateurs d’apprendre efficacement des données.

DANS CE CHAPITRE

Comprendre comment fonctionne l’apprentissage automatique en coulisses

•

Reconnaître les différentes parties d’un processus d’apprentissage

•

Découvrir les étapes de l’optimisation de l’apprentissage automatique lors de l’utilisation de la descente de gradient

•

Faire la distinction entre entraînement par lot, par mini-lot et en ligne

Chapitre 8

Descente du gradient

L’apprentissage automatique peut apparaître comme une sorte de tour de passe-passe pour tout nouveau venu dans la discipline. C’est une chose à laquelle il faut s’attendre pour toute application d’une découverte scientifique avancée, comme le disait Arthur C. Clarke, futurologue et grand auteur de science-fiction (le père du célèbre 2001 : l’Odyssée de l’espace), dans sa troisième loi, qui affirme : « Toute technologie suffisamment avancée ne peut être distinguée de la magie. » Cependant, l’apprentissage automatique n’a rien de magique. Il s’agit de la conception et du développement de formules mathématiques et d’algorithmes qui permettent aux ordinateurs d’apprendre.

Partant du principe que le monde lui-même est une représentation de formules mathématiques et statistiques, les algorithmes d’apprentissage automatique s’efforcent d’apprendre à connaître ces formules en les retraçant à partir d’un nombre limité d’observations. De même qu’il n’est pas nécessaire de voir tous les arbres du monde pour apprendre à en reconnaître un (parce que les humains peuvent comprendre les caractéristiques distinctives des arbres), les algorithmes d’apprentissage automatique peuvent utiliser la puissance de calcul des ordinateurs et la grande disponibilité de données sur tous les sujets pour apprendre à résoudre un grand nombre de problèmes importants et utiles.

Bien que l’apprentissage automatique soit intrinsèquement complexe, c’est l’homme qui l’a conçu et, à ses débuts, il s’est contenté d’imiter la façon dont nous apprenons du monde. Nous pouvons exprimer des problèmes de données simples et des algorithmes d’apprentissage de base s’appuyant sur la manière dont un enfant perçoit et comprend le monde, ou résoudre un problème d’optimisation difficile en utilisant, sous forme mathématique, l’analogie de la descente d’une montagne en empruntant la bonne pente. Ce chapitre vous aide à comprendre l’apprentissage automatique comme une technologie qui n’a rien de magique. À cette fin, les sections suivantes proposent une théorie de base et se penchent ensuite sur des exemples qui illustrent la théorie.

Reconnaître les différents types d’apprentissage

L’apprentissage se décline sous différentes formes, en fonction de l’algorithme et de ses objectifs. On peut diviser les algorithmes d’apprentissage automatique en trois groupes principaux en fonction de leur objectif :

• apprentissage supervisé ;

• apprentissage non supervisé ;

• apprentissage par renforcement.

Apprentissage supervisé

On parle d’apprentissage supervisé lorsqu’un algorithme apprend à partir d’exemples de données et de réponses cibles associées, qui peuvent consister en des valeurs numériques ou des chaînes de caractères, telles que des classes ou des étiquettes, afin de prédire ultérieurement la réponse correcte lorsqu’il est confronté à de nouveaux exemples. L’approche supervisée est en effet similaire à l’apprentissage humain sous la supervision d’un enseignant. L’enseignant fournit de bons exemples que l’élève doit mémoriser, et l’élève déduit ensuite des règles générales à partir de ces exemples spécifiques.

Dans l’apprentissage automatique, vous êtes l’enseignant qui définit les données nécessaires à la résolution d’un problème. L’étudiant est l’algorithme, généralement appelé prédicteur ou modèle, qui peut prédire une solution après avoir appris à partir des données que vous avez fournies. Les données se composent de caractéristiques, qui sont des séquences informatives de données numériques et non numériques, chacune représentant une caractéristique du problème, et d’une réponse associée (ou résultat), qui est la solution à apprendre.

Le processus d’apprentissage est généralement désigné par le terme entraînement. En outre, il convient de faire la distinction entre les problèmes de régression, dont l’objectif est une valeur numérique, et les problèmes de classification, dont l’objectif est une variable qualitative, telle qu’une classe ou une étiquette. Si l’on se réfère aux exemples utilisés dans le livre, une tâche de régression détermine les prix moyens des maisons dans une zone, et une tâche de classification distingue les types d’espèces animales ou végétales sur la base de leurs mesures physiques.

Apprentissage non supervisé

On parle d’apprentissage non supervisé lorsqu’un algorithme apprend à partir d’exemples simples sans aucune réponse associée, laissant à l’algorithme le soin de déterminer lui-même les modèles de données. Ce type d’algorithme tend à restructurer les données en quelque chose d’autre, comme de nouvelles caractéristiques de données qui peuvent représenter une classe ou de nouvelles valeurs utiles pour une analyse supplémentaire ou pour l’entraînement d’un modèle prédictif.

L’apprentissage non supervisé est très utile pour fournir aux humains des indications sur la signification des données et de nouvelles données utiles pour les algorithmes d’apprentissage automatique supervisé. En tant que type d’apprentissage, il ressemble aux méthodes utilisées par les humains pour déterminer que certains objets ou événements appartiennent à la même classe, par exemple en observant le degré de similitude entre les objets. La segmentation ou l’analyse de clusters sont des exemples courants d’algorithme d’apprentissage non supervisé ; il s’agit d’une technique qui vous aide à diviser un ensemble d’exemples en groupes similaires.

Apprentissage par renforcement

L’apprentissage par renforcement se produit lorsque vous présentez séquentiellement à l’algorithme des exemples dépourvus d’étiquettes, comme dans l’apprentissage non supervisé. Cependant, vous accompagnez chaque exemple d’un retour d’information positif ou négatif en fonction de la solution proposée par l’algorithme. L’apprentissage par renforcement est lié à des applications pour lesquelles l’algorithme doit prendre des décisions (le produit est donc prescriptif, et pas seulement descriptif, comme dans l’apprentissage non supervisé), et les décisions ont des conséquences.

L’apprentissage par renforcement s’apparente à l’apprentissage par essais et erreurs pour les humains. Les erreurs vous aident à apprendre parce qu’elles entraînent une pénalité (coût, perte de temps, regret, douleur, etc.), ce qui vous montre qu’une certaine conduite a moins de chances de réussir que d’autres.

Les ordinateurs qui apprennent à jouer seuls à des jeux vidéo sont un exemple intéressant d’apprentissage par renforcement. Dans ce cas, une application présente à l’algorithme des exemples de situations spécifiques, par exemple lorsque le joueur est coincé dans un labyrinthe tout en évitant un ennemi. L’application permet à l’algorithme de connaître le résultat des actions qu’il entreprend, et l’apprentissage se fait en essayant d’éviter ce qu’il découvre être dangereux et de poursuivre la survie. Vous pouvez voir comment la société DeepMind a créé un programme d’apprentissage par renforcement qui joue à de vieux jeux vidéo Atari à l’adresse https://www.youtube.com/watch?v=V1eYniJ0Rnk. En regardant la vidéo, vous remarquerez que le programme est d’abord maladroit et peu habile, mais qu’il s’améliore progressivement avec l’entraînement jusqu’à devenir un champion.

Le processus d’apprentissage

Bien que l’apprentissage supervisé soit le plus populaire et le plus fréquemment utilisé des trois types d’apprentissage automatique, tous les algorithmes répondent à la même logique. L’idée centrale est que vous pouvez représenter la réalité à l’aide d’une fonction mathématique que l’algorithme ne connaît pas à l’avance, mais qu’il peut deviner après avoir vu un certain nombre de données. Vous pouvez exprimer la réalité et toute sa complexité en termes de fonctions mathématiques inconnues que les algorithmes d’apprentissage automatique trouvent et que l’on peut utiliser par la suite. Ce concept est l’idée à la base de toutes sortes d’algorithmes d’apprentissage automatique. Pour proposer des exemples clairs, ce chapitre se concentre sur la classification supervisée comme étant le plus emblématique de tous les types d’apprentissage, et il fournit des explications sur son fonctionnement interne que vous pourrez étendre plus tard à d’autres types d’approches d’apprentissage.

L’objectif d’un classificateur supervisé est d’attribuer une classe (également appelée étiquette) à un exemple après avoir examiné certaines caractéristiques de l’exemple lui-même. Ces caractéristiques peuvent être quantitatives (valeurs numériques) ou qualitatives (valeurs non numériques telles que des chaînes de caractères). Pour attribuer correctement des classes, le classificateur doit d’abord examiner de près un certain nombre d’exemples connus (exemples auxquels une classe a déjà été attribuée), chacun accompagné des mêmes caractéristiques. Cette procédure d’apprentissage, également appelée phase d’entraînement, implique l’observation de nombreux exemples et de leurs étiquettes par le classificateur, ce qui l’aide à apprendre afin de pouvoir fournir une réponse en termes de classe lorsqu’il voit un exemple sans classe plus tard, au moment de la prédiction.

Les données que vous utilisez pour la phase d’entraînement et les données que vous employez pour faire de nouvelles prédictions à l’aide de votre modèle entraîné (la phase appelée test) doivent partager exactement les mêmes caractéristiques que celles utilisées lors de l’entraînement, sinon les prédictions ne fonctionneront pas correctement.

Correspondance avec une fonction inconnue

Pour donner une idée de ce qui se passe dans le processus d’entraînement, imaginez un enfant qui apprend à distinguer les arbres des autres objets. Avant que l’enfant ne puisse le faire de façon indépendante, un enseignant lui présente un certain nombre d’images d’arbres, avec tous les faits qui permettent de distinguer un arbre des autres objets du monde. Il peut s’agir de caractéristiques telles que sa matière (bois), ses parties (tronc, branches, feuilles ou aiguilles, racines) et son emplacement (planté dans le sol). L’enfant se fait une idée de ce à quoi ressemble un arbre en comparant l’affichage des caractéristiques de l’arbre avec les images d’objets différents, tels que des meubles en bois, mais qui n’ont pas d’autres caractéristiques en commun avec un arbre.

Un classificateur d’apprentissage automatique fonctionne de la même manière. Il développe ses capacités cognitives en créant une formule mathématique qui inclut toutes les caractéristiques données de manière à créer une fonction capable de distinguer une classe d’une autre. Imaginons qu’il existe une formule mathématique, également appelée fonction cible (ou fonction objectif), pour exprimer les caractéristiques d’un arbre. Dans ce cas, un classificateur d’apprentissage automatique peut rechercher la représentation de la fonction cible sous la forme d’une réplique ou d’une approximation (une fonction différente qui marche de la même manière). Pouvoir exprimer une telle formule mathématique est la capacité de représentation du classificateur.

D’un point de vue mathématique, vous pouvez exprimer le processus de représentation dans l’apprentissage automatique en utilisant le terme équivalent de mappage (mapping). Ce mappage (ou mise en correspondance) se produit lorsque vous découvrez la construction d’une fonction en observant ses sorties. Une mise en correspondance réussie dans l’apprentissage automatique est similaire à l’intériorisation par un enfant de l’idée d’un objet. Dans ce cas, l’enfant comprend les règles abstraites dérivées des faits du monde d’une manière efficace, de sorte qu’il lui est possible de reconnaître un arbre lorsqu’il en voit un.

Une telle représentation (règles abstraites dérivées de faits réels) est possible parce que l’algorithme d’apprentissage a de nombreux paramètres internes (constitués de vecteurs et de matrices de valeurs), qui sont comme la mémoire de l’algorithme pour les idées adaptées à son activité de mappage reliant les caractéristiques aux classes de réponses. Le volume et le type des paramètres internes délimitent le type de fonctions cibles qu’un algorithme peut apprendre. Dans l’algorithme, un moteur d’optimisation change les valeurs initiales des paramètres pendant l’apprentissage pour représenter la fonction cachée de la cible.

Lors de l’optimisation, l’algorithme recherche parmi les variantes possibles de ses combinaisons de paramètres la meilleure combinaison qui permette une mise en correspondance correcte entre les caractéristiques et les classes au cours de l’entraînement. Ce processus évalue de nombreuses fonctions cibles candidates potentielles parmi celles que l’algorithme d’apprentissage peut deviner. L’ensemble de toutes les fonctions potentielles que l’algorithme d’apprentissage peut évaluer constitue l’espace des hypothèses. Vous pouvez appeler le classificateur résultant, avec tous ses paramètres définis, une hypothèse, une façon de dire, dans l’apprentissage automatique, que l’algorithme a défini des paramètres pour reproduire la fonction cible et qu’il est maintenant prêt à élaborer des classifications correctes (un fait démontré ultérieurement).

L’espace des hypothèses doit contenir toutes les variantes des paramètres de tous les algorithmes d’apprentissage automatique que vous voulez essayer de faire correspondre à une fonction inconnue pour résoudre un problème de classification. Différents algorithmes peuvent avoir différents espaces d’hypothèses. Ce qui importe vraiment, c’est qu’un espace des hypothèses contienne la fonction cible (ou son approximation, qui est une fonction différente, mais similaire).

Vous pouvez imaginer cette phase comme le moment où un enfant, dans un effort pour comprendre l’idée d’un arbre, expérimente de nombreuses idées créatives différentes en rassemblant des connaissances et des expériences (une analogie pour les caractéristiques données). Naturellement, les parents sont impliqués dans cette phase et fournissent des données environnementales pertinentes. Dans l’apprentissage automatique, quelqu’un doit fournir les bons algorithmes d’apprentissage, certains paramètres non apprenables (appelés hyperparamètres), choisir un ensemble d’exemples à partir desquels apprendre et sélectionner les caractéristiques qui accompagnent les exemples. Tout comme un enfant ne peut pas toujours apprendre à distinguer le bien du mal s’il est laissé seul au monde, les algorithmes d’apprentissage automatique ont besoin d’êtres humains pour apprendre avec succès.

Même après avoir terminé le processus d’apprentissage, un classificateur d’apprentissage automatique ne peut souvent pas établir une correspondance univoque entre les exemples et la fonction de classification cible, car de nombreuses correspondances fausses et erronées sont également possibles, comme le montre la Figure 8.1. Dans de nombreux cas, les correspondances fausses et erronées se produisent parce que l’algorithme ne dispose pas de suffisamment de points de données pour découvrir la bonne fonction. Le bruit et les exemples erronés, lorsqu’ils sont mélangés à des données correctes peuvent également causer des problèmes, comme le montre la Figure 8.2.

[picture]

Figure 8.1 Un manque de certitudes rend difficile la détection de la fonction cible.

Le bruit dans les données du monde réel est la norme. De nombreux facteurs étrangers et les erreurs qui se produisent lors de l’enregistrement des données faussent les valeurs des caractéristiques. Un bon algorithme d’apprentissage automatique doit distinguer les signaux qui peuvent correspondre à la fonction cible et ignorer le bruit extérieur.

Explorer les fonctions de coût

La force motrice derrière l’optimisation en apprentissage automatique est la réponse d’une fonction interne à l’algorithme, appelée fonction de coût. Vous pouvez voir d’autres termes utilisés dans certains contextes, tels que fonction de perte, fonction objectif, fonction de score ou encore fonction d’erreur, mais dans tous les cas, la fonction de coût est une fonction d’évaluation qui mesure à quel point l’algorithme d’apprentissage automatique cartographie la fonction cible qu’il s’efforce de deviner. De plus, une fonction de coût détermine la performance d’un algorithme d’apprentissage automatique dans une prédiction supervisée ou un problème d’optimisation non supervisé (dans ce dernier cas, la fonction de coût n’est pas liée au résultat cible, mais aux caractéristiques elles-mêmes).

[picture]

Figure 8.2 Le bruit peut entraîner des erreurs dans les points de données.

La fonction de coût compare les prédictions de l’algorithme aux résultats réels enregistrés dans le monde réel. La comparaison d’une prédiction et de sa valeur réelle à l’aide d’une fonction de coût détermine le niveau d’erreur de l’algorithme. Parce qu’il s’agit d’une formulation mathématique, la fonction de coût exprime le niveau d’erreur sous une forme numérique, une valeur de coût qui doit être minimisée. La fonction de coût transmet à l’algorithme d’apprentissage ce qui est réellement important et significatif pour vos objectifs. Par conséquent, vous devez choisir, ou définir avec précision, la fonction de coût sur la base d’une compréhension du problème que vous voulez résoudre ou du niveau de réussite que vous voulez atteindre.

Par exemple, lorsque l’on considère les prévisions des marchés boursiers, la fonction de coût exprime l’importance d’éviter des prévisions erronées. Dans ce cas, vous voulez gagner de l’argent en évitant des pertes importantes. Dans la prévision des ventes, le problème est différent, car vous devez réduire l’erreur dans les situations courantes et fréquentes, et non dans les situations rares et exceptionnelles, de sorte que vous utilisez une fonction de coût différente.

Lorsque le problème consiste à prédire qui va probablement tomber malade à cause d’une certaine maladie, vous accordez de l’importance aux algorithmes qui peuvent avoir une forte probabilité d’identifier les personnes qui présentent les mêmes caractéristiques et qui sont en fait tombées malades plus tard. En fonction de la gravité de la maladie, vous pouvez également préférer que l’algorithme choisisse à tort certaines personnes qui, au final, ne tombent pas malades, plutôt que de manquer les personnes qui tombent réellement malades.

La fonction de coût est le véritable moteur du succès d’une application d’apprentissage automatique. Elle est aussi essentielle au processus d’apprentissage que la représentation (la capacité d’approximation de certaines fonctions mathématiques) et l’optimisation (la façon dont les algorithmes d’apprentissage automatique définissent leurs paramètres internes). La plupart des algorithmes optimisent leur propre fonction de coût, et vous n’avez alors d’autre choix que de les appliquer tels quels. Certains algorithmes permettent de choisir parmi un certain nombre de fonctions possibles, offrant ainsi plus de flexibilité. Lorsqu’un algorithme utilise une fonction de coût directement dans le processus d’optimisation, cette fonction est utilisée en interne. Étant donné que les algorithmes sont paramétrés pour travailler avec certaines fonctions de coût, l’objectif d’optimisation peut différer de votre objectif désiré. Dans ce cas, vous mesurez les résultats à l’aide d’une fonction de coût externe qui, par souci de clarté terminologique, est alors appelée une fonction d’erreur ou fonction de perte (si elle doit être minimisée), ou encore fonction de score (si elle doit au contraire être maximisée).

En ce qui concerne votre cible, une bonne pratique consiste à définir la fonction de coût qui donne les meilleurs résultats pour résoudre votre problème, puis à déterminer quels algorithmes fonctionnent le mieux pour l’optimiser afin de définir l’espace des hypothèses que vous souhaitez tester. Lorsque vous travaillez avec des algorithmes qui n’autorisent pas la fonction de coût que vous souhaitez, vous pouvez toujours influencer indirectement leur processus d’optimisation pour qu’il corresponde à votre fonction de coût préférée en fixant leurs hyperparamètres (les paramètres que vous devez fournir pour que l’algorithme fonctionne) et en sélectionnant vos caractéristiques d’entrée par rapport à votre fonction de coût. Enfin, lorsque vous avez rassemblé tous les résultats des algorithmes, vous les évaluez en utilisant la fonction de coût que vous avez choisie, puis vous décidez quelle combinaison d’algorithmes, d’hyperparamètres et de caractéristiques est la meilleure pour résoudre votre problème.

Lorsqu’un algorithme apprend à partir de données, la fonction de coût guide le processus d’optimisation en indiquant les changements dans les paramètres internes qui sont les plus bénéfiques pour faire de meilleures prédictions. L’optimisation se poursuit au fur et à mesure que la réponse de la fonction de coût s’améliore, itération par itération. Lorsque la réponse stagne ou commence à se dégrader, il est temps d’arrêter d’ajuster les paramètres de l’algorithme, car celui-ci n’est plus capable d’obtenir de meilleurs résultats de prédiction à partir de là. Lorsque l’algorithme travaille sur de nouvelles données et fait des prédictions, la fonction de coût aide à évaluer si son comportement est correct et réellement efficace.

Décider de la fonction de coût est une activité souvent sous-estimée dans l’apprentissage automatique. C’est une tâche fondamentale, car elle détermine comment l’algorithme se comporte au cours de la phase d’apprentissage et comment il gère le problème que vous voulez résoudre. Ne vous fiez jamais aux options par défaut, mais demandez-vous toujours ce que vous voulez obtenir grâce à l’apprentissage automatique, et vérifiez quelle fonction de coût peut le mieux représenter le but que vous voulez atteindre.

Dans la Partie 4, vous découvrirez quelques algorithmes d’apprentissage automatique, et, dans la Partie 5, vous verrez comment appliquer la théorie à des problèmes réels, en présentant des études de classification pour évaluer du texte et les sentiments. Si vous avez besoin de choisir une fonction de coût, des explications et des exemples d’apprentissage automatique introduisent une gamme de fonctions d’erreur pour la régression et la classification, dont la racine de l’erreur quadratique moyenne, la perte logistique, l’exactitude, la précision, le rappel et l’aire sous la courbe (AUC). Ne vous inquiétez pas si ces termes ne sont pas clairs à ce stade, car ils seront expliqués plus en détail dans les Parties 4 et 5.

Descendre la courbe d’optimisation

L’algorithme de descente de gradient offre un exemple parfait du fonctionnement de l’apprentissage automatique, et il résume les concepts exprimés jusqu’à présent dans le livre parce que vous pouvez le comprendre à l’aide d’exemples visuels intuitifs, et pas seulement à l’aide d’une formulation mathématique. De plus, bien qu’il ne s’agisse que d’une des nombreuses méthodes possibles, la descente de gradient est une approche largement utilisée, qui est appliquée à une série d’algorithmes d’apprentissage automatique présentés dans le livre, tels que les modèles linéaires, les réseaux de neurones ou encore les machines à renforcement de gradient.

La descente de gradient élabore un résultat en partant d’une solution aléatoire lorsqu’on lui donne un ensemble d’entrées (une matrice de données composée de caractéristiques et d’une réponse). Elle procède ensuite à diverses itérations en utilisant le retour d’information de la fonction de coût, modifiant ainsi ses paramètres avec des valeurs qui améliorent progressivement la solution aléatoire initiale et réduisent l’erreur. Même si l’optimisation peut nécessiter un grand nombre d’itérations avant d’aboutir à une bonne correspondance, elle s’appuie sur les changements qui améliorent le plus la fonction de coût de la réponse au cours de chaque itération. La Figure 8.3 montre un exemple de processus d’optimisation complexe avec quelques minima locaux (les points minimums au milieu des vallées) et un endroit où le processus peut se bloquer (à cause de la surface plate au point d’ancrage) et ne peut pas continuer sa descente.

[picture]

Figure 8.3 Représentation graphique des données des paramètres par rapport à la sortie de la fonction de coût.

Sur la base de la Figure 8.3, vous pouvez visualiser le processus d’optimisation comme une marche en haute montagne, par une journée brumeuse, les paramètres étant les différents chemins pour descendre dans la vallée. Une optimisation par descente de gradient se produit à chaque étape. À chaque itération, l’algorithme choisit le chemin qui réduit le plus l’erreur, quelle que soit la direction prise. L’idée est que si les étapes ne sont pas trop grandes (ce qui amènerait l’algorithme à sauter par-dessus la cible), le fait de toujours suivre la direction la plus descendante permettra de trouver le lieu le plus bas.

Malheureusement, il n’est pas toujours possible de trouver le point le plus bas, car l’algorithme peut arriver à des vallées intermédiaires, ce qui donne l’illusion qu’il a atteint la cible. Cependant, dans la plupart des cas, la descente de gradient conduit l’algorithme d’apprentissage automatique à découvrir la bonne hypothèse pour cartographier le problème avec succès. La Figure 8.4 montre comment un point de départ différent peut faire la différence. Le point de départ A aboutit à un minimum local, tandis que le point B, situé non loin de là, parvient à atteindre le minimum global.

[picture]

Figure 8.4 Visualisation de l’effet du point de départ sur le résultat.

Dans un processus d’optimisation, vous faites la distinction entre différents résultats de ce processus. Vous pouvez avoir un minimum global qui est effectivement l’erreur minimale de la fonction de coût, mais vous pouvez aussi avoir beaucoup de minima locaux – des solutions qui semblent produire l’erreur minimale alors que ce n’est pas le cas (les vallées intermédiaires où l’algorithme se bloque). Pour remédier à cette situation, compte tenu de l’initialisation aléatoire du processus d’optimisation, il est bon d’exécuter l’optimisation plusieurs fois. Cela signifie qu’il faut essayer différentes séquences de chemins descendants et ne pas rester bloqué dans le même minimum local.

Optimiser avec le big data

L’apprentissage automatique se résume à un problème d’optimisation dans lequel vous recherchez un minimum global à partir d’une certaine fonction de coût. Par conséquent, l’élaboration d’une optimisation à partir de toutes les données disponibles est clairement un avantage, car, à chaque itération, cela vous permet de trouver la meilleure correction des paramètres d’apprentissage par rapport à toutes les données. C’est la raison pour laquelle la plupart des algorithmes d’apprentissage automatique préfèrent utiliser toutes les données disponibles, et ils veulent qu’elles soient accessibles dans la mémoire de l’ordinateur.

En outre, vous devez également faire la distinction entre les algorithmes statistiques et les autres optimisations. Les techniques d’apprentissage basées sur des algorithmes statistiques font appel au calcul et à l’algèbre matricielle et ont besoin de toutes les données en mémoire. D’autres algorithmes, basés sur la recherche pas à pas de la meilleure solution suivante en procédant itération par itération à travers une solution partielle (comme la descente de gradient discutée dans la section précédente), peuvent avoir un avantage lorsqu’ils développent une hypothèse basée sur toutes les données. Lorsque toutes les données sont utilisées, ils peuvent détecter les signaux plus faibles sur place et éviter d’être trompés par le bruit.

Étant donné l’avantage d’utiliser toutes les données disponibles, vous devez tenir compte du fait que le matériel dont vous disposez peut constituer un goulot d’étranglement. Lorsque vous travaillez avec des données dans les limites de la mémoire de l’ordinateur (en partant du principe que votre ordinateur a environ 8 ou 16 Go de mémoire vive), vous travaillez dans la mémoire centrale. Vous pouvez résoudre la plupart des problèmes d’apprentissage automatique en exploitant cette approche. Les algorithmes qui fonctionnent avec la mémoire centrale sont appelés algorithmes par lot (batch) parce que, comme dans une usine où les machines traitent des lots uniques de matériaux, ces algorithmes apprennent à manipuler et à prédire un lot unique de données à la fois, représenté par une matrice de données.

Cependant, parfois, les données ne peuvent pas tenir dans la mémoire centrale parce qu’il y en a trop. Les données dérivées du Web constituent un exemple typique d’informations qui ne s’insèrent pas facilement dans la mémoire. En outre, les données générées par les capteurs, les dispositifs de suivi, les satellites et la vidéosurveillance sont souvent problématiques en raison de leur volume par rapport à la quantité de RAM d’un ordinateur. Cependant, elles peuvent être facilement stockées sur un disque dur, étant donné la disponibilité de dispositifs de stockage bon marché et de grande taille capables de contenir facilement plusieurs téraoctets de données.

Tirer parti de l’échantillonnage

Quelques stratégies peuvent sauver la situation lorsque les données sont trop volumineuses pour tenir dans la mémoire d’un seul ordinateur :

Sous-échantillonnage : les données sont remodelées par une sélection de cas (et parfois même de caractéristiques) sur la base d’un échantillonnage statistique dans une matrice de données plus facile à gérer, mais réduite. Il est clair que la réduction de la quantité de données ne permet pas toujours d’obtenir exactement les mêmes résultats que lors d’une analyse globale. Travailler avec une quantité moindre que les données disponibles peut même produire des modèles moins puissants. Cependant, si le sous-échantillonnage est exécuté correctement, l’approche peut générer des résultats presque équivalents et toujours fiables. Un sous-échantillonnage réussi doit utiliser correctement l’échantillonnage statistique, en recourant à des tirages d’échantillons aléatoires ou stratifiés.

Échantillonnage aléatoire : on crée un échantillon en choisissant au hasard les exemples qui en font partie. Plus l’échantillon est grand, plus il a de chances de ressembler à la structure et à la variété originales des données, mais même avec peu d’exemples tirés au sort, les résultats sont souvent acceptables, tant en termes de représentation des données originales qu’à des fins d’apprentissage automatique.

Échantillonnage stratifié : on contrôle la distribution finale de la variable cible ou de certaines caractéristiques des données que l’on juge essentielles pour reproduire avec succès les caractéristiques des données complètes. Parmi les exemples classiques, on peut citer le tirage d’un échantillon dans une salle de classe composé de différentes proportions d’hommes et de femmes afin de deviner la taille moyenne. Si les femmes sont, en moyenne, plus petites que les hommes et en plus faible proportion, vous voulez tirer un échantillon qui reproduit la même proportion afin d’obtenir une estimation fiable de la taille moyenne. Si, par erreur, vous n’échantillonnez que des hommes, vous surestimerez la taille moyenne. L’utilisation de connaissances préalables en matière d’échantillonnage (comme le fait de savoir que le sexe peut jouer un rôle dans l’estimation de la taille) est très utile pour obtenir des échantillons adaptés à l’apprentissage automatique, comme l’explique le chapitre suivant.

Après avoir choisi une stratégie d’échantillonnage, vous devez tirer un sous-échantillon composé d’un nombre suffisant d’exemples, compte tenu des limites de votre mémoire, pour représenter la variété des données. Les données à haute dimensionnalité, caractérisées par de nombreux cas et de nombreuses caractéristiques, sont plus difficiles à sous-échantillonner parce qu’il est nécessaire de tirer un échantillon beaucoup plus grand, qui peut même ne pas tenir dans votre mémoire centrale.

Exploitation du parallélisme

Au-delà du sous-échantillonnage, une autre solution possible pour ajuster les données en mémoire est d’utiliser le parallélisme, qui partage les données entre plusieurs ordinateurs connectés en réseau. Chaque ordinateur traite une partie des données pour l’optimisation. Une fois que chaque ordinateur a effectué ses propres calculs et que toutes les tâches d’optimisation parallèles ont été réduites à une seule, une solution est trouvée.

Pour comprendre le fonctionnement de cette solution, comparez le processus de construction d’une voiture pièce par pièce par un seul ouvrier, et le fait que de nombreux ouvriers travaillent séparément sur des agrégats de pièces automobiles, laissant un seul ouvrier pour effectuer l’assemblage final. En plus d’une exécution plus rapide de l’assemblage, vous n’avez pas besoin de garder toutes les pièces en usine en même temps. De la même manière, vous n’avez pas besoin de conserver toutes les parties des données dans un seul ordinateur, mais vous pouvez profiter du fait qu’elles sont traitées séparément sur différents systèmes, ce qui vous permet de surmonter les difficultés liées aux limites de la mémoire centrale.

Cette approche est à la base de la technologie dite MapReduce (https://fr.wikipedia.org/wiki/MapReduce) et des frameworks Apache Hadoop et Apache Spark, qui se concentrent sur le découpage d’un problème entre plusieurs machines, et finalement sur la réduction de leurs sorties pour obtenir la solution souhaitée.

Malheureusement, il n’est pas facile de diviser tous les algorithmes d’apprentissage automatique en processus séparables, et ce problème limite la mise en œuvre d’une telle approche. Plus important encore, l’installation et l’entretien d’un réseau d’ordinateurs prêts pour ce type de traitement des données entraînent des coûts et du temps considérables pour la configuration et la maintenance, ce qui limite l’applicabilité de cette approche aux grandes organisations seulement.

Apprentissage out-of-core

Une autre solution d’emploi de la mémoire consiste à s’appuyer sur des algorithmes out-of-core (littéralement en dehors du cœur, c’est-à-dire conçus pour traiter des données trop volumineuses pour être stockées en une seule fois dans la mémoire principale d’un ordinateur) qui conservent les données sur le périphérique de stockage et les introduisent par morceaux dans la mémoire de l’ordinateur en vue de leur traitement. Le processus d’alimentation est appelé streaming. Comme les morceaux sont plus petits que la mémoire centrale, l’algorithme peut les traiter correctement et les utiliser pour mettre à jour l’optimisation de l’algorithme d’apprentissage automatique. Après la mise à jour, le système les rejette en faveur de nouveaux morceaux, que l’algorithme utilise pour l’apprentissage. Ce processus se répète jusqu’à ce qu’il n’y ait plus de morceaux. Les morceaux peuvent être petits (en fonction de la mémoire centrale), et le processus est appelé apprentissage par mini-lots, ou ils peuvent même être constitués d’un seul exemple, appelé apprentissage en ligne.

La descente de gradient décrite précédemment peut, comme d’autres algorithmes itératifs, fonctionner correctement avec une telle approche ; cependant, il faut plus de temps pour parvenir à une optimisation, car la trajectoire du gradient est plus erratique et non linéaire par rapport à une approche par lots. L’algorithme en ligne peut même atteindre une solution insatisfaisante, par rapport à sa version par lots en mémoire, lorsqu’il n’utilise pas suffisamment d’itérations à partir d’exemples de données.

Lorsqu’elle travaille avec des mises à jour répétées de ses paramètres basés sur des mini-lots et des exemples uniques, la descente de gradient prend le nom de descente de gradient stochastique. Elle atteindra une solution d’optimisation correcte si deux conditions préalables sont remplies :

• Les exemples transmis en continu sont extraits de manière aléatoire (d’où le terme « stochastique » , qui rappelle l’idée d’une extraction aléatoire à partir d’une distribution d’exemples).

• Un taux d’apprentissage approprié est défini comme étant fixe ou flexible en fonction du nombre d’observations ou d’autres critères.

Le fait de ne pas tenir compte de la première condition préalable implique que vous devez également tenir compte de l’ordre des exemples, ce qui est parfois un effet indésirable. Le taux d’apprentissage rend l’apprentissage plus ou moins ouvert aux mises à jour, ce qui rend l’apprentissage lui-même plus ou moins flexible pour traiter les caractéristiques des exemples, qui sont vues plus loin dans le flux.

Le paramètre d’apprentissage peut faire une différence dans la qualité d’une optimisation out-of-core parce qu’un taux d’apprentissage trop élevé, bien que plus rapide dans l’optimisation, peut contraindre les paramètres aux effets des exemples bruyants ou erronés vus au début ou pendant le flux. En raison de ces effets, un taux d’apprentissage élevé peut plus facilement conduire à des solutions qui ne sont pas suffisamment optimisées. Le choix d’un taux d’apprentissage plus faible nécessite davantage de calculs et de données, mais il s’agit souvent d’un choix plus sûr. D’autre part, un taux d’apprentissage élevé rend l’algorithme réactif aux dernières observations transmises. Ainsi, un algorithme réactif peut s’avérer avantageux lorsque l’algorithme apprend à partir de sources qui évoluent naturellement et qui sont rapidement modifiées, telles que les données du secteur de la publicité numérique, où les nouvelles campagnes publicitaires commencent souvent à modifier le niveau d’attention et de réponse des personnes ciblées.

DANS CE CHAPITRE

Expliquer à quel point un échantillonnage correct est critique dans l’apprentissage automatique

•

Mettre en évidence les erreurs dues aux biais et à la variance

•

Proposer différentes approches en matière de validation et de tests

•

Mettre en garde contre les échantillons biaisés, le surapprentissage, le sous-apprentissage et le snooping

Chapitre 9

Valider l’apprentissage automatique

Ce chapitre vous aide à comprendre comment l’apprentissage automatique fonctionne avec les données. Il commence par examiner les données elles-mêmes et explique comment les applications d’apprentissage automatique échantillonnent et utilisent les données pour créer une solution.

Une solution d’apprentissage automatique ne se résume pas à des données. L’algorithme que vous utilisez et la manière dont vous l’employez déterminent le résultat. La section suivante du chapitre vous aide à comprendre certains concepts algorithmiques de base à l’aide d’algorithmes standard tels que la régression linéaire et les K plus proches voisins.

Une fois que vous avez des données à explorer et un algorithme pour ce faire, vous devez valider et tester les solutions que vous créez. Il est de votre responsabilité de vous assurer que la solution fournit plus qu’un simple résultat, c’est-à-dire un résultat correct et qui reflète le monde réel.

Il n’est pas nécessaire de saisir manuellement le code source de ce chapitre. Il est beaucoup plus facile d’utiliser le code source téléchargeable de ce chapitre dans le dossier ML4D2E de la source téléchargeable pour le code Python et dans le dossier ML4D2ER de la source téléchargeable pour le code R. Les fichiers d’exemple de ce chapitre commencent par ML4D2E ; 09 ; (pour Python) ou ML4D2E_R ; 09 ; (pour R). Reportez-vous à l’introduction pour savoir comment trouver ces fichiers source.

Utilisation d’échantillons de données

Avoir des échantillons (sous la forme de jeux de données) et un algorithme d’apprentissage automatique sous la main ne garantit pas que la résolution d’un problème d’apprentissage est possible, ou que les résultats correspondront à la solution souhaitée. Par exemple, si vous voulez que votre ordinateur distingue la photo d’un chien de celle d’un chat, vous pouvez lui fournir de bons exemples d’images de chiens et de chats. Vous entraînez alors un classificateur « chien versus chat » basé sur un algorithme d’apprentissage automatique qui pourrait alors produire la probabilité pour qu’une photo donnée représente un chien ou un chat. Bien sûr, le résultat est une probabilité, et non pas une assurance absolue que la photo est celle d’un chien ou d’un chat.

En fonction de la probabilité que le classificateur renvoie, vous pouvez décider de la classe (chien ou chat) d’une photo. Lorsque la probabilité est plus élevée pour un chien, vous pouvez minimiser le risque de faire une mauvaise évaluation en choisissant les plus grandes chances en faveur d’un chien. Plus la différence entre la probabilité d’un chien et celle d’un chat est élevée, plus vous pouvez avoir confiance en votre choix. Un choix serré se produira vraisemblablement en raison d’une certaine ambiguïté dans la photo (la photo n’est pas claire ou le chien a un physique ambigu). Il se peut même qu’il ne s’agisse pas d’un chien et que l’algorithme ne sache rien du raton laveur, qui est pourtant représenté sur la photo.

Telle est la puissance de l’entraînement d’un classificateur : vous posez le problème et vous proposez les échantillons, chacun étant soigneusement marqué de l’étiquette ou de la classe que l’algorithme doit apprendre (l’étiquetage est une activité difficile en soi, comme vous le découvrirez dans les chapitres suivants). Votre ordinateur entraîne l’algorithme pendant un certain temps et vous obtenez finalement un modèle qui fournit une réponse ou une probabilité. En fin de compte, une probabilité n’est qu’une opportunité – ou un risque, d’un autre point de vue – de proposer une solution et d’obtenir une réponse correcte. À ce stade, vous pouvez avoir l’impression d’avoir traité tous les problèmes et croire que le travail est terminé, mais vous devez encore valider les résultats. Ce chapitre vous aide à découvrir pourquoi l’apprentissage automatique n’est pas simplement une activité qui consiste à appuyer sur un bouton et à attendre la fin du traitement, mais qu’il nécessite le plus souvent une réflexion approfondie, une planification minutieuse et avant tout une expérimentation contrôlée.

Ce chapitre et les suivants vous aideront à comprendre les concepts de l’apprentissage automatique à l’aide de petits jeux de données qui illustrent les techniques sans complexité excessive. Les exemples s’appuient souvent sur un jeu de données simplifié, c’est-à-dire un jeu de données comportant peu d’exemples et de caractéristiques, mais un tel jeu de données n’a pas grand-chose à voir avec le monde réel. Assurément, certains problèmes du monde réel sont relativement simples, mais la majorité de ceux que vous aborderez à l’aide des techniques apprises dans ce chapitre nécessiteront des jeux de données beaucoup plus complexes.

Vérifier les erreurs hors échantillon

Les sections suivantes vous aideront à examiner la base des erreurs lorsque vous utilisez un algorithme entraîné pour analyser des données que l’algorithme n’a pas encore vues, c’est-à-dire des données hors échantillon (out-of-sample). L’objectif de l’entraînement d’un algorithme est de pouvoir continuer à l’utiliser pour comprendre les données qui viennent après les données dont vous disposez au départ. Par conséquent, des erreurs dans cette analyse peuvent causer de graves problèmes qui risquent d’invalider les résultats de l’algorithme. Par exemple, vous pouvez utiliser l’apprentissage automatique pour analyser les tendances du marché. Si l’algorithme employé échoue, vous risquez d’acheter des actions d’une entreprise qui n’a aucune chance de succès. Les sections suivantes examinent les erreurs qui peuvent intervenir à différents niveaux :

• échantillonner les données ;

• s’assurer qu’une source de données n’est pas biaisée ou problématique à d’autres égards ;

• comprendre le rôle des biais et de la variance ;

• contrôler la complexité des modèles ;

• créer une solution équilibrée ;

• visualiser la solution.

Comprendre la notion d’échantillon

Lorsque vous recevez pour la première fois les données utilisées pour entraîner un algorithme, il ne s’agit que d’un échantillon de données. Généralement, sauf dans de rares circonstances, les données que vous recevez ne sont pas toutes celles que vous pourriez obtenir. Par exemple, si vous recevez des données sur les ventes de votre service marketing, les données reçues ne sont pas toutes les données possibles sur les ventes car, à moins que les ventes ne soient arrêtées, il y aura toujours de nouvelles données représentant de nouvelles ventes à l’avenir.

Si vos données ne sont pas toutes les données possibles, il s’agit en fait d’un d’échantillon. Un échantillon est une sélection et, comme pour toutes les sélections, les personnes qui les ont choisies de cette manière peuvent avoir différentes motivations. Par conséquent, lorsque vous recevez des données, la première question que vous devez vous poser est de savoir comment quelqu’un les a sélectionnées. Si quelqu’un les a sélectionnées arbitrairement, sans critères spécifiques, vous devriez avoir un échantillon aléatoire et vous pouvez raisonnablement vous attendre à ce que, si les choses ne changent pas par rapport au passé, les données futures ne différent pas trop de celles que vous avez sous la main.

Les statistiques prévoient que l’avenir ne différera pas trop du passé. Il est donc possible de fonder des prévisions futures sur des données passées en utilisant la théorie de l’échantillonnage aléatoire. Si vous sélectionnez des exemples au hasard sans critère, vous avez de bonnes chances de choisir une sélection d’exemples qui :

• ne s’éloigne pas trop de vos exemples réels ;

• n’aura pas de grandes différences par rapport aux exemples précédents.

En termes statistiques, vous pouvez vous attendre à ce que votre échantillon aléatoire soit représentatif de la distribution réelle de vos exemples. Si cette distribution persiste à l’avenir, les enseignements tirés du passé éclaireront votre modèle sur l’avenir.

Cependant, lorsque l’échantillon que vous recevez est quelque peu spécifique, cela peut poser un problème lors de l’entraînement de l’algorithme. En effet, ces données spécifiques peuvent forcer votre algorithme à apprendre quelque chose de différent de ce qu’il aurait pu produire en utilisant des données aléatoires. Par exemple, si vous recevez les données des ventes d’un seul magasin, ou des magasins d’une seule région (ce qui est en fait un échantillon spécifique), l’algorithme peut ne pas apprendre à prévoir les ventes futures de tous les magasins dans toutes les régions. Cet échantillon spécifique cause des problèmes car les autres magasins peuvent fonctionner différemment, et suivre des règles différentes de celles que vous observez.

Pour vous assurer que votre algorithme apprend correctement à partir des données, vous devez toujours vérifier ce que l’algorithme a appris à partir des données en échantillon (in-sample, les données utilisées pour l’apprentissage) en testant votre hypothèse sur des données hors échantillon. Les données hors échantillon (out-of-sample) sont des données dont vous ne disposiez pas au moment de l’apprentissage. Elles sont indispensables pour tester votre modèle lorsque vous souhaitez créer un modèle de prévision.

À la recherche du graal de la généralisation

La généralisation est la capacité d’apprendre à partir des données disponibles les règles générales que vous pouvez appliquer à toutes les autres. Les données hors échantillon deviennent donc essentielles pour déterminer s’il est possible d’apprendre à partir de la source et dans quelle mesure.

Quelle que soit la taille de votre ensemble de données en échantillon, il peut produire des prédictions très improbables dans la réalité lorsque votre processus d’échantillonnage ne produit pas d’échantillons représentatifs. En statistiques, lorsqu’un processus d’échantillonnage ne produit pas d’échantillons représentatifs pour une raison quelconque, le processus est biaisé, ce qui entraîne un biais. Il existe une anecdote sur la déduction à partir d’échantillons biaisés. Elle concerne l’élection présidentielle américaine de 1936 entre Alfred Landon et Franklin D. Roosevelt, au cours de laquelle le Literary Digest a utilisé des informations de sondage biaisées pour prédire le vainqueur.

À l’époque, le Literary Digest, un magazine respectable et populaire, interrogeait ses lecteurs pour déterminer quel serait le prochain président des États-Unis, une pratique qu’il avait suivie avec succès depuis 1916. La réponse du sondage avait été étonnamment en faveur de Landon, avec plus de 57 % de consensus sur ce candidat. Le magazine disposait également d’un échantillon tellement énorme – plus de 10 millions de personnes (dont seulement 2,4 millions avaient répondu) – que le résultat semblait inattaquable : un vaste échantillon couplé à une grande différence entre le gagnant et le perdant tend à ne pas soulever beaucoup de doutes. Pourtant, le sondage s’est soldé par un échec total. En fin de compte, la marge d’erreur était de 19 %, Landon n’ayant obtenu que 38 % des voix et Roosevelt 62 %. C’est la plus grande erreur jamais commise pour un sondage d’opinion.

Le problème est que le magazine a interrogé des personnes dont les noms ont été tirés de tous les annuaires téléphoniques des États-Unis, ainsi que de la liste des abonnés au magazine et des listes de clubs et d’associations, ce qui a permis de recueillir plus de dix millions de noms. Ceci est impressionnant, mais à la fin de la Grande Dépression, posséder un téléphone, s’abonner à un magazine ou faire partie d’un club signifiait que l’on était riche, de sorte que l’échantillon ne comprenait que des électeurs aisés et ignorait complètement les électeurs à faible revenu, qui représentaient pourtant la majorité (ce qui entraînait un biais de sélection). En outre, le sondage a souffert d’un biais de non-réponse, car seulement 2,4 millions de personnes ont répondu, et les personnes qui répondent aux sondages ont tendance à être différentes de celles qui n’y répondent pas¹. L’ampleur de l’erreur pour cet incident particulier a marqué le début d’une approche plus scientifique de l’échantillonnage.

De tels exemples classiques de biais de sélection montrent que, si le processus de sélection génère un biais dans un échantillon, le processus d’apprentissage aura le même biais. Cependant, il arrive parfois que les biais soient inévitables et difficiles à déceler. Par exemple, si vous allez pêcher avec un filet, vous ne pouvez voir que le poisson que vous capturez, et pas celui qui est passé à travers les mailles du filet.

Un autre exemple est fourni par la Seconde Guerre mondiale. À l’époque, les concepteurs amélioraient constamment les avions de guerre américains en ajoutant des blindages supplémentaires aux pièces qui subissaient le plus de dégâts au retour des bombardements. Il a fallu le raisonnement du mathématicien Abraham Wald pour faire remarquer que les concepteurs avaient besoin de renforcer les endroits qui n’avaient pas de trous de balle au retour des avions. Ces endroits étaient tellement critiques qu’un avion qui était touché à l’un de ces endroits ne rentrait pas à la base, et, par conséquent, personne ne pouvait observer ses dommages (une sorte de biais de survie où les survivants eux-mêmes déformaient les données). Ce biais de survie est encore un problème de nos jours. En effet, il peut affecter de manière significative tant la recherche clinique (si l’on n’étudie que les survivants de pathologies ou d’accidents) que les études sur les investissements financiers (lorsqu’on tente de retracer les raisons d’un succès financier).

Un raisonnement préliminaire sur vos données et les résultats des tests avec des éléments hors échantillon peut vous aider à repérer, ou au moins à avoir une intuition, des problèmes d’échantillonnage possibles. Toutefois, recevoir de nouvelles données hors échantillon est souvent difficile, coûteux et exige des investissements en termes de timing. Dans l’exemple de vente dont il a été question plus haut, vous devez attendre longtemps avant de tester votre modèle de prévision des ventes – peut-être une année entière – afin de déterminer si votre hypothèse fonctionne. De plus, rendre les données prêtes à être utilisées peut prendre beaucoup de temps. Par exemple, dans le cas de photos de chiens et de chats, vous devez passer du temps à étiqueter un plus grand nombre de photos récupérées sur le Web ou dans une base de données.

Un raccourci possible pour éviter de déployer trop d’efforts supplémentaires consiste à obtenir des éléments hors échantillon à partir de votre stock de données disponibles. Vous réservez une partie de votre échantillon sur la base d’une séparation entre données d’entraînement et données de test dictée par le temps ou par un échantillonnage aléatoire. Si le temps est un élément important dans votre problème (comme dans la prévision des ventes), vous allez rechercher une étiquette temporelle pour l’utiliser comme séparateur. Les données antérieures à une certaine date apparaissent en tant que données en échantillon, tandis que celles qui sont postérieures à cette date vont apparaître en tant que données hors échantillon. Il en va de même lorsque vous choisissez des données au hasard : ce que vous avez extrait en tant que données en échantillon ne sert qu’à des fins d’entraînement, tandis que ce qui reste est consacré aux tests, et sert de données hors échantillon.

Expérimenter le fonctionnement du biais et de la variance

Maintenant que vous en savez plus sur les parties de vos données qui se trouvent en échantillon et hors échantillon, vous savez aussi que l’apprentissage dépend beaucoup des premières (les données en échantillon). Cette partie est effectivement importante, parce que vous voulez découvrir un point de vue sur le monde, et, comme avec tous les points de vue, le résultat peut être faux, déformé, ou partiel. Vous savez également que vous avez besoin d’un jeu de données hors échantillon pour vérifier si le processus d’apprentissage fonctionne correctement. Cependant, ces aspects ne constituent qu’une partie du tableau. Lorsque vous faites fonctionner un algorithme d’apprentissage automatique sur des données afin de deviner une certaine réponse, vous prenez effectivement un pari, et ce pari ne provient pas seulement de l’échantillon que vous utilisez pour l’apprentissage. Ce n’est pas tout. Pour l’instant, imaginez que vous avez librement accès à des données appropriées, sans biais et en échantillon, de sorte que le problème ne réside pas dans les données. Au lieu de cela, vous devez vous concentrer sur la méthode d’apprentissage et de prédiction.

Tout d’abord, considérez que vous pariez que l’algorithme peut raisonnablement deviner la réponse. Vous ne pouvez pas toujours faire cette hypothèse, car il est impossible de deviner certaines réponses, quoi que vous sachiez à l’avance. Par exemple, vous ne pouvez pas déterminer entièrement le comportement des êtres humains en connaissant leurs antécédents et leur comportement, en raison des éléments suivants :

• un effet aléatoire dans le processus de génération du comportement (la partie irrationnelle en nous, par exemple) ;

• le libre arbitre (le problème est également d’ordre philosophique/ religieux et les opinions divergentes sont nombreuses).

Par conséquent, vous ne pouvez deviner que certains types de réponses, et pour beaucoup d’autres, comme lorsque vous essayez de prédire le comportement des gens, vous devez accepter un certain degré d’incertitude (qui, avec de la chance, est acceptable par rapport à vos objectifs).

Deuxièmement, vous devez considérer que vous pariez que la relation entre l’information dont vous disposez et la réponse que vous voulez prédire peut être exprimée sous la forme d’une formule mathématique quelconque, et que votre algorithme d’apprentissage automatique est réellement capable de deviner cette formule. La capacité de votre algorithme à deviner la formule mathématique qui sous-tend une réponse est intrinsèquement intégrée dans les rouages de cet algorithme. Certains algorithmes peuvent deviner presque tout ; d’autres ont un ensemble limité d’options. L’éventail des formules mathématiques qu’un algorithme peut deviner est l’ensemble de ses hypothèses possibles. Par conséquent, une hypothèse est un algorithme unique, spécifié dans tous ses paramètres, et donc capable d’une formulation unique et spécifique.

Les mathématiques sont fantastiques. Elles peuvent décrire une grande partie du monde réel en utilisant une notation simple, et c’est le cœur de l’apprentissage automatique, car tout algorithme d’apprentissage a une certaine capacité à représenter une formulation mathématique. Certains algorithmes, comme la régression linéaire, utilisent explicitement une formulation mathématique spécifique pour représenter la façon dont une réponse (par exemple, le prix d’une maison) se relie à un ensemble d’informations prédictives (comme les informations sur le marché, l’emplacement de la maison, la superficie de la propriété, etc.).

La régression linéaire offre une formulation compréhensible, mais elle ne conviendra pas à tous vos problèmes, car tous les problèmes ne peuvent pas être représentés de la même manière que la régression linéaire. D’autres algorithmes sophistiqués, tels que les arbres de décision (qui feront l’objet du chapitre suivant), sont assez compliqués à représenter à l’aide d’une formulation mathématique explicite, mais ils sont tellement adaptables qu’ils peuvent facilement se rapprocher d’un large éventail de formulations et donc résoudre un plus grand nombre de problèmes.

Certaines formulations sont tellement complexes que même s’il est possible de les représenter sur papier, cela s’avère trop difficile en termes pratiques. Néanmoins, les formulations complexes peuvent présenter certains avantages importants par rapport aux formulations simples, en fonction du problème qu’elles doivent résoudre. Prenons l’exemple d’une formulation simple et facile à expliquer, basée sur la régression linéaire. La régression linéaire est simplement la formulation d’un séparateur (qui peut être une simple ligne ou un plan multidimensionnel) dans un espace de coordonnées donné par la réponse et tous les prédicteurs. Dans l’exemple le plus simple, lorsqu’une ligne représente le modèle, vous avez une réponse, y, et un seul prédicteur, x, avec une formulation de :

y = \beta_1x_1x_1 + \beta_0

Dans une situation simple de réponse prédite avec une seule caractéristique, un tel modèle est parfait lorsque vos données se présentent sous la forme d’une ligne. Cependant, que se passe-t-il si ce n’est pas le cas et, qu’au lieu de cela, on obtient une forme qui est une courbe ? Pour représenter la situation, il suffit d’observer les représentations bidimensionnelles suivantes, comme le montre la Figure 9.1.

Lorsque la relation entre une variable cible et un prédicteur ressemble à une ligne ou à un nuage de points aléatoires dans un nuage de points, les erreurs produites par les mappages d’un modèle de régression linéaire n’apparaissent pas systématiquement, mais plutôt de manière aléatoire, car certains points se trouveront au-dessus de la ligne de régression cartographiée et d’autres en dessous. Dans le cas d’un nuage de points en courbe, les choses se passent différemment, car la ligne de régression peut parfois être exacte, mais elle peut aussi être systématiquement erronée. En fait, certains points seront toujours au-dessus de la ligne (situation de sous-estimation), d’autres en dessous (surestimation).

Étant donné la simplicité de son mappage de la réponse, un algorithme pourrait avoir tendance à surestimer ou à sous-estimer systématiquement les règles réelles qui sous-tendent des données complexes. Dans ce cas, l’algorithme est affecté par l’erreur de biais. L’erreur de biais est caractéristique des algorithmes plus simples qui ne peuvent pas exprimer des formulations mathématiques complexes et qui, dans certaines situations, sont donc incapables de cartographier correctement la relation entre les caractéristiques et la variable cible.

[picture]

Figure 9.1 Exemple d’un modèle linéaire qui peine à représenter une fonction en courbe.

Garder à l’esprit la complexité du modèle

Tout comme la simplicité des formulations est un problème, le recours automatique à un mappage de formulations très complexes n’apporte pas toujours une solution. En fait, vous ne connaissez pas la véritable complexité qui se dissimule derrière la réponse attendue (par exemple, si elle s’inscrit dans une ligne droite ou dans une courbe). Par conséquent, tout comme la simplicité peut créer une réponse inadéquate (comme sur la Figure 9.1), il est également possible d’en arriver à représenter la complexité des données par une cartographie trop complexe.

Le problème d’une cartographie complexe, c’est qu’elle comporte de nombreux termes et paramètres – et dans certains cas extrêmes, votre algorithme peut avoir plus de paramètres que vos données n’ont d’exemples. Comme vous devez spécifier tous les paramètres, l’algorithme commence alors à mémoriser tout ce qui se trouve dans les données, non seulement les signaux (les données qui comptent), mais aussi les éléments suivants :

• le bruit aléatoire ;

• les erreurs ;

• les caractéristiques moins spécifiques de l’échantillon.

Dans certains cas, l’algorithme peut même mémoriser les exemples. Toutefois, à moins que vous ne travailliez sur un problème comportant un nombre limité de caractéristiques simples avec peu de valeurs distinctes, il est très peu probable que vous rencontriez deux fois le même exemple, étant donné le nombre énorme de combinaisons possibles de toutes les caractéristiques disponibles dans l’ensemble de données.

Quand une telle mémorisation se produit, vous pouvez avoir l’illusion que tout fonctionne bien parce que votre algorithme d’apprentissage automatique semble avoir bien ajusté les données de l’échantillon. Au lieu de cela, les problèmes peuvent rapidement devenir évidents lorsque vous commencez à le faire travailler avec des données hors échantillon et que vous remarquez qu’il produit des erreurs dans ses prédictions, mais aussi des erreurs qui varient beaucoup lorsque vous réapprenez à partir des mêmes données, mais avec une approche légèrement différente. Il y a surapprentissage lorsque votre algorithme a trop appris à partir de vos données, jusqu’au point de cartographier des formes de courbes et des règles qui n’existent pas, comme le montre la Figure 9.2. Tout léger changement dans la procédure ou dans les données d’entraînement produit des prévisions erratiques.

Conserver des solutions équilibrées

Pour créer de bonnes solutions, les modèles d’apprentissage automatique font un compromis entre la simplicité (qui implique un biais plus élevé) et la complexité (qui génère une plus grande variance dans les estimations). Si vous avez l’intention d’atteindre les meilleures performances prédictives, vous devez trouver une solution intermédiaire en comprenant ce qui fonctionne le mieux, ce que vous faites en procédant par essais et erreurs sur vos données. Du fait que les données sont ce qui dicte la solution la plus appropriée pour le problème de prédiction, vous n’avez ni une panacée ni une solution récurrente facile pour résoudre tous vos dilemmes d’apprentissage automatique.

Un théorème couramment cité dans le folklore mathématique est le théorème du « no-free-lunch » (mot à mot, « pas de repas gratuit » ) de David Wolpert et William Macready, qui stipule que « deux algorithmes d’optimisation sont équivalents lorsque l’on fait la moyenne de leurs performances pour tous les problèmes possibles » . Si les algorithmes sont équivalents dans l’abstrait, aucun n’est supérieur à l’autre à moins d’être prouvé dans un problème pratique spécifique (voir la discussion à http://www.no-free-lunch.org/ pour plus de détails sur les théorèmes « no-free-lunch » ; deux d’entre eux sont utilisés pour l’apprentissage automatique).

[picture]

Figure 9.2 Un modèle des k plus proches voisins s’ajuste correctement au problème sur l’image de gauche et devient trop complexe en essayant de représenter une fonction de courbe sur l’image de droite.

En particulier, dans son article « The Lack of A Priori Distinctions between Learning Algorithms » , Wolpert affirme qu’il n’y a pas de distinction a priori (raisonnement ou connaissance qui procède d’une déduction théorique, plutôt que de l’observation ou de l’expérience) entre les algorithmes, qu’ils soient simples ou complexes (pour plus de détails, voir l’article à https://direct.mit.edu/neco/article-abstract/8/7/1341/6016/The-Lack-of-A-Priori-Distinctions-Between-Learning?redirectedFrom=fulltext). Ce sont les données qui dictent ce qui fonctionne et dans quelle mesure cela fonctionne. En fin de compte, vous ne pouvez pas toujours vous fier à un seul algorithme d’apprentissage automatique, mais vous devez en tester plusieurs et trouver celui qui convient le mieux à votre problème.

Outre le principe de l’expérimentation de l’apprentissage automatique, qui consiste à essayer tout ce que l’on peut faire, il existe une autre règle empirique à prendre en compte : le rasoir d’Occam ; ce principe est attribué à Guillaume d’Occam, philosophe et théologien du XIVe siècle². Le rasoir d’Occam stipule que les théories doivent être réduites au minimum afin de représenter la vérité de manière plausible (d’où le rasoir). Le principe ne dit pas que les solutions les plus simples sont les meilleures, mais que, entre une solution simple et une solution plus complexe offrant le même résultat, la solution la plus simple est toujours préférable. Ce principe est à la base même de notre méthodologie scientifique moderne, et même Albert Einstein semble s’y être souvent référé en déclarant que « tout devrait être aussi simple que possible, mais pas plus simple »³. Résumons ce que nous avons découvert jusqu’à présent :

• Pour obtenir la meilleure solution d’apprentissage automatique, essayez tout ce que vous pouvez sur vos données et représentez les performances obtenues par des courbes d’apprentissage.

• Commencez par des modèles plus simples, comme des modèles linéaires, et préférez toujours une solution plus simple lorsqu’elle fonctionne pratiquement aussi bien qu’une solution complexe. Vous bénéficiez de ce choix lorsque vous travaillez sur des données hors échantillon provenant du monde réel.

• Vérifiez toujours la performance de votre solution à l’aide de données hors échantillon, comme indiqué dans les sections précédentes.

Représenter des courbes d’apprentissage

Pour visualiser dans quelle mesure un algorithme d’apprentissage automatique souffre d’un biais ou d’une variance par rapport à un problème de données, vous pouvez tirer parti d’un type de graphique appelé courbe d’apprentissage. Les courbes d’apprentissage sont des représentations graphiques dans lesquelles on trace les performances d’un ou plusieurs algorithmes d’apprentissage automatique en fonction de la quantité de données qu’ils utilisent pour l’entraînement. Les valeurs tracées sont les mesures de l’erreur de prédiction, et la métrique est mesurée à la fois en tant que performance en échantillon et validation croisée ou hors échantillon.

Si le graphique illustre la performance par rapport à la quantité de données, il s’agit d’une courbe d’apprentissage. Lorsqu’il représente la performance par rapport à différents hyperparamètres ou à un ensemble de caractéristiques apprises choisies par le modèle, on parle plutôt de courbe de validation. Pour créer une courbe d’apprentissage, vous devez procéder comme suit :

• Divisez vos données en jeux en échantillon et hors échantillon (divisez vos données en utilisant un ratio 70/30 ou utilisez une technique d’échantillonnage appelée validation croisée, expliquée plus loin dans cette section).

• Créez des portions de vos données d’entraînement avec une taille croissante. En fonction de la taille des données disponibles pour l’entraînement, augmentez les portions de 10 % ou, si vous disposez de beaucoup de données, augmentez le nombre d’échantillons en vous basant sur une échelle de puissances comme 10³, 10⁴, 10⁵, etc.

• Entraînez des modèles sur les différents sous-ensembles de données. Testez et enregistrez leurs performances sur les mêmes données d’entraînement et sur le jeu hors échantillon.

• Tracez les résultats enregistrés sur deux courbes, l’une pour les résultats en échantillon et l’autre pour les résultats hors échantillon (voir la Figure 9.3). Si vous avez utilisé la technique de validation croisée au lieu d’un fractionnement entraînement/test, vous pouvez également tracer des limites exprimant la stabilité du résultat sur plusieurs validations (intervalles de confiance) sur la base de l’écart-type des résultats eux-mêmes.

[picture]

Figure 9.3 Courbes d’apprentissage affectées par un biais élevé (à gauche) et une variance élevée (à droite).

Idéalement (mais ce n’est pas toujours le cas, comme l’explique le prochain conseil), vous devriez obtenir deux courbes avec des points d’erreur de départ différents : plus élevés pour la partie hors échantillon et plus bas pour la partie en échantillon. Au fur et à mesure que la taille jeu d’entraînement augmente, la différence d’espace entre les deux courbes devrait se réduire jusqu’à ce que, à partir d’un certain nombre d’observations, elles se rapprochent d’une valeur d’erreur commune. De façon notable, après l’impression de votre graphique, des problèmes se posent notamment lorsque :

• Les deux courbes ont tendance à converger, mais vous ne pouvez pas voir qu’elles se rapprochent l’une de l’autre, parce que vous n’avez pas assez d’exemples. Cette situation montre que vous devez augmenter la taille du jeu de données si vous voulez réussir à apprendre avec l’algorithme d’apprentissage automatique testé.

• Le point de convergence final entre les deux courbes présente une erreur élevée, ce qui signifie que votre algorithme a trop de biais. L’ajout d’échantillons supplémentaires n’aidera pas dans ce cas puisque vous avez une convergence avec la quantité de données dont vous disposez. Vous devriez augmenter le nombre de caractéristiques ou utiliser un algorithme d’apprentissage plus complexe comme solution.

• Les deux courbes ne tendent pas vers une convergence, parce que la courbe hors échantillon commence à se comporter de façon erratique. Une telle situation est clairement le signe d’une variance élevée des estimations, que vous pouvez réduire en augmentant le nombre d’échantillons (à un certain stade, l’erreur hors échantillon recommencera à diminuer), en réduisant le nombre de caractéristiques, ou, parfois, en fixant simplement certains paramètres clés de l’algorithme d’apprentissage.

Les courbes d’apprentissage sont fournies dans le cadre du package scikit-learn à l’aide de la fonction learning_curve qui prépare tous les calculs à votre place⁴. Les exemples du livre intègrent de façon pratique la fonction pour réaliser le tracé des résultats, car la fonction ne produit qu’une séquence de vecteurs de résultats.

Bien que l’on s’attende à ce qu’un algorithme s’adapte mieux aux données en échantillon qu’aux données hors échantillon, il est toujours possible de trouver une perte hors échantillon qui soit meilleure que la perte en échantillon. Plusieurs raisons peuvent expliquer cette situation, notamment le fait que les données hors échantillon sont, pour une raison ou une autre, plus simples que les données en échantillon en raison de la manière dont on sépare les données hors échantillon des données en échantillon et de leur traitement.

Entraînement, validation et test

Les sections suivantes examinent les méthodes employées pour s’assurer qu’un modèle fonctionne comme prévu. Dans ces sections, vous rencontrerez trois divisions des données à utiliser pour ces tâches :

• Entraînement : utilisation d’une partie du jeu de données pour définir l’algorithme du modèle.

• Validation : évaluation non biaisée de l’adéquation du modèle pour s’assurer qu’il fonctionne avec les données hors échantillon.

• Test : traitement des problèmes potentiels tels que le snooping en effectuant un ajustement final du modèle sur le jeu de données.

Division des données

Dans un monde parfait, vous devriez effectuer un test sur des données que votre algorithme d’apprentissage automatique n’a jamais apprises auparavant. Cependant, il n’est pas toujours possible d’attendre de nouvelles données, que ce soit en termes de temps et/ou de coût. Comme premier remède simple, vous pouvez diviser aléatoirement vos données en jeu d’entraînement (en échantillon) et en jeu de test (hors échantillon). La répartition courante est de 25 à 30 % pour les tests, et de 75 à 70 % pour l’entraînement. Vous divisez vos données constituées de votre réponse et de vos caractéristiques en même temps, en conservant la correspondance entre chaque réponse et ses caractéristiques.

La deuxième solution se présente lorsque vous devez ajuster votre algorithme d’apprentissage. Dans ce cas, se référer aux données de la division de test pour vérifier si votre réglage fonctionne n’est pas une bonne pratique, car cela entraîne un autre type de surapprentissage appelé snooping (voir plus loin dans le chapitre). Pour surmonter ce problème, vous avez besoin d’une troisième division, appelée jeu de validation, qui est une partie des données utilisées pour évaluer le modèle et affiner les hyperparamètres du modèle. Il est conseillé de diviser les exemples en trois parties : 70 % pour l’entraînement, 20 % pour la validation et 10 % pour le test.

Vous devez effectuer la division de façon aléatoire, c’est-à-dire indépendamment de l’ordre initial des données. Sinon, votre test ne sera pas fiable, car cet ordre pourrait entraîner une surestimation (lorsqu’il est en soi significatif) ou une sous-estimation (lorsque la distribution diffère trop). La solution consiste à s’assurer que la distribution du jeu de test n’est pas très différente de la distribution du jeu d’entraînement, et que l’ordre séquentiel se retrouve dans les données fractionnées. Par exemple, vérifiez si les numéros d’identification, lorsqu’ils sont disponibles, sont continus dans vos jeux. Parfois, même si vous respectez strictement l’échantillonnage aléatoire, vous ne pouvez pas toujours obtenir des distributions similaires entre les jeux, surtout lorsque votre nombre d’échantillons est faible.

Lorsque le nombre d’échantillons n est élevé, par exemple n>10 000, vous pouvez créer en toute confiance un jeu de données divisé de manière aléatoire. Lorsque le jeu de données est plus petit, la comparaison de la similarité entre les statistiques de base telles que la moyenne, le mode, la médiane et la variance de la réponse et des caractéristiques dans les ensembles d’apprentissage et de test vous aidera à comprendre si l’ensemble de test est approprié. Si vous n’êtes pas sûr que la division soit correcte parce que les statistiques sont trop différentes, il vous suffit d’en recréer une nouvelle.

Recourir à la validation croisée

Un problème notable lié à la division des jeux d’entraînement et de test est que, si votre échantillon est petit, vous pouvez introduire un biais dans le test en réduisant la taille des données d’entraînement dans l’échantillon. En outre, lorsque vous divisez les données, vous risquez d’exclure certains exemples utiles de l’entraînement. Parfois, les données sont si complexes qu’un jeu de test, bien qu’apparemment similaire à un jeu d’entraînement, ne l’est pas vraiment, car les combinaisons de valeurs sont différentes (ce qui est typique des jeux de données ayant de nombreuses dimensions). Ces problèmes ajoutent à l’instabilité des résultats de l’échantillonnage lorsque vous n’avez pas beaucoup d’exemples. Le risque de diviser vos données d’une manière défavorable explique également pourquoi la division entraînement/ test n’est pas la solution privilégiée par les spécialistes lorsqu’il s’agit d’évaluer et de mettre au point une solution d’apprentissage automatique.

La validation croisée basée sur les k-plis (k-folds) est la solution. Elle repose sur une division aléatoire, mais cette fois-ci, elle divise vos données en un nombre k de plis (chacun étant une partition distincte de vos données) de taille égale. Ensuite, chaque pli est utilisé à tour de rôle comme jeu de test et les autres sont utilisés pour l’entraînement. Chaque itération utilise un pli différent comme test, ce qui produit une estimation de l’erreur. Après avoir testé un pli par rapport aux autres utilisés pour l’entraînement, un autre pli, différent du précédent, est employé et la procédure se répète pour produire une nouvelle estimation de l’erreur. Le processus se poursuit jusqu’à ce que tous les k-plis soient utilisés une fois comme jeu de test et que vous disposiez d’un nombre k d’estimations d’erreur que vous pouvez calculer en une estimation d’erreur moyenne (le score de validation croisée) et une erreur standard des estimations. La Figure 9.4 illustre le fonctionnement de ce processus.

[picture]

Figure 9.4 Représentation graphique du fonctionnement de la validation croisée.

Cette procédure présente les avantages suivants :

• Elle fonctionne bien, quel que soit le nombre d’échantillons. En augmentant le nombre de plis, vous augmentez la taille de votre jeu d’entraînement (plus k est grand, plus le jeu d’entraînement est grand, moins il y a de biais) et vous diminuez la taille du jeu de test. Le paramètre k doit rester raisonnable, car il s’agit d’un processus d’échantillonnage et si k est trop grand, vos échantillons seront trop semblables.

• Les différences de distribution pour les plis individuels n’ont pas autant d’importance. Lorsqu’un pli présente une distribution différente des autres, il n’est utilisé qu’une seule fois en tant que jeu de test et il est mélangé aux autres dans le cadre du jeu d’entraînement lors des tests restants.

• Vous testez en fait toutes les observations, ce qui permet de tester pleinement votre hypothèse d’apprentissage automatique à l’aide de toutes les données dont vous disposez.

• En prenant la moyenne des résultats, vous pouvez obtenir une estimation probabiliste de la performance prédictive. En outre, l’écart-type des résultats peut indiquer l’ampleur de la variation à laquelle il faut s’attendre dans les données réelles hors échantillon. Une variation plus importante dans les performances de la validation croisée vous informe de l’existence de données extrêmement variées que l’algorithme est incapable d’appréhender correctement.

L’utilisation de la validation croisée k-fold est toujours le choix optimal, sauf si les données que vous utilisez ont un ordre qui compte. Par exemple, il peut s’agir d’une série chronologique, telle que des ventes. Dans ce cas, vous ne devez pas employer une méthode d’échantillonnage aléatoire, mais plutôt vous appuyer sur une division entraînement/test basée sur l’ordre original des données, de sorte que la séquence temporelle soit préservée et que vous puissiez tester sur les derniers échantillons de cette série ordonnée.

À la recherche de solutions de rechange pour la validation

Vous disposez de plusieurs alternatives à la validation croisée, qui sont toutes dérivées des statistiques. La première à considérer – mais seulement si vous avez un jeu de données composé de peu échantillons – c’est la validation croisée dite leave-one-out (validation croisée d’un contre tous, abrégée en anglais en LOOCV). Elle est analogue à la validation croisée k-fold, la seule différence étant que k, le nombre de plis, est exactement n, le nombre d’échantillons. Par conséquent, dans la validation croisée LOOCV, vous construisez n modèles (qui peuvent se transformer en un nombre énorme lorsque vous avez de multiples observations), et vous testez chacun d’entre eux sur une seule observation hors échantillon. En plus d’exiger un travail de calcul intensif et la construction de nombreux modèles pour vérifier votre hypothèse, le problème de LOOCV est que chaque résultat obtenu est fortement corrélé avec tous les autres (parce qu’ils partagent la plupart des exemples d’apprentissage, à l’exception d’un seul). Par conséquent, vous ne pouvez pas vous fier aux estimations LOOCV comme vous le feriez avec celles de la validation croisée, car vous testez toujours essentiellement le même échantillon de données. Il est compréhensible que si vous travaillez avec un très petit jeu de données, LOOCV soit votre seul choix possible pour la validation.

Une autre solution de rechange provenant des statistiques est le bootstrapping, une méthode utilisée depuis longtemps pour estimer la distribution d’échantillonnage des statistiques, qui sont présumées ne pas suivre une distribution précédemment supposée. Le bootstrapping travaille en construisant un nombre (le plus grand possible) d’échantillons de taille n (la taille originale des données en échantillon).

Il s’agit d’un processus avec répétition, ce qui signifie qu’il peut reprendre plusieurs fois un même échantillon pour l’utiliser dans le cadre du rééchantillonnage. Le bootstrapping a l’avantage d’offrir un moyen simple et efficace pour estimer la mesure de l’erreur véritable. En fait, les mesures d’erreur lors d’un bootstrap ont généralement beaucoup moins de variance que celles obtenues par validation croisée. D’un autre côté, la validation devient plus compliquée en raison de l’échantillonnage avec remplacement, de sorte que votre jeu de validation provient des échantillons hors bootstrapping. De plus, l’utilisation répétée de certains échantillons d’entraînement peut conduire à un certain biais dans les modèles construits avec le bootstrapping.

Si vous utilisez des échantillons hors bootstrapping pour votre test, vous remarquerez que la taille du jeu de test peut sensiblement varier, selon le nombre d’éléments uniques dans les données en échantillon, ce qui représente probablement environ un tiers de la taille initiale de celles-ci. Ce simple extrait de code Python illustre la simulation aléatoire d’un certain nombre de bootstraps :

import numpy as np

n = 1000
results = list()

for j in range(10000):
   chosen = np.random.randint(0, n, size=n)
   results.append(len(np.unique(chosen)) / n)

avg_oob = 1 - np.mean(results)
print(f"Avg out-of-boostrap: {avg_oob*100:0.3}%")

Le code crée un grand nombre d’essais, chaque essai étant échantillonné avec répétition (ce qui signifie qu’un cas peut être réutilisé plusieurs fois dans l’échantillon) et calcule ensuite le nombre d’exemples uniques et la proportion de cas laissés chaque fois hors échantillon. Enfin, la moyenne de la proportion d’échantillons exclus est calculée sur l’ensemble des essais. L’exécution du programme peut prendre un certain temps et vos résultats peuvent être différents en raison de la nature aléatoire de l’échantillonnage. Voici un résultat représentatif : hors bootstrapping : 36,8 %.

Optimisation par validation croisée

La validation d’une hypothèse d’apprentissage automatique permet d’optimiser l’algorithme choisi. Les sections suivantes vous aideront à comprendre comment optimiser une solution de validation croisée.

Sources de performance prédictive

Comme nous l’avons vu dans les sections précédentes, l’algorithme fournit la majeure partie des performances prédictives de vos données, étant donné sa capacité à détecter des signaux à partir des données et à ajuster la véritable forme fonctionnelle de la fonction prédictive sans surapprentissage et sans générer une grande variance d’estimation. Tous les algorithmes d’apprentissage automatique ne sont pas adaptés à vos données, et aucun algorithme ne peut convenir à tous les problèmes. C’est à vous de trouver le bon algorithme pour un problème spécifique.

Une deuxième source de performance prédictive est constituée par les données elles-mêmes lorsqu’elles sont convenablement transformées et sélectionnées pour améliorer les capacités d’apprentissage de l’algorithme choisi. Le Chapitre 11 aborde la question de la transformation et de la sélection des données.

La dernière source de performance prédictive provient du réglage fin des hyperparamètres de l’algorithme, qui sont les paramètres que vous définissez (configurez) avant l’apprentissage et qui ne sont pas appris à partir des données. Leur rôle est de définir a priori une hypothèse, tandis que d’autres paramètres la spécifient a posteriori (raisonnement ou connaissance qui procède d’observations ou d’expériences), après que l’algorithme a interagi avec les données et, en utilisant un processus d’optimisation, a trouvé que certaines valeurs de paramètres fonctionnent mieux pour obtenir de bonnes prédictions. Tous les algorithmes d’apprentissage automatique ne nécessitent pas un réglage important des hyperparamètres, mais c’est le cas de certains des algorithmes les plus complexes. Bien que ces algorithmes fonctionnent toujours, le fait d’actionner les bons leviers peut faire une grande différence dans l’exactitude des prédictions. Même lorsque les hyperparamètres ne sont pas appris à partir de données, vous devez tenir compte des données sur lesquelles vous travaillez lorsque vous décidez des hyperparamètres, et vous devez faire votre choix sur la base d’une validation croisée et d’une évaluation minutieuse des possibilités.

Les algorithmes complexes d’apprentissage automatique, les plus exposés à la variance des estimations, présentent de nombreux choix exprimés via un grand nombre de paramètres. En jouant avec ces paramètres, ils s’adaptent plus ou moins aux données à partir desquelles ils apprennent. Aller trop loin peut même amener l’algorithme à détecter de faux signaux à partir des données. Cela fait des hyperparamètres eux-mêmes une source de variance non détectée si vous commencez à trop les manipuler sur la base d’une référence fixe comme un jeu de test ou un schéma de validation croisée répété.

Explorer l’espace des hyperparamètres

Les combinaisons possibles de valeurs que peuvent former les hyperparamètres rendent difficile la recherche d’optimisations. Comme décrit lors de la discussion sur la descente de gradient, un espace d’optimisation peut contenir des combinaisons de valeurs qui fonctionnent mieux ou moins bien. Même après avoir trouvé une bonne combinaison, vous n’êtes pas assuré que c’est la meilleure option (c’est le problème du risque de rester coincé dans les minima locaux en minimisant l’erreur, un problème décrit au Chapitre 8 à propos de la descente de gradient).

En pratique, la meilleure façon de vérifier les hyperparamètres d’un algorithme appliqué à des données spécifiques est de les tester tous par validation croisée et de choisir la meilleure combinaison. Cette approche, appelée recherche par quadrillage (grid-search dont l’implémentation dans scikit-learn se trouve à https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.GridSearchCV.html), offre des avantages indiscutables en permettant d’échantillonner systématiquement la gamme des valeurs possibles à introduire dans l’algorithme et de repérer le moment où le minimum général se produit. D’un autre côté, la recherche par quadrillage présente également de sérieux inconvénients, car elle est très gourmande en ressources informatiques (vous pouvez facilement effectuer cette tâche en parallèle sur les ordinateurs multicœurs modernes) et prend beaucoup de temps. En outre, les tests systématiques et intensifs augmentent la possibilité de commettre des erreurs, car certains résultats de validation apparaissant comme bons, sont en réalité faux ; cela peut être dû au bruit présent dans le jeu de données. Il existe des alternatives à la recherche par quadrillage. Au lieu de tout tester, vous pouvez essayer ce qui suit :

Explorer l’espace des valeurs possibles des hyperparamètres en étant guidé par des techniques d’optimisation non linéaire lourdes et mathématiquement complexes (comme la méthode Nelder-Mead).

Employer une approche bayésienne (où le nombre de tests est minimisé en tirant parti des résultats antérieurs).

Utiliser la recherche aléatoire.

L’optimisation bayésienne, bien que complexe sur le plan conceptuel, est mise en œuvre dans un package compatible avec scikit-learn : scikit-optimize (vous le trouverez à https://scikit-optimize.github.io/stable/). Il suffit d’installer le package et d’utiliser un wrapper de recherche hyperparamétrique scikit-learn, tel que BayesSearchCV, qui a la même syntaxe de commande que grid-search et random-search dans scikit-learn⁵.

Étonnamment, la recherche aléatoire⁶ fonctionne incroyablement bien, est simple à comprendre et n’est pas seulement basée sur une chance aveugle, même si elle peut sembler l’être au départ. En fait, l’idée principale derrière cette technique est que, si vous choisissez suffisamment de tests aléatoires, vous avez en fait assez de possibilités pour repérer les bons paramètres sans gaspiller d’énergie à tester des variantes légèrement différentes de combinaisons ayant des performances similaires.

La représentation graphique illustrée à la Figure 9.5 explique pourquoi la recherche aléatoire fonctionne bien. Une exploration systématique, bien qu’utile, tend à tester chaque combinaison, ce qui se transforme en un gaspillage d’énergie si certains paramètres n’influencent pas le résultat. Une recherche aléatoire teste moins de combinaisons, mais plus dans la plage de chaque hyperparamètre, une stratégie qui s’avère gagnante si, comme c’est souvent le cas, certains paramètres sont plus importants que d’autres.

Pour que la recherche aléatoire fonctionne bien, vous devriez faire de 15 à 60 tests au maximum. Il est logique de recourir à la recherche aléatoire si une recherche par quadrillage nécessite un plus grand nombre d’expérimentations.

Sélection des caractéristiques pertinentes

Les hyperparamètres ne sont pas le seul aspect d’un modèle que vous pouvez remettre en question par la validation croisée et la recherche par quadrillage. Toutes les transformations que vous appliquez aux données peuvent être évaluées de la même manière afin de déterminer si elles améliorent l’efficacité de l’algorithme d’apprentissage automatique par rapport au problème sur lequel vous travaillez.

[picture]

Figure 9.5 Comparaison entre recherche par quadrillage et recherche aléatoire.

En particulier, la sélection des caractéristiques est une transformation importante que vous pouvez appliquer aux données afin d’obtenir de meilleurs résultats. Avec la sélection de caractéristiques, vous ne fournissez qu’un sous-ensemble de caractéristiques du jeu de données dont vous attendez les meilleurs résultats. En fait, toutes les caractéristiques ne sont pas utiles et certaines peuvent même nuire au processus d’apprentissage. Les erreurs, le bruit et une grande similitude avec d’autres caractéristiques (forte corrélation entre un ensemble de caractéristiques) peuvent perturber l’algorithme et augmenter la variance de l’estimation.

La sélection des caractéristiques est donc une autre partie de votre travail de validation des données. Elle peut modifier de manière significative les résultats que vous obtenez grâce à l’apprentissage automatique. Dans ce cas, la validation est toujours nécessaire, car les effets des caractéristiques des données sont encore plus imprévisibles que le réglage des hyperparamètres. Vous pouvez vous passer de la validation uniquement lorsque vous êtes certain que certaines caractéristiques fonctionnent sur votre problème. Toutefois, cette approche est plus typique des statistiques, dans lesquelles de nombreux modèles sont axés sur la théorie, ce qui signifie qu’ils sont davantage fondés sur des études préliminaires, des raisonnements et des expériences que sur les données elles-mêmes.

L’article « Two cultures » , de Leo Breiman (https://projecteuclid.org/euclid.ss/1009213726), traite des différentes approches de l’apprentissage automatique et des statistiques face aux problèmes. Il fournit des points de vue supplémentaires sur la manière et les raisons de l’existence d’une approche d’apprentissage automatique et d’une approche statistique, ainsi que sur leurs différences. Si vous adoptez l’approche consistant à vérifier directement les données pour déterminer quelles sont les caractéristiques les plus utiles – ce qu’il faut conserver et ce qu’il faut écarter –, vous avez trois options distinctes :

Statistiques univariées : vous utilisez une statistique simple telle que la corrélation de Pearson ou le test du Khi-deux pour éliminer les caractéristiques les moins utiles. Dans scikit-learn, vous pouvez utiliser SelectKBest avec les fonctions chi2, f_classif ou f_regression (sklearn.feature_selection.SelectKBest) pour ne conserver que les caractéristiques les plus liées à la prédiction. Comme vous analysez chaque caractéristique seule, vous ne pouvez pas déterminer s’il existe des synergies entre elles qui pourraient aider la prédiction.

Sélection basée sur un modèle : certains algorithmes d’apprentissage automatique sélectionnent eux-mêmes les caractéristiques les plus utiles. Dans les chapitres suivants, vous découvrirez que les modèles arborescents peuvent fournir des classements pour les caractéristiques. Vous découvrirez également que les modèles linéaires ont leurs propres méthodes pour déterminer quelles caractéristiques doivent être moins pondérées, voire exclues. Le grand avantage de ces méthodes est qu’elles sont holistiques, c’est-à-dire qu’elles évaluent la contribution de chaque caractéristique dans l’ensemble des caractéristiques, de sorte qu’elles proposent toujours l’un des meilleurs ensembles possibles de caractéristiques. D’un autre côté, la sélection est fortement liée à la forme fonctionnelle de l’algorithme d’apprentissage automatique qui a effectué la sélection ; par conséquent, la sélection pourrait être moins efficace si vous essayez de l’appliquer à un autre type d’algorithme.

Sélection itérative des caractéristiques : cette approche se situe à mi-chemin entre le test des caractéristiques individuellement et celui de l’ensemble des caractéristiques. Vous testez un algorithme en ajoutant progressivement les caractéristiques les plus utiles, en supprimant les moins utiles, ou les deux. Cette approche peut réduire considérablement le nombre de caractéristiques utilisées (appliquée à n’importe quel type de modèle), mais elle est également coûteuse d’un point de vue informatique. Scikit-learn met en œuvre la stratégie itérative Recursive Feature Elimination (RFE) (mise en œuvre avec validation)⁷, qui commence par utiliser toutes les caractéristiques et supprime une partie des moins utiles après chaque itération.

Éviter les biais d’échantillonnage et les pièges de fuite des données

Un dernier aspect à inclure dans notre approche des questions de validation est l’examen d’un remède possible au biais des données en échantillon. Ce type de biais peut survenir dans vos données avant que l’apprentissage automatique ne soit mis en œuvre, et il entraîne une variance élevée des estimations qui suivent. De plus, cette section fournit un avertissement sur le problème des fuites de données qui peuvent se produire lorsque certaines informations hors échantillon passent dans les données en échantillon. Ce problème peut survenir lorsque vous préparez les données ou lorsque votre modèle d’apprentissage automatique est prêt à fonctionner.

Ce remède, appelé assemblage de prédicteurs, fonctionne parfaitement lorsque votre échantillon d’entraînement n’est pas complètement déformé et que sa distribution est différente de celle hors échantillon, mais pas de manière irrémédiable, par exemple lorsque toutes vos classes sont présentes, mais pas dans la bonne proportion. Dans ce cas, la variance des estimations affecte vos résultats et vous pouvez éventuellement la stabiliser de plusieurs façons : par rééchantillonnage, comme dans le bootstrapping ; par sous-échantillonnage (en prenant un échantillon de l’échantillon) ; ou en utilisant des échantillons plus petits (ce qui augmente le biais).

Pour comprendre comment cette technique fonctionne si efficacement, visualisez l’image d’une cible et de son point de mire. Si votre échantillon affecte les prédictions, certaines d’entre elles seront exactes et d’autres seront fausses de manière aléatoire. Si vous changez votre échantillon, les bonnes prédictions continueront d’être bonnes, mais les mauvaises deviendront des variations entre différentes valeurs. Certaines valeurs seront la prédiction exacte que vous recherchez ; d’autres oscilleront simplement autour de la bonne.

En comparant les résultats, vous pouvez deviner que la réponse récurrente est la bonne. Vous pouvez également faire la moyenne des réponses et deviner que la bonne réponse se trouve au milieu des valeurs. Avec la cible en tête, vous pouvez visualiser la superposition de photos de différents jeux : si le problème est la variance, vous finirez par deviner que la cible se trouve dans la zone la plus fréquemment touchée ou au moins au centre de tous les tirs.

Dans la plupart des cas, une telle approche s’avère correcte et améliore considérablement vos prédictions en matière d’apprentissage automatique. Lorsque votre problème est le biais, et non la variance, l’utilisation de l’assemblage n’est pas nuisible à moins que vous ne sous-échantillonniez trop peu d’échantillons. Une bonne règle de base pour le sous-échantillonnage est de prendre un échantillon de 70 à 90 % par rapport aux données originales de l’échantillon. Si vous voulez que l’assemblage fonctionne, vous devez procéder comme suit :

❶ Itérez un grand nombre de fois à travers vos données et modèles (d’un minimum de trois itérations à des centaines de fois).

❷ Chaque fois que vous itérez, sous-échantillonnez (ou sinon bootstrappez) vos données en échantillon.

❸ Utilisez l’apprentissage automatique pour le modèle sur les données rééchantillonnées et prédisez les résultats hors échantillon. Mémorisez ces résultats pour une utilisation ultérieure.

❹ À la fin des itérations, pour chaque cas hors échantillon que vous voulez prédire, prenez toutes ses prédictions et faites-en la moyenne si vous réalisez une régression. Prenez la classe la plus fréquente si vous faites une classification.

Les pièges des fuites de données peuvent vous surprendre car ils peuvent s’avérer être une source inconnue et non détectée de problèmes dans vos processus d’apprentissage automatique. Ce problème est appelé snooping (littéralement espionnage ou encore furetage), qui est le fait de trop observer les données hors échantillon et de s’y adapter trop souvent. Cela peut facilement se produire si vous vérifiez souvent votre travail sur le jeu de test et que vous continuiez à sélectionner ou à régler votre algorithme de manière à ce qu’il soit plus performant sur le jeu de test. En bref, le snooping est une sorte de surapprentissage, et pas seulement sur les données d’apprentissage, mais aussi sur les données de test. En effet, la façon dont vous organisez vos expériences et vos tests rend le problème de surapprentissage lui-même plus difficile à détecter jusqu’à ce que vous soyez confronté à la réalité en utilisant de nouvelles données que vous n’avez jamais testées. En général, vous vous rendez compte que le problème est lié au snooping lorsque vous avez déjà appliqué l’algorithme d’apprentissage automatique à votre entreprise ou à un service destiné au public, ce qui fait tout le monde peut s’en rendre compte.

Vous pouvez éviter le phénomène de snooping de deux façons. Tout d’abord, lorsque vous travaillez sur les données, veillez à bien séparer les données d’entraînement, de validation et de test. De plus, lors du traitement, n’utilisez jamais des informations de validation ou de test, même si elles proviennent des échantillons les plus simples et les plus innocents. Il y a pire encore : appliquer une transformation complexe en utilisant toutes les données. En finance, par exemple, il est bien connu que le calcul de la moyenne et de l’écart-type (qui peuvent en fait vous en apprendre beaucoup sur les conditions du marché et les risques) à partir de toutes les données d’entraînement et de test peut faire fuiter de précieuses informations sur vos modèles. En cas de fuite, les algorithmes d’apprentissage automatique effectuent des prédictions sur le jeu de test plutôt que sur les données hors échantillon des marchés, ce qui signifie qu’ils n’ont pas fonctionné du tout, avec pour conséquence une perte d’argent.

Vérifiez la performance de vos données hors échantillon. En fait, vous pouvez rapporter certaines informations de votre snooping sur les résultats des tests pour vous aider à déterminer que certains paramètres sont meilleurs que d’autres, ou vous amener à choisir un algorithme d’apprentissage automatique plutôt qu’un autre. Pour chaque modèle ou paramètre, appliquez votre choix en fonction des résultats de la validation croisée ou de l’échantillon de validation.

1 Vous en apprendrez plus sur le sondage erroné du Literary Digest à https://web.archive.org/web/20200419192416/https://www.math.upenn.edu/∼deturck/m170/wk4/lecture/case1.html

2 https://math.ucr.edu/home/baez/physics/General/occam.html

3 Voir https://quoteinvestigator.com/2011/05/13/einstein-simple/ pour plus de détails.

4 Voir les détails à https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.learning_curve.html

5 https://scikit-optimize.github.io/stable/auto_examples/sklearn-grid-searchcv-replacement.html

6 https://scikit-optimize.github.io/stable/auto_examples/sklearn-grid-searchcv-replacement.html

7 https://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.RFECV.html

DANS CE CHAPITRE

Essayer un perceptron pour séparer les classes par une ligne

•

Partitionner récursivement des données d’entraînement avec des arbres de décision

•

Découvrir les règles du tennis et survivre au Titanic

•

Tirer parti de la probabilité bayésienne pour analyser des données textuelles

Chapitre 10

Débuter avec des apprenants simples

Àpartir de ce chapitre, les exemples vont commencer à illustrer les principes de base de l’apprentissage basé sur les données. L’idée est d’aborder certaines des stratégies d’apprentissage les plus simples – fournir quelques formules (seulement celles qui sont essentielles), des intuitions sur leur fonctionnement, et des exemples Python avec scikit-learn pour expérimenter avec certaines de leurs caractéristiques les plus typiques. Le chapitre commence par un examen de l’utilisation du perceptron pour séparer les classes.

À la base de toutes les principales techniques d’apprentissage automatique présentées dans le livre, il y a toujours un algorithme basé sur des combinaisons linéaires quelque peu interdépendantes, des variations dans la division de l’échantillon par des arbres de décision, ou une sorte de raisonnement probabiliste bayésien. Ce chapitre utilise des arbres de classification pour notre démonstration. La seule exception est l’algorithme des K plus proches voisins (ou KNN pour K-Nearest Neighbors), qui, basé sur un raisonnement analogique, est traité séparément dans un chapitre consacré à la détection de la similarité dans les données (Chapitre 12).

La maîtrise de ces techniques de base permet d’aborder plus tard des techniques d’apprentissage plus complexes et de mieux les comprendre (et les utiliser). Si vous maîtrisez certaines des techniques de base maintenant, vous pourrez plus tard créer des algorithmes encore plus efficaces en employant des ensembles d’algorithmes les plus simples – ceux qui sont considérés comme des apprenants faibles. Dans ce chapitre, vous verrez comment utiliser différentes techniques pour prédire quand il convient de jouer au tennis en fonction des conditions météorologiques.

À la fin du voyage, aucun algorithme n’apparaîtra plus comme une boîte noire. L’apprentissage automatique a une forte composante intuitive et humaine, parce que c’est une création humaine (du moins pour le moment, à moins que quelque chose de singulier n’apparaisse), et qu’il est basé sur des analogies avec la manière dont nous apprenons du monde ou sur l’imitation de la nature (par exemple, comment nous savons que le cerveau fonctionne). Si les idées fondamentales de la discipline sont correctement transmises, aucun algorithme n’est vraiment trop difficile à comprendre. Le chapitre démontre cette approche en utilisant la probabilité bayésienne pour analyser des échantillons de texte.

Il n’est pas nécessaire de saisir manuellement le code source de ce chapitre. Il est beaucoup plus facile d’utiliser le code source téléchargeable de ce chapitre dans le dossier ML4D2E de la source téléchargeable pour le code Python et dans le dossier ML4D2ER de la source téléchargeable pour le code R. Les fichiers d’exemple de ce chapitre commencent par ML4D2E ; 10 ; (pour Python) ou ML4D2E_R ; 10 ; (pour R). Reportez-vous à l’introduction pour savoir où trouver ces fichiers source.

Découvrir l’incroyable Perceptron

Vous pouvez commencer le voyage vers la découverte du fonctionnement des algorithmes d’apprentissage automatique en observant des modèles qui trouvent leurs réponses grâce à des lignes et des surfaces pour diviser les échantillons en classes, ou pour estimer des prédictions de valeurs. Ce sont des modèles linéaires, et ce chapitre présente l’un des premiers algorithmes linéaires employés en apprentissage automatique : le perceptron. Les chapitres suivants complètent l’aperçu sur les modèles linéaires. Le Chapitre 13 présente la régression linéaire et sa famille d’algorithmes dérivés statistiquement ; le Chapitre 14 vous aide à découvrir les réseaux de neurones, et le Chapitre 15 traite des machines à vecteurs de support. Toutefois, avant d’aborder ces sujets, vous devez comprendre l’histoire intéressante du perceptron.

À deux pas du miracle ?

Frank Rosenblatt du Cornell Aeronautical Laboratory a conçu le perceptron en 1957 sous le parrainage de l’United States Naval Research. Rosenblatt était un psychologue et un pionnier dans le domaine de l’intelligence artificielle. Maîtrisant les sciences cognitives, il a eu l’idée de créer un ordinateur capable d’apprendre par essais et erreurs, tout comme le fait un humain.

L’idée a été développée avec succès, et au début, le perceptron n’était pas conçu comme un simple logiciel, car il avait été créé comme un logiciel fonctionnant sur du matériel dédié. L’emploi d’une telle combinaison a permis une reconnaissance plus rapide et plus précise des images complexes que tout autre ordinateur de l’époque. La nouvelle technologie souleva de grandes attentes et provoqua une énorme controverse lorsque Rosenblatt affirma que le perceptron était l’embryon d’un nouveau type d’ordinateur qui pourrait marcher, parler, voir, écrire, et même se reproduire et être conscient de son existence. S’il disait vrai, cela aurait été un outil puissant, qui aurait permis au monde de découvrir une IA forte, c’est-à-dire une intelligence artificielle dont les capacités et les fonctions sont similaires à celles d’un cerveau humain.

Inutile de dire que le perceptron n’a pas réalisé les attentes de son créateur. Il a rapidement montré une capacité limitée, même dans sa spécialisation en reconnaissance d’images. La déception générale a déclenché le premier hiver de l’IA et l’abandon temporaire du connexionnisme jusqu’aux années 1980.

Le connexionnisme est l’approche de l’apprentissage automatique basée sur les neurosciences ainsi que l’exemple des réseaux biologiquement interconnectés. Vous pouvez faire remonter les racines du connexionnisme jusqu’au perceptron (revoyez la section « Rôle des statistiques dans l’apprentissage automatique » du Chapitre 2 pour une discussion sur les cinq tribus de l’apprentissage automatique).

Le perceptron est un algorithme itératif qui s’efforce de déterminer, par approximations successives et répétitives, le meilleur ensemble de valeurs pour un vecteur, w, qui est aussi appelé vecteur des coefficients. Le vecteur w peut aider à prédire la classe d’un échantillon lorsque vous le multipliez par la matrice des caractéristiques, X (contenant l’information exprimée en valeurs numériques) et l’ajoutez ensuite à un terme constant, appelé le biais. La sortie est une prédiction dans le sens où les opérations décrites précédemment produisent un nombre dont le signe devrait permettre de prédire la classe de chaque échantillon dans un problème de classification binaire.

Jusqu’à présent, ce livre a abordé trois significations différentes du concept de biais : le biais d’échantillonnage, le biais d’erreur dans les prédictions d’un algorithme d’apprentissage automatique et, maintenant, le biais en tant que terme constant dans certaines formules d’apprentissage automatique, comme dans le perceptron, basé sur la sommation de valeurs pondérées.

La spécialité naturelle du perceptron est la classification binaire. Cependant, vous pouvez l’utiliser pour prédire plusieurs classes à l’aide de modèles multiples (un pour chaque supposition de classe, une stratégie d’entraînement appelée one-versus-all ou OVA, autrement dit un contre tous). En dehors de la classification, le perceptron ne peut pas fournir beaucoup plus d’informations. Par exemple, vous ne pouvez pas l’utiliser pour estimer la probabilité de faire des prédictions exactes. En termes mathématiques, le perceptron essaie de minimiser la formulation de la fonction d’erreur suivante, mais il ne le fait que pour les échantillons qui sont mal classés (en d’autres termes, dont le signe ne correspond pas à la bonne classe) :

Erreur = \sum_{i\in M} y_i (x^T_iw+b)

La formule, qui est un exemple de fonction de coût telle que définie au Chapitre 8, n’inclut que les éléments de la matrice X qui, dans le jeu actuel de w, ont reçu un signe mal classifié. Pour comprendre la fonction de la formule, il faut considérer qu’il n’y a que deux classes. Les éléments de la première classe sont exprimés par une valeur de +1 en réponse au vecteur y, alors que les éléments de l’autre classe sont codés par conséquent sous la forme –1. La formule reprend chacun des échantillons mal classés et, pour chacun, multiplie leurs caractéristiques par le vecteur w, puis ajoute le biais b.

Du fait de la multiplication des échantillons mal classifiés, la matrice X, multipliée par le vecteur de poids w, qui est entre parenthèses, génère un produit de vecteurs ; vous devriez transposer le vecteur des caractéristiques x d’un échantillon de sorte que le résultat de la multiplication avec w soit un nombre. C’est un aspect de la multiplication matricielle abordé au Chapitre 7.

Multiplier deux vecteurs revient à créer une somme pondérée des valeurs du premier vecteur en utilisant les valeurs du second vecteur comme poids. Par conséquent, si xi a cinq caractéristiques et que le vecteur w ait cinq coefficients, le résultat de leur multiplication est la somme des cinq caractéristiques, chacune étant multipliée d’abord par son coefficient respectif. La multiplication matricielle rend la procédure compacte à exprimer par une formule, mais finalement, l’opération ne diffère pas d’une moyenne pondérée.

Après avoir obtenu le résultat de la multiplication des vecteurs, vous additionnez les valeurs au biais et vous multipliez le tout par la valeur que vous auriez dû prévoir (soit +1 pour la première classe et –1 pour la seconde). Comme vous ne travaillez qu’avec des échantillons mal classifiés, le résultat de l’opération est toujours négatif, car la multiplication de deux valeurs de signes différents est toujours négative.

Enfin, après avoir exécuté le même calcul pour tous les échantillons mal classifiés, vous regroupez tous les résultats et vous les additionnez. Vous obtenez ainsi un nombre négatif, mais qui devient positif à cause du signe négatif en tête de la formule d’erreur présentée plus haut (ce qui revient à tout multiplier par –1). La taille du résultat augmente à mesure que le nombre d’erreurs de perception devient plus important.

En observant attentivement les résultats, vous vous rendez compte que la formule est conçue de manière intelligente (bien qu’elle soit loin d’être miraculeuse, comme le prévoyait Rosenblatt). La sortie est plus petite lorsque le nombre d’erreurs est plus bas. Lorsqu’il n’y a pas d’erreur de classification, le résultat de la somme est égal à zéro. En mettant la formule sous cette forme, vous dites à l’ordinateur d’essayer d’obtenir une classification parfaite et de ne jamais abandonner. L’idée est que, lorsqu’il trouve les bonnes valeurs du vecteur w, et qu’il n’y a pas d’erreurs de prédiction, il ne reste plus qu’à appliquer la formule suivante :

\widehat y = sign(Xw+q)

L’exécution de la formule produit un vecteur de prédictions (le y surmonté d’un accent circonflexe) contenant une séquence de valeurs +1 et –1 qui correspondent aux classes attendues.

Atteindre les limites de la non-séparabilité

Le secret des calculs du perceptron réside dans la manière dont l’algorithme met à jour les valeurs du vecteur w. De telles mises à jour se produisent en choisissant au hasard l’un des échantillons mal classifiés (appelons-le xt) et en modifiant le vecteur w à l’aide d’une simple addition pondérée :

w = w + \eta (x_t*y_t)

La lettre grecque eta (η) représente le taux d’apprentissage. C’est un nombre en virgule flottante compris entre 0 et 1. Lorsque vous définissez cette valeur comme étant proche de zéro, cela peut limiter la capacité de la formule à trop mettre à jour le vecteur w, alors que si vous la définissez comme étant proche de un, le processus de mise à jour impactera complètement les valeurs du vecteur w. Définir différents taux d’apprentissage peut accélérer ou ralentir le processus d’apprentissage. De nombreux autres algorithmes utilisent cette stratégie, et un η plus bas est utilisé pour améliorer le processus d’optimisation en réduisant la quantité de sauts brusques de valeurs de w après une mise à jour. Le compromis est qu’il faut alors attendre plus longtemps avant d’obtenir les résultats finaux.

La stratégie de mise à jour fournit l’intuition de ce qui se passe quand on utilise un perceptron pour apprendre les classes. Si vous imaginez les échantillons projetés sur un plan cartésien, le perceptron n’est rien de plus qu’une ligne essayant de séparer la classe positive de la classe négative. Si vous vous souvenez des notions d’algèbre linéaire apprises sur les bancs de l’école, tout ce qui peut s’exprimer sous la forme y = xb+a est en fait une ligne dans un plan.

Initialement, lorsque w est défini à zéro ou à des valeurs aléatoires, la ligne de séparation n’est que l’une des droites infinies possibles dans un plan, comme l’illustre la Figure 10.1. La phase de mise à jour la définit en la forçant à se rapprocher du point mal classifié. L’utilisation d’itérations multiples pour définir les erreurs place la ligne à la frontière exacte entre les deux classes.

[picture]

Figure 10.1 Ligne de séparation d’un perceptron entre deux classes.

Bien qu’il s’agisse d’un algorithme très intelligent, le perceptron a montré ses limites assez rapidement. Mis à part sa capacité à deviner deux classes en n’utilisant que des fonctionnalités quantitatives, il présentait un inconvénient important : si deux classes n’avaient pas de frontière du fait de leur mélange, l’algorithme ne pouvait pas trouver de solution et se mettait à jour de façon infinie.

Si vous ne pouvez pas diviser deux classes réparties sur deux dimensions ou plus par une ligne ou un plan, elles sont séparables de façon non linéaire. Surmonter ce type de situation est l’un des défis que l’apprentissage automatique doit relever pour devenir efficace dans le cas de problèmes complexes basés sur des données réelles, et pas seulement sur des données artificielles créées pour des recherches universitaires.

Lorsque la question de la séparabilité non linéaire a été étudiée et que les praticiens ont commencé à ne plus s’intéresser au perceptron, les experts ont rapidement émis l’hypothèse qu’ils pourraient résoudre le problème en créant un nouvel espace de caractéristiques (en recombinant les caractéristiques originales en de nouvelles caractéristiques) dans lequel des classes auparavant inséparables sont ajustées pour devenir séparables. Ainsi, le perceptron redeviendrait aussi bon qu’auparavant. Malheureusement, la création de nouveaux espaces de caractéristiques est un vrai défi, car elle exige une puissance de calcul qui n’est que partiellement disponible aujourd’hui. Ce sujet avancé est abordé plus loin dans le livre, lors de l’étude des stratégies d’apprentissage d’algorithmes plus complexes, comme les réseaux de neurones et les machines à vecteurs de support.

Ces dernières années, l’algorithme a connu un renouveau grâce au big data : un perceptron, en fait, n’a pas besoin de travailler avec toutes les données en mémoire, mais il peut le faire à l’aide de simples échantillons (il n’a besoin de mettre à jour son vecteur des coefficients que quand un cas de mauvaise classification rend cela nécessaire). C’est donc un algorithme parfait pour l’apprentissage en ligne, tel que l’apprentissage à partir du big data en utilisant de petits lots ou même en utilisant un seul échantillon à la fois.

Cultiver des arbres de classification gloutons

Les arbres de décision ont une longue histoire. Le premier algorithme de ce type remonte aux années 1970, mais si l’on considère les expériences et les premières recherches originales, l’utilisation des arbres de décision remonte encore plus loin. En tant qu’algorithmes de base de la tribu des symbolistes, les arbres de décision jouissent d’une longue popularité en raison de leur approche intuitive. Leurs résultats se traduisent facilement en règles et sont donc tout à fait compréhensibles par l’homme. Ils sont également extrêmement faciles à utiliser et polyvalents. Toutes ces caractéristiques en font un outil efficace et attrayant pour les modèles qui nécessitent des transformations mathématiques complexes de la matrice de données en entrée ou un réglage extrêmement précis de leurs hyperparamètres.

Prédire les résultats en divisant les données

En utilisant un échantillon d’observations comme point de départ, l’algorithme retrace les règles qui ont généré les classes en sortie (ou les valeurs numériques lorsque l’on travaille sur un problème de régression) en divisant la matrice d’entrée en partitions de plus en plus petites jusqu’à ce que le processus déclenche une règle pour arrêter. Un tel retour du particulier vers le général est typique de la déduction inverse humaine, telle qu’elle est traitée par la logique et la philosophie. Dans un contexte d’apprentissage automatique, un tel raisonnement inverse est obtenu en appliquant une recherche parmi toutes les façons possibles de diviser les données d’entraînement en échantillon et de décider, d’une manière « gloutonne » , d’utiliser le découpage qui maximise les mesures statistiques sur les partitions résultantes.

Un algorithme est dit glouton lorsqu’il choisit toujours de maximiser le résultat à chaque étape du processus d’optimisation, indépendamment de ce qui pourrait se produire dans les étapes suivantes. En d’autres termes, l’algorithme cherche à maximiser l’étape actuelle sans essayer d’atteindre une optimisation globale.

La division sert à appliquer un principe simple : chaque partition des données initiales doit faciliter la prédiction du résultat cible, qui est caractérisé par une distribution différente et plus favorable des classes (ou valeurs) que l’échantillon original. L’algorithme crée des partitions en divisant les données. Il détermine ces fractionnements en évaluant d’abord les caractéristiques, puis les valeurs des caractéristiques qui pourraient apporter l’amélioration maximale d’une mesure statistique spéciale qui joue le rôle de la fonction de coût dans un arbre de décision.

Un certain nombre de mesures statistiques déterminent comment effectuer les fractionnements dans un arbre de décision. Toutes adhèrent à l’idée qu’un fractionnement doit améliorer l’échantillon original, ou un autre fractionnement possible, lorsqu’il rend la prédiction plus sûre. Parmi les mesures les plus utilisées, mentionnons :

l’impureté de Gini ;

le gain d’information ;

la réduction de la variance (pour les problèmes de régression).

Ces mesures fonctionnent de la même manière, de sorte que ce chapitre se concentre sur le gain d’information parce qu’il s’agit de la mesure la plus intuitive, et qu’il montre comment un arbre de décision peut détecter plus facilement une capacité prédictive accrue (ou un risque réduit) pour un certain fractionnement. Ross Quinlan a créé un algorithme d’arbre de décision basé sur le gain d’information (ID3) dans les années 1970, et il est toujours très populaire grâce à sa récente amélioration sous le nom de C4.5. Le gain d’information repose sur la formule de l’entropie de l’information, une formulation généralisée qui décrit la valeur attendue des informations contenues dans un message :

Entropie = \sum -p_i\ log_2\ p_i

Dans cette formule, p est la probabilité pour une classe (exprimée dans la plage allant de 0 à 1) et log2 est le logarithme de base 2. En commençant par un échantillon dans lequel vous voulez classifier deux classes ayant la même probabilité (une distribution 50/50), l’entropie maximale possible est la suivante :

Entropie = -0.5*log2(0.5) -0.5*log2(0.5) = 1.0

Cependant, lorsque l’algorithme de l’arbre de décision détecte une caractéristique qui peut diviser le jeu de données en deux partitions, où la distribution des deux classes est de 40/60, l’entropie moyenne diminue :

Entropie = -0.4*log2(0.4) -0.6*log2(0.6) = 0.97

Notez la somme de l’entropie pour toutes les classes. En utilisant une division 40/60, la somme est inférieure au maximum théorique de 1 (ce qui diminue l’entropie). Pensez à l’entropie comme une mesure du désordre dans les données : moins il y a de désordre, plus il y a d’ordre et plus il est facile de deviner la bonne classe. Après un premier fractionnement, l’algorithme tente de diviser davantage les partitions obtenues en utilisant la même logique de réduction de l’entropie. Il fractionne progressivement toute partition de données successive jusqu’à ce qu’il n’y ait plus de fractionnement possible parce que le sous-échantillon est un élément unique ou parce qu’il a satisfait à une règle d’arrêt.

Les règles d’arrêt sont des limites à l’expansion d’un arbre. Ces règles fonctionnent en considérant trois aspects d’une partition : sa taille initiale, la taille de la partition résultante et le gain d’information réalisable par le biais de la division. Les règles d’arrêt sont importantes parce que les algorithmes des arbres de décision approximent un grand nombre de fonctions ; cependant, le bruit et les erreurs dans les données peuvent facilement influencer cet algorithme. Par conséquent, selon l’échantillon, l’instabilité et la variance des estimations qui en résultent influent sur les prévisions des arbres de décision.

À titre d’exemple, examinons ce qu’un arbre de décision peut accomplir en utilisant l’un des jeux de données originaux de Ross Quinlan ayant servi à présenter et décrire l’algorithme ID3 dans l’article de 1986 « Induction of Decision Trees »⁸. Le jeu de données est assez simple. Il est composé de seulement 14 observations relatives aux conditions météorologiques, avec des résultats qui indiquent s’il est approprié ou non de jouer au tennis en plein air. Nous trouvons ici quatre caractéristiques : aspect du ciel, température, humidité et vent, toutes exprimées à l’aide de classes qualitatives au lieu de mesures (vous pourriez spécifier la température, l’humidité et la force du vent avec des valeurs numériques) afin de donner une compréhension plus intuitive de la façon dont ces caractéristiques sont liées au résultat. L’exemple suivant utilise pandas pour charger un jeu de données feather contenant les données du jeu de tennis :

import pandas as pd

filename = 'https://github.com/lmassaron/datasets/'
filename += 'releases/download/1.0/tennis.feather'
tennis = pd.read_feather(filename)

Avant que l’algorithme puisse apprendre à partir de ces données, il doit séparer les variables prédictives de la cible et transformer les variables prédictives parce qu’elles sont catégorielles (elles sont représentées par des chaînes de caractères). Les arbres de décision peuvent travailler avec des données catégorielles de deux manières différentes :

En convertissant toutes les données catégorielles en caractéristiques binaires : chaque étiquette catégorielle est représentée par sa valeur lorsque l’étiquette est présente dans un cas.

En convertissant chaque caractéristique catégorielle en caractéristique numérique : remplacez les étiquettes catégorielles par des codages numériques à l’aide de fonctions telles que LabelEncoder() de scikit-learn⁹.

Dans cette dernière option, l’arbre de décision trouvera un moyen de partitionner les données codées par étiquette afin de mieux réaliser une prédiction. Bien que cette approche fonctionne bien, elle génère un arbre complexe. Par conséquent, pour plus de clarté, il est préférable d’utiliser la première option et de créer des caractéristiques binaires pour chaque combinaison de caractéristiques et ses valeurs :

X = tennis[['outlook', 'temperature',
'humidity', 'wind']]
X = pd.get_dummies(X)
y = tennis.play

L’algorithme peut maintenant adapter toutes les données disponibles et extraire ses règles.

from sklearn.tree import DecisionTreeClassifier
dt = DecisionTreeClassifier()
dt.fit(X, y)

Après avoir créé l’arbre, vous pouvez l’inspecter en utilisant dtreeviz, une bibliothèque spécialisée dans la représentation des arbres de décision. Terence Parr de l’université de San Francisco (http://parrt.cs.usfca.edu) et Prince Grover (ancien élève de la même faculté) ont développé la bibliothèque dtreeviz. Vous trouverez le package et les conseils d’installation à https://github.com/parrt/dtreeviz, et vous pouvez lire des informations sur le développement et le fonctionnement du package dans l’article du blog du professeur Parr intitulé « How to visualize decision trees » (soit « Comment visualiser les arbres de décision » ) à https://explained.ai/decision-tree-viz/.

Après avoir installé le package, vous pouvez l’exécutez et tracer la représentation de l’arbre illustrée à la Figure 10.2 :

from dtreeviz.trees import dtreeviz
viz = dtreeviz(dt, X, y,
              target_name='play_tennis',
              feature_names=X.columns,
              class_names=["No", "Yes"],
              scale=2.0)
viz

Pour lire les nœuds de l’arbre représenté à la Figure 10.2, débutez par le nœud le plus élevé, qui commence par indiquer la règle utilisée pour diviser ce nœud en tous les nœuds suivants. Étant donné que chaque division est binaire, vous pouvez voir deux barres, chacune représentant la distribution de la cible après la division des données en deux.

La première division de l’arbre de décision est basée sur le fait que le ciel est couvert ou non. À droite, vous trouverez la première feuille terminale : si le ciel est couvert (la probabilité est supérieure à 0,50), vous pouvez toujours jouer. Sur la partie gauche, le partitionnement se poursuit sur la base d’un taux d’humidité élevé, puis il continue avec d’autres divisions, aboutissant parfois à une feuille.

[picture]

Figure 10.2 Visualisation de l’arbre de décision construit à partir des données du jeu de tennis.

À la fin, vous obtenez sept feuilles terminales. Certaines d’entre elles comprennent un seul cas, mais vous remarquerez que chaque feuille terminale ne contient qu’un seul type de cible, ce qui implique que l’arbre de décision a créé un ensemble de règles déterministes pour classer les cas dans lesquels il est possible de jouer au tennis.

Élagage des arbres en surnombre

Même si les données du jeu de tennis de la section précédente illustrent les détails d’un arbre décisionnel, elles ont peu d’attrait probabiliste parce qu’elles proposent un ensemble d’actions déterministes (il n’y a pas d’instructions contradictoires). L’entraînement avec des données réelles ne comporte généralement pas de règles aussi précises, ce qui laisse de la place à l’ambiguïté et à une probabilité optimiste.

On peut prendre un autre exemple, plus réaliste, en se basant sur un jeu de données décrivant les taux de survie des passagers du RMS Titanic, le paquebot britannique qui a coulé dans l’Atlantique Nord en avril 1912, après avoir heurté un iceberg. Il existe plusieurs versions de ce jeu de données, et celle que nous utilisons pour cet exemple est composée de quelques caractéristiques clés telles que le sexe, l’âge, le nombre de frères et sœurs ou de conjoints à bord (sibsp), le nombre de parents ou d’enfants à bord (parch), la classe de passagers (pclass) et le taux de survie, exprimé sous forme binaire, où 1 signifie que l’individu a survécu. Comme nous l’avons fait précédemment avec le jeu de données Play Tennis, il faut charger les données sous la forme d’un fichier feather directement à partir d’Internet. Cet exemple divise également ces données en un jeu d’entraînement et un jeu de validation (qui représente 25 % des données initiales disponibles).

filename = 'https://github.com/lmassaron/datasets/'
filename += 'releases/download/1.0/titanic.feather'
 titanic = pd.read_feather(filename)
 from sklearn.model_selection import train_test_split

 X = titanic.iloc[:,:-1]
 y = titanic.iloc[:,-1]

 (X_train, X_test,
 y_train, y_test) = train_test_split(X, y,
                          random_state=0, shuffle=True)

Les arbres de décision ont plus de variance que de biais dans leurs estimations. Pour moins risquer des problèmes de surapprentissage, l’exemple spécifie que le fractionnement minimum doit impliquer au moins cinq échantillons :

dt = DecisionTreeClassifier(min_samples_split=5)
dt.fit(X_train, y_train)
accuracy = dt.score(X_test, y_test)

print(f"test accuracy: {accuracy:0.3}")

La précision du test de 0,774 est un bon résultat. Toutefois, vous pouvez l’améliorer, car le code permet à l’arbre de décision de développer ses branches sans contrainte. L’utilisation d’une approche plus contrôlée permet habituellement d’obtenir de meilleures solutions, ce qui implique généralement d’élaguer l’arbre. L’élagage a lieu lorsque l’arbre est complètement développé. En partant des feuilles terminales et en remontant vers l’arrière, le code élague les branches lorsque leur présence n’apporte qu’une faible amélioration au gain d’informations global. En laissant initialement l’arbre se développer, les branches qui apportent peu d’amélioration sont tolérées, car elles peuvent débloquer des branches et des feuilles plus intéressantes. En remontant des feuilles à la racine et en ne conservant que les branches qui ont une certaine valeur prédictive, on réduit la variance du modèle, ce qui rend les règles qui en découlent plus frugales et plus efficaces au moment de la prédiction.

Scikit-learn met en œuvre l’élagage coût-complexité, qui calcule différents élagages et leur coût en termes d’entropie ou d’impureté totale (pour plus d’informations sur la terminologie et les spécifications, voir https://scikit-learn.org/stable/modules/tree.html#minimal-cost-complexity-pruning). Le code commence par calculer les valeurs des alphas d’élagage (un paramètre de complexité qui spécifie l’importance à accorder à l’élagage de l’arbre) et leur coût en termes d’impureté (mesure du degré de pureté d’une région contenant des points de données de différentes classes). Plus l’alpha est élevé, plus l’arbre est simple et plus la variance du modèle est faible, mais cela est compensé par un certain biais. En utilisant la validation croisée ou un jeu de validation, il est possible de trouver le bon équilibre dans l’élagage afin d’obtenir les meilleurs résultats, comme le montre la Figure 10.3 :

path = dt.cost_complexity_pruning_path(X_train, y_train)
ccp_alphas, impurities = path.ccp_alphas, path.impurities

import matplotlib.pyplot as plt

fig, ax = plt.subplots(dpi=90)
ax.plot(ccp_alphas[:-1], impurities[:-1], marker='o',
       drawstyle="steps-post")
ax.set_xlabel("effective alpha")
ax.set_ylabel("total impurity of leaves")
ax.set_title("Total Impurity vs effective alpha")
plt.show()

Comme le montre la Figure 10.3, l’impureté totale croît assez rapidement pour de faibles variations des alphas. Vous ne pouvez trouver les meilleures valeurs qu’en validant l’alpha expérimenté sur le jeu de validation :

best_pruning = list()
for ccp_alpha in ccp_alphas:
   if ccp_alpha > 0:
       dt = DecisionTreeClassifier(random_state=0,
   ccp_alpha=ccp_alpha)
       dt.fit(X_train, y_train)
       best_pruning.append([ccp_alpha, dt.score(X_test,

[picture]

Figure 10.3 Visualisation des alphas d’élagage et de leur coût d’impureté.

           y_test)])
best_pruning = sorted(best_pruning,
                     key=lambda x:x[1], reverse=True)
best_ccp_alpha = best_pruning[0][0]
dt = DecisionTreeClassifier(random_state=0,
                           ccp_alpha=best_ccp_alpha)
dt.fit(X_train, y_train)
accuracy = dt.score(X_test, y_test)

print(f"test accuracy: {accuracy:0.3}")

print("Number of nodes in the last tree is: {} with
          ccp_alpha: {:0.3}"
     .format(dt.tree_.node_count, best_ccp_alpha))

Après avoir trouvé l’alpha ayant la meilleure précision sur le jeu de validation, le code entraîne à nouveau l’arbre de décision en utilisant cet alpha pour une croissance contrôlée. Le résultat est un ensemble clair de règles qui expliquent la différence entre la vie et la mort sur le RMS Titanic.

from dtreeviz.trees import dtreeviz

viz = dtreeviz(dt, X, y,
              target_name='survived',
              feature_names=X.columns,
              class_names=["No", "Yes"],
              scale=1.5)
viz

Pour un arbre de décision, l’élagage est un peu comme une séance de brainstorming. Tout d’abord, le code génère toutes les ramifications possibles de l’arbre (comme pour les idées dans une session de brainstorming). Puis, lorsque le brainstorming se termine, le code ne conserve que ce qui fonctionne vraiment. Un graphe de l’arborescence (voir la Figure 10.4) révèle que seules quelques règles ont de l’importance pour la survie, les plus importantes étant le genre (être un homme pénalise la survie) et la classe (être dans la troisième classe, la plus pauvre, menace également la survie).

[picture]

Figure 10.4 Visualisation de l’arbre de décision élagué construit à partir du jeu de données Titanic.

Prendre un virage probabiliste

L’algorithme bayésien naïf (ou naïf bayésien, selon les sources), un autre algorithme de base pour l’apprentissage, est plus semblable au perceptron discuté précédemment qu’à l’arbre de décision, car il est basé sur un ensemble de valeurs employées pour obtenir une prévision. Comme pour le perceptron et les arbres de décision, il s’agit d’un algorithme historique, utilisé depuis les années 1950, bien que sous des noms différents et des formes légèrement différentes. De plus, il est réputé pour être un algorithme efficace pour apprendre à partir d’un texte, et il penche clairement du côté de la tribu bayésienne. Étant donné sa simplicité et le fait qu’il fonctionne avec peu de prétraitement, il est devenu la référence pour la plupart des problèmes textuels en apprentissage automatique avant de tester des solutions plus complexes.

Comprendre le bayésien naïf

Comme pour le perceptron, l’algorithme bayésien naïf exige des valeurs qui sont des probabilités d’un résultat obtenu dans un certain contexte (une probabilité conditionnelle). De plus, vous multipliez les valeurs au lieu de les additionner. L’algorithme est alimenté par des calculs de probabilité, et nécessite par conséquent des opérations typiques des probabilités.

Comme nous l’avons vu au Chapitre 7, lorsque les probabilités sont multipliées, cela signifie que les événements sont indépendants et ne s’influencent en aucune façon les uns les autres. Une telle hypothèse, bien que jugée simpliste et naïve, est fréquente dans de nombreux algorithmes d’apprentissage automatique de base parce qu’elle est incroyablement efficace quand on travaille avec une grande quantité de données.

En additionnant les valeurs ou en multipliant les probabilités, vous traitez chaque élément d’information comme une contribution distincte à la réponse. C’est une hypothèse parfois irréaliste parce que la réalité montre un monde d’interconnexions. Cependant, malgré ce manque de réalisme, l’algorithme bayésien naïf peut surpasser les techniques les plus complexes, comme l’ont décrit deux chercheurs de Microsoft, Michele Banko et Eric Brill, dans leur article mémorable « Scaling to Very Large Corpora for Natural Language Disambiguation » ( « Passage à l’échelle de très grands corpus pour la désambiguïsation des langues naturelles » : https://aclanthology.org/P01-1005/).

L’algorithme bayésien naïf se rapporte au théorème de Bayes discuté dans la section « Améliorer le hasard avec le théorème de Bayes » du Chapitre 7. Il représente une forme simplifiée du théorème lui-même. Pour déterminer la classe d’un échantillon, l’algorithme accomplit les actions suivantes :

❶ Il apprend les probabilités reliant les caractéristiques à chacune des classes possibles.

❷ Il multiplie toutes les probabilités liées à chaque classe résultante.

❸ Il normalise les probabilités en divisant chacune d’elles par leur somme totale.

❹ Il prend comme réponse la classe ayant la probabilité la plus élevée.

Dans l’exemple précédent des données du jeu de tennis, on observe que les différentes distributions des prévisions d’ensoleillement, de ciel couvert et de pluie sont liées à la réponse positive et négative à la question de savoir s’il faut jouer au tennis. L’utilisation de la fonction crosstab de pandas peut permettre de vérifier rapidement cette observation :

pd.crosstab(tennis.outlook,
           tennis.play.replace({0:'NO', 1:'YES'}))

La sortie montre neuf réponses positives et cinq réponses négatives :

Prévisions | NON | OUI
Couvert | 0 | 4
Pluie | 2 | 3
Ensoleillé | 3 | 2

En analysant les réponses positives, vous constatez que, pour une réponse positive, un ciel ensoleillé apparaît deux fois sur neuf (probabilité : 2/9 = 0,22), couvert quatre fois sur neuf (probabilité : 4/9 = 0,44) et pluvieux trois fois sur neuf (probabilité : 3/9 = 0,33). Vous pouvez répéter la même procédure en utilisant les réponses négatives (vous ne pouvez pas jouer au tennis) avec des probabilités de 3/5, 0/5 et 2/5 respectivement pour le soleil, le ciel couvert et la pluie. Vous pouvez également utiliser la fonction crosstab pour obtenir des pourcentages basés sur les colonnes. Cela peut vous éviter d’effectuer certains calculs :

pd.crosstab(tennis.outlook,
           tennis.play.replace({0:'NO', 1:'YES'}),
           normalize='columns')

La sortie renvoie ce tableau :

Prévisions | NON | OUI
Couvert | 0,0 | 0,444444
Pluie | 0,4 | 0,333333
Ensoleillé | 0,6 | 0,222222

En utilisant le théorème de Bayes, vous pouvez déterminer que les probabilités que vous avez calculées sont en fait P(E|B), soit la probabilité que, compte tenu d’une certaine croyance (comme celle de jouer au tennis), vous avez certaines preuves (le temps qu’il fait, dans ce cas) :

P(B|E) = P(E|B)*P(B) / P(E)

La formule fournit la réponse dont vous avez besoin, parce que c’est la probabilité d’une certaine croyance (de jouer ou de ne pas jouer) compte tenu de certaines preuves (les conditions météorologiques). Si vous estimez les probabilités pour chaque croyance, vous pouvez choisir celle qui a la probabilité la plus élevée, minimisant ainsi le risque de prédire quelque chose incorrectement. P(E|B) devient alors critique pour estimer les probabilités parce que P(B) est la probabilité générale d’une réponse positive ou négative (la probabilité antérieure) et elle est facile à déterminer. Dans ce cas, vous avez neuf résultats positifs et cinq négatifs. Ainsi, P(B) est 9 / (9 + 5) = 0,64 pour le versant positif et 0,36 pour le versant négatif.

Lorsque vous avez plusieurs éléments de preuve, comme dans cet exemple, P(E|B) est un composé de toutes les probabilités P(E|B) uniques disponibles. Nous avons dans ce cas des jeux de probabilités pour l’état du ciel, la température, l’humidité et le vent. Il n’est pas facile de les assembler, à moins de présumer qu’elles affectent la réponse séparément. Comme nous l’avons mentionné précédemment, les probabilités d’événements indépendants sont simplement multipliées entre elles, et le P(E|B) global devient la multiplication de tous les P(E|B) pour chaque caractéristique.

Il peut arriver que vous n’ayez pas de preuve pour une réponse. Ainsi, dans cet exemple, vous n’avez pas de cas où vous ne jouez pas au tennis lorsque le ciel est couvert. Le résultat est une probabilité nulle, et dans une multiplication, une probabilité nulle renvoie toujours zéro, quelles que soient les autres probabilités impliquées. Un manque de preuves pour une réponse peut se produire lorsque vous n’échantillonnez pas assez d’éléments. Une bonne pratique consiste à modifier les probabilités observées à l’aide d’une constante, appelée correction de Laplace, qui consiste à ajouter des preuves fictives pour estimer la probabilité. En utilisant une telle correction dans cet exemple, la probabilité de 0/5 deviendrait (0 + 1) / (5 + 1) = 0,17.

P(E) n’est pas intéressante pour cet exemple, et vous devriez l’ignorer. La raison pour laquelle P(E) n’a pas d’importance est qu’elle représente la probabilité de voir un certain nombre de caractéristiques dans la réalité, et qu’elle varie naturellement d’un échantillon à un autre (par exemple, votre emplacement pourrait avoir de l’influence sur les conditions météorologiques). Cependant, vous ne comparez pas des probabilités entre les échantillons. Vous comparez les probabilités dans chaque échantillon pour déterminer la prédiction la plus probable le concernant. Dans le même exemple, la probabilité d’un certain ensemble de preuves est la même parce que vous n’avez que cet ensemble pour chaque résultat possible. Que l’ensemble de preuves soit rare n’a pas d’importance ; en fin de compte, vous devez prédire cet élément indépendamment des autres, de sorte que vous pouvez en toute sécurité exclure P(E) en lui donnant une valeur de 1. L’exemple suivant utilise des commandes Python de base pour montrer comment obtenir une prévision en fonction de certaines conditions météorologiques :

prob_outcomes = tennis.play.value_counts(sort=False,
               normalize=True)
outlook = pd.crosstab(tennis.outlook, tennis.play,
                     normalize='columns')
temperature = pd.crosstab(tennis.temperature,
                  tennis.play, normalize='columns')
humidity = pd.crosstab(tennis.humidity, tennis.play,
                      normalize='columns')
wind = pd.crosstab(tennis.wind, tennis.play,
                  normalize='columns')

Après avoir exécuté le morceau de code précédent, vous avez tous les éléments nécessaires pour faire une prédiction. Faites comme si vous deviez deviner la condition suivante :

Outlook = Sunny, Temperature = Mild, Humidity = Normal,
         Wind = Weak

Pour obtenir les informations requises, on calcule d’abord la probabilité d’un résultat sur la base de la probabilité globale, puis on la précise en multipliant les probabilités conditionnelles basées sur les preuves spécifiques dont on dispose :

proba = prob_outcomes
proba *= outlook.loc["Sunny"]
proba *= temperature.loc["Mild"]
proba *= humidity.loc["Normal"]
proba *= wind.loc["Weak"]
print(proba)

Le résultat en termes de probabilité pour une réponse positive est de 0,028219, ce qui est plus grand que la réponse négative, qui est de 0,006857. La probabilité plus élevée du résultat positif confirme que, dans ces conditions, l’algorithme prédit que vous pouvez jouer au tennis.

Estimer la réponse avec l’algorithme bayésien naïf

Maintenant que vous savez comment cela fonctionne, l’algorithme bayésien naïf devrait paraître assez simple et fortement naïf dans son hypothèse. Vous devriez aussi savoir que c’est une bonne chose que de multiplier les probabilités. Vous devez également tenir compte des points suivants :

• Traiter le cas des probabilités nulles à l’aide de la correction de Laplace.

• Convertir les caractéristiques numériques en caractéristiques qualitatives, car il est plus facile d’estimer la probabilité pour des classes comprenant des plages de nombres.

• Utiliser uniquement des caractéristiques dénombrées (valeurs égales ou supérieures à zéro) – bien que certaines variantes de l’algorithme puissent traiter des caractéristiques binaires et des valeurs négatives.

• Imputer des valeurs dans les caractéristiques manquantes (lorsqu’il manque une probabilité importante dans le calcul), et supprimer les caractéristiques redondantes et non pertinentes (le fait de conserver ces caractéristiques rendrait plus difficile l’estimation bayésienne naïve).

En particulier, les caractéristiques non pertinentes peuvent affecter considérablement les résultats. Lorsque vous travaillez avec quelques exemples comportant de nombreuses caractéristiques, le bruit qui peut être présent dans les données peut vraiment fausser vos résultats. La solution consiste à sélectionner un sous-ensemble de caractéristiques, en ne filtrant que les plus importantes. Le Chapitre 13 discute de cette technique en utilisant des modèles linéaires (la présence d’informations inutiles affecte également les modèles linéaires). Lorsque vous avez suffisamment d’échantillons et que vous passez du temps à corriger les caractéristiques, l’algorithme bayésien naïf apporte des solutions efficaces à de nombreux problèmes de prédiction impliquant l’analyse de données textuelles, tels que :

• Détection des spams dans les e-mails : permet de ne stocker que des informations utiles dans votre boîte de réception.

• Classification de texte : quelle que soit la source (informations en ligne, tweets ou autres flux textuels), vous pouvez correctement classer le texte dans la bonne catégorie (sports, politique, économie, etc.).

• Tâches de traitement de texte : permet d’effectuer une correction orthographique ou de deviner la langue d’un texte.

• Analyse de sentiment : détecte le sentiment derrière un texte écrit (positif, négatif, neutre ou une autre émotion humaine).

Comme exemple d’application pratique, cette section crée un détecteur de spam.

Tout d’abord, vous devez télécharger sur Internet le jeu de données sur le spam au format feather. Vous êtes ensuite prêt à exécuter le code de l’exemple.

filename = 'https://github.com/lmassaron/datasets/'
filename += 'releases/download/1.0/spam.feather'
spam = pd.read_feather(filename)
X = spam.iloc[:,:-1]
y = spam.iloc[:, -1]

Hewlett-Packard Labs a collecté le jeu de données et classifié 4 601 e-mails comme étant ou non des spams en utilisant 57 caractéristiques, dont beaucoup étaient des comptages normalisés du nombre de fois que certains mots ou symboles apparaissaient dans le courriel. Le référentiel gratuit d’apprentissage automatique de l’UCI¹⁰ contient également le jeu de données.

Si vous téléchargez le jeu de données sur le spam et que vous vérifiiez ses caractéristiques (à l’aide de la commande head(spam), par exemple), vous remarquerez que certaines de ces caractéristiques sont des mots, alors que d’autres indiquent la présence de certains caractères ou styles d’écriture (comme les majuscules). De façon plus notable, certaines des caractéristiques ne sont pas des nombres entiers représentant des dénombrements, mais plutôt des flottants allant de 0 à 100. Ils indiquent la présence de chaque caractéristique dans le texte sous forme d’un pourcentage (comme la caractéristique charDollar qui représente le pourcentage de présence du signe dollar dans la phrase et varie de 0 à 6).

Les caractéristiques exprimées en pourcentages de certains mots ou caractères du texte représentent une stratégie judicieuse pour équilibrer la probabilité plus élevée de trouver certains éléments si le texte est long. L’utilisation de pourcentages au lieu de dénombrements normalise les textes et permet de les considérer comme étant de la même longueur.

L’application d’un modèle de bayésien naïf ne nécessite pas la fixation de nombreux hyperparamètres. Vous pouvez définir la correction de Laplace à l’aide du paramètre alpha et définir différentes probabilités a priori, P(B), à l’aide du paramètre class_prior et fournir des probabilités différentes de celles qui peuvent être apprises à partir des données.

from sklearn.model_selection import cross_val_predict
from sklearn.naive_bayes import BernoulliNB

nb = BernoulliNB()

preds = cross_val_predict(nb, X, y, cv=5, n_jobs=-1)

Il existe plusieurs modèles de bayésien naïf. Le modèle de Bernoulli qui vient d’être présenté convient aux indicateurs binaires (on ne compte pas exactement les mots, mais on vérifie s’ils sont présents). Cependant, il existe également une version multinomiale (plus adaptée aux nombres entiers) et une version gaussienne (qui s’attend à ce que les caractéristiques soient normalement distribuées, c’est-à-dire qu’elles aient des valeurs positives et négatives). Python propose une gamme complète de modèles de bayésien naïf dans le package scikit-learn¹¹.

Pour tester l’efficacité du modèle, on utilise la validation croisée afin de fournir des prédictions en plus des estimations d’erreur à l’aide de cross_val_predict¹². Le jeu de données est divisé selon le schéma de validation croisée et toutes les prédictions des jeux de validation hors échantillon sont regroupées. Bien qu’il ne s’agisse pas d’une méthode valable pour mesurer la généralisation d’un modèle dans certains cas, elle donne un aperçu des performances du modèle en utilisant les données du jeu d’entraînement.

cross_val_predict est particulièrement utile lorsque vous souhaitez effectuer une prédiction sur les mêmes données de test et lorsque vous utilisez conjointement plusieurs modèles différents. Le Chapitre 16 traite de l’utilisation d’ensembles de modèles de cette manière.

Il est temps d’évaluer les prédictions de la validation croisée à l’aide de la commande classification_report. Cette fois, outre les mesures de précision habituelles, l’exemple utilise des mesures d’erreur qui sont plus informatives sur les performances du modèle de spam.

from sklearn.metrics import classification_report
cr = classification_report(y_true=y, y_pred=preds,
                 target_names=['non spam', 'spam'])
print(cr)

Les principaux résultats du rapport sont la précision, le rappel et le score f1 :

precision recall f1-score support
non spam 0.89 0.92 0.90 2788
spam 0.87 0.82 0.84 1813
accuracy 0.88 4601
macro avg 0.88 0.87 0.87 4601

weighted avg 0.88 0.88 0.88 4601

La précision et le rappel, ainsi que leur optimisation conjointe à l’aide du score f1, peuvent résoudre des problèmes qui ne sont pas abordés par la précision. La précision consiste à être précis dans ses suppositions. Elle mesure le pourcentage de fois où, lors de la prévision d’une classe, celle-ci s’est avérée juste. Par exemple, vous pouvez utiliser la précision lorsque vous diagnostiquez un cancer chez des patients après avoir évalué les données relatives à leurs examens. Dans ce cas, votre précision est le pourcentage de patients réellement atteints d’un cancer parmi ceux qui ont été diagnostiqués. Par conséquent, si vous avez diagnostiqué dix patients malades et que neuf d’entre eux sont réellement malades, votre précision est de 90 %.

Les conséquences ne sont pas les mêmes selon que l’on ne diagnostique pas un cancer chez un patient qui en est atteint ou que l’on diagnostique un cancer chez un patient en bonne santé. La précision ne représente qu’un élément du tableau général, car il y a des patients atteints de cancer que vous avez diagnostiqués comme étant en bonne santé, et c’est un problème très grave. La mesure du rappel traite l’autre élément important du tableau général. Elle indique, pour l’ensemble d’une classe, le pourcentage de réponses correctes. Par exemple, dans l’exemple précédent, la mesure de rappel est le pourcentage de patients dont vous avez correctement deviné qu’ils avaient un cancer. S’il y a 20 patients atteints de cancer et que vous n’aviez diagnostiqué que neuf d’entre eux, votre taux de rappel sera de 45 %.

Lorsque vous utilisez votre modèle, vous pouvez être précis tout en ayant un faible rappel, ou avoir un rappel élevé tout en perdant de la précision. Heureusement, la précision et le rappel peuvent être maximisés conjointement à l’aide du score f1, qui utilise la formule suivante : f1 = 2 * (précision * rappel) / (précision + rappel). L’utilisation du score f1 garantit que vous obtiendrez toujours la meilleure combinaison de précision et de rappel.

Dans le cas de cet exemple, vous pouvez voir que f1 indique que le modèle est moins performant lorsqu’il traite les exemples de spam. Cela pourrait être dû au faible nombre d’exemples de cette classe. En effet, comme c’est souvent le cas dans les problèmes réels, il est difficile et coûteux d’obtenir tous les exemples dont vous avez besoin. Les courriers électroniques non sollicités sont beaucoup moins fréquents que les courriers électroniques ordinaires dans ce jeu de données et le jeu de données est déséquilibré parce qu’il ne contient pas suffisamment d’exemples de courrier électronique non sollicité.

Lorsque vous souhaitez effectuer une classification et que vos classes sont déséquilibrées, c’est-à-dire qu’elles ont un nombre différent d’exemples par classe, vous devez rééquilibrer votre jeu de données, faute de quoi le modèle d’apprentissage automatique risque d’être moins performant. Le package imbalance-learn¹³ propose un ensemble de solutions de pointe, des tutoriels et une compatibilité totale avec les algorithmes de scikit-learn.

De plus, outre le fait que les deux classes ont une précision similaire, le modèle a un rappel plus faible pour les courriels de spam, ce qui implique qu’il ne détecte pas certains types de spam. Vous pouvez résoudre ce problème en augmentant le nombre d’exemples, en ajustant votre algorithme (si possible) ou en testant un nouveau type d’algorithme sur le problème. Détecter le spam n’est finalement pas si compliqué, mais le problème est d’éviter d’écarter des courriels importants au cours du processus (faux positifs, où un positif est un courriel de spam) et d’éviter d’ignorer des courriels de spam (faux négatifs). C’est pourquoi l’évaluation de la précision et du rappel est un passage clé après l’entraînement de votre détecteur de spam.

8 https://dl.acm.org/doi/10.1023/A%3A1022643204877

9 https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.LabelEncoder.html

10 https://archive.ics.uci.edu/dataset/94/spambase

11 https://scikit-learn.org/stable/modules/naive_bayes.html

12 https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.cross_val_predict.html

13 https://github.com/scikit-learn-contrib/imbalanced-learn

Partie 4

Apprendre du big data (avec intelligence)

Dans cette partie…

Préparer les données pour leur utilisation

Comprendre les données via la similarité

Travailler avec des modèles linéaires

Travailler avec des réseaux de neurones

Utiliser des machines à vecteurs de support

Analyser à plusieurs niveaux à l’aide d’ensembles

DANS CE CHAPITRE

Obtenir et nettoyer les données

•

Réparer et transformer les données

•

Créer de nouvelles caractéristiques significatives

•

Utiliser des modèles univariés et multivariés

Chapitre 11

Prétraiter les données

Lorsque vous construisez une nouvelle maison, avant de penser à une belle architecture, à une amélioration esthétique ou même à des meubles conçus pour l’embellir, vous devez construire une fondation solide sur laquelle vous pourrez bâtir des murs. En outre, plus le terrain sur lequel vous devez travailler est difficile, plus il vous faudra du temps et des efforts. Si vous négligez de créer une fondation solide, rien de ce qui y est construit ne pourra résister longtemps au temps et à la nature.

Le même problème se pose dans le domaine de l’apprentissage automatique. Quel que soit le niveau de sophistication de l’algorithme d’apprentissage, si vous ne préparez pas bien votre fondation – c’est-à-dire vos données – votre algorithme ne durera pas longtemps quand vous le testerez dans des situations réelles. Vous ne pouvez pas préparer des données simplement en les regardant : vous devez faire l’effort de les examiner de près. Malheureusement, le temps consacré au nettoyage des données peut représenter environ 80 % du temps total que vous consacrez à un projet d’apprentissage automatique.

La préparation des données se fait en plusieurs étapes (qui sont détaillées dans les sections qui suivent) :

❶ Obtenir des données significatives (c’est ce qu’on appelle aussi la vérité de terrain), c’est-à-dire des données que quelqu’un a correctement mesurées ou étiquetées.

❷ Acquérir suffisamment de données pour que l’algorithme apprenant fonctionne correctement. Vous ne pouvez pas dire à l’avance de quelle quantité de données vous aurez besoin, car tout dépend de l’algorithme que vous prévoyez d’utiliser. Ce n’est qu’après avoir testé celui-ci que vous serez en mesure de déterminer si vos estimations souffrent d’un biais élevé ou de la variance de l’algorithme que vous avez décidé d’utiliser.

❸ Disposer les données collectées dans une matrice.

❹ Traiter les données de mauvaise qualité, telles que les éléments manquants (un problème qui se produit souvent), les distributions déformées, les redondances et les échantillons anormaux.

❺ Créer de nouvelles caractéristiques (si nécessaire) qui soient mieux adaptées à votre algorithme pour obtenir la réponse attendue.

Il n’est pas nécessaire de saisir manuellement le code source de ce chapitre. Il est beaucoup plus facile d’utiliser le code source téléchargeable de ce chapitre dans le dossier ML4D2E de la source téléchargeable pour le code Python et dans le dossier ML4D2ER de la source téléchargeable pour le code R. Les fichiers d’exemple de ce chapitre commencent par ML4D2E ; 11 ; (pour Python) ou ML4D2E_R ; 11 ; (pour R). Reportez-vous à l’introduction pour des détails sur la manière de trouver ces fichiers source.

Collecter et nettoyer les données

Bien que ce livre se concentre sur la façon dont les machines apprennent à partir des données, aucune recette magique n’existe dans le monde des algorithmes lorsqu’il s’agit de données (comme l’indique le théorème du « no-free-lunch » , évoqué au Chapitre 9). Même les fonctions d’apprentissage sophistiquées et avancées se heurtent à un mur et sont moins performantes lorsque vous ne les alimentez pas avec ce qui suit :

• des quantités suffisamment importantes de données qui conviennent à l’algorithme que vous utilisez ;

• des données propres et bien préparées, adaptées à l’apprentissage automatique

Le Chapitre 9 traite du rôle de la quantité de données dans l’apprentissage lorsqu’il explique les compromis entre biais et variance. Pour rappel, de grandes quantités de données peuvent s’avérer bénéfiques lorsque la variabilité des estimations pose problème, car les données spécifiques utilisées pour l’apprentissage influencent fortement les prédictions (c’est le problème du surapprentissage). Un plus grand nombre de données peut faciliter le processus de prédiction, car un plus grand nombre d’exemples aidera les algorithmes d’apprentissage automatique à désambiguïser le rôle (déterminer précisément comment les données sont utilisées et dans quelle mesure elles sont importantes) de chaque signal tiré des données et à mieux utiliser ces signaux lors de la modélisation de la prédiction.

Outre la quantité de données, le besoin d’une « propreté » de ces données est compréhensible – c’est exactement comme la qualité de l’enseignement que vous recevez à l’école. Si vos professeurs ne vous enseignent que des absurdités, fournissent des exemples erronés, passent du temps à plaisanter et ne sont pas sérieux, vous ne réussirez pas vos examens, même si vous êtes intelligent. Il en va de même pour les algorithmes simples comme complexes – si vous ne leur donnez que des déchets comme données, ils ne produiront que des prédictions absurdes.

Selon le principe du « déchet en entrée, déchet en sortie » (ou GIGO, garbage in, garbage out en anglais), de mauvaises données peuvent vraiment nuire à l’apprentissage automatique. Les données de mauvaise qualité peuvent prendre plusieurs formes :

données manquantes ;

valeurs aberrantes ;

distributions de valeurs asymétriques ;

redondance des informations ;

caractéristiques mal analysées et mal développées.

De mauvaises données ne sont peut-être pas mauvaises dans le sens où elles seraient erronées. Bien souvent, cela signifie simplement qu’elles ne sont pas conformes aux normes que vous avez établies pour vos données : une étiquette écrite de plusieurs façons différentes, des valeurs erratiques provenant d’un débordement d’autres champs de données, des dates écrites dans des formats invalides ou encore du texte non structuré que vous auriez dû structurer dans une caractéristique catégorielle. Appliquer des règles de validité pour vos données dans vos bases et travailler à la conception de meilleurs tableaux de données ainsi qu’à l’exactitude de leur processus de stockage peut s’avérer d’une aide inestimable pour l’apprentissage automatique et permettre de vous concentrer sur la résolution de problèmes de données plus complexes.

Réparer les données manquantes

Même si vous avez suffisamment d’échantillons à portée de main pour entraîner des algorithmes d’apprentissage simples ou complexes, ils doivent présenter des valeurs complètes dans leurs caractéristiques, sans aucune donnée manquante. Avoir un échantillon incomplet rend impossible la connexion de tous les signaux à l’intérieur et entre les caractéristiques. Les valeurs manquantes rendent également difficile l’apprentissage de l’algorithme pendant l’entraînement. Vous devez donc faire quelque chose pour corriger les données manquantes. Le plus souvent, vous pouvez ignorer les valeurs qui sont absentes, ou essayer de les réparer en devinant une valeur de remplacement probable. Cependant, trop d’absences rendent les prédictions plus incertaines du fait que les informations manquantes pourraient masquer n’importe quelle valeur possible. Par conséquent, plus il y a de valeurs manquantes dans les caractéristiques, et plus les prédictions sont variables et imprécises.

Identifier les données manquantes

Dans un premier temps, comptez le nombre d’éléments absents dans chaque caractéristique. Lorsqu’une caractéristique a trop de cas manquants, il se peut que vous deviez la supprimer des jeux de données d’entraînement et de test. Une bonne règle empirique est de laisser tomber une caractéristique si plus de 90 % de ses instances sont manquantes.

Certains algorithmes d’apprentissage ne savent pas comment traiter les valeurs manquantes et signaler les erreurs lors des phases d’entraînement et de test, alors que d’autres modèles les considèrent comme des valeurs nulles, ce qui entraîne une sous-estimation de la valeur ou probabilité prévue (c’est juste comme si une partie de la formule ne fonctionnait pas correctement). Par conséquent, vous devez remplacer toutes les valeurs manquantes dans votre matrice de données par des valeurs appropriées pour que l’apprentissage automatique se déroule de manière adéquate.

De nombreuses raisons expliquent l’absence de données, mais l’essentiel est de savoir si ce phénomène se produit de manière aléatoire ou dans un ordre précis. Les données manquantes aléatoires sont idéales parce que vous pouvez estimer leur valeur en utilisant une simple moyenne, une médiane ou un autre algorithme d’apprentissage automatique, sans vous faire trop de soucis. Les problèmes surviennent lorsque les données manquantes ne sont pas aléatoires, mais qu’il y a une certaine logique derrière. Dans ce cas, les valeurs manquantes ont un fort biais en faveur de certains types d’échantillons. Pensez par exemple au cas de l’étude du revenu dans une population. Les gens riches (pour des raisons fiscales, probablement) ont tendance à cacher leur revenu réel en déclarant qu’ils ne savent pas. Les pauvres, en revanche, peuvent dire qu’ils ne veulent pas déclarer leur revenu par crainte d’un jugement négatif. Si vous manquez d’informations concernant certaines couches de la population, la réparation des données manquantes peut être difficile et trompeuse, parce que vous pouvez penser que de tels cas sont tous semblables, alors qu’ils sont très différents. Par conséquent, vous ne pouvez pas vous contenter d’utiliser des valeurs moyennes pour remplacer les valeurs manquantes et vous devez donc utiliser des approches complexes et les ajuster avec soin. En outre, il est difficile d’identifier les cas où les données ne sont pas manquantes au hasard, car cela nécessite une inspection plus approfondie de la façon dont les valeurs manquantes sont associées à d’autres caractéristiques du jeu de données.

Lorsque des données manquent selon une répartition aléatoire, vous pouvez facilement réparer les valeurs absentes parce que vous obtenez des indices sur leur valeur réelle à partir d’autres caractéristiques. Lorsque ce n’est pas le cas, vous ne pouvez pas obtenir de bons indices à partir des autres informations disponibles, à moins de comprendre l’association entre celles-ci et le cas manquant. Par conséquent, si vous devez déterminer un revenu non déclaré dans vos données, et que cette information n’a pas été communiquée par une personne qui est riche, vous ne pouvez pas remplacer la valeur manquante par une simple moyenne puisque, dans ce cas, vous vous contenteriez du revenu moyen de la population globale. Au lieu de cela, vous devriez utiliser une moyenne des revenus des gens riches comme substitut.

Lorsque des données ne sont pas absentes de manière aléatoire, le fait que la valeur est manquante est instructif en soi, parce que cela aide à retrouver le groupe manquant. Vous pouvez laisser la corvée de recherche de la raison de cette absence à votre algorithme d’apprentissage automatique en construisant une nouvelle fonction binaire qui signalera quand la valeur d’une caractéristique est manquante. Par conséquent, l’algorithme d’apprentissage automatique déterminera par lui-même la meilleure valeur de remplacement à utiliser.

Choisir la bonne stratégie de remplacement

Vous disposez de quelques stratégies pour traiter efficacement les données manquantes. Votre stratégie peut changer si vous devez traiter des valeurs manquantes dans des caractéristiques quantitatives (des valeurs exprimées par des nombres) ou qualitatives. Les caractéristiques qualitatives, bien qu’exprimées aussi par des nombres, font en réalité référence à des concepts, de sorte que leurs valeurs sont quelque peu arbitraires, et vous ne pouvez pas vous servir d’une moyenne ou de tout autre calcul de manière significative.

Lorsque vous travaillez avec des caractéristiques qualitatives, votre estimation de la valeur doit toujours produire des entiers, basés sur les nombres utilisés en tant que codes. Voici les stratégies courantes pour le traitement des données manquantes :

• Remplacer les valeurs manquantes par une constante calculée telle que la moyenne ou la valeur médiane. Si votre caractéristique est une catégorie, vous devez fournir une valeur spécifique, car la numérotation est arbitraire et l’utilisation de la moyenne ou de la médiane n’a aucun sens. Utilisez cette stratégie lorsque les valeurs manquantes sont aléatoires.

• Remplacer les valeurs manquantes par une quantité qui se trouve en dehors de la plage de valeurs normales de la caractéristique. Par exemple, si la caractéristique est positive, remplacez les valeurs manquantes par des valeurs négatives. Cette approche fonctionne bien avec les algorithmes basés sur les arbres de décision (comme ceux expliqués dans le chapitre précédent) et les caractéristiques qualitatives.

• Remplacer les valeurs manquantes par 0, ce qui fonctionne bien avec les modèles de régression et les caractéristiques normalisées (un sujet abordé au Chapitre 15). Cette approche s’applique également aux caractéristiques qualitatives lorsqu’elles contiennent des valeurs binaires.

• Interpoler les valeurs manquantes lorsqu’elles font partie d’une série de valeurs liées au temps. Cette approche ne fonctionne que pour des valeurs quantitatives. Par exemple, si votre caractéristique concerne les ventes quotidiennes, vous pouvez utiliser une moyenne mobile pour les sept derniers jours ou choisir la valeur utilisée à la même heure de la semaine précédente.

• Imputer leur valeur à l’aide d’une information provenant d’autres caractéristiques prédictives (mais n’utilisez jamais la variable réponse). Utilisez une bibliothèque telle que missing.py ou missforest.py¹⁴, Multiple Imputation with Chained Equations (MICE)¹⁵, Autoimpute¹⁶ pour imputer la valeur.

Une autre bonne pratique consiste à créer une nouvelle caractéristique binaire pour chaque caractéristique dont vous avez réparé les valeurs. La caractéristique binaire suivra les variations provoquées par un remplacement ou une imputation avec une valeur positive, et votre algorithme d’apprentissage automatique peut déterminer quand il doit effectuer des ajustements supplémentaires aux valeurs que vous avez réellement utilisées.

En Python, le traitement des valeurs manquantes n’est possible qu’en utilisant la structure de données ndarray du package NumPy. Python marque les valeurs manquantes avec une valeur spéciale qui apparaît à l’écran sous la forme NaN (Not a Number). La structure de données DataFrame du package pandas offre des méthodes à la fois pour remplacer les valeurs manquantes et pour supprimer des caractéristiques.

L’exemple Python suivant montre comment effectuer des tâches de remplacement. Il commence par créer un jeu de données avec 5 observations et 3 caractéristiques, appelées « A » , « B » , « C » :

import pandas as pd
import numpy as np
data = pd.DataFrame([[1,2,np.nan],[np.nan,2,np.nan],
                    [3,np.nan,np.nan],[np.nan,3,8],
                    [5,3,np.nan]],columns=['A','B','C'])
print(data,'\n') # affiche les données
# compte le nombre de valeurs NaN pour chaque caractéristique
print(data.isnull().sum(axis=0))

Voici le résultat du processus de création :

     A    B   C
0  1.0  2.0  NaN
1  NaN  2.0  NaN
2  3.0  NaN  NaN
3  NaN  3.0  8.0
4  5.0  3.0  NaN

A    2
B    1
C    4
dtype: int64

Comme la caractéristique C n’a qu’une seule valeur, vous pouvez la supprimer du jeu de données. Le code remplace ensuite les valeurs manquantes de la caractéristique B par une valeur moyenne et interpole la valeur de la caractéristique A parce qu’elle montre un ordre progressif.

# Supprime définitivement C du jeu de données
data.drop('C', axis=1, inplace=True)
# Crée un espace réservé pour les valeurs manquantes de B
data['missing_B'] = data['B'].isnull().astype(int)
# Remplit les éléments manquants de B en utilisant la moyenne de
B
data['B'].fillna(data['B'].mean(), inplace=True)
# Interpole A
data['A'].interpolate(method='linear', inplace=True)
print(data)

Voici à quoi ressemble maintenant le jeu de données :

    A    B  missing_B
0 1.0  2.0         0
1 2.0  2.0         0
2 3.0  2.5         1
3 4.0  3.0         0
4 5.0  3.0         0

La sortie affichée constitue le jeu de données final. Notez bien que la moyenne de B n’est pas une valeur entière, si bien que vous voyez une valeur de 2.5 pour la valeur manquante. Cette approche est logique si B est numérique. S’il s’agissait d’une catégorie, et que la numérotation marquait une classe, le code aurait dû remplir la caractéristique en utilisant la commande data['B'].fillna(data['B'].mode().iloc[0], inplace=True), qui emploie le mode, c’est-à-dire la première valeur la plus fréquemment utilisée dans la série.

Comme le montre cet exemple, parfois vous ne pouvez pas faire grand-chose avec des échantillons à qui il manque beaucoup de valeurs dans leurs caractéristiques. Dans de tels cas, si l’échantillon est destiné à l’entraînement (les échantillons de test ne devraient pas être supprimés), retirez-le du jeu (une procédure appelée suppression par liste) afin que les cas incomplets n’affectent pas l’apprentissage. Si, au lieu de cela, l’échantillon fait partie de votre test, vous ne devriez pas le supprimer, mais plutôt l’utiliser pour obtenir une évaluation de la façon dont votre algorithme d’apprentissage automatique gère de telles situations.

Transformer les distributions

Bien que les statistiques soient basées sur l’attente que les caractéristiques possèdent certaines distributions de valeurs, l’apprentissage automatique n’a généralement pas de telles contraintes. Un algorithme d’apprentissage automatique n’a pas besoin de connaître à l’avance le type de distribution des données sur lequel il va travailler, mais l’apprend directement à partir des données utilisées pour l’entraînement.

En probabilités, une distribution est un tableau de valeurs ou une fonction mathématique qui relie chaque valeur possible d’une caractéristique à la probabilité qu’une telle valeur puisse se produire. Les distributions de probabilités sont généralement (mais pas uniquement) représentées dans des graphiques dont l’axe (horizontal) des abscisses représente les valeurs possibles de la caractéristique, et dont l’axe (vertical) des ordonnées représente la probabilité de ces occurrences. La plupart des modèles statistiques reposent sur une distribution normale, symétrique et avec une forme en cloche caractéristique.

Même en utilisant des algorithmes d’apprentissage provenant des statistiques, vous n’avez jamais besoin de transformer une distribution réelle pour qu’elle ressemble à une distribution normale ou à toute autre distribution statistique notable (comme les distributions uniformes ou de Poisson). Les algorithmes d’apprentissage automatique sont généralement assez intelligents pour savoir comment gérer par eux-mêmes toute distribution présente dans les caractéristiques. Cependant, même si la transformation des distributions réelles n’est pas nécessaire pour qu’un algorithme d’apprentissage automatique fonctionne correctement, elle peut quand même être bénéfique pour les raisons suivantes :

• faire en sorte que la fonction de coût minimise mieux l’erreur dans les prédictions ;

• faire converger l’algorithme correctement et plus rapidement.

Vous pouvez appliquer des transformations à la variable réponse pour minimiser le poids de tout cas extrême. Lorsque vous prédisez des valeurs (comme dans un problème de régression) et que certaines valeurs sont trop extrêmes par rapport à la majorité des éléments, vous pouvez appliquer des transformations qui tendent à réduire la distance entre les valeurs. Par conséquent, en s’efforçant de minimiser l’erreur, l’algorithme ne se concentrera pas trop sur les situations extrêmes et il obtiendra une solution générale. Dans de tels cas, vous choisissez normalement la transformation logarithmique, mais cette transformation nécessite des valeurs positives. Si vous travaillez uniquement avec des nombres positifs et que le problème est le cas du zéro, ajoutez 1 à vos valeurs pour qu’aucune d’elles ne soit nulle (log(1) vaut en fait zéro, ce qui en fait un nouveau point de départ pratique.)

Si vous ne pouvez pas utiliser une transformation logarithmique, appliquez une racine cubique, qui préserve le signe. Dans certains cas (lorsque les valeurs extrêmes sont énormes), vous voudrez peut-être plutôt appliquer la transformation inverse (c’est-à-dire diviser 1 par la valeur de votre réponse).

Certains algorithmes d’apprentissage automatique qui sont basés sur la descente de gradient ou sur des mesures de distance (comme les algorithmes décrits au Chapitre 14, tels que les K-moyennes et les k plus proches voisins) sont très sensibles à l’échelle des valeurs numériques fournies. Par conséquent, pour que l’algorithme converge plus rapidement ou fournisse une solution plus exacte, il est nécessaire de rééchelonner (ou recalibrer) la distribution. Le recalibrage modifie la plage des valeurs des caractéristiques et peut également affecter la variance. Vous pouvez effectuer ce recalibrage des caractéristiques de deux façons différentes :

• Utiliser la normalisation statistique (normalisation des scores z) : centrez la moyenne sur zéro (en soustrayant la moyenne), puis divisez le résultat par l’écart-type. Après une telle transformation, la plupart des valeurs se situent entre –3 et +3.

• Utiliser la transformation min-max : soustrayez la valeur minimale de la caractéristique, puis divisez-la par la plage (valeur maximale moins valeur minimale). Cela recalibre toutes les valeurs entre 0 et 1. C’est préférable à la normalisation lorsque l’écart-type original est trop petit (les valeurs d’origine sont trop proches, comme si elles se collaient toutes autour de la moyenne) ou lorsque l’on veut préserver les valeurs zéro dans une matrice creuse (un concept abordé au Chapitre 7, qui explore les mathématiques utilisées dans l’apprentissage automatique).

Python fournit des fonctions de transformation des distributions. À part les fonctions mathématiques log() et exp(), qui nécessitent l’importation du package NumPy en Python, la normalisation et la transformation min-max demandent un peu plus de travail. On emploie les fonctions et les classes du module de prétraitement de scikit-learn, telles que : sklearn.preprocessing.scale, sklearn.preprocessing. StandardScaler, et sklearn.preprocessing.MinMaxScaler (vous trouverez plus d’informations à ce sujet à https://scikit-learn.org/stable/modules/preprocessing.html).

La Partie 5 de l’ouvrage donne quelques exemples de normalisation et de transformation qui montrent comment appliquer l’apprentissage automatique à des problèmes réels de données, et donc aussi gérer le prétraitement de ces données.

Créer vos propres caractéristiques

Parfois, les données brutes que vous obtenez de diverses sources n’ont pas les caractéristiques nécessaires pour effectuer des tâches d’apprentissage automatique. Dans ce cas, vous devez créer vos propres caractéristiques afin d’obtenir le résultat souhaité. Créer une caractéristique ne signifie pas fabriquer des données à partir de rien, comme vous le verrez dans les sections qui suivent. On produit de nouvelles caractéristiques à partir de données existantes.

Comprendre la nécessité de créer des caractéristiques

L’une des grandes limites des algorithmes d’apprentissage automatique est qu’il peut être impossible de deviner une formule qui pourrait lier votre réponse aux fonctions que vous utilisez. Parfois, cette incapacité à deviner se produit parce que vous ne pouvez pas représenter cette réponse à l’aide des informations dont vous disposez (ce qui signifie que vous n’avez pas les bonnes informations). Dans d’autres cas, les informations que vous avez fournies n’aident pas l’algorithme à apprendre correctement. Par exemple, si vous modélisez le prix de propriétés immobilières, la surface du terrain est assez prédictive parce que les propriétés plus grandes ont tendance à coûter plus cher. Mais, si au lieu de la surface, vous fournissez à votre algorithme d’apprentissage automatique la longueur des côtés du terrain (disons les coordonnées en latitude et en longitude de ses coins), votre algorithme peut ne pas savoir quoi faire des informations que vous avez communiquées. Certains algorithmes parviendront à trouver la relation entre les caractéristiques, mais la plupart n’y parviendront pas.

La réponse à ce problème est la création de caractéristiques. Il s’agit d’une partie de l’apprentissage automatique qui est considérée plus comme un art que comme une science, car elle implique une intervention humaine dans le mélange créatif des caractéristiques existantes. On effectue cette tâche au moyen de l’addition, de la soustraction, de la multiplication et du ratio pour générer de nouvelles caractéristiques dérivées avec plus de puissance prédictive que les originaux.

Bien connaître le problème et savoir comment un être humain le résoudrait fait partie de la création de caractéristiques. Ainsi, en reprenant l’exemple précédent, le fait que la surface du terrain est liée au prix de la propriété est de notoriété publique. Si la surface n’apparaît pas dans vos caractéristiques lorsque vous essayez de deviner la valeur d’une propriété, vous pouvez récupérer cette information à partir des données existantes, ce qui augmente les performances des prédictions. Que vous vous fiiez au bon sens, aux connaissances communes ou à une expertise spécialisée, vous pouvez faire beaucoup pour votre algorithme d’apprentissage automatique si vous déterminez d’abord quelle information devrait être la plus efficace pour le problème que vous avez à traiter, et que vous essayez ensuite de la rendre disponible ou de la dériver à partir de vos caractéristiques.

Créer automatiquement des caractéristiques

Vous pouvez créer de nouvelles caractéristiques automatiquement. Une façon d’automatiser ce travail consiste à utiliser le développement polynomial. Des moyens spécifiques sont disponibles pour réaliser cette opération afin que vous puissiez créer automatiquement des caractéristiques en Python. Vous verrez quelques exemples détaillés lorsque vous travaillerez avec des modèles de régression et des machines à vecteurs de support plus loin dans le livre. Pour l’instant, vous devez saisir les concepts sous-jacents.

Dans le développement polynomial, vous créez automatiquement des multiplications entre les caractéristiques (que l’on nomme interactions en statistiques) ainsi que des puissances (par exemple, le calcul du carré ou du cube d’une caractéristique). La création d’une nouvelle caractéristique en utilisant la multiplication aide à suivre le comportement des caractéristiques dans leur ensemble. Par conséquent, il est utile de représenter les relations complexes entre vos caractéristiques qui peuvent suggérer des situations particulières qui se produisent lorsque certaines valeurs d’une caractéristique sont accompagnées de certaines valeurs d’une autre caractéristique.

Le rapport entre le bruit émis par une voiture et le prix de la voiture est un bon exemple d’interaction. Les consommateurs n’apprécient pas les voitures bruyantes, à moins d’acheter une voiture de sport, auquel cas le bruit du moteur est un plus qui rappelle au propriétaire la puissance du moteur (et son coût plus élevé). Une voiture de sport attire également l’attention des passants, si bien que le bruit jouera un rôle important dans la mise en valeur du véhicule. En revanche, le bruit au volant d’une voiture familiale moins chère n’est pas apprécié. De nombreux modèles peuvent facilement saisir la relation complexe entre le prix et le bruit d’une voiture si vous créez une fonction d’interaction.

Dans une application d’apprentissage automatique, en essayant de prédire le taux de préférence pour une certaine voiture, des caractéristiques telles que le bruit et le prix du véhicule sont prédictives en soi. Cependant, en multipliant les deux valeurs et en les ajoutant à l’ensemble des caractéristiques, on peut sans équivoque laisser entendre à un algorithme d’apprentissage que la cible est une voiture de sport (lorsque vous multipliez les niveaux de bruit élevés par un prix élevé).

Les puissances aident en créant des relations non linéaires entre la réponse et les caractéristiques, indiquant des situations spécifiques. Autre exemple : imaginez que vous devez prédire les dépenses annuelles d’une personne. L’âge est un bon prédicteur, car à mesure que les gens vieillissent et mûrissent, leur vie et leur situation familiale changent aussi. Les étudiants commencent dans la pauvreté, puis trouvent du travail et peuvent fonder une famille. D’un point de vue général, les dépenses ont tendance à augmenter, tout comme l’âge, du moins dans une certaine mesure. La retraite marque habituellement un point où les dépenses ont tendance à diminuer. L’âge contient de telles informations, mais c’est une caractéristique qui tend à croître, et le fait de relier les dépenses à cette croissance n’aide pas à décrire l’inversion qui se produit à un certain âge. L’ajout de la caractéristique au carré aide à créer un effet de contrepoids au vieillissement lui-même, qui est faible au début, mais qui croît rapidement avec l’âge. L’effet final est une parabole, avec une croissance initiale caractérisée par un pic de dépenses à un certain âge, suivie d’une baisse.

Comme nous l’avons déjà mentionné, connaître à l’avance de telles dynamiques (bruit et voiture de sport, consommation et vieillissement) peut vous aider à créer les bonnes caractéristiques. Mais si vous ne connaissez pas à l’avance ces dynamiques, le développement polynomial les créera automatiquement pour vous car, étant donné un certain ordre, il créera des interactions et des puissances de cet ordre. L’ordre indiquera le nombre de multiplications et la puissance maximale à appliquer aux caractéristiques existantes. Ainsi, un développement d’ordre 2 élève toutes les caractéristiques à la puissance deux et multiplie chaque caractéristique par toutes les autres (on obtient la multiplication de toutes les combinaisons de deux caractéristiques). De toute évidence, plus le nombre est élevé, et plus de caractéristiques nouvelles seront créées, mais beaucoup d’entre elles seront redondantes et contribueront simplement à faire que votre algorithme d’apprentissage automatique risquera de tomber dans le surapprentissage des données.

Lorsque vous utilisez le développement polynomial, vous devez faire attention à l’explosion des caractéristiques que vous créez. Comme les puissances augmentent linéairement, si vous avez cinq caractéristiques et avez besoin d’un développement polynomial d’ordre 2, chaque caractéristique est élevée à la puissance deux. Augmenter l’ordre de l’une d’entre elles ne fait qu’ajouter une nouvelle puissance pour chaque caractéristique d’origine. Au lieu de cela, les interactions augmentent en fonction des combinaisons des caractéristiques jusqu’à cet ordre. En fait, avec cinq caractéristiques et un développement polynomial d’ordre 2, les dix combinaisons uniques du couplage des caractéristiques sont créées. En utilisant les lettres a à e, les combinaisons seraient les suivantes :

a b
a c
a d
a e
b c
b d
b e
c d
c e
d e

Augmenter l’ordre à 3 nécessitera la création de toutes les combinaisons uniques de deux caractéristiques, plus les combinaisons uniques de trois caractéristiques, c’est-à-dire 20 caractéristiques.

Principes de base de la décomposition en valeurs singulières

La factorisation est le processus qui consiste à décomposer un objet jusqu’à ce qu’il soit impossible de le décomposer davantage dans le sens mathématique. Par exemple, lorsque vous travaillez avec un scalaire, tel que 18, vous pouvez le décomposer en nombres premiers de 2 * 3². Vous pouvez également décomposer une matrice, en la divisant en plus petits morceaux qui décrivent la matrice à l’aide de ses éléments constitutifs. Il existe plusieurs façons de décomposer une matrice, et la décomposition en valeurs singulières (abrégée en anglais en SVD pour Singular Value Decomposition) est l’une d’entre elles. Le plus grand avantage de la SVD est que chaque matrice possède une SVD, ce qui la rend souvent plus stable que d’autres méthodes, telles que la décomposition en valeur absolue. En raison de cette stabilité, la SVD est souvent utilisée dans les cas suivants :

compression ;

débruitage ;

réduction des données ;

régression linéaire des moindres carrés ;

compression d’images.

L’objectif de la SVD est de réduire la matrice à ses éléments constitutifs afin de simplifier les calculs ultérieurs. En visualisant la SVD, on voit l’équation suivante :

A = U * Σ * VT

Dans cette équation :

A est une matrice m-x-n que l’on veut décomposer ;

U est une matrice m-x-m ;

Σ est une matrice diagonale m-x-n ;

VT est un transposition d’une matrice n-x-n.

Les valeurs diagonales de Σ sont les valeurs singulières de la matrice A originale. Les colonnes de U sont les valeurs singulières de gauche de A et les lignes de VT sont les valeurs singulières de droite de A. Essentiellement, lorsque vous multipliez ces trois matrices ensemble, vous obtenez la matrice originale A. Cependant, vous pouvez utiliser les parties décomposées pour mieux comprendre la matrice A. Voici un code qui montre comment fonctionne la SVD :

import numpy as np
from scipy.linalg import svd

A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(A)

# Effectue la décomposition
U, s, VT = svd(A)
print("\nU\n", U)
print("\ns\n", s)
print("\nVT\n", VT)

# Crée la matrice Sigma m x n
Sigma = np.diag(s)
print("\nSigma matrix\n", Sigma)

# Reconstruit la matrice originale
B = U.dot(Sigma.dot(VT))
print("\nReconstructed matrix\n", B)

Ce code montre ce qu’il faut faire pour déconstruire la matrice et la reconstruire lorsqu’on travaille avec une matrice carrée (3 × 3, dans cet exemple). Notez que vous devez créer une matrice diagonale, Sigma, afin d’obtenir la multiplication correcte du produit scalaire pour reconstruire la matrice A ultérieurement. Cependant, il se peut que vous ne disposiez pas d’une matrice carrée, ce qui signifie que vous devez spécifier la taille de Sigma de la manière suivante :

Sigma = np.zeros((A.shape[0], A.shape[1]))
Sigma[:A.shape[1], :A.shape[1]] = np.diag(s)

Lorsque vous exécutez ce code, vous voyez la déconstruction de la matrice A et sa reconstruction en tant que matrice B :

[[1 2 3]
 [4 5 6]
 [7 8 9]]

U
 [[-0.21483724 0.88723069 0.40824829]
  [-0.52058739 0.24964395 -0.81649658]
  [-0.82633754 -0.38794278 0.40824829]]

s
 [1.68481034e+01 1.06836951e+00 1.47280825e-16]

VT
 [[-0.47967118 -0.57236779 -0.66506441]
  [-0.77669099 -0.07568647 0.62531805]
  [ 0.40824829 -0.81649658 0.40824829]]

Matrice Sigma
 [[1.68481034e+01 0.00000000e+00 0.00000000e+00]
  [0.00000000e+00 1.06836951e+00 0.00000000e+00]
  [0.00000000e+00 0.00000000e+00 1.47280825e-16]]

Matrice reconstruite
 [[1. 2. 3.]
  [4. 5. 6.]
  [7. 8. 9.]]

Réorganiser les données

Idéalement, en apprentissage automatique, vous pouvez obtenir les meilleurs résultats lorsque vos caractéristiques ne sont pas complètement corrélées entre elles, et que chacune d’entre elles a une certaine puissance prédictive par rapport à la réponse que vous modélisez. En réalité, vos caractéristiques sont souvent corrélées entre elles, affichant un degré élevé de redondance dans les informations disponibles pour le jeu de données.

Le fait d’avoir des données redondantes signifie que la même information est répartie sur plusieurs caractéristiques. S’il s’agit exactement de la même information, cela représente une colinéarité parfaite. Si, au contraire, il ne s’agit pas exactement de la même information mais qu’elle varie d’une façon ou d’une autre, vous avez une colinéarité entre deux ou une multicolinéarité entre plus de deux caractéristiques.

Les données redondantes sont un problème pour lequel la théorie statistique a créé il y a déjà longtemps des solutions (parce que les calculs statistiques peuvent beaucoup souffrir de la multicolinéarité). Ce chapitre présente ce sujet sous un angle statistique, en employant les concepts de variance, de covariance et de corrélation. Vous pouvez imaginer chaque caractéristique comme comportant des éléments informatifs différents, mélangés dans des proportions différentes :

• Variance unique : la part d’information qui est propre à une caractéristique particulière et qui, lorsqu’elle est corrélée ou associée à la réponse, peut contribuer directement à la prédiction de la réponse elle-même.

• Variance partagée : la part d’information commune à d’autres caractéristiques. Cette communalité peut être due à une relation de causalité entre elles (la variance et son applicabilité sont abordées dans la section « Utilisation des statistiques » du Chapitre 7). Dans ce cas, si l’information partagée est pertinente pour la réponse, l’algorithme d’apprentissage aura de la difficulté à choisir la caractéristique à retenir. Et lorsqu’une caractéristique est choisie pour sa variance partagée, elle apporte aussi son bruit aléatoire spécifique.

• Composante de bruit aléatoire : information causée par des problèmes de mesure ou par le hasard et qui n’est pas utile pour représenter la réponse, mais qui parfois, par pur hasard (oui, la chance ou la malchance fait partie du hasard), peut sembler liée à la réponse elle-même.

La variance unique, la variance partagée et le bruit aléatoire fusionnent et ne peuvent pas être séparés facilement. En utilisant la sélection de caractéristiques, vous réduisez l’impact du bruit en choisissant l’ensemble minimum de caractéristiques fonctionnant le mieux avec votre algorithme d’apprentissage automatique. Une autre approche possible est basée sur l’idée que vous pouvez fusionner cette information redondante en utilisant une moyenne pondérée, créant ainsi une nouvelle caractéristique dont la composante principale est la variance partagée de plusieurs caractéristiques, et son bruit est une moyenne du bruit précédent et d’une variance unique.

Par exemple, si A, B et C partagent la même variance, en utilisant la compression vous pouvez obtenir une composante (considérée comme une nouvelle caractéristique) qui est la somme pondérée des trois caractéristiques, telle que 0.5*A+0.3*B+0.2*C. Vous décidez des poids en fonction d’une technique particulière utilisant la SVD, qui est expliquée dans la section précédente.

La SVD a de nombreuses applications, non seulement dans la compression de données, mais aussi dans la recherche de facteurs latents (caractéristiques cachées dans nos données) et dans les systèmes de recommandation, qui permettent de découvrir ce que quelqu’un pourrait aimer en termes de produits ou de films à partir de sélections précédentes.

L’ACP fonctionne simplement et de manière directe : elle prend comme entrée un jeu de données et renvoie un nouveau jeu de données reconstruit de la même forme. Dans ce nouveau jeu de données, toutes les caractéristiques, appelées composantes, ne sont pas corrélées, et les composantes les plus informatives apparaissent au début du jeu de données.

L’ACP offre également un rapport sur la correspondance entre chaque composante et le jeu de données initial. En additionnant la valeur informative des nouvelles composantes, vous allez peut-être constater que quelques composantes expriment 90 %, voire 95 %, de l’information originale. Ne prendre que ces quelques composantes équivaut à utiliser les données d’origine, ce qui permet d’obtenir une compression de vos données en supprimant les redondances et en réduisant le nombre de caractéristiques.

L’exemple suivant se réfère au jeu de données Boston et utilise l’implémentation scikit de l’ACP en Python. Voici un extrait de code Python qui teste l’efficacité d’une analyse en composantes principales :

from sklearn.datasets import load_boston
from sklearn.decomposition import PCA
from sklearn.preprocessing import scale
import numpy as np
from sklearn.datasets import load_boston
from sklearn.decomposition import PCA
from sklearn.preprocessing import scale
import numpy as np
boston = load_boston()
X, y = boston.data, boston.target
pca = PCA().fit(X)

Après le calcul de l’ACP, on affiche la puissance informative de ce nouveau jeu de données reconstruit :

print (' '.join(['%5i'%(k+1) for k in range(13)]))
print (' '.join(['-----']*13))
print (' '.join(["%0.3f" % (variance) for variance
           in pca.explained_variance_ratio_]))
print (' '.join(["%0.3f" % (variance) for variance
           in np.cumsum(pca.explained_variance_ratio_)]))

    1    2    3    4     5    6    7    8     9…
----- ----- ----- ----- ----- ----- ----- ----- -----…
0.806 0.163 0.021 0.007 0.001 0.001 0.000 0.000 0.000…
0.806 0.969 0.990 0.997 0.998 0.999 1.000 1.000 1.000…

Dans le rapport affiché, les treize composantes (dont seulement neuf figurent ici) représentent un jeu de données cumulatif qui dépasse 85 % du jeu original avec 6 composantes sur 13, et 95 % avec 9 composantes (pour obtenir la valeur de 85+ pour cent, additionnez la deuxième série de chiffres et vous obtenez une valeur de 7,76, qui, divisée par 9, donne 0,86 ou 86 pour cent). Utiliser un jeu de données reconstruit avec moins de composantes que le nombre de caractéristiques d’origine s’avère souvent bénéfique pour les processus d’apprentissage automatique en réduisant la consommation de mémoire et le temps de calcul, tout en contenant la variance des estimations, ce qui assure ainsi la stabilité des résultats.

Délimiter les données anomales

Parfois, en explorant vos données, vous tomberez sur des valeurs qui diffèrent de celles auxquelles vous vous attendiez en ce sens qu’elles sont trop élevées ou trop basses, ou simplement qu’elles sont inhabituelles et hautement improbables. Ce sont des valeurs aberrantes, aussi appelées anomalies, lorsque vous êtes certain qu’elles le sont vraiment parce que quelque chose n’a pas fonctionné. Au contraire, si elles sont tout simplement inhabituelles parce que c’est la première fois que vous voyez de telles valeurs, mais que ce sont des informations légitimes, il s’agit simplement de nouveautés, donc d’un nouveau type d’échantillon.

Dans certains cas, un échantillon est une valeur aberrante parce qu’il possède une combinaison unique et inhabituelle de valeurs. Parfois, il s’agit d’une association avec une valeur impossible ou très improbable (par exemple, vous trouvez un enfant avec un diplôme universitaire dans un jeu de données). Parfois aussi, un subtil mélange de plusieurs valeurs rend l’échantillon rare (si rare qu’il ne peut être vrai).

Les valeurs aberrantes constituent un problème pour l’apprentissage à partir des données. Comme les machines apprennent en observant des échantillons et en en extrayant des règles et des modèles, un cas étrange peut s’avérer difficile à comprendre et forcer l’algorithme à reconsidérer ce qu’il a appris jusqu’ici. En particulier, de nombreuses fonctions de coût travaillent sur la base de la différence entre la valeur observée et la valeur attendue. Une valeur qui se situe très en dehors de la plage attendue présentera un écart énorme par rapport à cette valeur attendue, ce qui amènera le processus d’apprentissage à s’adapter brusquement à l’anomalie en sous-évaluant les valeurs régulières (qui produisent au contraire des écarts relativement faibles).

Cependant, tous les algorithmes d’apprentissage automatique ne sont pas sensibles aux valeurs aberrantes, et il y a quelques précautions à prendre lorsqu’un algorithme est en mode apprentissage. Par exemple, vous pouvez mettre lentement à jour les paramètres que l’algorithme est en train d’apprendre afin qu’il ne tienne pas immédiatement pour acquis chaque valeur rencontrée, mais seulement les plus fréquentes. C’est une bonne pratique de faire face aux imprévus avant le début de l’entraînement en adoptant un bon style de vérification des données, même si vous avez fourni des remèdes du côté de l’apprentissage.

Utiliser une stratégie univariée

Les valeurs aberrantes manifestes faussent les distributions de données et affectent profondément vos statistiques descriptives. La valeur moyenne est habituellement si différente en présence de valeurs aberrantes que vous remarquerez un grand écart entre moyenne et médiane. De plus, les valeurs aberrantes affectent les relations entre les caractéristiques et peuvent affaiblir ou renforcer une association entre deux caractéristiques, ce qui peut entraîner une plus grande confusion dans votre algorithme d’apprentissage.

Vous avez aussi des valeurs aberrantes subtiles, celles qui n’affectent pas clairement vos distributions, mais plutôt qui peuplent des parties vides de l’espace des données, forçant ainsi l’algorithme d’apprentissage à s’adapter à leur présence. En réalité, l’adaptation à de tels cas équivaut à l’apprentissage d’informations bruyantes, ce qui augmente la variance des estimations.

Une bonne stratégie pour trouver des valeurs aberrantes manifestes consiste à observer certaines statistiques descriptives clés et à pratiquer une certaine analyse exploratoire des données (AED), un terme inventé par John Tukey, un statisticien américain qui, dans son livre Exploratory Data Analysis de 1977, a parlé de la nécessité d’une meilleure intégration entre analyse graphique et mesures descriptives. C’est l’approche univariée. Par exemple, comme nous l’avons vu dans les sections précédentes de ce chapitre, une grande différence entre la moyenne et la médiane indique qu’il faut être prudent avec les données. Vous construisez la moyenne en additionnant toutes les valeurs numériques d’une caractéristique, de sorte que les valeurs élevées comme faibles entrent directement dans son calcul, puis vous la divisez par le nombre de valeurs. Vous construisez la médiane en ordonnant les valeurs de l’élément et en choisissant celle du milieu (si vous disposez d’un grand nombre d’exemples, vous pouvez simplement prendre la moyenne des deux observations du milieu). L’ordre n’est en fait pas affecté par les valeurs hautes ou basses aux extrémités. Beaucoup d’autres statistiques reposent sur un ordre et sont qualifiées de robustes en raison de leur résistance aux anomalies.

En ce qui concerne le problème de la mesure de l’association entre les caractéristiques, les chapitres qui suivent expliquent en détail la métrique de corrélation. Cette métrique, qui est basée sur la moyenne, est influencée par les observations périphériques. Vous disposez plus largement d’un volant de mesures d’association robustes, telles que la corrélation de rang de Spearman, un bon choix lorsque vous voulez vérifier si les classements de deux métriques sont cohérents.

L’écart interquartile est une autre métrique, qui est basé sur l’ordre des échantillons du fait qu’il dérive de la différence entre la valeur des caractéristiques à 75 % de cet ordre, moins leur valeur à 25 %. En calculant l’écart interquartile, vous obtenez la plage de valeurs qui comprenait les 50 % centraux de vos échantillons. Empiriquement, en ajoutant à la limite supérieure et en retirant de la limite inférieure de l’écart interquartile sa valeur multipliée par 1,5, vous devriez avoir une estimation approximative de jusqu’où vous pourriez étendre la plage de la caractéristique avant de vous rendre compte que les valeurs auxquelles vous êtes réellement confronté sont trop élevées ou trop basses. Cette extension de la mesure de l’écart interquartile est à la base de la « boîte à moustaches » (boxplot), une représentation visuelle conçue par John Tukey. Dans ce type de boîte, vous pouvez trouver la médiane, puis les limites de l’écart interquartile décrites comme une boîte, suivies de l’extension de cet écart multiplié par 1,5 dans les deux directions, comme des sortes de moustaches. Enfin, tous les éléments à l’extérieur des moustaches sont tracés, ce qui vous donne des indices sur les valeurs aberrantes suspectes. Par exemple, en Python, vous pouvez utiliser la méthode boxplot à partir d’un DataFrame Pandas (la Figure 11.1 illustre le résultat obtenu) :

from sklearn.datasets import load_boston
import pandas as pd
%matplotlib inline
boston = load_boston()
X, y = boston.data, boston.target
X = pd.DataFrame(X, columns=boston.feature_names)
X.boxplot('LSTAT',return_type='axes')

[picture]

Figure 11.1 Boîte à moustaches de la caractéristique LSTAT du jeu de données Boston.

Le tracé de la Figure 11.1 révèle comment certaines valeurs au-delà de 30 sont en fait suspectées d’être aberrantes. En utilisant l’écart interquartile et des boîtes à moustaches, vous pouvez découvrir la plupart des valeurs aberrantes, mais malheureusement pas toutes, car certaines sont bien cachées dans vos données.

Recours aux modèles multivariés

Une autre stratégie basée sur une découverte visuelle consiste à utiliser des diagrammes de dispersion, qui tracent les données dans un graphique bidimensionnel en utilisant vos caractéristiques deux par deux.

Tracer toutes les combinaisons de vos caractéristiques peut toutefois prendre beaucoup de temps. Une solution peut être de créer une matrice de nuages de points, mais, si vous avez trop de caractéristiques, le problème de la complexité de la représentation ne disparaîtra pas. Dans un tel cas, la restructuration et la compression de vos données peuvent constituer un moyen efficace de réduire la complexité. L’analyse en composantes principales combine des caractéristiques et, ce faisant, élimine des problèmes de redondance et d’accentuation qui touchent plus de caractéristiques en même temps.

Les deux ou trois premières composantes d’une ACP contiennent le plus d’informations à partir de vos données. Vous pouvez facilement les tracer (en utilisant seulement quelques graphiques) et utiliser cette sortie pour découvrir les valeurs aberrantes. Voici le code permettant de tracer les deux premières composantes (comme le montre la Figure 11.2) :

from sklearn.decomposition import PCA
from sklearn.preprocessing import scale
pca = PCA()
pca.fit(scale(X))
C = pca.transform(scale(X))

import matplotlib.pyplot as plt
plt.scatter(C[:,0],C[:,1], s=2**7, edgecolors='white',
           alpha=0.85, cmap='autumn')
plt.grid() # ajoute un quadrillage
plt.xlabel('Component 1') # ajoute une étiquette à l’axe des x
plt.ylabel('Component 2') # ajoute une étiquette à l’axe des y

[picture]

Figure 11.2 Diagramme de dispersion des deux premières composantes d’une ACP du jeu de données Boston.

De plus, il ne faut pas négliger les deux dernières composantes parce qu’elles stockent l’information unique et résiduelle disponible dans les données et qu’elles peuvent, de façon surprenante, révéler des valeurs aberrantes moins évidentes. Ainsi, dans l’exemple suivant, en utilisant Python et le jeu de données Boston, vous ne pouvez repérer que deux groupes périphériques en observant les deux dernières composantes. Voici le code utilisé pour tracer la sortie des deux dernières composantes (voir la Figure 11.3 ; votre sortie peut ne pas être exactement identique) :

plt.scatter(C[:,11],C[:,12], s=2**7, edgecolors='white',
alpha=0.85, cmap='autumn')
plt.grid() # ajoute un quadrillage
plt.xlabel('Component 12') # ajoute une étiquette à l’axe des x
plt.ylabel('Component 13') # ajoute une étiquette à l’axe des y

[picture]

Figure 11.3 Diagramme de dispersion des deux dernières composantes d’une ACP sur le jeu de données Boston.

Une solution alternative à l’ACP consiste à utiliser des techniques de clustering, telles que les K-moyennes. Nous y reviendrons dans le chapitre suivant.

14 https://github.com/epsilon-machine/missingpy/blob/master/missingpy/missforest.py.

15 https://www.statsmodels.org/dev/imputation.html.

16 https://pypi.org/project/autoimpute.

DANS CE CHAPITRE

Comprendre les différences entre les échantillons

•

Grouper les données de manière significative

•

Classification et régression après avoir recherché des voisins dans les données

•

Saisir les difficultés du travail dans un espace de données à haute dimension

Chapitre 12

Exploiter les similitudes

Une rose est une rose. Un arbre est un arbre. Une voiture est une voiture. Même si vous pouvez faire des assertions simples comme celles-ci, un échantillon de chaque type d’élément ne suffit pas pour identifier tous les éléments qui correspondent à cette classification. Après tout, il existe de nombreuses espèces d’arbres et de nombreuses variétés de roses. Si vous évaluez ce genre de problème dans un cadre d’apprentissage automatique, vous trouvez des caractéristiques dont les valeurs changent fréquemment, et des caractéristiques qui persistent systématiquement (par exemple, un arbre est toujours composé de bois et a un tronc et des racines). Lorsque vous regardez de près les valeurs des caractéristiques qui se répètent constamment, vous pouvez deviner que certains objets observés sont à peu près du même type.

Ainsi, les enfants sont capables de découvrir par eux-mêmes ce que sont les voitures en regardant certaines caractéristiques. Après tout, les voitures ont (presque) toutes quatre roues et roulent sur la route. Mais que se passe-t-il quand un enfant voit un autobus ou un camion ? Heureusement, quelqu’un est là pour expliquer ce qu’il en est des cas des grandes voitures, et ouvrir le monde de l’enfant à des définitions plus larges. Dans ce chapitre, vous découvrirez comment les machines peuvent apprendre en exploitant les similitudes dans les éléments suivants :

• Une méthode supervisée : tirer des leçons des échantillons précédents. Par exemple, une voiture a quatre roues ; par conséquent, si un nouvel objet a quatre roues, il pourrait s’agir d’une voiture.

• Une méthode non supervisée : inférer un regroupement sans étiquettes pour apprendre. Si l’on prend l’exemple d’une série d’éléments ayant tous des racines et étant composés de bois, ils devraient être classés dans le même groupe même s’ils n’ont pas de nom.

Les deux algorithmes dont nous allons nous préoccuper, les k-moyennes (K-means en anglais), un algorithme de clustering (ou regroupement) non supervisé, et les k plus proches voisins (ou KNN pour K-Nearest Neighbors en anglais), un algorithme de régression et de classification supervisé, fonctionnent en exploitant les similarités entre les échantillons. Ils offrent un bon aperçu des avantages et des inconvénients de l’organisation du monde en éléments plus ou moins similaires.

Il n’est pas nécessaire de saisir manuellement le code source de ce chapitre. Il est beaucoup plus facile d’utiliser le code source téléchargeable de ce chapitre dans le dossier ML4D2E de la source téléchargeable pour le code Python et dans le dossier ML4D2ER de la source téléchargeable pour le code R. Les fichiers d’exemple de ce chapitre commencent par ML4D2E ; 12 ; (pour Python) ou ML4D2E_R ; 12 ; (pour R). Reportez-vous à l’introduction pour savoir où trouver ces fichiers source.

Mesurer la similarité entre vecteurs

Vous pouvez facilement comparer des échantillons à partir de vos données en utilisant des calculs si vous considérez chacun d’eux comme un vecteur. Les sections suivantes décrivent comment mesurer la similarité entre vecteurs pour effectuer des tâches telles que le calcul de la distance entre des échantillons à des fins d’apprentissage.

Comprendre la similarité

Dans un format vectoriel, vous pouvez voir chaque caractéristique de vos échantillons sous la forme d’une série de coordonnées, chacune pointant vers une position dans une dimension de l’espace différente, comme le montre la Figure 12.1. Si un vecteur a deux éléments, c’est-à-dire qu’il n’a que deux caractéristiques, travailler avec lui revient à vérifier la position d’un objet sur une carte en exploitant le premier nombre pour la position sur l’axe est-ouest et le second sur l’axe nord-Sud.

Par exemple, les nombres entre parenthèses (1,2), (3,2) et (3,3) servent tous à représenter des points. Chacun est une liste ordonnée de valeurs (appelée tuple) qui peut être facilement localisée et affichée sur une carte en utilisant la première valeur de la liste pour la coordonnée x (l’axe horizontal), et la seconde pour la coordonnée y (l’axe vertical).

[picture]

Figure 12.1 Exemples de valeurs tracées sous forme de points sur un graphique.

Le résultat est un nuage de points, et vous en trouverez des exemples dans ce chapitre comme dans le reste du livre.

Si votre jeu de données, sous forme matricielle, a de nombreuses caractéristiques numériques (les colonnes), idéalement ce nombre de caractéristiques représente les dimensions de l’espace de données, tandis que les lignes (les échantillons) représentent chaque point, qui est mathématiquement un vecteur. Lorsque votre vecteur comporte plus de deux éléments, la visualisation devient problématique, car il n’est pas facile de représenter les dimensions supérieures à la troisième (après tout, nous vivons dans un monde tridimensionnel). Cependant, vous pouvez vous efforcer de transmettre davantage de dimensionnalités par un certain expédient, par exemple en utilisant la taille, la forme ou la couleur pour d’autres dimensions. De toute évidence, ce n’est pas une tâche facile, et le résultat est souvent loin d’être intuitif. Cependant, vous pouvez vous faire une idée de l’emplacement des points dans votre espace de données en affichant systématiquement de nombreux graphiques tout en considérant les dimensions deux par deux. De tels diagrammes sont appelés matrices de diagrammes de dispersion.

Ne vous inquiétez pas des questions de dimensionnalité multiple. Vous étendez les règles que vous avez apprises en deux ou trois dimensions à des dimensions multiples, de sorte que si une règle fonctionne dans un espace bidimensionnel, elle fonctionne également dans un espace multidimensionnel (il faut cependant rester prudent en raison de la malédiction de la dimensionnalité expliquée dans la section « Comprendre le paramètre k » , plus loin dans ce chapitre). Par conséquent, tous les exemples se réfèrent d’abord à des éléments bidimensionnels.

Calcul des distances pour l’apprentissage

Un algorithme peut apprendre en se servant de vecteurs de nombres qui utilisent des mesures de distance. Souvent, l’espace impliqué par vos vecteurs est un espace métrique, c’est-à-dire un espace dans lequel les distances respectent certaines conditions spécifiques :

• Il n’existe pas de distances négatives, et votre distance est nulle seulement lorsque le point de départ et le point d’arrivée coïncident (c’est la non-négativité).

• La distance est la même d’un point à un autre et vice versa (c’est la symétrie).

• La distance entre un point initial et un point final est toujours inférieure, ou pire égale, à la distance totale entre le point initial et un troisième point, et de là vers le point final (c’est l’inégalité triangulaire, ce qui signifie qu’il n’y a pas de raccourci ; en géométrie euclidienne, on dit que le chemin le plus court d’un point à un autre est la ligne droite).

Dans un espace métrique, nous avons la distance euclidienne, la distance de Manhattan et la distance de Tchebychev. Ce sont toutes des distances qui peuvent s’appliquer aux vecteurs numériques.

Distance euclidienne

La plus courante est la distance euclidienne, également appelée norme l2 de deux vecteurs¹⁷. Dans un plan bidimensionnel, la distance euclidienne se représente comme la droite reliant deux points, et on la calcule comme étant la racine carrée de la somme de la différence au carré entre les éléments de deux vecteurs. Dans le tracé précédent, la distance euclidienne entre les points (1,2) et (3,3) peut être calculée en Python sous la forme math.sqrt(math.pow((1-3),2)+math.pow((2-3),2)), ce qui donne une distance d’environ 2,236.

Distance de Manhattan

La distance de Manhattan (également décrite comme la norme l1 de deux vecteurs) est une autre mesure utile. On calcule la distance de Manhattan en additionnant la valeur absolue de la différence entre les éléments des vecteurs. Si la distance euclidienne indique le trajet le plus court, la distance de Manhattan montre le trajet le plus long, ressemblant aux directions d’un taxi se déplaçant entre deux pâtés de maisons. Par exemple, la distance de Manhattan entre les points (1,2) et (3,3) est abs(1–3) + abs(2–3), ce qui donne 3.

Distance de Tchebychev

La distance de Tchebychev, ou métrique maximale, prend le maximum de la différence absolue entre les éléments des vecteurs. C’est une mesure de distance qui peut représenter le déplacement d’un roi dans le jeu d’échecs ou, en logistique d’entrepôt, les opérations requises par un pont roulant pour déplacer une caisse d’un endroit à un autre. Dans l’apprentissage automatique, la distance de Tchebychev peut s’avérer utile lorsque vous avez de nombreuses dimensions à prendre en compte et que la plupart d’entre elles ne sont pas pertinentes ou sont redondantes (dans le cas de la distance de Tchebychev, vous choisissez simplement celle dont la différence absolue est la plus grande). Dans l’exemple utilisé dans les sections précédentes, la distance est de 2, ou max(abs(1-3), abs(2-3)).

Utiliser des distances pour localiser les clusters

En travaillant dans un espace bien ordonné, vous trouverez naturellement des éléments similaires les uns à côté des autres, comme des livres sur le même sujet dans une bibliothèque. Dans une bibliothèque, des livres similaires se trouvent sur la même étagère, et dans la même section. Imaginez, par exemple, que vous êtes un bibliothécaire chargé de rassembler tous les livres sur le même sujet sans aucune indication utile d’un index ou d’une étiquette préexistants. Dans ce cas, le fait de regrouper des objets ou des éléments similaires est désigné sous le nom de clustering (de l’anglais cluster, grappe). Dans l’apprentissage automatique, il s’agit d’une tâche non supervisée qui permet de créer des étiquettes lorsque aucune n’est disponible, ou lorsque la création empirique d’une nouvelle étiquette est utile.

Avec l’exemple de la bibliothèque, une bonne solution au problème de l’absence d’index ou d’étiquettes serait de choisir des livres ici et là au hasard, chacun situé dans un rayonnage et sur une étagère différents, puis de chercher des livres similaires dans toutes les directions. Vous pourriez vous éloigner le plus possible du livre original tant que cela fait sens. En peu de temps, en fonction de l’emplacement des livres, vous pouvez partitionner votre bibliothèque en zones homogènes de livres traitant de thèmes similaires. Après avoir lu un seul livre représentatif de chaque zone, vous pourriez facilement et en toute confiance étiqueter tous les livres qui s’y trouvent par sujet.

Dans le même ordre d’idée (en partant d’un élément et en regardant dans toutes les directions à l’intérieur d’une plage donnée), toute une série d’algorithmes statistiques, appelés algorithmes de partition, vous aide à explorer les dimensions des données autour des échantillons de départ en regroupant ceux qui sont similaires. Parmi les algorithmes de partition, k-moyennes est le plus connu et le plus populaire. Il trouve généralement de bonnes solutions en tirant parti de la proximité d’éléments similaires dans un espace de données, en traçant les limites des classes et, en fin de compte, en récupérant toute structure de groupe inconnue. Les k-moyennes permettent d’étiqueter, de synthétiser et parfois de mieux comprendre la dynamique cachée dans les données. Elles peuvent vous aider à atteindre les objectifs suivants :

• étiqueter les échantillons en groupes distincts ;

• créer de nouvelles caractéristiques (les étiquettes des groupes) pour les tâches d’apprentissage supervisées (ces étiquettes sont très utiles en tant que nouvelles caractéristiques dans l’apprentissage à partir de texte ou d’images) ;

• classer les échantillons anomaux en groupes, ce qui aide à les localiser facilement.

K-moyennes n’est pas le seul algorithme capable d’effectuer des tâches de clustering (ou regroupement). Le clustering (aussi connu sous le nom d’analyse de cluster) a une longue histoire, et il y a des algorithmes de différentes sortes pour l’effectuer. Il existe des méthodes de regroupement qui organisent les échantillons en structures arborescentes (regroupement hiérarchique, une approche intéressante, bien que moins évolutive¹⁸). D’autres recherchent les parties denses de l’espace de données (DBScan est une méthode intensive basée sur la densité¹⁹). D’autres cherchent à déterminer si un groupe peut être dérivé de certaines distributions statistiques, telles qu’une gaussienne (les modèles de mélange gaussien sont l’algorithme le plus populaire de ce type de méthode basée sur la distribution²⁰). Parmi tant d’alternatives (non seulement les méthodes, mais aussi les résultats peuvent diverger de manière significative), k-moyennes est devenu un algorithme très performant dans l’apprentissage automatique, et ceci pour de bonnes raisons :

• Il est simple et intuitif à comprendre.

• Il peut être rapide et s’adapte très bien à de grandes quantités de données.

• Il ne nécessite pas de garder trop d’informations en mémoire.

• Sa sortie est utile comme entrée pour d’autres algorithmes d’apprentissage automatique.

Vérifier les hypothèses et les attentes

Les k-moyennes reposent sur des hypothèses que certaines personnes contestent et que vous devez connaître. Tout d’abord, l’algorithme suppose que vos données possèdent des groupes appelés clusters (on parle également de grappes en français ; en fait, toutes les données ne sont pas caractérisées par des clusters naturels). Il suppose également que de tels groupes sont constitués d’échantillons similaires, avec l’élément de départ, appelé prototype ou centroïde, au centre du cluster. Enfin, il suppose que, dans l’espace de données, les groupes ont une forme rigoureusement sphérique. Par conséquent, malheureusement, les k-moyennes n’autorisent pas les formes étranges, ce qui peut être une faiblesse de cette technique, car le monde réel n’est pas toujours géométriquement distribué. Ce sont là des hypothèses théoriques. L’algorithme peut bien fonctionner lorsque les données remplissent les conditions, mais, sinon, vous devez vérifier le résultat de près.

L’algorithme des k-moyennes fonctionne avec des métriques numériques, car il est basé sur la distance euclidienne. Toutes vos données doivent se présenter sous la forme d’un nombre représentant une mesure (techniquement appelée mesure métrique ; par exemple, les mesures métriques quotidiennes de base sont les mètres et les kilos). Vous ne pouvez pas utiliser des caractéristiques dont les valeurs sont attribuées arbitrairement. En d’autres termes, les mesures doivent avoir un rapport avec la réalité. Cependant, même si ce n’est pas idéal, vous pouvez utiliser des nombres ordinaux (comme 1er, 2e, 3e, et ainsi de suite, parce que cela représente un ordre semblable à une mesure). Même si c’est un peu problématique, vous pouvez également utiliser des caractéristiques binaires (1/0), comme cela est décrit dans le chapitre précédent.

La distance euclidienne est la racine d’une grande somme, et donc vos caractéristiques doivent être toutes à la même échelle, car sinon les caractéristiques avec la plus grande plage domineront la distance (et vous créerez des clusters seulement sur ces caractéristiques). La même domination se produit également si certaines caractéristiques sont corrélées, c’est-à-dire qu’elles partagent une partie de leur contenu informatif (variance). Encore une fois, certaines caractéristiques influencent le résultat plus que d’autres. Une solution consiste à transformer vos données avant les k-moyennes en normalisant statistiquement toutes les caractéristiques et en les transformant en composantes par un algorithme de réduction de la dimensionnalité tel que l’analyse en composantes principales (ACP), décrite dans le chapitre précédent.

Les k-moyennes s’attendent également à ce que vous sachiez déjà combien de clusters vos données contiennent. Cependant, ce n’est pas un gros problème si vous ne le savez pas, car vous pouvez estimer ou essayer beaucoup de solutions, en partant de celles qui sont souhaitables. Étant donné que l’algorithme repose sur un grand nombre d’hypothèses théoriques et pratiques, il trouve toujours une solution (c’est la raison pour laquelle tout le monde l’aime tant). Lorsque vous travaillez avec des données sans clusters, ou que vous demandez le mauvais nombre de clusters, cela peut fournir des résultats trompeurs. Vous pouvez distinguer les bons résultats des mauvais en vous basant sur les éléments suivants :

• Heuristique : vous pouvez mesurer la qualité du clustering.

• Reproductibilité : des résultats aléatoires ne peuvent pas être répliqués.

• Compréhensibilité : les solutions absurdes sont rarement des solutions réelles.

• Facilité d’utilisation : vous vous souciez de la façon dont l’apprentissage automatique résout pratiquement les problèmes et vous ne vous souciez pas de sa justesse en termes d’hypothèses.

L’algorithme des k-moyennes est un algorithme non supervisé, donc à moins que vous ne connaissiez la solution du clustering au préalable, vous n’avez aucune erreur à mesurer en termes de déviance ou d’exactitude. Après avoir obtenu une solution, vérifiez toujours la réalité à l’aide des mesures de qualité du clustering pour voir si le résultat :

• est reproductible dans différentes conditions ;

• logique ;

• peut vous aider à résoudre votre problème.

Inspecter le mécanisme de l’algorithme des k-moyennes

L’algorithme des k-moyennes exécute les tâches d’une manière spécifique. En comprenant la procédure que l’algorithme utilise pour effectuer son travail, vous pouvez mieux comprendre comment l’exploiter. Voici le détail de cette procédure :

❶ Après avoir indiqué à l’algorithme qu’il y a k clusters dans les données (où k est un nombre entier), celui-ci choisit k échantillons aléatoires comme étant les centroïdes originaux de vos k grappes.

❷ L’algorithme affecte tous les échantillons à chacun des k clusters en fonction de leur distance euclidienne au centroïde de chacun. Le centroïde le plus proche gagne l’échantillon, qui fait alors partie de son cluster.

❸ Après avoir affecté tous les échantillons aux clusters, l’algorithme recalcule le nouveau centroïde de chacun en faisant la moyenne de tous les éléments qui font partie du groupe. Après le premier tour, les nouveaux centroïdes ne coïncideront probablement plus avec un échantillon réel. À ce stade, considérez plutôt les centroïdes comme des échantillons idéaux (en fait, des prototypes).

❹ Si ce n’est pas le premier tour, après avoir calculé la moyenne, l’algorithme vérifie dans quelle mesure la position des centroïdes a changé dans l’espace de données. S’il n’y a pas eu beaucoup de changement par rapport au cycle précédent, l’algorithme suppose qu’il a une solution stable et il la renvoie. Sinon, il répète les Étapes 2 et 3. Après avoir modifié la position des centroïdes, l’algorithme réassigne une partie des échantillons à un cluster différent, ce qui conduit probablement à un changement dans la position du centroïde.

Étant donné les passages répétitifs entre les Étapes 4 et 2, et ce jusqu’à ce que la sortie vérifie une certaine condition de convergence, il est clair que l’algorithme des k-moyennes est itératif. Itération après itération, les centroïdes initiaux, que l’algorithme choisit au hasard, se déplacent jusqu’à ce que l’algorithme trouve une solution stable (les échantillons ne se déplacent plus entre les clusters, ou du moins peu le font.) À ce stade, une fois que l’algorithme a convergé, vous pouvez vous attendre à ce que :

• Toutes vos données soient séparées en clusters (ainsi chaque échantillon aura une et une seule étiquette de cluster).

• Tous les clusters tendent à avoir le maximum de cohésion interne possible. Vous pouvez calculer la cohésion pour chaque cluster en soustrayant la position du centroïde de celle de chaque échantillon, puis en mettant au carré le résultat de chaque soustraction (donc en enlevant le signe), et enfin en additionnant tous les résultats. Ainsi, vous obtenez la cohésion qui, dans une analyse de cluster, tend toujours à être le minimum possible (mesure de compacité ou WSS, pour within-cluster sum of squares, soit somme des carrés intra-cluster).

• Tous les clusters ont une différence externe maximale possible. Cela signifie que si vous prenez la différence de chaque centroïde avec la moyenne de l’espace de données (le grand centroïde), que vous mettiez au carré chaque différence, et que vous les multipliiez par le nombre d’échantillons de leur cluster respectif, puis que vous additionniez tous les résultats, vous obtenez le maximum possible (mesure de séparation ou BSS, pour between-cluster sum of squares, soit somme des carrés entre clusters).

Comme la somme des carrés entre les clusters dépend du résultat du calcul intra-cluster, vous n’avez besoin d’examiner qu’un seul d’entre eux (généralement, WSS suffit). Parfois, la position de départ tombe mal, et l’algorithme ne converge pas vers une solution appropriée. Les données sont toujours partitionnées, vous ne pouvez donc deviner que l’algorithme a effectué le travail de façon acceptable qu’en calculant la somme des carrés intra-cluster de la solution et en la comparant aux calculs précédents.

Si vous exécutez les k-moyennes plusieurs fois et que vous enregistriez les résultats, vous pouvez facilement repérer les exécutions de l’algorithme avec une valeur WSS trop haute et une valeur BSS trop basse par rapport à ce qui devrait se passer. En fonction de votre puissance de calcul et de la taille du jeu de données, l’exécution de nombreux essais peut prendre beaucoup de temps, et vous devez décider comment trouver le bon compromis entre temps et sécurité dans le choix d’une bonne solution.

Ajuster l’algorithme des k-moyennes

Pour obtenir les meilleurs résultats possible avec l’algorithme des k-moyennes, vous devez l’ajuster. Cela nécessite d’avoir des idées claires des objectifs poursuivis :

• Si le but est exploratoire, arrêtez-vous au nombre de clusters qui définit une solution logique et vous pouvez déterminer quels clusters utiliser en les nommant.

• Si vous travaillez avec des données abstraites, examiner la valeur de WSS ou d’une autre métrique d’ajustement peut vous aider à trouver la bonne solution.

• Si vous avez besoin des résultats du cluster pour alimenter un algorithme supervisé, utilisez la validation croisée pour déterminer la solution qui apporte plus de puissance prédictive.

L’étape suivante exige que vous décidiez d’une implémentation. Le langage Python offre deux versions de l’algorithme dans le package scikit-learn. Le premier est l’algorithme classique, sklearn.cluster. KMeans. Vous pouvez également utiliser une version par mini-lots, sklearn.cluster.MiniBatchKMeans, qui diffère de la version standard en ce sens qu’elle peut calculer de nouveaux centroïdes et réassigner toutes les étiquettes de cluster précédentes sur des parties des données (au lieu de faire tous les calculs après avoir évalué l’ensemble des échantillons).

L’avantage des mini-lots est qu’ils peuvent traiter des données qui ne tiennent pas dans la mémoire disponible de votre ordinateur en récupérant des échantillons par petits morceaux sur disque. L’algorithme traite chaque bloc, met à jour les clusters, puis charge le bloc suivant. Le seul goulot d’étranglement possible est la vitesse de transfert des données. Le processus prend plus de temps que l’algorithme classique, mais, lorsqu’il est terminé (ce qui peut demander quelques passages sur toutes vos données), vous aurez un modèle complet qui n’est pas très différent du modèle que vous auriez pu obtenir en utilisant l’algorithme standard.

sklearn.cluster.MiniBatchKMeans a deuxm éthodes d’ajustement:

• fit : fonctionne avec les données en mémoire et s’arrête après le traitement des informations disponibles en fonction de la taille du lot que vous avez définie à l’aide du paramètre batch_size.

• partial_fit : traite les données en mémoire, mais reste ensuite ouvert pour redémarrer lorsqu’on lui présente de nouvelles données, de sorte qu’il est parfait pour le streaming de données par blocs à partir du disque ou d’un réseau tel qu’Internet.

sklearn.cluster.KMeans offre tous les paramètres standard discutés précédemment : le nombre de clusters (n_clusters) et la méthode d’initialisation (init). De plus, il offre la possibilité de précalculer les distances (precompute_distances). Si le nombre d’itérations effectives est élevé, sklearn.cluster.KMeans calcule les distances de manière répétitive, ce qui gaspille du temps. Si vous avez de la mémoire et besoin de vitesse, il vous suffit de régler precompute_distances sur TRUE, autrement dit de demander à stocker tous les calculs par avance. Vous pouvez également demander à l’algorithme de travailler en parallèle (en réglant n_job sur –1 ou -2 si vous souhaitez qu’un processeur reste libre pour d’autres opérations) lorsqu’il doit créer plusieurs solutions simultanément (à cause de différentes initialisations aléatoires). Les distances précalculées et le traitement en parallèle font de l’implémentation Python la plus rapide actuellement disponible.

Expérimenter la fiabilité des k-moyennes

La première expérience destinée à démontrer les nuances de l’algorithme des k-moyennes utilise un nouveau jeu de données, Palmer Penguins²¹ (les manchots de l’archipel Palmer), dont les données ont été collectées et rendues publiques par le Docteur Kristen Gorman et la station Palmer, Antarctica LTER, membre du Long Term Ecological Research Network (Horst AM, Hill AP, Gorman KB – 2020).

Le jeu de données Palmer Penguins est un jeu de données récent qui, d’un point de vue pédagogique, remplace avantageusement l’ancien jeu de données Iris. Le jeu de données Iris, introduit par le statisticien Ronald Fisher dans l’un de ses articles en 1936, est un exemple populaire de jeu de données concernant trois espèces de fleurs d’iris dont les pétales et les sépales (partie de la fleur qui soutient les pétales lorsqu’elle est en fleur) ont été mesurés. En raison de sa popularité, le jeu de données Iris apparaît dans de nombreux livres et tutoriels, même si ses données n’étaient pas toujours adaptées à de nombreuses tâches statistiques et d’apprentissage automatique.

Le jeu de données Palmer Penguins enregistre le genre, la masse corporelle, la longueur des nageoires, la longueur et la profondeur du bec (appelé culmen) de trois espèces de manchots (Adélie, à jugulaire et Gentoo)²². Ces manchots vivent sur des îles de l’archipel Palmer, dans l’Antarctique. Les exemples ne sont pas équilibrés entre les classes (il y a moins de manchots à jugulaire), mais ils sont équilibrés entre les genres (le comptage concerne les femelles et les mâles).

L’exemple suivant télécharge les données à partir du référentiel du jeu de données. Il répartit ensuite les données entre les caractéristiques (en excluant le genre, mais en conservant toutes les métriques numériques) et les étiquettes. En utilisant LabelEncoder de scikit-learn, l’exemple convertit les noms des espèces de manchots en nombres et affiche leurs encodages.

import numpy as np
import pandas as pd
from sklearn.preprocessing import LabelEncoder

link = "https://github.com/lmassaron/datasets/"
link += "releases/download/1.0/penguins.feather"
penguins = pd.read_feather(link)

features = penguins.iloc[:,1:5]
le = LabelEncoder()
labels = le.fit_transform(penguins.species)
target_names = dict(zip(range(3), le.classes_))
print(target_names)

L’expérience utilise les deux versions des k-moyennes disponibles dans scikit-learn : l’algorithme standard et la version avec mini-lots. Dans le package de scikit-learn, vous devez définir au préalable une caractéristique pour chaque algorithme d’apprentissage, en spécifiant ses paramètres. Par conséquent, vous créez deux caractéristiques, k_means et mb_k_means, qui nécessitent trois clusters, une procédure d’initialisation intelligente (appelée 'k-means++'), et en prenant une valeur élevée pour le nombre d’itérations maximum (ne vous inquiétez pas, l’algorithme est généralement assez rapide). Étant donné que la taille du corps et les diverses mesures sont à des échelles différentes, vous devez d’abord préparer un pipeline qui normalisera les caractéristiques et supprimera toutes les corrélations à l’aide de l’ACP. Ce faisant, vous pouvez être sûr que la mesure de la distance euclidienne calculée par k-moyennes fonctionne bien. Enfin, vous transformez les données et adaptez les caractéristiques, et après un court délai, les calculs se terminent.

from sklearn.pipeline import Pipeline
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import MiniBatchKMeans, KMeans

pca = Pipeline([('scaling', StandardScaler()),
               ('pca', PCA())])

k_means = KMeans(n_clusters=3, init='k-means++',
           max_iter=999, n_init=10, random_state=101)

mb_k_means = MiniBatchKMeans(n_clusters=3,
            init='k-means++', max_iter=999,
            batch_size=30, n_init=10, random_state=101)

pca_features = pca.fit_transform(features)
print('Explained variance:')
print(pca.named_steps['pca'].explained_variance_)

k_means.fit(pca_features)
mb_k_means.fit(pca_features)

Pendant l’exécution, le code affiche la quantité de variance expliquée par les dimensions de l’ACP : 2,75, 0,78, 0,37 et 0,11. La première dimension est dominante et les deux premières dimensions expliquent la majorité de la variance. Le fait d’afficher la sortie bidimensionnelle des résultats permet de comprendre comment l’analyse en clusters a fonctionné. Le code suivant permet d’afficher à l’écran une belle représentation graphique de la distribution des points sur une carte constituée de la première et de la deuxième dimension de l’ACP (Figure 12.2).

import matplotlib.pyplot as plt

plt.scatter(pca_features[:,0], pca_features[:,1],
           s=2**7, c=labels,
           edgecolors='lightgrey', alpha=0.85,
           cmap='Spectral')

plt.xlabel('pca_0')
plt.ylabel('pca_1')
plt.grid()

km_centers = k_means.cluster_centers_
mbkm_centers = mb_k_means.cluster_centers_

plt.scatter(km_centers[:,0], km_centers[:,1],
           s=2**6, marker='s', c='orange',
           edgecolors='black')
plt.scatter(mbkm_centers[:,0], mbkm_centers[:,1],
           s=2**8, marker='*', c='yellow',
           edgecolors='black')

for class_no in range(0,3):
   plt.annotate(target_names[class_no],
        (pca_features[labels==class_no, 0].mean(),
         pca_features[labels==class_no, 1].mean()),
         fontsize=20)
plt.show()

Vous noterez que le graphique affiche aussi les centroïdes – ceux de l’algorithme standard sous forme de carrés et ceux de la version avec mini-lots sous forme d’étoiles – et qu’ils ne diffèrent pas tant que cela. Ce fait démontre comment les différentes procédures d’apprentissage aboutissent à des conclusions presque identiques. Il est parfois étonnant de constater que même des algorithmes différents peuvent aboutir aux mêmes conclusions. Quand cela ne se produit pas, il se peut que vous ayez trop de variance dans les estimations et que chaque algorithme ait une stratégie d’apprentissage très différente. Pourtant, vous remarquerez que les algorithmes d’apprentissage automatique ont souvent tendance à obtenir les mêmes signaux forts (bien qu’à la fin, ils en fassent un usage différent).

[picture]

Figure 12.2 Clusters de manchots tracés sur un graphique basé sur les premières dimensions de l’ACP.

Toutes les informations sur l’algorithme sont maintenant stockées dans les caractéristiques. Par exemple, en saisissant k_means.cluster_ centers_, vous pouvez obtenir toutes les coordonnées des centroïdes élaborées par la procédure des k-moyennes.

Expérimenter la convergence des centroïdes

Bien que vous puissiez maintenant calculer le résultat et savoir que les différentes versions et exécutions des k-moyennes ont tendance à arriver à des solutions similaires, vous devez encore comprendre comment vous pouvez atteindre le résultat. Dans l’expérience de cette section, vous allez suivre l’évolution d’un centroïde (le second) au fil des itérations en visualisant son vecteur de coordonnées étape par étape tout au long de l’optimisation.

import numpy as np
np.set_printoptions(precision=3, suppress=True)

for iteration in range(1, 10):
   k_means = KMeans(n_clusters=3, init='random',
       max_iter=iteration, n_init=1, random_state=0)
   k_means.fit(pca_features)
   print("Iteration: %i - 2nd centroid: %s" %
       (iteration, k_means.cluster_centers_[1]))

Voici les résultats :

 Iteration: 1 - 2nd centroid: [-0.802 0.586 0.243 -0.033]
 Iteration: 2 - 2nd centroid: [-0.513 0.899 0.254 -0.03 ]
 Iteration: 3 - 2nd centroid: [-0.442 0.98 0.297 -0.041]
 Iteration: 4 - 2nd centroid: [-0.408 0.992 0.351 -0.036]
 Iteration: 5 - 2nd centroid: [-0.398 1.004 0.376 -0.035]
 Iteration: 6 - 2nd centroid: [-0.394 1.013 0.384 -0.03 ]
 Iteration: 7 - 2nd centroid: [-0.394 1.013 0.384 -0.03 ]
 Iteration: 8 - 2nd centroid: [-0.394 1.013 0.384 -0.03 ]
 Iteration: 9 - 2nd centroid: [-0.394 1.013 0.384 -0.03 ]

En observant l’ajustement des valeurs au fur et à mesure des itérations, on constate que le taux d’évolution diminue jusqu’aux dernières itérations, lorsque le changement à chaque passage est si faible que vous ne pouvez pas le voir sans utiliser plusieurs décimales.

Le module de clustering de scikit-learn contient toutes les versions présentées ici des k-moyennes ainsi que d’autres algorithmes de clustering. Vous pouvez le trouver à https://scikit-learn.org/stable/modules/clustering.html#clustering.

L’expérience suivante, toujours basée sur scikit-learn, vise à vérifier la capacité de l’algorithme de k-moyennes à deviner les vrais clusters dans le jeu de données Palmer Penguins. Il est parfois difficile de reconstruire les clusters originaux à l’aide d’un algorithme, car il peut manquer les bonnes caractéristiques ou avoir des caractéristiques non pertinentes pour la tâche qui peuvent conduire l’algorithme à une solution différente.

pca = Pipeline([('scaling', StandardScaler()),
               ('pca', PCA())])

k_means = KMeans(n_clusters=3, init='k-means++',
           max_iter=999, n_init=10, random_state=101)

pca_features = pca.fit_transform(features)
k_means.fit(pca_features)

from sklearn.metrics import confusion_matrix

cm = confusion_matrix(y_true=labels,
                     y_pred=k_means.labels_)
cm_str = str(cm).split('\n')
for row in range(3):
    print(f"{target_names[row]:15} {cm_str[row]}")

Le code affiche une matrice de confusion, qui est un outil très utile dans les classifications pour comparer vos prédictions aux étiquettes correctes (ce que l’on appelle la vérité de terrain). Dans une matrice de confusion, on lit un tableau dont les lignes représentent les classes correctes et les colonnes les classes prédites :

Adelie       [[  0 22 124]
Chinstrap    [  0 63  5]
Gentoo       [119  0  0]]

Dans cette analyse en clusters, les espèces Adélie et Gentoo (manchot papou) se concentrent respectivement sur le dernier et le premier cluster (mais 5 Chinstrap, des manchots à jugulaire, sont également classés dans le dernier cluster). Le deuxième cluster (que l’on peut lire dans la deuxième colonne) a une prédominance de manchots à jugulaire, mais un quart des éléments est constitué de manchots Adélie. K-moyennes n’a pas été en mesure de récupérer les classes naturelles correctes à ce moment-là. En général, l’augmentation du nombre de clusters résout ce type de problème, bien que cela puisse générer de nombreux clusters qui ne diffèrent que légèrement les uns des autres. Du point de vue de l’apprentissage automatique, le fait d’avoir plus de clusters n’est pas un problème, mais cela peut rendre la compréhension plus compliquée pour les humains. Cette expérience teste différentes solutions de clustering dans une boucle itérative dont le résultat est illustré à la Figure 12.3.

Étant donné que vous ne souhaitez pas mesurer la solution de clustering idéale sur la base des étiquettes originales des manchots (dans de nombreux problèmes réels, vous appliquez l’analyse de clusters lorsque vous ne disposez d’aucune étiquette de vérité de terrain), l’exemple utilise le score de Calinski et Harabasz. Ce score, également connu sous le nom de rapport de variance, fournit une mesure objective basée sur le rapport entre l’homogénéité du cluster (ce que l’on appelle en statistiques la dispersion intra-cluster) et le degré de séparation des cluster (la dispersion inter-clusters). L’idée centrale du score est que votre solution doit présenter les meilleurs clusters denses et bien séparés.

from sklearn.metrics import calinski_harabasz_score
ch_scores = list()
max_clus = 20
for clus_no in range(2, max_clus+1):
    k_means = KMeans(n_clusters=clus_no,
                    init='k-means++', max_iter=999,

[picture]

Figure 12.3 Représentation graphique du score de Calinski et Harabasz pour différentes solutions de clustering.

                    n_init=10, random_state=101)
   k_means.fit(pca_features)
   ch = calinski_harabasz_score(pca_features,
                                k_means.labels_)
   ch_scores.append(ch)

plt.plot(range(2, max_clus+1), ch_scores, 'o-')
plt.xlabel('no clusters')
plt.ylabel('calinski_harabasz_score')
plt.show()

Une boucle sur de nombreuses solutions permet de tracer le score des différentes solutions. Lorsque vous augmentez le nombre de clusters, le score de Calinski et Harabasz tend généralement à s’améliorer. Cela s’explique par le fait que les petits clusters ont tendance à être plus cohérents et séparés. Les bonnes solutions ne sont pas celles qui ont le score le plus bas, mais plutôt celles où vous remarquez une discontinuité plus ou moins abrupte dans la descente du score (même un simple changement dans la pente). Une discontinuité signifie que l’algorithme a trouvé une différence structurelle notable en recherchant ce nombre de clusters dans les données. Dans cet exemple, une bonne solution semble être d’opter pour cinq clusters, comme le montre la Figure 12.3. Le code teste ensuite cette valeur en calculant une matrice de confusion :

k_means = KMeans(n_clusters=5, init='k-means++',
             max_iter=999, n_init=10, random_state=101)
k_means.fit(pca_features)

cm = confusion_matrix(y_true=labels,
                     y_pred=k_means.labels_)
for row in range(3):
    print(f"{target_names[row]:10} {cm[row,:]}")

Vous obtenez alors une matrice de confusion dans laquelle les espèces de manchots sont mieux séparées :

Adelie      [ 0 60 84 2  0]
Chinstrap   [ 0  0  5 63  0]
Gentoo      [57  0  0 0 62]

Le tracé montre pourquoi une solution à cinq clusters est meilleure que les solutions précédentes en vous aidant à visualiser les centroïdes des clusters sur les deux premières dimensions de l’ACP :

plt.scatter(pca_features[:,0], pca_features[:,1],
         s=2**7, c=k_means.labels_,
         edgecolors='white', alpha=0.85, cmap='winter')

km_centers = k_means.cluster_centers_
plt.scatter(km_centers[:,0], km_centers[:,1],
     s=2**8, marker='*', c='white', edgecolors='black')

plt.xlabel('pca_0')
plt.ylabel('pca_1')
plt.grid()
plt.show()

La solution présentée à la Figure 12.4 montre comment deux centroïdes supplémentaires aident à diviser le nuage de points de gauche où les espèces Adélie et Chinstrap sont tout à fait adjacentes, sans séparation claire.

Lorsque vous travaillez avec des données réelles, vous avez rarement une vérité de terrain pour vos vérifications. Faites confiance à votre intuition et au score de Calinski et Harabasz si vous ne pouvez pas vous fier à un test. Si vous n’êtes pas satisfait des solutions, cherchez des moyens d’ajouter ou de supprimer une ou plusieurs de vos caractéristiques.

Recherche de similitudes avec les k plus proches voisins

Qu’il s’agisse de deviner un nombre ou une classe, l’idée derrière la stratégie d’apprentissage de l’algorithme des k plus proches voisins (KNN, pour K-Nearest Neighbors en anglais) est toujours la même. L’algorithme trouve les observations les plus similaires à celle que vous devez prédire, ce qui vous permet d’obtenir une bonne intuition de la réponse possible en faisant la moyenne des valeurs voisines, ou en choisissant la classe de réponse la plus fréquente parmi celles-ci.

[picture]

Figure 12.4 Espèces de manchots représentées par cinq clusters.

La stratégie d’apprentissage d’un KNN s’apparente assez à la mémorisation. C’est comme si vous vous souveniez de ce que devrait être la réponse lorsque la question présente certaines caractéristiques (sur la base d’échantillons d’entraînement), plutôt que de vraiment connaître la réponse, car vous comprenez la question au moyen de règles de classification spécifiques. Dans un sens, KNN est souvent défini comme un algorithme paresseux parce qu’il n’y a pas d’apprentissage réel au moment de l’entraînement, seulement l’enregistrement des données.

Être un algorithme paresseux implique que KNN est assez rapide pour l’entraînement, mais très lent pour la prédiction. En effet, la plupart des activités de recherche et de calculs sur les voisins se font à ce moment-là. Cela implique également que l’algorithme est très gourmand en mémoire, car vous devez y stocker vos données (ce qui signifie qu’il y a une limite aux applications possibles lorsqu’il s’agit de big data). Dans l’idéal, KNN peut faire la différence lorsque vous travaillez sur la classification et que vous avez de nombreuses étiquettes à traiter (par exemple, lorsqu’un agent logiciel poste une étiquette sur un réseau social ou lorsqu’il propose une recommandation de vente). KNN peut facilement traiter des centaines d’étiquettes, alors que d’autres algorithmes d’apprentissage doivent spécifier un modèle différent pour chaque étiquette.

Habituellement, KNN détermine les voisins d’une observation après avoir utilisé une mesure comme la distance euclidienne (le choix le plus courant) ou de Manhattan (fonctionne mieux lorsque vous avez de nombreuses caractéristiques redondantes dans vos données). Il n’existe pas de règles absolues concernant la meilleure métrique de distance à utiliser. Cela dépend vraiment de l’implémentation dont vous disposez. Vous devez également tester chaque distance comme une hypothèse distincte et vérifier par validation croisée quelle mesure fonctionne le mieux avec le problème que vous résolvez.

Comprendre le paramètre k

Le paramètre k est celui sur lequel vous pouvez travailler pour qu’un algorithme KNN fonctionne bien en termes de prédiction et de régression. La valeur k, un nombre entier, est le nombre de voisins que l’algorithme doit prendre en considération pour trouver une réponse. Plus le paramètre k est petit, et plus l’algorithme s’adaptera aux données que vous lui présentez, ce qui risque d’entraîner un surapprentissage, tout en produisant cependant une délimitation complexe entre les classes. Plus le paramètre k est grand, plus l’algorithme fait abstraction des hauts et des bas des données réelles, ce qui permet d’obtenir des courbes bien plus lisses entre les classes de données, mais au détriment de la prise en compte d’échantillons non pertinents.

En règle générale, essayez d’abord l’entier le plus proche de la racine carrée du nombre d’échantillons disponibles comme paramètre k pour l’algorithme des KNN. Par exemple, si vous avez 1 000 échantillons, commencez par k = 31, puis diminuez cette valeur dans une recherche par quadrillage étayée par une validation croisée.

L’utilisation d’échantillons non pertinents ou inadaptés est un risque qu’un algorithme KNN prend lorsque le nombre d’éléments qu’il utilise pour sa fonction de distance augmente. L’illustration précédente du problème de la dimension des données montre comment calculer un espace de données bien ordonné, comme une bibliothèque dans laquelle vous pourriez chercher des livres similaires dans les mêmes rayonnages, étagères, et sections. Cependant, les choses risquent d’être bien plus compliquées si la bibliothèque a plusieurs étages. À ce moment-là, les livres à l’étage et au rez-de-chaussée ne se ressemblent pas nécessairement ; par conséquent, le fait d’être à proximité mais sur un étage différent, ne garantit pas que les livres sont similaires. Ajouter plus de dimensions affaiblit le rôle de celles qui sont utiles, mais ce n’est que le début de vos problèmes.

Imaginez maintenant que vous avez plus que les trois dimensions de la vie quotidienne (et même quatre si vous considérez le temps). Plus il y a de dimensions, plus vous gagnez bien sûr de place dans votre bibliothèque (comme en géométrie, vous multipliez les dimensions pour avoir une idée du volume). À un certain point, vous aurez tellement d’espace que vos livres pourront être facilement rangés dans l’espace restant. Par exemple, avec 20 caractéristiques binaires représentant votre bibliothèque, vous pourriez avoir 2 à la puissance 20 combinaisons possibles, soit 1 048 576 emplacements différents possibles. C’est formidable d’avoir un million de rangements à votre disposition, mais si vous n’avez pas un million de livres pour les remplir, une bonne partie de votre bibliothèque sera vide. Vous récupérez par conséquent un livre et vous cherchez des livres similaires avec lesquels le ranger. Toutes vos étagères à proximité sont en fait vides, et vous devez alors aller loin avant de trouver un autre rayonnage non vide. Pensez-y bien : vous commencez avec Le Guide du voyageur galactique et vous finissez par avoir un livre sur le jardinage comme son plus proche voisin. C’est la malédiction de la dimensionnalité. Plus il y a de dimensions, plus vous risquez de faire l’expérience d’une fausse similitude, en confondant le lointain et le proche.

L’utilisation de paramètres k ayant la bonne taille permet d’atténuer le problème car plus il y a de voisins à trouver, et plus KNN doit chercher loin – mais vous avez d’autres solutions. L’ACP, qui consiste à supprimer les dimensions les moins significatives, peut comprimer l’espace, le rendre plus dense et éliminer le bruit et les informations non pertinentes et redondantes. En outre, la sélection des caractéristiques peut faire l’affaire, en retenant uniquement les caractéristiques qui peuvent vraiment aider KNN à trouver les bons voisins.

Comme expliqué au Chapitre 9 à propos de la validation des tâches d’apprentissage automatique, une sélection par étapes, vérifiée par validation croisée, peut faire fonctionner un algorithme KNN, car elle ne conserve que les caractéristiques qui sont réellement fonctionnelles pour la tâche.

KNN est un algorithme sensible aux valeurs aberrantes. Les voisins sur les limites de votre nuage de données dans l’espace de données peuvent être des échantillons aberrants, ce qui peut rendre vos prédictions erratiques. Vous devez nettoyer vos données avant de les utiliser. Exécuter un algorithme de k-moyennes en premier lieu peut vous aider à identifier des valeurs aberrantes rassemblées en groupes qui leur sont propres (les valeurs aberrantes aiment rester dans des groupes séparés ; vous pouvez les voir comme des sortes d’ermites dans vos données). De plus, le fait de garder votre voisinage large peut vous aider à minimiser (mais parfois pas à éviter complètement) le problème au détriment d’un ajustement plus faible aux données (plus de biais que de surapprentissage).

Expérimenter avec un algorithme flexible

L’algorithme KNN est mis en œuvre dans scikit-learn (voir https://scikit-learn.org/stable/modules/neighbors.html#neighbors) pour les tâches de classification et de régression. L’algorithme est particulièrement efficace lors de l’utilisation de la distance euclidienne grâce à des algorithmes tels que l’arbre à billes et l’arbre k-d, qui sont plus rapides et moins gourmands en ressources informatiques qu’une recherche brute du voisin le plus proche parmi les exemples d’apprentissage.

L’algorithme KNN est particulièrement populaire pour les systèmes de recommandation, car il obtient la réponse à la préférence d’un client en apprenant des clients similaires. Par exemple, Spotify, le service de streaming musical, et Facebook, l’entreprise de médias sociaux, ont tous deux développé des algorithmes KNN pour leurs activités. Spotify a développé ANNOY (https://github.com/spotify/annoy) et Facebook a publié FAISS²³. Ces deux algorithmes sont open source et incroyablement rapides, même lorsqu’ils sont basés sur des jeux d’entraînement de taille considérable, composés de téraoctets de données.

La clé de l’algorithme est de choisir le meilleur k, le nombre de voisins pour dériver la prédiction. Un k faible produit des estimations avec une variance plus élevée et une plus grande sensibilité aux valeurs aberrantes. D’autre part, des valeurs k plus élevées génèrent des prédictions plus biaisées. Il n’est possible de trouver la bonne valeur de k qu’en testant systématiquement l’effet de différentes valeurs au moyen d’une validation croisée. L’expérience suivante utilise une recherche par quadrillage sur un KNN afin de classer correctement les espèces de manchots. Elle recherche la meilleure valeur de k par validation croisée et affiche ensuite le résultat et la meilleure valeur de k. L’apprentissage des hyperparamètres corrects à l’aide de la validation croisée garantit que vous trouverez la meilleure valeur, non seulement pour les données uniques analysées, mais aussi pour toutes les autres données possibles provenant de la même source.

from sklearn.model_selection import GridSearchCV
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import KFold

knn = KNeighborsClassifier(metric='euclidean')
kfold = KFold(n_splits=10, shuffle=True,
             random_state=0)
param_grid = {"n_neighbors" : range(1, 16)}
experiment = GridSearchCV(knn, param_grid=param_grid,
                         cv=kfold)

Après avoir défini les paramètres de recherche par quadrillage, nous testons d’abord le travail sur les données originales (qui présentent des caractéristiques à différentes échelles).

experiment.fit(features, labels)
print(f"best params: {experiment.best_params_}")
print(
   f"best cv accuracy: {experiment.best_score_:0.3}")

Avec une précision de 0,86 en validation croisée, le k idéal est 1. L’exemple répète maintenant le test en utilisant les données après la normalisation et la transformation de l’ACP :

experiment.fit(pca_features, labels)
print(f"best params: {experiment.best_params_}")
print(
   f"best cv accuracy: {experiment.best_score_:0.3}")

La recherche par validation croisée indique que le réglage de k à la valeur 3 donne la meilleure précision, atteignant une précision validée croisée de 0,99. Comme prévu, en raison des différentes échelles des caractéristiques, la distance euclidienne n’a pas fonctionné en premier avec les données d’origine, mais a permis d’obtenir une classification presque parfaite lorsque le code a égalisé les échelles et supprimé les corrélations entre les caractéristiques.

La deuxième expérience avec l’algorithme des KNN démontre comment un algorithme aussi simple peut apprendre des formes et des arrangements non linéaires d’échantillons dans l’espace de données. Le bloc de code prépare un jeu de données délicat : en deux dimensions, deux classes sont disposées en cercles concentriques, comme le montre la Figure 12.5.

from sklearn.datasets import make_circles, make_blobs

strange_data = make_circles(n_samples=500,
                       shuffle=True, noise=0.15,
                       random_state=101, factor=0.5)
center = make_blobs(n_samples=100, n_features=2,
        centers=1, cluster_std=0.1, center_box=(0, 0))
first_half = np.row_stack((strange_data[0][:250,:],
                          center[0][:50,:]))
first_labels = np.append(strange_data[1][:250],
                        np.array([0]*50))
second_half = np.row_stack((strange_data[0][250:,:],
                           center[0][50:,:]))
second_labels = np.append(strange_data[1][250:],
                         np.array([0]*50))
plt.scatter(first_half[:,0], first_half[:,1], s=2**7,
           c=first_labels, edgecolors='white',
           alpha=0.85, cmap='winter')
plt.grid()
plt.show()

[picture]

Figure 12.5 Le jeu de données en « œil de bœuf », un nuage de points non linéaire difficile à apprendre.

Après avoir construit le jeu de données, vous pouvez tester l’expérience en configurant l’algorithme de classification pour apprendre les modèles dans les données après avoir fixé un voisinage à la valeur 3, réglé les poids pour qu’ils soient uniformes (scikit-learn permet de pondérer les observations moins éloignées lorsqu’il est temps d’en faire la moyenne ou de choisir les observations les plus fréquentes) et choisi la distance euclidienne comme métrique. En fait, les algorithmes de scikit-learn permettent à la fois de régresser et de classer en utilisant différentes métriques, telles que distances euclidiennes, de Manhattan ou de Tchebychev, comme le montre ce code Python :

from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score

kNN = KNeighborsClassifier(n_neighbors=3,
                          weights='uniform',
                          algorithm='auto',
                          metric='euclidean')
kNN.fit(first_half,first_labels)
acc = accuracy_score(y_true=second_labels,
                    y_pred=kNN.predict(second_half))

print(f"Learning accuracy score: {acc:0.3}")

17 Voir la discussion sur les normes l1, l2 et linf à https://rorasa.wordpress.com/2012/05/13/l0-norm-l1-norm-l2-norm-l-infinity-norm.

18 https://scikit-learn.org/stable/modules/clustering.html#hierarchical-clustering.

19 https://scikit-learn.org/stable/modules/clustering.html#dbscan.

20 https://scikit-learn.org/stable/modules/mixture.html#mixture.

21 Pour en savoir plus sur le jeu des données relatives aux manchots de Palmer, consultez le site https://allisonhorst.github.io/palmerpenguins.

22 Voir http://marinebio.net/marinescience/04benthon/AApenguins.htm pour plus d’informations.

23 https://engineering.fb.com/data-infrastructure/faiss-a-library-for-efficient-similarity-search.

DANS CE CHAPITRE

Découvrir des modèles linéaires pour la régression et la classification

•

Limiter l’influence des variables moins utiles et redondantes

•

Sélectionner des sous-ensembles de caractéristiques efficaces

•

Apprendre à partir du big data avec la descente de gradient stochastique

Chapitre 13

Travailler avec des modèles linéaires en toute simplicité

Vous ne pouvez pas avoir une vue d’ensemble des algorithmes d’apprentissage automatique de base sans découvrir la famille des modèles linéaires, un type courant et excellent d’algorithme à utiliser comme point de départ lorsque vous essayez de faire des prédictions à partir de données. Les modèles linéaires comprennent une vaste famille de modèles dérivés de la science statistique, bien que seuls deux d’entre eux, la régression linéaire et la régression logistique, soient fréquemment mentionnés et utilisés (vous pouvez vous faire une idée de l’étendue de la famille des modèles linéaires en jetant un coup d’œil aux modèles proposés par scikit-learn²⁴. Ce chapitre explique comment utiliser les modèles linéaires pour la régression et la classification.

Les statisticiens, les économétriciens et les scientifiques de nombreuses disciplines utilisent depuis longtemps des modèles linéaires pour confirmer leurs théories au moyen de la validation des données et pour obtenir des prévisions pratiques. Une abondante littérature traite de nombreuses applications ainsi que des tests sophistiqués et des mesures statistiques conçus pour vérifier et valider en détail l’applicabilité des modèles linéaires à de nombreux types de problèmes de données. Ce chapitre vous aide à vous concentrer sur des questions telles que la limitation de l’influence des caractéristiques moins utiles et redondantes, la sélection de sous-ensembles de caractéristiques efficaces et l’utilisation de la descente de gradient stochastique (voir la procédure d’optimisation au Chapitre 8) pour travailler avec de grands jeux de données.

Il n’est pas nécessaire de saisir manuellement le code source de ce chapitre. Il est beaucoup plus facile d’utiliser le code source téléchargeable de ce chapitre dans le dossier ML4D2E de la source téléchargeable pour le code Python et dans le dossier ML4D2ER de la source téléchargeable pour le code R. Les fichiers d’exemple de ce chapitre commencent par ML4D2E ; 13 ; (pour Python) ou ML4D2E_R ; 13 ; (pour R). Reportez-vous à l’introduction pour savoir où trouver ces fichiers source.

Commencer à combiner des caractéristiques

La régression a une longue histoire dans différents domaines : statistique, économie, psychologie, sciences sociales et sciences politiques. En plus d’être capable d’un large éventail de prédictions impliquant des valeurs numériques, des classes binaires et multiples, des probabilités et des données de comptage, la régression linéaire aide également à comprendre les différences de groupe, à modéliser les préférences des consommateurs et à quantifier l’importance d’une caractéristique dans un modèle. Les sections suivantes expliquent comment combiner des caractéristiques lors de l’utilisation de diverses formes de régression.

Vue d’ensemble de la régression

Dépouillée de la plupart de ses propriétés statistiques, la régression demeure un algorithme simple, compréhensible mais efficace pour la prédiction des valeurs et des classes. Rapides à entraîner, faciles à expliquer à des non-techniciens, et simples à implémenter dans n’importe quel langage de programmation, la régression linéaire et la régression logistique sont le premier choix de la plupart des praticiens de l’apprentissage automatique lorsqu’ils construisent des modèles à comparer, en tant que solution de base (standard de base), à des modèles plus sophistiqués. Les modèles linéaires sont également utilisés pour déterminer les caractéristiques clés d’un problème, pour expérimenter des combinaisons de caractéristiques et pour obtenir un aperçu des nouvelles procédures de création de caractéristiques.

La régression linéaire fonctionne en combinant la somme pondérée des caractéristiques numériques. L’ajout d’un nombre constant, appelé biais, complète la sommation. Le biais représente la ligne de base de la prédiction lorsque toutes les caractéristiques ont une valeur nulle. Il peut jouer un rôle important dans la production de prédictions par défaut, en particulier lorsque certaines caractéristiques sont manquantes (et ont donc une valeur nulle). Voici la formule courante pour une régression linéaire :

y = βX + α

Dans cette expression, y est le vecteur des valeurs de réponse. Les vecteurs de réponse possibles sont par exemple le prix des maisons dans une ville ou encore la vente d’un produit, ce qui représente simplement toute réponse de type numérique, telle qu’une mesure ou une quantité. Le symbole X indique la matrice des caractéristiques à utiliser pour deviner le vecteur y. X est une matrice qui ne contient que des valeurs numériques. La lettre grecque alpha (α) représente le biais, qui est une constante, tandis que la lettre bêta (β) est un vecteur de coefficients qu’un modèle de régression linéaire utilise avec le biais pour créer la prévision. L’utilisation des lettres grecques alpha et bêta dans la régression est très répandue, au point que la plupart des praticiens évoquent le vecteur de coefficients sous le nom de régression bêta.

Vous pouvez donner un sens à cette expression de différentes façons. Pour simplifier les choses, imaginez que X est en fait composé d’une seule caractéristique (décrite comme un prédicteur dans la pratique statistique), de sorte que vous pouvez le représenter comme un vecteur nommé x. Quand un seul prédicteur est disponible, le calcul est une régression linéaire simple. Maintenant que vous avez une formulation plus simple, vos souvenirs d’algèbre et de géométrie du lycée vous disent que la formulation y=βx+α est une ligne dans un plan avec des coordonnées le long d’un axe x (l’abscisse) et d’un axe y (l’ordonnée).

Lorsque vous avez plusieurs caractéristiques (une régression linéaire multiple), vous ne pouvez plus utiliser un simple plan avec des coordonnées composées de x et y. L’espace s’étend maintenant sur plusieurs dimensions, chaque dimension étant une caractéristique. Votre formule est donc plus complexe, composée de plusieurs valeurs x, chacune pondérée par son propre bêta. Par exemple, si vous avez quatre caractéristiques (de sorte que l’espace global est à cinq dimensions parce que vous avez quatre caractéristiques et une cible), la formule de la régression, telle qu’expliquée à partir de la forme matricielle, est la suivante :

y = β_1x_1 + β_2x_2 + β_3x_3 + β_4x_4 + α

Cette formule complexe, qui existe dans un espace multidimensionnel, n’est plus une ligne, mais plutôt un plan ayant autant de dimensions que l’espace des caractéristiques et de la cible. Il s’agit d’un hyperplan, et sa surface identifie les valeurs de réponse pour chaque combinaison possible de valeurs dans les dimensions des caractéristiques.

Cette discussion explique la régression dans sa signification géométrique, mais vous pouvez aussi la considérer simplement comme une grande somme pondérée. Vous pouvez décomposer la réponse en plusieurs parties, chacune faisant référence à une caractéristique et contribuant dans une certaine proportion au résultat final. La signification géométrique est particulièrement utile pour discuter des propriétés de la régression, mais l’image de la sommation pondérée vous aide à mieux comprendre les exemples pratiques. Par exemple, si vous voulez prédire un modèle pour des dépenses publicitaires, vous pouvez créer un modèle de régression comme celui-ci :

ventes = βpub * publicité + βmag * magasins + βprix * prix+ α

Dans cette formule, les ventes sont la somme pondérée des dépenses publicitaires, du nombre de magasins distribuant le produit et du prix de ce produit. Vous pouvez rapidement démystifier la régression linéaire en expliquant ses composantes. Tout d’abord, vous avez le biais, la constante α, qui sert de point de départ. Ensuite, vous avez trois valeurs de caractéristiques, chacune exprimée dans une échelle différente (la publicité représente beaucoup d’argent, le prix est une valeur abordable, et le nombre de magasins est une quantité positive), et chacune d’elles est recalibrée par son coefficient bêta respectif.

Chaque bêta présente une valeur numérique qui décrit l’intensité de la relation avec la réponse. Il comporte également un signe qui montre l’effet d’un changement dans une caractéristique. Lorsqu’un coefficient bêta est proche de zéro, l’effet de l’élément sur la réponse est faible, mais si sa valeur est loin d’être nulle, soit positive soit négative, l’effet devient significatif et l’élément est important pour le modèle de régression.

Pour obtenir une estimation de la valeur cible, vous calibrez chaque bêta à l’échelle de la mesure de la caractéristique. Une valeur bêta élevée a plus ou moins d’effet sur la réponse en fonction de l’échelle de la caractéristique. Une bonne habitude consiste à normaliser les caractéristiques (en soustrayant la moyenne et en divisant par l’écart-type) pour éviter d’être dupé par des valeurs bêta élevées sur des caractéristiques ayant une petite échelle et pour comparer différents coefficients bêta. Les valeurs bêta obtenues sont comparables, ce qui permet de déterminer celles qui ont le plus d’impact sur la réponse (celles qui ont la plus grande valeur absolue).

Si la valeur bêta est positive, l’augmentation de la caractéristique augmentera aussi la réponse, tandis que la diminution de la caractéristique diminuera la réponse. Inversement, si le bêta est négatif, la réponse sera de sens contraire à la caractéristique : quand l’une augmente, l’autre diminue. Chaque bêta dans une régression représente un impact. En utilisant l’algorithme de descente de gradient discuté au Chapitre 8, la régression linéaire peut trouver le meilleur ensemble de coefficients bêta (et de biais) pour minimiser une fonction de coût donnée par la différence au carré entre les prédictions et les valeurs réelles :

J(w) = \frac{1}{2n}\sum(Xw -y)^2

Cette formule indique le coût J en fonction de w, le vecteur des coefficients du modèle linéaire. Le coût est la somme des différences au carré des valeurs de réponse par rapport aux valeurs prévues (la multiplication Xw) divisée par deux fois le nombre d’observations (n). L’algorithme s’efforce de trouver les valeurs de solution minimales possibles pour la différence entre les valeurs cibles réelles et les prédictions dérivées de la régression linéaire.

Vous pouvez exprimer graphiquement le résultat de l’optimisation sous forme de distances verticales entre les points de données et la ligne de régression. La ligne de régression représente bien la variable réponse lorsque les distances sont faibles. Si vous additionnez les carrés des distances, la somme est toujours le minimum possible lorsque vous calculez correctement la ligne de régression (aucune autre combinaison de bêta n’entraînera une erreur moindre).

Résoudre les problèmes avec une approche d’apprentissage automatique

En statistiques, les praticiens font souvent référence à la régression linéaire par l’expression moindres carrés ordinaires (MCO). On peut aussi estimer différemment la solution, en se basant sur le calcul matriciel (afin de résoudre l’estimation en utilisant une approche analytique, les équations normales de forme fermée, comme l’explique Sebastian Raschka dans cet article : https://sebastianraschka.com/faq/docs/closed-form-vs-gd.html). L’utilisation de cette approche peut s’avérer irréalisable pour de nombreux problèmes, car elle dépend du calcul de l’inversion de la matrice des données, ce qui n’est pas toujours possible. De plus, les calculs inverses des matrices sont assez lents lorsque la matrice d’entrée est grande. Dans l’apprentissage automatique, vous obtenez les mêmes résultats en utilisant l’optimisation par descente de gradient, qui traite plus facilement et plus rapidement de grandes quantités de données, estimant ainsi une solution à partir de n’importe quelle matrice d’entrée.

La seule exigence de la descente de gradient est de standardiser (moyenne nulle et variance unitaire) ou de normaliser (valeurs des caractéristiques comprises entre +1 et −1) les caractéristiques, car le processus d’optimisation est sensible à des caractéristiques dont l’échelle est différente. Les implémentations standard normalisent implicitement (vous n’avez donc pas à vous souvenir de ce détail), mais certaines implémentations spécifiques (comme Stochastic Gradient Decent, SGD, dans scikit-learn : https://scikit-learn.org/stable/modules/sgd.html) requièrent une procédure de mise à l’échelle explicite que vous ne devez pas oublier d’appliquer. Par conséquent, il est prudent de dire qu’il faut toujours normaliser les prédicteurs. La descente de gradient calcule la solution en optimisant les coefficients de manière lente et systématique. Elle base sa formule de mise à jour sur la dérivée mathématique de la fonction de coût :

w_j = w_j - \alpha * \frac{1}{n} \sum(Xw -y)*x_j

Le poids unique wj, relatif à la caractéristique j, est mis à jour en lui soustrayant un terme constitué d’une différence divisée par le nombre d’échantillons (n) et d’un facteur d’apprentissage alpha, qui détermine l’impact de la différence sur le nouveau wj résultant (un petit alpha réduit l’effet de la mise à jour). La partie de la formule qui indique (Xw –y) calcule la différence entre la prédiction du modèle et la valeur à prédire. En calculant cette différence, vous indiquez à l’algorithme la taille de l’erreur de prédiction. La différence est ensuite multipliée par la valeur de la caractéristique j. La multiplication de l’erreur par la valeur de la caractéristique impose une correction du coefficient de la caractéristique proportionnelle à la valeur de cette caractéristique elle-même. Comme les caractéristiques sont regroupées dans une sommation, la formule ne fonctionnera pas avec efficacité, peu importe si vous mélangez des caractéristiques d’échelle différente – plus l’échelle est grande, plus elle domine la sommation. Par exemple, mélanger des mesures exprimées en kilomètres et en centimètres n’est pas une bonne idée dans une descente de gradient, à moins de les transformer en utilisant une normalisation.

Les mathématiques font partie de l’apprentissage automatique. Parfois vous ne pouvez pas éviter d’apprendre des formules parce qu’elles s’avèrent utiles pour comprendre la manière dont les algorithmes fonctionnent. Après avoir démystifié des formules matricielles complexes en les transformant en ensembles de sommations, vous pouvez déterminer comment les algorithmes travaillent en coulisses et agir pour améliorer leur comportement en ayant les bonnes données et des paramètres correctement définis.

L’exemple Python qui suit utilise le jeu de données Boston de scikit-learn pour essayer de deviner les prix des logements à Boston en utilisant une régression linéaire. Le jeu de données contient des informations collectées par le service de recensement américain sur les logements dans la région de Boston, Massachusetts, États-Unis. Le jeu de données a été publié à l’origine par D. Harrison et D.L. Rubinfeld dans « Hedonic prices and the demand for clean air » dans le Journal of Environmental Economics and Management, vol. 5, p. 81-102 (Elsevier, 1978).

Le jeu de données est petit, car il ne compte que 596 cas et 14 attributs. Deux attributs peuvent être utilisés pour les tâches de régression : NOX, le niveau d’oxyde nitreux, et MEDV, la valeur médiane des propriétés résidentielles. Cet exemple utilise la valeur médiane des maisons occupées par leur propriétaire (exprimée en milliers de dollars américains). Le jeu de données présente cependant quelques défauts (voir l’astuce suivante pour découvrir les problèmes au-delà de la petite taille de l’ensemble). Malgré cela, il constitue le meilleur exemple pour apprendre la régression que l’on puisse trouver dans un livre. Pour apprendre par l’expérimentation, utilisez plutôt le jeu de données sur le logement d’Ames (Dean De Cock, 2011, « Ames, Iowa: Alternative to the Boston Housing Data as an End of Semester Regression Project » , Journal of Statistics Education, 19 : 3, DOI²⁵, qui peut être consulté à http://jse.amstat.org/v19n3/decock/AmesHousing.txt.

Cet exemple prédit la valeur des logements. Il tente également de déterminer les caractéristiques qui influencent le plus le résultat. La normalisation des prédicteurs s’avère très utile si vous souhaitez déterminer les caractéristiques influentes. Le code divise les données en jeu d’entraînement et jeu de test, en mélangeant l’ordre de chaque jeu et en les stratifiant à l’aide d’une caractéristique influente afin d’améliorer les résultats du modèle.

import pandas as pd
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import scale

boston = load_boston()
X = pd.DataFrame(scale(boston.data),
                columns = boston.feature_names)
y = boston.target

X_train, X_test, y_train, y_test = train_test_split(X, y,
                 shuffle=True, stratify=X['CHAS']>0,
                 test_size=0.2, random_state=42)

Le jeu de données Boston ne peut pas être analysé tel quel, quel que soit l’algorithme d’apprentissage automatique choisi pour les raisons suivantes :

Il est ordonné, et l’ordre est lié aux valeurs cibles.

Il existe une caractéristique importante, nommée CHAS, qui marque quelques emplacements en limite de la rivière Charles dont la valeur est remarquable. Vous devez la stratifier afin d’avoir quelques exemples liés à la rivière Charles dans les jeux d’entraînement et de test.

Un problème se pose avec MEDV, la variable cible, car cette variable est plafonnée à 50,00 (il n’y a que 16 valeurs de ce type dans le jeu de données). Les valeurs de la caractéristique MEDV sont exprimées en milliers de dollars, de sorte qu’une valeur de 50,00 équivaut à 50 000,00 $.

La classe de régression dans scikit-learn fait partie du module linear_ model. Comme vous avez déjà recalibré les caractéristiques d’entraînement, vous n’avez pas besoin de décider d’autres préparations ou de paramètres spéciaux lorsque vous utilisez cet algorithme.

from sklearn.linear_model import LinearRegression

lm = LinearRegression()
lm.fit(X_train, y_train)

Maintenant que l’algorithme est ajusté, vous pouvez utiliser la méthode score pour afficher la mesure R².

print(f"R2: {lm.score(X_train, y_train):0.3}")

Voici le résultat de la méthode score (qui, par défaut, fournit la valeur R²) :

train R2: 0.735

Comprendre le R au carré

R², également appelé coefficient de détermination, est une mesure allant de 0 à 1, qui montre comment l’utilisation d’un modèle de régression permet de mieux prédire la réponse qu’une simple moyenne. Le coefficient de détermination, qui dérive de la pratique statistique, est directement lié à la somme des erreurs quadratiques (ce qui implique que moins d’erreurs se traduisent par une mesure R² plus élevée). Vous pouvez calculer la valeur R² à la main à l’aide de la formule suivante :

R^2 = 1 - \frac{\sum(Xw - y)^2}{\sum(\overline{y} - y)^2}

La partie supérieure de la division représente la différence habituelle entre la réponse et la prédiction, tandis que la partie inférieure correspond à la différence entre la moyenne de la réponse et la réponse elle-même. Vous mettez au carré et vous additionnez les deux différences pour vos échantillons. En termes de code, vous pouvez facilement traduire cette formulation en commandes de programmation et ensuite comparer le résultat avec ce que le modèle de scikit-learn indique :

import numpy as np

mean_y = np.mean(y_train)
squared_errors_mean = np.sum((y_train - mean_y)**2)
squared_errors_model = np.sum((y_train -
                          lm.predict(X_train))**2)
R2 = 1 - (squared_errors_model / squared_errors_mean)

print (f"Computed R2: {R2}")

Dans ce cas, la valeur de R² sur les données ajustées précédemment est d’environ 0,735, ce qui, d’un point de vue absolu, est assez bon pour un modèle de régression linéaire (les valeurs supérieures à 0,90 sont rares et sont parfois des indicateurs de sérieux problèmes tels que le snooping ou la fuite des données).

Puisque R² se rapporte à la somme des erreurs quadratiques et représente la façon dont les points de données peuvent représenter une droite dans une régression linéaire, il se rapporte également à la mesure de la corrélation statistique. En statistiques, la corrélation est une mesure allant de +1 à –1 qui indique comment deux caractéristiques sont liées linéairement (c’est-à-dire, si vous les tracez ensemble, elles indiquent comment les lignes se ressemblent). Lorsque vous mettez une corrélation au carré, vous obtenez une proportion de la part de variance des deux caractéristiques. De la même façon, peu importe le nombre de prédicteurs dont on dispose, on peut aussi calculer R2 comme étant la quantité d’informations expliquée par le modèle (la même que la corrélation au carré), et donc s’approcher de 1 signifie être capable d’expliquer la plupart des données en utilisant le modèle.

Le calcul de R² sur le même jeu de données que celui utilisé pour l’entraînement est courant en statistiques. En science des données et en apprentissage automatique, il est toujours préférable de tester les résultats sur des données qui ne sont pas employées pour l’entraînement. Les algorithmes complexes peuvent mémoriser les données au lieu d’en tirer des enseignements. Dans certaines circonstances, ce problème peut également se produire lorsque vous utilisez des modèles plus simples, comme la régression linéaire. Pour comprendre ce qui motive les estimations dans le modèle de régression multiple, vous devez examiner l’attribut coefficients_, qui est un tableau contenant les coefficients bêta de régression.

print([feat+':'+str(round(coef, 1)) for feat, coef in
      zip(boston.feature_names, lm.coef_)])

Vous obtenez ce résultat :

['CRIM:-0.6', 'ZN:0.9', 'INDUS:0.2', 'CHAS:0.8',
 'NOX:-2.1', 'RM:2.7', 'AGE:0.2', 'DIS:-2.9',
 'RAD:2.4', 'TAX:-1.8', 'PTRATIO:-2.2', 'B:1.0',
 'LSTAT:-4.0']

La caractéristique DIS, qui contient les distances pondérées jusqu’à cinq centres d’emploi, a le plus grand changement unitaire absolu. Dans l’immobilier, une maison trop éloignée des intérêts des gens (comme le travail) en diminue la valeur. AGE ou INDUS, qui sont à la fois des proportions qui décrivent l’âge du bâtiment et l’existence d’activités non commerciales dans la zone, n’influencent pas autant le résultat ; la valeur absolue de leurs coefficients bêta est beaucoup plus faible.

Mélanger des caractéristiques de différents types

L’outil de régression linéaire, efficace mais simple, pose un certain nombre de problèmes. Parfois, en fonction des données que vous utilisez, ces problèmes peuvent survenir en même temps. Cette section vous indique comment traiter le plus grand nombre d’entre eux facilement et efficacement.

La régression linéaire ne peut modéliser les réponses que sous forme de données quantitatives. Lorsque vous avez besoin de modéliser des catégories comme réponse, vous devez recourir à la régression logistique, abordée plus loin dans ce chapitre. Lorsque vous travaillez avec des prédicteurs, vous obtenez de meilleurs résultats en utilisant des caractéristiques numériques continues, bien que vous puissiez utiliser à la fois des nombres ordinaux et, avec certaines transformations, des catégories qualitatives.

Une caractéristique qualitative (également appelée caractéristique catégorielle) peut exprimer une couleur, telle que la couleur d’un produit ou la profession d’une personne. Chaque caractéristique qualitative possède des valeurs uniques pour chaque classe, comme les différentes couleurs ou professions. Vous avez un certain nombre d’options pour transformer une caractéristique qualitative en utilisant une technique telle que l’encodage binaire (l’approche la plus courante). Lorsque vous fabriquez une caractéristique qualitative binaire, vous créez autant de caractéristiques que de classes dans la caractéristique. Chaque caractéristique contient des valeurs nulles à moins que sa classe n’apparaisse dans les données, auquel cas elle prend la valeur un. Cette procédure est appelée encodage one-hot. Un exemple Python simple utilisant le module de prétraitement de scikit-learn montre comment effectuer un encodage one-hot :

from sklearn.preprocessing import OneHotEncoder
from sklearn.preprocessing import LabelEncoder

lbl = LabelEncoder()
enc = OneHotEncoder(sparse=False)
qualitative = ['red', 'red', 'green', 'blue',
              'red', 'blue', 'blue', 'green']

labels = lbl.fit_transform(qualitative).reshape(-1, 1)

print(enc.fit_transform(labels))

Vous obtenez ainsi une matrice à trois colonnes composées de zéros et de un. Chaque colonne correspond à l’une des étiquettes 'red', 'green', ou 'blue'. Le code a d’abord encodé les étiquettes de la chaîne en nombres. Si vous affichez l’appel à la méthode lbl.classes_, vous obtiendrez la séquence ['blue', 'green', 'red'], ce qui signifie que la première étiquette, 'blue', a reçu la valeur 0 ; la deuxième, 'green', a reçu la valeur 1 ; et la dernière, 'red', a reçu la valeur 2. Ce même ordre numérique est ensuite encodé dans les trois colonnes binaires résultantes, car la première représente la présence de l’étiquette 'blue' dans les données originales, la deuxième l’étiquette 'green' et la troisième l’étiquette 'red'. Vous pouvez vous en assurer en vérifiant la correspondance entre les valeurs originales et les valeurs 1 correspondantes dans la matrice.

En statistiques, quand on veut construire une caractéristique binaire à partir d’une caractéristique catégorielle, on transforme tous les niveaux sauf un, car on utilise la formule de calcul de la matrice inverse, qui a un certain nombre de limitations. Dans l’apprentissage automatique, on utilise la descente de gradient, qui n’est pas basée sur l’opération d’inversion de la matrice, de sorte que l’on transforme tous les niveaux.

Le traitement d’une caractéristique catégorielle n’est pas l’une des transformations de données nécessaires que vous devez appliquer si vous souhaitez exécuter un modèle linéaire (ainsi que de nombreux autres algorithmes d’apprentissage automatique). Si une matrice de données est manquante et que vous ne la traitiez pas correctement, le modèle cessera de fonctionner. Par conséquent, vous devez imputer les valeurs manquantes (par exemple, en remplaçant une valeur manquante par la valeur moyenne calculée à partir de la caractéristique elle-même). Une solution spécifique aux modèles linéaires consiste à utiliser une valeur zéro pour le cas manquant et à créer une caractéristique binaire supplémentaire dont les valeurs unitaires pointent sur les valeurs manquantes dans la caractéristique.

De plus, les valeurs aberrantes (les valeurs en dehors de la plage normale) perturbent la régression linéaire car le modèle tente de minimiser la valeur carrée des erreurs (aussi appelées résidus). Les valeurs aberrantes ont des résidus importants, ce qui oblige l’algorithme à se concentrer davantage sur elles que sur les points réguliers. Vous pouvez utiliser les indications fournies au Chapitre 11 pour détecter et traiter toute valeur aberrante susceptible d’influencer votre modèle linéaire.

La plus grande limitation de la régression linéaire est que le modèle est une sommation de termes indépendants, puisque chaque caractéristique est seule dans cette sommation, multipliée seulement par son propre bêta. Cette forme mathématique est parfaite pour exprimer une situation dans laquelle les caractéristiques ne sont pas liées. Par exemple, l’âge et la couleur des yeux d’une personne ne sont pas des termes connexes parce qu’ils ne s’influencent pas mutuellement. Vous pouvez donc les considérer comme des termes indépendants et, dans la sommation d’une régression, il est logique qu’ils restent séparés. En revanche, l’âge et la couleur des cheveux d’une personne ne sont pas sans rapport, car le vieillissement fait blanchir les cheveux. Lorsque vous placez ces caractéristiques dans une sommation de régression, c’est comme si vous additionniez les mêmes informations. En raison de cette limitation, vous ne pouvez pas déterminer comment représenter l’effet des combinaisons de caractéristiques sur le résultat.

En d’autres termes, vous ne pouvez pas représenter des situations complexes avec vos données. Comme le modèle est constitué de simples combinaisons de caractéristiques pondérées, il exprime plus de biais que de variance dans ses prédictions. En fait, après avoir ajusté les valeurs des résultats observés, la solution proposée par les modèles linéaires est toujours un mélange proportionnellement recalibré de caractéristiques. Malheureusement, vous ne pouvez pas représenter fidèlement certaines relations entre une réponse et une caractéristique en utilisant cette approche. Dans de nombreux cas, la réponse dépend des caractéristiques d’une manière non linéaire : certaines valeurs de caractéristiques agissent comme des obstacles, après quoi la réponse augmente ou diminue soudainement, se renforce ou s’affaiblit, voire s’inverse.

Par exemple, considérez la façon dont la taille des êtres humains évolue depuis leur enfance. Si on observe ce qui se passe dans une certaine tranche d’âge, la relation entre l’âge et la taille est d’une certaine manière linéaire : plus l’enfant grandit (en âge), et plus il grandit (en taille). Cependant, certains enfants grandissent plus (taille plus importante) et d’autres grandissent plus vite (croissance plus rapide). Cette observation vaut lorsque l’on s’attend à ce qu’un modèle linéaire trouve une réponse moyenne. Cependant, passé un certain âge, les enfants cessent de grandir et leur taille reste constante pendant une longue période de leur vie, diminuant lentement à un âge plus avancé. Il est clair qu’une régression linéaire ne peut pas saisir une telle relation puisqu’elle est en fait non linéaire (on pourrait la représenter par une courbe plus ou moins parabolique).

Le montant dépensé pour les produits de consommation courante est un autre exemple lié à l’âge. Dans les premières phases de leur vie, les gens ont tendance à dépenser moins. Les dépenses augmentent au milieu de l’existence (en raison de la disponibilité d’un revenu plus important et/ou de dépenses plus élevées en raison des obligations familiales), mais elles diminuent (généralement) de nouveau dans la dernière partie de la vie (il s’agit clairement d’une autre relation non linéaire). Observer et penser plus intensément le monde qui nous entoure montre qu’il existe de nombreuses relations non linéaires.

Comme la relation entre la cible et chaque caractéristique prédictive est basée sur un seul coefficient, vous n’avez pas la possibilité de représenter des relations complexes comme une parabole (une valeur unique de x maximisant ou minimisant la réponse), une croissance exponentielle ou une courbe non linéaire plus complexe, à moins bien entendu d’enrichir la caractéristique. La façon la plus simple de modéliser des relations complexes consiste à utiliser des transformations mathématiques des prédicteurs à l’aide de développements polynomiaux. Le développement polynomial, pour un certain degré d, crée des puissances de chaque caractéristique jusqu’à la puissance d et d combinaisons de tous les termes. Par exemple, si vous commencez par un modèle linéaire simple tel que le code Python suivant :

y = β₁x₁ + β₂x₂ + α

et que vous utilisiez ensuite un développement polynomial du second degré, ce modèle devient :

y = β₁x₁ + β₂x₂ + α + β₃(x₁)² + β₄(x₂)² + β₅x₁x₂

Vous opérez l’addition en partant de la formulation originale en utilisant les puissances et les combinaisons des prédicteurs existants. Plus le degré du développement polynomial augmente, et plus le nombre de termes dérivés augmente aussi. L’exemple Python suivant utilise le jeu de données Boston (dans la continuité des exemples précédents) pour vérifier l’efficacité de la technique. En cas de succès, l’expansion polynomiale peut mettre en évidence des relations non linéaires dans les données et éventuellement résoudre les problèmes initiaux de non-séparabilité entre les classes (le problème rencontré avec l’algorithme du perceptron au Chapitre 10) aux dépens d’un nombre accru de prédicteurs.

from sklearn.preprocessing import PolynomialFeatures

pf = PolynomialFeatures(degree=2)
poly_X_train = pf.fit_transform(X_train)

lm = LinearRegression()
lm.fit(poly_X_train, y_train)
r2 = reg_lm.score(poly_X_train, y_train)

print(f"train R2: {r2:0.3}")

Le R² résultant du modèle sur les données ajustées est maintenant de 0,934, une amélioration notable par rapport au 0,735 précédent, ce qui implique que le développement polynomial peut mieux représenter certaines caractéristiques. Cependant, lorsque vous essayez d’utiliser le développement polynomial sur le jeu de test, vous remarquez que le résultat n’est pas beaucoup plus satisfaisant que celui obtenu avec un modèle sur les données ordinaires :

poly_X_test = pf.transform(X_test)
r2 = lm.score(poly_X_test, y_test)
print(f"test R2: {r2:0.3}")

Le développement polynomial n’apporte pas toujours d’avantages au jeu de test. En augmentant le nombre de caractéristiques, vous réduisez le biais des prédictions au détriment de l’augmentation de leur variance. Un développement trop important peut entraver la capacité du modèle à représenter des règles générales, ce qui le rend incapable d’effectuer des prédictions sur de nouvelles données.

Passer aux probabilités

Jusqu’à présent, le chapitre n’a pris en compte que les modèles de régression, qui expriment des valeurs numériques en tant que sorties produites par l’apprentissage à partir des données. La plupart des problèmes, cependant, nécessitent également une classification. Les sections suivantes expliquent comment vous pouvez traiter à la fois les sorties numériques et les sorties de classification.

Spécifier une réponse binaire

Une solution à un problème impliquant une réponse binaire (le modèle doit choisir entre deux classes possibles) serait de coder un vecteur de réponse sous la forme d’une séquence de un et de zéros (ou de valeurs positives et négatives, tout comme le fait le perceptron). Le code Python suivant prouve à la fois la faisabilité et les limites de l’utilisation d’une réponse binaire :

from sklearn.linear_model import LinearRegression

import numpy as np

y = np.array([0, 0, 0, 0, 1, 1, 1, 1])

X = np.array([1, 2, 3, 4, 5, 6, 7, 8]).reshape(8,1)

lm = LinearRegression()

lm.fit(X, y)

preds = lm.predict(X)

for y_true, y_pred in zip(y, preds):

    print(f"{y_true} -> {y_pred:+0.3}")

Les résultats vont de moins zéro à plus de la valeur un, mais la plupart sont loin des valeurs cibles de zéro et de un :

0 -> -0.167
0 -> +0.0238
0 -> +0.214
0 -> +0.405
1 -> +0.595
1 -> +0.786
1 -> +0.976
1 -> +1.17

Dans les statistiques, la régression linéaire ne peut pas résoudre les problèmes de classification, car cela créerait une série d’hypothèses statistiques violées. Ainsi, pour les statistiques, l’utilisation de modèles de régression à des fins de classification est principalement un problème théorique et non pas un problème pratique. Dans l’apprentissage automatique, le problème de la régression linéaire est qu’elle sert de fonction linéaire qui essaie de minimiser les erreurs de prédiction. Par conséquent, selon la pente de la ligne calculée, elle peut ne pas être capable de résoudre le problème des données.

Lorsqu’une régression linéaire est chargée de prédire deux valeurs, telles que 0 et +1, qui représentent deux classes, elle essaie de calculer une ligne qui donne des résultats proches des valeurs cibles. Dans certains cas, même si les résultats sont précis, la sortie est trop éloignée des valeurs cibles, ce qui force la ligne de régression à s’ajuster afin de minimiser les erreurs additionnées. Le changement entraîne moins d’erreurs de déviance au total, mais plus de cas mal classés.

Contrairement à l’exemple du perceptron présenté au Chapitre 10, la régression linéaire ne produit pas des résultats acceptables lorsque la priorité est la précision de la classification, comme le montre l’exemple de code précédent. Par conséquent, elle ne marchera pas de manière satisfaisante dans de nombreuses tâches de classification. La régression linéaire fonctionne mieux sur un continuum d’estimations numériques. Toutefois, pour les tâches de classification, vous avez besoin d’une métrique plus appropriée, telle que la probabilité d’appartenance à une classe.

Grâce à la formule suivante, vous pouvez transformer des estimations numériques par régression linéaire en probabilités qui sont plus aptes à décrire la manière dont une classe correspond à une observation :

p(y = 1) = \frac{exp(r)}{(1 + exp(r))}}

Dans cette formule, la cible est la probabilité que la réponse y corresponde à la classe 1. La lettre r est le résultat de la régression, la somme des caractéristiques pondérées par leurs coefficients. La fonction exponentielle, exp(r), correspond au nombre d’Euler e élevé à la puissance de r. Une régression linéaire utilisant cette formule de transformation (également appelée fonction de liaison) pour changer ses résultats en probabilités est une régression logistique.

La régression logistique est la même qu’une régression linéaire, sauf que les données y contiennent des nombres entiers indiquant la classe relative à l’observation et qu’une fonction de coût différente est optimisée (la fonction de coût de l’entropie croisée qui est spécifique aux probabilités). À titre d’exemple, en utilisant le jeu de données Boston du module scikit-learn, vous pouvez essayer de deviner ce qui rend les maisons d’un secteur trop chères (les 10 % les plus chers correspondent à des valeurs médianes supérieures ou égales à 35) :

from sklearn.linear_model import LogisticRegression

lr = LogisticRegression()
lr.fit(X_train, y_train>=35)

from sklearn.metrics import accuracy_score
in_sample_acc = accuracy_score(y_train>=35,
                              lr.predict(X_train))
out_sample_acc = accuracy_score(y_test>=35,
                              lr.predict(X_test))

print(f'In-sample accuracy: {in_sample_acc:0.3}')
print(f'Out-of-sample accuracy: {out_sample_acc:0.3}')

L’exemple teste l’efficacité de la régression logistique en termes de précision (pourcentage de prédictions correctes) pour les jeux d’entraînement et de test, ce qui donne environ 0,963 pour l’entraînement en échantillon et 0,951 pour l’entraînement hors échantillon. Les coefficients calculés indiquent la probabilité qu’une classe particulière fasse partie de la classe cible (qui représente toute classe codée à l’aide d’une valeur de 1). Si un coefficient augmente la probabilité, il sera positif ; dans le cas contraire, le coefficient est négatif.

for var, coef in zip(X_train.columns, lr.coef_[0]):
    print(f"{var:7} : {coef:+0.3}")

La séquence des coefficients apparaît à l’écran :

CRIM   : +0.337
ZN     : -0.0501
INDUS  : -0.196
CHAS   : -0.0514
NOX    : -0.334
RM     : +1.3
AGE    : +0.201
DIS    : -0.682
RAD    : +1.19
TAX    : -0.254
PTRATIO : -0.713
B      : +0.269
LSTAT  : -2.63

Vous pouvez lire la signification de chaque caractéristique dans le guide du jeu de données sur le logement à Boston de l’université de Toronto²⁶.

À la lecture des résultats, on constate qu’à Boston, la criminalité (CRIM) a un certain effet sur les prix, mais pas celui auquel on pourrait s’attendre. En effet, lorsqu’on interprète les coefficients, on les lit en regardant leur signe et leur valeur absolue. Le signe positif de CRIM indique que plus le taux de criminalité est élevé, plus la probabilité d’avoir une zone où le prix médian est d’environ 35 est élevée. Payer plus cher pour un logement dans une zone où le taux de criminalité est plus élevé n’a pas beaucoup de sens d’un point de vue logique, et c’est l’une des raisons pour lesquelles il faut que des personnes examinent les données et les résultats à la recherche de corrélations illogiques. En ce qui concerne l’augmentation de la probabilité, on applique la fonction d’exponentiation (np.exp) à la valeur du coefficient et on obtient le nombre de fois que la caractéristique augmente ou diminue la probabilité moyenne. Dans ce cas, np.exp(+0,337) donne environ 1,4, ce qui correspond à l’augmentation de la probabilité. Cependant, le niveau de pauvreté (LSTAT), l’accessibilité aux autoroutes (RAD) et le nombre moyen de pièces par logement (RM) ont tous des effets beaucoup plus importants et le signe du coefficient est correct.

Ne vous laissez pas abuser par les coefficients d’un modèle linéaire. Le fait que le modèle soit une combinaison de caractéristiques multipliées par des coefficients donne à beaucoup de gens l’impression qu’ils peuvent extraire une certaine signification d’un modèle de régression. En réalité, un modèle de régression est une optimisation mathématique, et de nombreuses caractéristiques sont fortement corrélées à d’autres, ce qui peut brouiller leur rôle dans le modèle. Par conséquent, les coefficients que vous obtenez peuvent ne représenter qu’un effet secondaire de la procédure d’optimisation, alors que d’autres caractéristiques corrélées ont le bon signe et la bonne valeur. Dans ce cas, CRIM est fortement corrélé avec RM et RAD (leur corrélation est supérieure à 0,9, et vous voyez les effets de la multicolinéarité, c’est-à-dire un ensemble de caractéristiques qui sont d’une certaine manière similaires l’une à l’autre). Par conséquent, ces caractéristiques ont probablement éclipsé le coefficient CRIM.

En outre, contrairement à la régression linéaire, la régression logistique ne produit pas simplement la classe résultante (dans ce cas un 1 ou un 0), mais estime également la probabilité que l’observation fasse partie de l’une des deux classes.

print('classes:',lr.classes_)
print('\nProbs:\n',lr.predict_proba(X_test)[:3,:])

Vous obtenez les étiquettes des classes et les probabilités correspondantes pour les trois premiers cas du jeu de test :

classes: [False True]

Probs:
 [[0.98071307 0.01928693]
 [0.99819753 0.00180247]
 [0.84429169 0.15570831]]

Dans le jeu de test, le premier exemple a une probabilité de 98 % de ne pas être une zone d’habitation chère. Lorsque vous effectuez des prédictions à l’aide de cette approche, vous connaissez également la probabilité que votre prévision soit exacte et vous pouvez agir en conséquence, en ne choisissant que des prédictions ayant le bon niveau d’exactitude. Par exemple, vous pourriez ne choisir que des prédictions dont la probabilité dépasse 80 %, bien que la régression logistique fonctionne normalement avec un seuil de probabilité de 50 %.

En utilisant les probabilités, vous pouvez deviner une classe (la plus probable), mais vous pouvez aussi classer toutes vos prédictions dans cette classe. Cela est particulièrement utile dans le cas d’analyses à des fins médicales, en classant une prédiction en termes de probabilité par rapport à d’autres cas.

Gérer plusieurs classes

Dans un problème précédent, l’algorithme des k plus proches voisins (KNN) a automatiquement déterminé comment gérer un problème avec des classes multiples (le Chapitre 12 présente un exemple montrant comment deviner une solution unique à partir de trois espèces de manchots). La plupart des algorithmes qui prédisent des probabilités ou un score pour une classe traitent automatiquement les problèmes multiclasses en utilisant deux stratégies différentes :

• Une contre le reste (ou OvR, pour One Versus Rest) : l’algorithme compare chaque classe avec les autres classes, en construisant un modèle pour chacune d’entre elles. La classe avec la probabilité la plus élevée est la classe choisie. Dans ces conditions, si un problème a trois classes à deviner, l’algorithme utilise également trois modèles. Ce livre utilise la classe OneVsRestClassifier de scikit-learn pour démontrer cette stratégie.

• Une contre une (ou OvO, pour One Versus One) : l’algorithme compare chaque classe à chaque classe restante, en construisant un nombre de modèles équivalent à n*(n-1)/2, où n est le nombre de classes. Dans ces conditions, si un problème a cinq classes à deviner, l’algorithme utilise dix modèles. Ce livre utilise la classe OneVsOneClassifier de scikit-learn pour démontrer cette stratégie. La classe qui gagne le plus est celle qui est choisie.

OvR est la stratégie multiclasse par défaut avec la régression logistique.

Deviner les bonnes caractéristiques

Le fait d’avoir de nombreuses caractéristiques à utiliser peut sembler répondre aux besoins de l’apprentissage automatique pour comprendre pleinement un problème. Cependant, le fait simplement d’avoir des caractéristiques ne résout rien : vous avez besoin des bonnes caractéristiques pour résoudre les problèmes. Les sections qui suivent expliquent comment vous assurer que vous disposez précisément des bonnes caractéristiques lorsque vous effectuez des tâches d’apprentissage automatique.

Définir le résultat des caractéristiques qui ne fonctionnent pas ensemble

Comme cela a été mentionné précédemment, le fait d’avoir de nombreuses caractéristiques et de les faire fonctionner ensemble peut indiquer à tort que votre modèle marche bien alors que ce n’est pas le cas. À moins que vous n’utilisiez la validation croisée, des métriques d’erreur telles que R² peuvent être trompeuses parce que le nombre de caractéristiques peut facilement les faire gonfler, même si la caractéristique ne contient pas d’information pertinente. L’exemple suivant montre ce qui arrive à R² lorsque vous n’ajoutez que des caractéristiques aléatoires.

r2_train = list()
r2_test = list()

for i in range(1, 129):
   X_train_extra = np.column_stack((X_train,
                   np.random.rand(len(X_train), i)))
   X_test_extra = np.column_stack((X_test,
                  np.random.rand(len(X_test), i)))
   lr = LinearRegression()
   lr.fit(X_train_extra, y_train)
   r2_train.append(lr.score(X_train_extra, y_train))
   r2_test.append(lr.score(X_test_extra, y_test))

Dans cet exemple, le code entraîne à nouveau un modèle de régression linéaire sur les données de Boston, après avoir ajouté au jeu d’entraînement et au jeu de test de nouvelles caractéristiques aléatoires. L’exemple enregistre le R² pour les deux jeux au fur et à mesure que le nombre de caractéristiques aléatoires augmente. Si vous tracez les résultats, ce qui semble être une augmentation de la capacité de prédiction en échantillon n’est en fait qu’une illusion. En vérifiant le jeu de test, vous constatez en fait que les performances du modèle ont diminué, comme le montre la Figure 13.1.

import matplotlib.pyplot as plt

fig, ax = plt.subplots(dpi=120)
plt.plot(r2_train, label='train')
plt.plot(r2_test, label='test')
plt.xlabel("additional random features")
plt.ylabel("R2")
plt.show()

[picture]

Figure 13.1 L’ajout de caractéristiques aléatoires augmente les performances en échantillon, mais dégrade les résultats des tests.

Ce qui semble être une capacité prédictive accrue n’est en réalité qu’une illusion. Vous pouvez révéler ce qui s’est passé en contrôlant le jeu de test et en découvrant que les performances du modèle ont diminué.

Résoudre le problème du surapprentissage en utilisant la sélection gloutonne

Non seulement les caractéristiques non pertinentes, telles que le bruit aléatoire, mais aussi les caractéristiques redondantes peuvent aussi jouer un rôle dans la performance prédictive du modèle et dans votre capacité à comprendre un modèle linéaire. Les sections précédentes reconnaissent que le fait d’avoir trop d’informations dans votre modèle linéaire peut être un problème :

Les caractéristiques non pertinentes peuvent nuire aux capacités de prédiction d’un modèle.

Le développement polynomial peut produire un bon ajustement de l’entraînement, mais un mauvais ajustement du test, ce qui indique qu’un surapprentissage s’est produit en raison du grand nombre de caractéristiques redondantes créées par le développement.

La régression logistique montre que le coefficient CRIM est contre-intuitif en raison de sa corrélation avec d’autres caractéristiques.

Pour résoudre ces problèmes, il est possible de simplifier le jeu d’entraînement en choisissant uniquement l’ensemble de caractéristiques le plus prédictif. Pour ce faire, vous pouvez aborder le problème en utilisant la validation croisée (qui garantit que votre choix est optimal) et une série d’outils fournis par scikit-learn. Le premier outil est la sélection récursive, qui permet de tester l’élimination, une à une, des caractéristiques les moins efficaces du jeu d’entraînement. En tant qu’heuristique, la sélection récursive utilise la valeur absolue du coefficient de régression pour éliminer progressivement les caractéristiques ayant le moins d’impact. Lorsqu’il ne reste plus que quelques caractéristiques, l’algorithme examine la progression de l’élimination et trouve l’ensemble des caractéristiques qui permettent au modèle d’obtenir de meilleures performances lors de la validation croisée. Dans cet exemple, vous partez du développement polynomial précédent et déterminez s’il est possible de réduire le développement à un ensemble de caractéristiques plus petit et plus prédictif :

from sklearn.feature_selection import RFECV
from sklearn.preprocessing import PolynomialFeatures

pf = PolynomialFeatures(degree=2)
poly_X_train = pf.fit_transform(X_train)
poly_X_test = pf.transform(X_test)
selector = RFECV(estimator=LinearRegression(), step=1,
                cv=3, scoring='r2')

selector.fit(poly_X_train, y_train)

n_features = selector.n_features_
print(f"Features reduced to {n_features} from "
      "{poly_X_train.shape[1]}")

Maintenant que la sélection est terminée, vous pouvez la tester à la fois sur le jeu d’entraînement et sur le jeu de test. Remarquez à quel point les performances de prédiction se sont améliorées (elles atteignent maintenant un R² de 0,826 par rapport à la valeur d’environ 0,6 indiquée à la Figure 13.1) :

r2_train = selector.score(poly_X_train, y_train)
print(f"R2 train: {r2_train:0.3}")

r2_test = selector.score(poly_X_test, y_test)
print(f"R2 test: {r2_test:0.3}")

Régler le problème du surapprentissage par la régularisation

La régularisation est une solution efficace, rapide et facile à mettre en œuvre lorsque vous disposez de nombreuses caractéristiques et que vous souhaitez réduire la variance des estimations due à la multicolinéarité entre vos prédicteurs. La régularisation fonctionne en ajoutant une pénalité à la fonction de coût. La pénalisation est une sommation des coefficients. Si les coefficients sont au carré (de sorte que les valeurs positives et négatives ne peuvent pas s’annuler l’une l’autre), il s’agit d’une régularisation L2 (aussi appelée régression Ridge). Lorsque vous utilisez la valeur absolue du coefficient, c’est une régularisation L1 (aussi appelée Lasso).

Cependant, la régularisation ne fonctionne pas toujours parfaitement. La régularisation L2 conserve toutes les caractéristiques du modèle et équilibre la contribution de chacune d’elles. Dans une solution L2, si deux caractéristiques sont bien corrélées, chacune contribue également à la solution pour une partie, alors que sans régularisation, leur contribution commune aurait été inégalement répartie.

Par ailleurs, L1 apporte des caractéristiques fortement corrélées hors du modèle en rendant leur coefficient nul, proposant ainsi une réelle sélection parmi les caractéristiques. En fait, mettre le coefficient à zéro, c’est comme exclure la caractéristique du modèle. Lorsque la multicolinéarité est élevée, le choix du prédicteur à mettre à zéro devient un peu aléatoire, et vous pouvez obtenir diverses solutions marquées par des caractéristiques différemment exclues. Une telle instabilité de la solution peut s’avérer gênante, ce qui rend la solution L1 préférable lorsque vous cherchez également à déterminer quelles caractéristiques sont importantes pour le modèle.

Des chercheurs ont trouvé une réponse en créant diverses solutions basées sur la régularisation L1, puis en examinant comment les coefficients se comportent entre ces solutions. Dans ce cas, l’algorithme ne choisit que les coefficients stables (ceux qui sont rarement mis à la valeur zéro). Vous pouvez en apprendre plus sur cette technique sur le site Web des projets compatibles avec scikit-learn²⁷ ainsi que sur le blog https://thuijskens.github.io/2018/07/25/stability-selection/. L’exemple suivant modifie l’exemple des développements polynomiaux en utilisant la régularisation L2 (régression Ridge) et réduit l’influence des coefficients redondants créés par la procédure de développement. Le degré de régularisation étant contrôlé par le paramètre alpha de RidgeCV (plus alpha est élevé, plus la régularisation est importante), l’exemple valide quel alpha est le meilleur pour votre modèle à l’aide d’une procédure de validation croisée interne. Il suffit de fournir un jeu de valeurs alpha à tester :

from sklearn.linear_model import RidgeCV

l2_lm = RidgeCV(alphas=[0.001, 0.01, 0.1, 1, 10, 100],
               normalize=True, cv=5, scoring='r2')
l2_lm.fit(X_train, y_train)

best_alpha = l2_lm.alpha_
r2_test = l2_lm.score(X_test, y_test)

print(f"best alpha is: {best_alpha}")
print(f"test R2: {r2_test:0.3}")

Voici le résultat de ce code :

best alpha is: 0.1
test R2: 0.753

Après avoir terminé l’ajustement des données d’entraînement et testé la performance prédictive, vous pouvez voir comment les coefficients obtenus ont changé :

for var, coef in zip(X_train.columns, l2_lm.coef_):
   print(f"{var:7} : {coef:+0.3}")

Étant donné que L2 tend à abaisser et à rééquilibrer les coefficients d’un modèle de régression linéaire dans le cas de caractéristiques corrélées, les coefficients obtenus sont désormais plus faciles à interpréter, et vous ne devriez plus trouver de signes ou de valeurs contre-intuitifs comme c’était le cas auparavant :

CRIM   : -0.438
ZN     : +0.546
INDUS  : -0.247
CHAS   : +0.886
NOX    : -1.32
RM     : +2.79
AGE    : +0.0356
DIS    : -2.09
RAD    : +1.02
TAX    : -0.718
PTRATIO : -1.88
B      : +0.943
LSTAT  : -3.49

L’exemple suivant utilise la régularisation L1, qui tend à ramener les coefficients à zéro lorsqu’ils sont redondants (L2 se contente de les réduire). Les résultats ne sont pas très différents dans ce cas de L2. Cette fois, l’exemple utilise la classe LassoCV de scikit-learn, qui est analogue à RidgeCV mais applique la régularisation L1 au lieu de L2 :

from sklearn.linear_model import LassoCV

l1_lm = LassoCV(alphas=[0.001, 0.01, 0.1, 1, 10, 100],
               normalize=True, cv=5)
l1_lm.fit(X_train, y_train)

best_alpha = l1_lm.alpha_
r2_test = l1_lm.score(X_test, y_test)

print(f"best alpha is: {best_alpha}")
print(f"test R2: {r2_test:0.3}")

Apprendre à raison d’un échantillonnage à la fois

Trouver les bons coefficients pour un modèle linéaire n’est qu’une question de temps et de mémoire. Cependant, il arrive parfois qu’un système n’ait pas assez de mémoire pour stocker un énorme jeu de données. Dans ce cas, vous devez recourir à d’autres moyens, comme apprendre d’un échantillonnage à la fois plutôt que de charger tous les éléments en mémoire. Les sections suivantes vous aident à comprendre l’approche de l’apprentissage avec « un échantillonnage à la fois » .

Utiliser la descente de gradient

La descente de gradient trouve la bonne façon de minimiser la fonction de coût à raison d’une itération à la fois. Après chaque étape, elle tient compte de toutes les erreurs additionnées du modèle, et met à jour les coefficients afin de rendre l’erreur encore plus petite lors de la prochaine itération des données. L’efficacité de cette approche découle de l’examen de tous les éléments de l’échantillon. L’inconvénient de cette approche est que vous devez charger toutes les données en mémoire.

Malheureusement, vous ne pouvez pas toujours stocker toutes les données en mémoire parce que certains jeux de données sont énormes. De plus, l’apprentissage à l’aide d’apprenants simples nécessite de grandes quantités de données afin de construire des modèles efficaces (plus de données aide à lever correctement les ambiguïtés sur la multicolinéarité). Obtenir et stocker des portions de données sur votre disque dur est toujours possible, mais ce n’est pas faisable en raison de la nécessité d’effectuer une multiplication matricielle, ce qui nécessiterait une grande quantité d’échange de données à partir du disque pour sélectionner les lignes et les colonnes. Les scientifiques qui ont travaillé sur ce problème ont trouvé une solution efficace. Au lieu d’apprendre à partir de toutes les données après avoir tout vu (ce qu’on appelle une itération), l’algorithme apprend d’un échantillonnage à la fois, tel qu’il a été prélevé dans le stockage en utilisant un accès séquentiel, puis progresse pour apprendre à partir du prochain échantillonnage. Lorsque l’algorithme a fini de tout apprendre, il recommence depuis le début à moins qu’un critère d’arrêt ne soit rempli (par exemple, terminer un nombre prédéfini d’itérations).

Un flux de données est formé des données qui proviennent du disque, à raison d’un échantillonnage à la fois. Le streaming est l’action qui consiste à transférer des données du stockage à la mémoire. Le style d’apprentissage out-of-core (apprentissage dit online, ou encore incrémental) se produit lorsqu’un algorithme apprend à partir d’un flux, ce qui est une stratégie d’apprentissage viable dont il est question à la fin du Chapitre 8.

Parmi les sources courantes de flux de données, on peut citer le trafic Web, les capteurs, les satellites ou encore les enregistrements de surveillance. Les informations produites instantanément par un capteur, ou les tweets générés par un fil X, sont d’autres exemples intuitifs de flux de données. Vous pouvez également diffuser en continu des données courantes que vous avez l’habitude de conserver en mémoire. Par exemple, si une matrice de données est trop grande, vous pouvez la traiter comme un flux de données et commencer à apprendre avec une ligne à la fois, en l’extrayant d’un fichier texte ou d’une base de données. Ce type de streaming est le style d’apprentissage online.

Comprendre en quoi SGD est différent

La descente de gradient stochastique (abrégée en anglais en SGD pour stochastic gradient descent) est une légère variante de l’algorithme de descente de gradient. Elle fournit une procédure de mise à jour pour l’estimation des coefficients bêta. Les modèles linéaires sont parfaitement à l’aise avec cette approche.

Dans SGD, la formulation reste la même que dans la version standard de la descente de gradient (appelée version par lots, par contraste avec la version online – ou en ligne), sauf pour la mise à jour. Dans ce cas, cette mise à jour est exécutée à raison d’une seule instance à la fois, ce qui permet à l’algorithme de laisser les données de base en mémoire et de ne placer en mémoire que la seule observation nécessaire pour modifier le vecteur de coefficient :

w_j = w_j - \alpha (Xw - y )x_j

Comme pour la descente de gradient, l’algorithme met à jour le coefficient, w, de la caractéristique j en soustrayant la différence entre la prédiction et la réponse réelle. Il multiplie ensuite la différence par la valeur de la caractéristique j et par un facteur d’apprentissage alpha (qui peut réduire ou augmenter l’effet de la mise à jour sur le coefficient).

Il y a d’autres différences subtiles entre l’utilisation de SGD et la descente de gradient classique. La différence la plus importante est le terme stochastique dans le nom de cet algorithme d’apprentissage en ligne. En fait, SGD s’attend à recevoir un échantillon à la fois, tiré au hasard parmi ceux qui sont disponibles (échantillonnage aléatoire). Le problème avec l’apprentissage en ligne est que l’ordre obtenu change la façon dont l’algorithme devine les coefficients bêta. C’est le principe de gloutonnerie dont il est question lorsqu’il s’agit d’arbres de décision (voir le Chapitre 10) : avec l’optimisation partielle, un échantillon peut changer la façon dont l’algorithme atteint la valeur optimale, créant un ensemble de coefficients différent de ce qui se serait produit sans lui.

À titre d’exemple pratique, n’oubliez pas que SGD peut mémoriser l’ordre dans lequel il voit les échantillons. Dans ces conditions, si l’algorithme travaille selon un ordre quelconque (historique, alphabétique ou, pire encore, lié à la variable réponse), il l’apprendra invariablement. Seul un échantillonnage aléatoire (pas d’ordre significatif) permet d’obtenir un modèle en ligne fiable qui fonctionne efficacement sur des données non vues. Lorsque vous diffusez des données en continu, vous devez réorganiser vos données au hasard (brassage des données).

L’algorithme SGD, contrairement à l’apprentissage par lots, nécessite un nombre beaucoup plus important d’itérations afin d’obtenir la bonne direction globale malgré les indications contraires qui proviennent d’échantillons individuels. En fait, l’algorithme se met à jour après chaque nouvel échantillon, et le cheminement vers un ensemble optimal de paramètres est plus erratique par rapport à une optimisation basée sur des lots, qui tend immédiatement à obtenir la bonne direction parce qu’elle est dérivée de données prises dans leur ensemble, comme le montre la Figure 13.2.

[picture]

Figure 13.2 Visualisation des différents chemins d’optimisation sur le même problème de données.

Dans ce cas, le taux d’apprentissage est d’autant plus important qu’il dicte la manière dont la procédure d’optimisation du SGD peut résister aux mauvais échantillons. En fait, si le taux d’apprentissage est élevé, un échantillon éloigné pourrait faire dérailler complètement l’algorithme et l’empêcher d’atteindre un bon résultat. D’un autre côté, des taux d’apprentissage élevés permettent à l’algorithme d’apprendre à partir des échantillons. Une bonne stratégie consiste à utiliser un taux d’apprentissage souple, c’est-à-dire commencer par un taux d’apprentissage flexible et le rendre rigide à mesure que le nombre d’échantillons vus augmente.

Si vous y réfléchissez bien, en tant que stratégie d’apprentissage, le taux d’apprentissage flexible est similaire à celui adopté par notre cerveau : flexible et ouvert pour apprendre quand nous sommes enfants et plus difficile (mais pas impossible) à changer quand nous devenons adultes. Python propose deux implémentations de SGD dans scikit-learn, l’une pour les problèmes de classification (SGDClassifier) et l’autre pour les problèmes de régression (SGDRegressor). Ces deux méthodes, outre la méthode fit (utilisée pour ajuster les données en mémoire), proposent une méthode partial_fit qui conserve en mémoire les résultats précédents et le programme d’apprentissage, et poursuit l’apprentissage au fur et à mesure de l’arrivée de nouveaux échantillons. Avec la méthode partial_fit, vous pouvez ajuster partiellement de petits morceaux de données, ou même des échantillons uniques, et continuer à fournir des données à l’algorithme jusqu’à ce qu’il trouve un résultat satisfaisant.

Les implémentations de classification et de régression SGD dans scikit-learn comportent toutes deux fonctions différentes de perte que vous pouvez appliquer à l’optimisation de la descente de gradient stochastique. Seules deux de ces fonctions renvoient aux méthodes traitées dans ce chapitre :

»  loss='squared_loss': moindres carrés ordinaires (MCO) pour la
   régression linéaire.

»  loss='log': régression logistique classique.

Les autres implémentations (hinge, huber, epsilon_insensitive) optimisent un type de fonction de perte similaire à celle du perceptron. Le Chapitre 15 les explique plus en détail lorsqu’il aborde les machines à vecteurs de support.

Pour démontrer l’efficacité de l’apprentissage out-of-core, l’exemple suivant met en place une brève expérience en Python utilisant la régression et squared_loss comme fonction de coût. Il montre comment les coefficients bêta changent à mesure que l’algorithme voit davantage d’échantillons. L’exemple passe également les mêmes données plusieurs fois pour renforcer l’apprentissage des modèles. L’utilisation d’un jeu de test garantit une évaluation équitable, fournissant des mesures de la capacité de l’algorithme à se généraliser aux données hors échantillon.

Vous pouvez librement réitérer l’expérience après avoir modifié les paramètres d’apprentissage en changeant le taux d’apprentissage (learning_rate) et ses paramètres associés eta0 et power_t. Le taux d’apprentissage détermine l’impact de chaque échantillon observé sur le processus d’optimisation et, en dehors de l’option constant, vous pouvez le modifier à l’aide des options optimal (adaptée à la classification) et invscaling (pour la régression). En particulier, des paramètres spécifiques, eta0 et power_t, contrôlent invscaling selon la formule du taux d’apprentissage (exprimé par la lettre grecque η, eta) :

ηη= η0/ t power_t

Lorsque le paramètre power_t est un nombre inférieur à un, la formule crée une décroissance progressive du taux d’apprentissage (t est le nombre d’échantillons vus), lui permettant de fixer ce qu’il a appris précédemment et le rendant résistant aux changements induits par les anomalies.

Cet exemple Python utilise le jeu de données Boston après l’avoir mélangé et scindé en jeux d’entraînement et de test. Il renvoie le vecteur des coefficients et les mesures d’erreur après avoir vu un nombre variable d’échantillons selon des puissances de deux (afin de représenter l’apprentissage à différentes étapes). L’expérience montre combien de temps il faut avant que R² n’augmente et que la valeur des coefficients se stabilise (notez que l’exécution de cet exemple peut prendre beaucoup de temps en fonction de votre système, et c’est donc peut-être le moment de prendre un café). Dans cet exemple, quelques itérations suffisent pour atteindre presque le maximum de R² possible. Vous noterez que le R² est moins bon dans le jeu d’entraînement (voir la Figure 13.3), alors que vous auriez dû vous attendre à ce qu’il soit meilleur que dans le jeu de test. Ceci est dû à la régularisation (l’exemple utilise L1), qui rend l’ajustement des données plus difficile.

from sklearn.linear_model import SGDRegressor

SGD = SGDRegressor(penalty='l1',
                  learning_rate='invscaling',
                  eta0=0.01, power_t=0.25)

r2_train = list()
r2_test = list()

for i in range(30):
   for j in range(len(X_train)):
       SGD.partial_fit(X_train.iloc[[j], :],
                       y_train[[j]])
   r2_train.append(SGD.score(X_train, y_train))
   r2_test.append(SGD.score(X_test, y_test))

import matplotlib.pyplot as plt
fig, ax = plt.subplots(dpi=120)
plt.plot(r2_train, label='train')
plt.plot(r2_test, label='test')
plt.xlabel("number of iterations")
plt.ylabel("R2")
plt.legend()
plt.show()

[picture]

Figure 13.3 Variation de R2 dans les jeux d’entraînement et de test à mesure que le nombre d’itérations augmente dans le modèle de régression SGD.

Quelle que soit la quantité de données, vous pouvez toujours ajuster un modèle de régression linéaire simple mais efficace en utilisant les capacités d’apprentissage en ligne de SGD.

24 https://scikit-learn.org/stable/modules/linear_model.html.

25 https://doi.org/10.1080/10691898.2011.11889627.

26 https://www.cs.toronto.edu/∼delve/data/boston/bostonDetail.html.

27 https://github.com/scikit-learn-contrib/stability-selection.

DANS CE CHAPITRE

Faire évoluer le perceptron vers le paradigme de l’interconnexion

•

Structurer des architectures neuronales constituées de nœuds et de connexions

•

Découvrir l’algorithme de la rétropropagation

•

Comprendre ce qu’est le deep learning et ce qu’il peut accomplir

Chapitre 14

Les réseaux neuronaux au service de la complexité

En voyageant dans le monde de l’apprentissage automatique, vous voyez souvent des métaphores du monde naturel utilisées pour expliquer les détails des algorithmes. Ce chapitre présente une famille d’algorithmes d’apprentissage qui s’inspire directement du fonctionnement du cerveau. Ce sont les réseaux de neurones, les algorithmes de base de la tribu des connexionnistes (reportez-vous au Chapitre 2 pour avoir la liste des différentes tribus).

Partant de l’idée de la rétro-ingénierie du traitement des signaux par le cerveau, les connexionnistes basent les réseaux de neurones sur des analogies biologiques et leurs composantes, en utilisant des termes « cérébraux » tels que neurones et axones, par exemple. Cependant, vous découvrirez, en vérifiant leurs formulations mathématiques, que les réseaux de neurones ne ressemblent rien de plus qu’à une sorte de régression linéaire sophistiquée et non linéaire. Pourtant, ces algorithmes sont extraordinairement efficaces dans le cas de problèmes complexes tels que la reconnaissance d’images et de sons, ou encore la traduction automatique. Ils peuvent également s’exécuter rapidement lorsqu’il s’agit de prédire (en fonction de leur taille, car certains modèles de réseaux de neurones récents sont énormes).

Les grands réseaux de neurones bien conçus utilisent le nom de deep learning (apprentissage profond) et sont à la base d’outils puissants très répandus tels que Siri, Google Assistant, Alexa et autres assistants numériques. Ils sont également à l’origine des applications d’apprentissage automatique les plus étonnantes. Par exemple, vous les voyez à l’œuvre dans cette incroyable démonstration de l’ancien directeur de la recherche chez Microsoft, Rick Rashid, qui s’exprime en anglais tout en étant simultanément traduit en chinois (https://www.youtube.com/watch?v=Nu-nlQqFCKg). Si une révolution de l’IA est sur le point de se produire, les capacités d’apprentissage accrues des réseaux de neurones en seront probablement le moteur.

Il n’est pas nécessaire de saisir manuellement le code source de ce chapitre. Il est beaucoup plus facile d’utiliser le code source téléchargeable de ce chapitre dans le dossier ML4D2E de la source téléchargeable pour le code Python et dans le dossier ML4D2ER de la source téléchargeable pour le code R. Les fichiers d’exemple de ce chapitre commencent par ML4D2E ; 14 ; (pour Python) ou ML4D2E_R ; 14 ; (pour R). Reportez-vous à l’introduction pour savoir où trouver ces fichiers source.

Retour sur le perceptron

L’algorithme au cœur des réseaux de neurones est, de manière cohérente, appelé neurone (on parle aussi d’unité). De nombreux neurones disposés dans une structure interconnectée forment un réseau de neurones, chacun d’eux étant relié aux entrées et sorties d’autres neurones. Ainsi, un neurone peut entrer des caractéristiques à partir d’échantillons ou des résultats d’autres neurones, en fonction de son emplacement dans le réseau de neurones.

Quelque chose de semblable au neurone, le perceptron, a été traité plus tôt dans ce livre, bien qu’il utilise une structure et un fonctionnement plus simples. Lorsque le psychologue Frank Rosenblatt conçut le perceptron, il le considéra comme une version mathématique simplifiée d’un neurone du cerveau. Un perceptron accomplit les étapes suivantes :

Il prend des valeurs en entrée de l’environnement proche (le jeu de données).

Il pondère ces valeurs (comme le font les cellules cérébrales avec les signaux électriques, en fonction de la force des connexions internes).

Il additionne toutes les valeurs pondérées.

Il s’active lorsque la somme dépasse un seuil.

Ce seuil donne comme valeur 1 ; sinon, sa prédiction est de 0. Malheureusement, un perceptron ne peut pas apprendre lorsque les classes qu’il essaie de traiter ne sont pas linéairement séparables. Cependant, les chercheurs ont découvert que, même si un seul perceptron ne pouvait pas apprendre l’opération logique XOR illustrée à la Figure 14.1 (le ou exclusif, qui n’est vrai que lorsque les entrées sont dissemblables), deux perceptrons travaillant ensemble le pouvaient.

[picture]

Figure 14.1 Il n’est pas possible d’apprendre le XOR logique en utilisant une seule ligne de séparation.

Les neurones d’un réseau de neurones sont une autre évolution du perceptron : ils prennent de nombreuses valeurs pondérées comme entrées, les additionnent et renvoient la somme comme résultat, tout comme le fait un perceptron. Cependant, ils fournissent aussi une transformation plus sophistiquée de la sommation, ce que le perceptron ne peut pas faire. En observant la nature, les scientifiques ont remarqué que les neurones reçoivent des signaux mais n’émettent pas toujours leur propre signal. Cela dépend de la quantité de signal reçu. Lorsqu’un neurone acquiert suffisamment de stimuli, il déclenche une réponse, sinon il reste silencieux. De la même manière, les neurones algorithmiques, après avoir reçu des valeurs pondérées, les additionnent et utilisent une fonction d’activation pour évaluer le résultat, ce qui le transforme de manière non linéaire. Par exemple, la fonction d’activation peut déclencher une valeur nulle à moins que l’entrée n’atteigne un certain seuil, ou elle peut atténuer ou augmenter une valeur en la redimensionnant de manière non linéaire, transmettant ainsi un signal recalibré.

Un réseau de neurones a différentes fonctions d’activation, comme le montre la Figure 14.2. La fonction linéaire (étiquetée marche binaire) n’applique aucune transformation, et elle est rarement utilisée, car elle réduit un réseau de neurones à une régression avec transformations polynomiales. Les réseaux de neurones utilisent généralement les fonctions d’activation sigmoïde (également appelée logistique), tangente hyperbolique (étiquetée TanH) ou ReLU (qui est de loin la plus courante aujourd’hui).

[picture]

Figure 14.2 Graphiques de différentes fonctions d’activation.

La figure montre comment une entrée (exprimée sur l’axe horizontal) peut transformer une sortie en une autre chose (exprimée sur l’axe vertical). Les exemples de fonctions d’activation montrent une marche binaire, une fonction logistique, une tangente hyperbolique et une fonction ReLU.

Vous en apprendrez plus sur les fonctions d’activation plus loin dans ce chapitre, mais notez pour l’instant qu’elles fonctionnent clairement bien dans certaines plages de valeurs d’entrée. Comme le montre la Figure 14.2, elles produisent des valeurs de fin d’échelle en dehors de certaines limites d’entrée. C’est pour cette raison que vous devriez toujours recalibrer les entrées d’un réseau de neurones en utilisant la normalisation statistique (moyenne nulle et variance unitaire) ou transformer l’entrée dans une plage allant de 0 à 1 ou de –1 à +1 (transformation min-max).

Aller de l’avant avec le feed-forward

Dans un réseau de neurones, vous devez d’abord considérer l’architecture, qui est la manière dont les composants du réseau sont organisés. Contrairement à d’autres algorithmes, dans lesquels il y a un pipeline fixe qui détermine comment ces algorithmes reçoivent et traitent les données, les réseaux de neurones exigent que vous décidiez comment l’information circule en fixant le nombre d’unités (les neurones) et leur distribution en couches, comme le montre la Figure 14.3.

La figure montre une architecture neuronale simple. Notez comment les couches filtrent l’information de manière progressive. Il s’agit d’une entrée dite feed-forward (ou propagation avant) parce que les données sont transmises dans une seule direction vers l’avant dans le réseau. Les connexions relient exclusivement les unités d’une couche avec les unités de la couche suivante (flux d’informations de gauche à droite). Il n’existe aucune connexion entre les unités d’une même couche ou avec des unités situées en dehors des couches suivantes. L’information passe généralement à la couche suivante, mais elle peut passer à des couches supplémentaires. L’exemple de la Figure 14.3 montre les connexions de gauche à droite, mais dans d’autres représentations, vous pouvez trouver des connexions de haut en bas ou de bas en haut. Les données traitées ne reviennent jamais vers les couches de neurones précédentes.

[picture]

Figure 14.3 Un exemple d’architecture d’un réseau de neurones.

Les réseaux neuronaux sont également considérés comme des graphes. Un graphe est simplement un ensemble de sommets, de nœuds ou de points reliés par des arêtes, des arcs ou des lignes. Dans le cas des réseaux de neurones, le graphe est appelé graphe acyclique dirigé (DAG) parce que :

• Il est impossible d’aller d’un sommet à un autre et de revenir au sommet de départ en utilisant n’importe quelle route.

• Il existe un ensemble prévisible de routes allant d’un sommet à l’autre.

Il existe des types de réseaux de neurones qui autorisent les boucles, bien que celles-ci se produisent à l’intérieur de certains neurones (et non pas entre les couches) et qu’elles soient limitées à la taille de l’entrée qu’elles reçoivent. Ces réseaux sont appelés réseaux neuronaux récurrents (RNN) et sont brièvement illustrés à la fin de ce chapitre.

Utiliser un réseau de neurones, c’est comme utiliser un système de filtrage stratifié pour l’eau : vous versez l’eau par le haut, et elle est filtrée vers le bas. L’eau n’a aucun moyen de revenir en arrière : elle va simplement vers l’avant et vers le bas, et jamais latéralement ou vers le haut. De la même manière, les réseaux de neurones forcent les caractéristiques des données à circuler et à se mélanger les unes aux autres uniquement en fonction de l’architecture du réseau. En utilisant la meilleure architecture pour mélanger les caractéristiques, le réseau de neurones crée de nouvelles caractéristiques composites dans chaque couche et aide à obtenir de meilleures prédictions. Malheureusement, il n’est pas possible de déterminer la « meilleure » architecture sans essayer empiriquement (ou systématiquement) différentes solutions et vérifier si les données obtenues en sortie aident à prédire vos valeurs cibles après avoir traversé le réseau.

La première et la dernière couche jouent un rôle important. La première couche, appelée couche d’entrée, reprend les caractéristiques de chaque échantillon des données traitées par le réseau. La dernière couche, appelée couche de sortie, diffuse les résultats.

Un réseau de neurones ne peut gérer que des informations numériques continues. Il ne peut pas être contraint de travailler avec des caractéristiques qualitatives (par exemple, des étiquettes indiquant une qualité telle que rouge, bleu ou vert dans une image). Vous pouvez partir de caractéristiques qualitatives en les transformant en une valeur numérique continue, telle qu’une série de valeurs binaires, comme nous l’avons vu au Chapitre 7 à propos du travail avec les données. Lorsqu’un réseau de neurones traite une caractéristique binaire, le neurone la considère comme un nombre générique et il transforme ces valeurs binaires en d’autres valeurs, même négatives, en les traitant au sein de plusieurs unités.

Notez la limitation qu’impose le fait de ne traiter que des valeurs numériques, car vous ne pouvez pas vous attendre à ce que la dernière couche produise une prédiction d’étiquette non numérique. Lorsqu’il s’agit d’un problème de régression, la dernière couche est une unique unité. De la même manière, lorsque vous travaillez avec une classification et que vous avez un résultat qui doit être choisi parmi un certain nombre de classes, vous devez avoir un nombre équivalent d’unités terminales, chacune représentant un score lié à la probabilité de la classe représentée. Par conséquent, lors de la classification d’un problème multiclasse (comme indiqué dans la section « Gérer plusieurs classes » du Chapitre 13), tel qu’une espèce de manchot, la couche finale comporte autant d’unités que d’espèces. Pour voir comment cela fonctionne, revoyez la démonstration du jeu de données Palmer Penguins dans la section « Expérimenter la fiabilité des k-moyennes » du Chapitre 12. Vous disposez de trois unités représentant l’une des trois espèces de manchots : Adélie, à jugulaire ou Gentoo. Pour chaque échantillon, la classe prédite est celle qui obtient le score le plus élevé à la fin.

Si les classes que vous prédisez peuvent coexister (un exemple d’entraînement peut appartenir à plusieurs classes), vous disposez d’une classification multiétiquette et vos neurones de sortie terminaux fourniront des estimations de probabilité pour chaque classe indépendamment les unes des autres. Parmi les exemples typiques de classification multiétiquette, on peut citer le cas où un réseau de neurones doit deviner tous les objets d’une photo. Tous les objets possibles constituent des classes pour le réseau de neurones, et chacun d’entre eux reçoit un score de probabilité d’être dans la photo. Tous les objets dont le score dépasse un seuil sont donc considérés comme faisant partie de la photo.

Contrairement à la classification multiétiquette, lorsque les classes que vous prédisez sont mutuellement exclusives (votre problème est une classification multiclasse, comme dans le jeu de données des manchots), la somme des probabilités dérivées de vos neurones de sortie doit être égale à 100 %. Il existe une couche finale spéciale, appelée couche Softmax, qui peut ajuster la probabilité de chaque classe de manière à ce que la somme des probabilités résultantes soit égale à 1,0 (ou 100 %).

Dans une classification, la couche finale peut représenter à la fois une partition des probabilités grâce à une couche Softmax (et vous avez un problème multiclasse dans lequel les probabilités totales s’élèvent à 100 %) ou une prédiction de score indépendante (parce qu’un échantillon peut avoir plusieurs classes, ce qui constitue un problème multiétiquette dans lequel les probabilités additionnées peuvent être supérieures à 100 %). Lorsque le problème de classification est une classification binaire, un seul nœud suffit.

Contrairement aux autres algorithmes abordés jusqu’à présent, les réseaux de neurones peuvent comporter plusieurs unités de sortie dans le cadre d’un problème de régression, chacune représentant un problème de régression différent (par exemple, dans le domaine de la prévision, vous pouvez avoir différentes prédictions selon le jour, la semaine, le mois, etc.).

S’enfoncer encore plus dans l’inconnu

Les réseaux de neurones ont des couches différentes, chacune ayant ses propres poids. Du fait que le réseau de neurones scinde les calculs par couches, il est important de connaître la couche de référence (celle utilisée pour le calcul), car elle permet de comptabiliser certaines unités et connexions. Ainsi, vous pouvez faire référence à chaque couche à l’aide d’un chiffre spécifique et parler de manière générique de chaque couche à l’aide de la lettre l (pour layer en anglais, soit couche en français), de sorte que la première couche s’appelle l1, la deuxième l2, etc. Chaque couche peut avoir un nombre différent d’unités, et le nombre d’unités situées entre deux couches détermine le nombre de connexions. En multipliant le nombre d’unités de la couche de départ par le nombre d’unités de la couche suivante, vous pouvez déterminer le nombre total de connexions entre les deux par la formule suivante :

nombre de connexions(l) = unités(l)*unités(l+1)

Une matrice de poids, généralement nommée avec la lettre grecque thêta majuscule (Θ), représente les connexions. Pour faciliter la lecture, ce livre utilise la lettre majuscule W, ce qui est un bon choix, car c’est une matrice. Ainsi, vous pouvez utiliser W¹ pour faire référence aux poids de connexion allant de la couche 1 à la couche 2, W² pour les connexions de la couche 2 à la couche 3, et ainsi de suite.

Vous pouvez voir les références aux couches entre l’entrée et la sortie comme étant des couches cachées, et compter les couches à partir de la première couche cachée. C’est juste une convention différente de celle qui est utilisée dans le livre. Les exemples du livre commencent toujours à compter à partir de la couche d’entrée, si bien que la première couche cachée est la couche numéro 2.

Les poids représentent la force de la connexion entre les neurones du réseau. Lorsque le poids de la connexion entre deux couches est faible, cela signifie que le réseau rejette les valeurs qui circulent entre elles et signale que le fait d’emprunter cette voie n’influencera probablement pas la prévision finale. Au contraire, une grande valeur positive ou négative affecte les valeurs que reçoit la couche suivante, déterminant ainsi certaines prédictions. Cette approche est clairement analogue à celle des cellules du cerveau, qui ne sont pas isolées mais en relation avec d’autres cellules. Au fur et à mesure que quelqu’un acquiert de l’expérience, les connexions entre les neurones ont tendance à se renforcer ou à s’affaiblir pour activer ou désactiver certaines régions dans le réseau cellulaire du cerveau, ce qui entraîne un traitement neuronal supplémentaire ou une activité avec des connexions fortes (une réaction à un danger, par exemple, si l’information traitée signale une situation de danger de mort).

Maintenant que vous connaissez certaines conventions concernant les couches, les unités et les connexions, vous pouvez commencer à examiner en détail les opérations que les réseaux de neurones exécutent. Pour débuter, vous pouvez appeler les entrées et les sorties de différentes manières :

• a : le résultat stocké dans une unité du réseau de neurones après avoir été traité par la fonction d’activation (appelée g). C’est la sortie finale qui est envoyée plus loin dans le réseau.

• z : la multiplication entre a et les poids de la matrice W, suivie de l’ajout d’une valeur de biais. z représente le signal passant par les connexions, comme dans le cas de l’eau qui s’écoule à une pression supérieure ou inférieure selon l’épaisseur du tuyau. De la même manière, les valeurs reçues de la couche précédente sont plus ou moins hautes ou basses en raison des poids de la connexion utilisés pour les transmettre.

Chaque couche successive d’unités d’un réseau de neurones traite progressivement les valeurs issues des caractéristiques. Il s’agit en quelque sorte d’un tapis roulant. Lorsque les données transitent dans le réseau, elles arrivent dans chaque unité comme une valeur produite par la somme des valeurs présentes dans la couche précédente et pondérée par les connexions représentées dans la matrice W. Lorsque les données avec un biais ajouté dépassent un certain seuil, la fonction d’activation augmente la valeur enregistrée dans l’unité. Sinon, elle éteint le signal en le réduisant ou en l’annulant. Après traitement par la fonction d’activation, le résultat est prêt à être transmis vers l’avant jusqu’à la connexion liée à la couche suivante. Ces étapes se répètent pour chaque couche jusqu’à ce que les valeurs atteignent la fin et que vous obteniez un résultat, comme le montre la Figure 14.4.

[picture]

Figure 14.4 Détail du processus de propagation avant dans un réseau neuronal.

La figure montre un détail du processus dans lequel deux unités « poussent » leurs résultats vers une autre unité. Cet événement a lieu dans toutes les parties du réseau. Lorsque vous comprenez le passage de deux neurones vers un seul, vous pouvez tout aussi bien comprendre l’ensemble du processus de feed-forward, même lorsque plusieurs couches et neurones sont impliqués. Pour détailler les choses, voici les sept étapes utilisées pour produire une prédiction dans un réseau de neurones constitué de quatre couches (comme celui illustré plus haut à la Figure 14.3) :

❶ La première couche (remarquez l’exposant 1 sur a) charge la valeur de chaque caractéristique dans une unité différente :

a⁽¹⁾ = X

❷ Les poids des connexions qui relient la couche d’entrée à la seconde couche sont multipliés par les valeurs des unités de la première couche. Une multiplication matricielle permet de pondérer et d’additionner les entrées de la deuxième couche. L’algorithme ajoute une constante de biais à la couche 2 avant d’exécuter la fonction d’activation dans l’étape suivante :

z(2) = W(1)a(1) + biais(1)

❸ La fonction d’activation transforme les entrées de la deuxième couche. Les valeurs résultantes sont prêtes à être passées aux connexions.

a⁽²⁾ = g(z⁽²⁾)

❹ Les connexions de la troisième couche pondèrent et additionnent les sorties de la deuxième couche. L’algorithme ajoute une constante de biais à la troisième couche avant d’exécuter la fonction d’activation.

z(3) = W(2)a(2) + biais(2)

❺ La fonction d’activation transforme les entrées de la troisième couche.

a⁽³⁾ = g(z⁽³⁾)

❻ Les sorties de la troisième couche sont pondérées et additionnées par les connexions à la couche de sortie. Enfin, l’algorithme ajoute une constante de biais à la quatrième couche avant d’exécuter la fonction d’activation à l’étape suivante.

z(4) = W(3)a(3) + biais(3)

❼ Les unités de sortie reçoivent leurs entrées et les transforment à l’aide de la fonction d’activation. Après cette transformation finale, les unités de sortie sont prêtes à diffuser les prédictions du réseau de neurones.

a⁽⁴⁾ = g(z⁽⁴⁾)

La fonction d’activation joue le rôle d’un filtre de signal, en aidant à sélectionner les signaux pertinents et à éviter les signaux faibles et bruyants (du fait qu’elle élimine les valeurs inférieures à un certain seuil). Les fonctions d’activation fournissent également une non-linéarité à la sortie parce qu’elles améliorent ou amortissent les valeurs qui les traversent d’une manière non proportionnelle.

D’un autre point de vue, les poids des connexions permettent de mélanger et de composer les caractéristiques d’une nouvelle manière, en créant de nouvelles caractéristiques d’une façon pas très différente d’un développement polynomial. L’activation rend non linéaire la recombinaison résultante des caractéristiques par les connexions. Ces deux composants du réseau de neurones permettent à l’algorithme d’apprendre des fonctions cibles complexes, qui représentent la relation entre les caractéristiques en entrée et le résultat cible.

La propagation avant des données de la couche d’entrée vers la couche de sortie se produit exclusivement lorsque vous faites une prédiction. Lorsque vous entraînez un réseau de neurones, vous avez à la fois une phase de propagation avant et une phase de rétropropagation. La phase de rétropropagation sert à affiner les ensembles de poids corrects dans le réseau. Étant donné que la phase de propagation avant dépend de la multiplication des poids, vous devez commencer avec certains poids, faute de quoi vous ne pourrez pas faire fonctionner votre réseau. L’initialisation fournit des poids initiaux, estimés de manière aléatoire.

La manière dont ces poids initiaux sont fixés est importante, car vous ne pouvez pas utiliser des nombres trop petits, sinon le signal serait trop faible pour que le réseau fonctionne. Cependant, il faut également éviter les nombres trop élevés, car les calculs deviennent trop lourds à gérer. Parfois, ils échouent, ce qui entraîne le problème de l’explosion du gradient²⁸ ou, plus souvent, la saturation des neurones, ce qui signifie qu’il est impossible d’entraîner correctement un réseau parce que tous les neurones sont toujours activés.

Initialiser votre réseau en utilisant tous les zéros est toujours une mauvaise idée, car si tous les neurones ont la même valeur, ils réagiront de la même manière à l’entrée d’apprentissage. Quel que soit le nombre de neurones que contient l’architecture, ils fonctionnent comme un seul neurone. Une solution simple consiste à commencer avec des poids initiaux aléatoires dans la plage requise pour les fonctions d’activation, qui sont les fonctions de transformation qui ajoutent de la flexibilité à la résolution de problèmes à l’aide du réseau. Il y a une autre solution simple qui consiste à fixer les poids à une moyenne nulle et à un écart-type (une distribution normale standard, en statistiques).

Il existe des initialisations de poids plus intelligentes pour les réseaux plus complexes, tels que ceux que l’on trouve dans cet article : https://towardsdatascience.com/weightinitialization-techniques-in-neural-networks-26c649eb3b78.

Revenir en arrière avec la rétropropagation

Du point de vue de l’architecture, un réseau de neurones fait un excellent travail en mélangeant les signaux provenant d’échantillons et en les transformant en nouvelles caractéristiques pour obtenir une approximation de fonctions non linéaires complexes (fonctions que vous ne pouvez pas représenter comme une ligne droite dans l’espace des caractéristiques). Pour produire cette capacité, les réseaux de neurones fonctionnent comme des approximateurs universels²⁹, ce qui signifie qu’ils peuvent deviner n’importe quelle fonction cible. Toutefois, il faut tenir compte du fait que cette caractéristique permet de modéliser des fonctions complexes (capacité de représentation) et d’apprendre efficacement à partir des données. L’apprentissage est possible dans un cerveau en raison de la formation et de la modification des synapses entre les neurones, en fonction des stimuli reçus via une expérience par essais et erreurs. Les réseaux de neurones permettent de reproduire ce processus sous la forme d’une formulation mathématique appelée rétropropagation.

Depuis son apparition au début des années 1970, l’algorithme de rétropropagation a fait l’objet de nombreuses corrections. Chaque amélioration du processus d’apprentissage des réseaux de neurones a donné lieu à de nouvelles applications et à un regain d’intérêt pour cette technique. En outre, l’actuelle révolution du deep learning, qui consacre la renaissance des réseaux de neurones abandonnés au début des années 1990, est le résultat de progrès importants dans la façon dont les réseaux de neurones apprennent de leurs erreurs. Comme on le voit dans d’autres algorithmes, la fonction de coût active la nécessité de mieux apprendre de certains échantillons (les erreurs importantes correspondent à des coûts élevés). Lorsqu’un échantillon présente une erreur importante, la fonction de coût fournit une valeur élevée qui est minimisée en modifiant les paramètres de l’algorithme.

Dans une régression linéaire, la recherche d’une règle de mise à jour à appliquer à chaque paramètre (le vecteur des coefficients bêta) est simple ; vous pouvez y parvenir par descente de gradient ou par une approche analytique. Cependant, dans un réseau de neurones, les choses sont un peu plus compliquées. L’architecture est variable et les coefficients des paramètres (les connexions) sont reliés entre eux car les connexions d’une couche dépendent de la façon dont les connexions des couches précédentes ont recombiné les entrées. La solution à ce problème est l’algorithme de rétropropagation. La rétropropagation est un moyen intelligent de propager les erreurs en arrière dans le réseau et de faire en sorte que chaque connexion ajuste ses poids en conséquence. Si vous transmettez d’abord l’information vers l’avant dans le réseau, il est temps ensuite de revenir en arrière et de donner un retour d’information sur ce qui s’est mal passé dans la phase suivante.

Découvrir comment fonctionne la rétropropagation n’est pas compliqué, même si la démonstration de son fonctionnement à l’aide de formules et de calculs mathématiques nécessite des dérivées et la preuve de certaines formulations, ce qui est assez délicat et dépasse le cadre de ce livre. Pour avoir une idée de la manière dont la rétropropagation fonctionne, partez de la fin du réseau, juste au moment où un échantillon a été traité et que vous avez obtenu une prédiction en sortie. À ce stade, vous pouvez le comparer avec l’élément réel et, en soustrayant les deux résultats, obtenir un décalage, qui est l’erreur. Maintenant que vous connaissez la disparité des résultats au niveau de la couche de sortie, vous pouvez progresser vers l’arrière afin de la distribuer dans toutes les unités du réseau.

La fonction de coût d’un réseau de neurones pour la classification est basée sur l’entropie croisée (comme dans la régression logistique) :

Co\hat{u}t = y * log(h_W(X)) + (1 - y) * log(1-h_W(X))

Il s’agit d’une formule impliquant des logarithmes. Elle se réfère à la prédiction produite par le réseau de neurones et est exprimée sous la forme hW(X) (qui se lit comme le résultat fourni par le réseau étant données les connexions W et X en entrée). Pour faciliter les choses, lorsque l’on pondère le coût, il est utile de simplement se figurer la formule comme étant le calcul du décalage entre les résultats attendus et la sortie du réseau de neurones.

La première étape de la transmission de l’erreur en sens inverse dans le réseau repose sur une multiplication en amont. Comme les valeurs fournies à la couche de sortie sont constituées des contributions de toutes les unités, proportionnellement au poids de leurs connexions, vous pouvez redistribuer l’erreur en fonction de chaque contribution. Par exemple, le vecteur d’erreurs d’une couche n du réseau, ce vecteur étant indiqué par la lettre grecque delta (δ), est le résultat de la formule suivante :

\delta^{(n)} = W^{(n)T} * \delta^{(n+1)}

Cette formule dit que, en partant du delta final, vous pouvez continuer à redistribuer ce delta en remontant dans le réseau et en utilisant les poids que vous avez utilisés pour faire avancer la valeur, et ce afin de partitionner l’erreur vers les différentes unités. De cette façon, vous pouvez redistribuer l’erreur terminale sur chaque unité neuronale et l’utiliser pour recalculer un poids plus approprié pour chaque connexion du réseau afin de minimiser l’erreur. Pour mettre à jour les poids W de la couche l, il suffit d’appliquer la formule suivante :

W^{(1)} = W^{(1)} + \eta*\delta^{(1)}*g'(z^{(1)})*a^{(1)}

La formule peut paraître à première vue déroutante, mais il s’agit d’une somme, et vous pouvez découvrir comment elle fonctionne en regardant ses éléments. Tout d’abord, observez la fonction g'. C’est la dérivée première de la fonction d’activation g, évaluée avec les valeurs d’entrée z. En fait, les dérivées sont un élément clé de la méthode d’optimisation par descente de gradient³⁰. La descente de gradient détermine comment réduire la mesure de l’erreur en trouvant, parmi les combinaisons possibles de valeurs, les poids qui réduisent le plus cette erreur.

La lettre grecque êta (η), parfois aussi appelée alpha (α) ou epsilon (ε) selon le manuel que vous consultez, est le taux d’apprentissage. Comme dans d’autres algorithmes, utilisant des stratégies d’apprentissage similaires, il réduit l’effet de la mise à jour suggérée par la dérivée de la descente du gradient. En fait, la direction fournie par les mises à jour des poids peut n’être que partiellement correcte, voire à peine correcte (à cause du bruit ou des erreurs dans les données ou à cause de la composition du lot d’échantillons traités). En faisant plusieurs petites étapes dans la descente, l’algorithme peut prendre une direction plus précise vers l’erreur minimale globale, qui est la cible que vous voulez atteindre (c’est-à-dire, un réseau de neurones produisant le moins d’erreurs de prédiction possible).

Différentes méthodes sont disponibles pour ajuster la bonne valeur d’êta, car l’optimisation en dépend largement. Une méthode consiste à régler la valeur d’êta à un niveau élevé au départ et à la réduire au cours du processus d’optimisation. Une autre méthode augmente ou diminue êta de façon variable en fonction des améliorations obtenues par l’algorithme : les grandes améliorations appellent un êta plus grand (puisque la descente est facile et droite), tandis que les petites améliorations appellent un êta plus petit de sorte que l’optimisation se déplacera plus lentement, à la recherche des meilleures opportunités de descendre. Imaginez cela comme si vous étiez sur un chemin tortueux en haute montagne : vous ralentissez et vous essayez de ne pas tomber ou d’être projeté hors du sentier en descendant trop vite.

La plupart des implémentations offrent un réglage automatique de la valeur correcte d’êta. Vous devez noter la pertinence de ce réglage lors de l’entraînement d’un réseau de neurones, car c’est l’un des paramètres importants, avec l’architecture des couches, à ajuster pour obtenir de meilleures prédictions.

Les mises à jour des poids peuvent se faire de différentes façons par rapport au jeu d’entraînement des échantillons :

• Mode en ligne (online) : la mise à jour des poids a lieu après que chaque échantillon a traversé le réseau. De cette manière, l’algorithme traite les échantillons d’apprentissage comme un flux à partir duquel on peut apprendre en temps réel. Ce mode est parfait lorsque vous devez apprendre out-of-core, c’est-à-dire lorsque le jeu d’entraînement ne peut pas tenir dans la mémoire RAM. Cependant, cette méthode est sensible aux valeurs aberrantes, et vous devez donc maintenir votre taux d’apprentissage à un niveau bas (par conséquent, l’algorithme est lent à converger vers une solution).

• Mode par lots (batch) : la mise à jour des poids s’effectue après avoir vu tous les échantillons dans le jeu d’entraînement. Cette technique rend l’optimisation rapide et moins sujette à une variance trop importante du processus en raison de la variabilité du flux des échantillons. En revanche, en mode batch, vous devez conserver tous vos échantillons en mémoire (en fait dans la RAM, plutôt que dans un flux). En mode batch, la rétropropagation prend en compte la somme des gradients de tous les échantillons.

• Mode par mini-lots (mini-batch, ou stochastique) : la mise à jour des poids a lieu une fois que le réseau a traité un sous-échantillon choisi au hasard dans le jeu d’entraînement. Cette approche combine les avantages du mode en ligne (faible consommation de mémoire) et du mode par lots (convergence rapide), tout en introduisant un élément aléatoire (le sous-échantillonnage) pour éviter que la descente du gradient ne soit bloquée dans un minimum local (une baisse de la fonction de coût qui n’est pas le véritable minimum).

Représentation du mode d’apprentissage d’un réseau

Étant donné l’architecture d’un réseau de neurones, vous pouvez imaginer avec quelle facilité l’algorithme pourrait apprendre presque n’importe quoi à partir des données, surtout si vous ajoutez trop de couches. En fait, l’algorithme fonctionne si bien que ses prédictions sont souvent affectées par une variance d’estimation élevée appelée le surapprentissage. Celui-ci fait que le réseau de neurones apprend chaque détail des échantillons du jeu d’entraînement, ce qui permet de les reproduire dans la phase de prédiction. Toutefois, en cas de surapprentissage, même si le réseau obtient d’excellents résultats avec le jeu d’entraînement, il ne pourra jamais prédire correctement quoi que ce soit de différent. Les sections suivantes traitent plus en détail de certains des problèmes liés au surapprentissage.

Comprendre le problème du surapprentissage

Lorsque vous utilisez un réseau de neurones pour un problème réel, vous devez prendre certaines mesures de précaution d’une manière beaucoup plus stricte que pour d’autres algorithmes. Les réseaux de neurones sont plus fragiles et plus sujets à des erreurs importantes que d’autres solutions d’apprentissage automatique.

Tout d’abord, vous divisez soigneusement vos données en jeux d’entraînement, de validation et de test. Avant que l’algorithme n’apprenne à partir des données, vous devez évaluer la qualité de vos paramètres :

Architecture (nombre de couches et de neurones) : l’utilisation d’une architecture appropriée offre de grandes possibilités de créer des modèles prédictifs puissants avec un risque élevé de surapprentissage.

Initialisation : contrôle de la vitesse à laquelle un réseau apprend à partir des données, mais cela peut ne pas suffire à empêcher le surapprentissage des données d’entraînement.

Fonctions d’activation : examen des données plusieurs fois pour apprendre au mieux les modèles (une epoch ou une itération marque chaque passage des données dans le réseau de neurones).

Paramètres d’apprentissage : servent à éviter le bruit aléatoire ou la mémorisation directe de certaines parties des échantillons.

Nombre d’itérations : définit le nombre d’epochs utiles. Plus le nombre d’itérations est élevé, plus le risque de surapprentissage est important.

En deep learning (apprentissage profond), on a conservé le terme anglais epoch pour désigner un cycle complet de traitement de l’ensemble des données d’apprentissage.

Vous avez deux solutions possibles pour résoudre les problèmes de qualité des données. La première est la régularisation, comme dans la régression linéaire et logistique. Vous pouvez additionner tous les coefficients de connexion, mis au carré ou en valeur absolue, pour pénaliser les modèles avec trop de coefficients présentant des valeurs élevées (obtenues par régularisation L2) ou avec des valeurs différentes de zéro (obtenues par régularisation L1). La deuxième solution est également efficace parce qu’elle permet de contrôler le moment où le surapprentissage se produit. On appelle cela un arrêt prématuré (early stopping) qui fonctionne en vérifiant la fonction de coût sur le jeu de validation au fur et à mesure que l’algorithme apprend du jeu d’entraînement (voir la Figure 14.5).

[picture]

Figure 14.5 Le surapprentissage se produit lorsqu’il y a trop d’itérations d’apprentissage sur les données.

Vous ne vous rendez peut-être pas compte du moment où votre modèle commence à surapprendre. La fonction de coût calculée à l’aide du jeu d’entraînement continue de s’améliorer au fur et à mesure que l’optimisation progresse. Cependant, dès que vous commencez à enregistrer du bruit à partir des données et que vous cessez d’apprendre des règles générales, vous pouvez vérifier la fonction de coût sur des données hors échantillon (le jeu de validation). À un moment donné, vous remarquerez qu’elle cesse de s’améliorer et commence à se dégrader, ce qui signifie que votre modèle a atteint sa limite d’apprentissage.

Choisir un framework

La meilleure façon d’apprendre à construire un réseau de neurones est d’en créer un, et si vous voulez mettre les mains dans le cambouis, en particulier en matière de deep learning, le plus simple est d’exploiter les fonctionnalités offertes par un framework. L’utilisation d’un framework d’apprentissage profond peut réduire considérablement le temps, le coût et la complexité du développement d’une solution de deep learning. Même si les frameworks d’apprentissage profond présentent de nombreuses caractéristiques des frameworks génériques (solutions de domaine cohérentes), ils offrent également des fonctionnalités spécifiques, comme l’accès au matériel spécialisé pour les GPU ou les TPU, et l’inclusion de calculs conçus pour être utilisés dans l’apprentissage profond.

Python offre une multitude de frameworks pour les réseaux de neurones et l’apprentissage profond. Au moment de la publication de ce livre, il en existe un certain nombre et notamment :

TensorFlow (https://www.tensorflow.org/) : ce produit est le leader des frameworks d’apprentissage profond. Il y a plusieurs raisons au succès de TensorFlow, mais la principale est la fourniture d’un environnement robuste dans un package relativement facile à utiliser.

PyTorch (https://pytorch.org/) : initialement développé par Facebook, de nombreuses autres organisations l’utilisent aujourd’hui, notamment X, Salesforce et l’université d’Oxford. Il est particulièrement populaire auprès des universitaires en raison de sa flexibilité et de sa facilité d’expérimentation.

MXNet (https://mxnet.apache.org/) : il s’agit d’un produit Apache qui prend en charge un grand nombre de langages, notamment Python, Julia, C++, R et JavaScript. De nombreuses grandes entreprises l’utilisent, notamment Microsoft, Intel et Amazon Web Services.

Chainer (https://chainer.org/) : écrit en pur Python, il s’appuie sur les bibliothèques NumPy (https://www.numpy.org/) et CuPy (https://cupy.dev/). Il est très populaire au Japon.

Les recherches menées sur le site suivant vous aideront à comprendre les forces et les faiblesses de chaque framework d’apprentissage profond : https://towardsdatascience.com/which-deep-learning-framework-is-growing-fastest-3f77f14aa318. Nous avons choisi TensorFlow pour les exemples de ce livre, ainsi que pour d’autres livres de la collection « Pour les Nuls » sur le sujet (tels que Deep Learning For Dummies et Data Science avec Python pour les Nuls, tous deux par John Mueller et Luca Massaron), en raison de sa diffusion, de sa facilité d’utilisation, de sa robustesse dans diverses applications et de l’excellent soutien de Google (TensorFlow, par exemple, fait partie de Colab).

Google a également intégré le paquet Keras dans la version 2.0 et supérieure de TensorFlow. Keras est un package qui permet de construire des réseaux de neurones complexes à l’aide de quelques séquences de commandes simples. Le principal argument de vente de Keras est qu’il met à la portée du plus grand nombre le processus de création d’applications à l’aide d’un framework d’apprentissage profond. Keras a vu le jour il y a quelques années sous la forme d’un package autonome développé par François Chollet. Au fil des ans, ce produit a joui d’une grande popularité parmi les praticiens en raison de sa praticité et de sa facilité d’utilisation, ce qui en fait également une solution facile pour réduire la complexité de TensorFlow.

À l’origine, Keras était moins un framework qu’une API (un ensemble de spécifications d’interface que vous pouvez utiliser avec plusieurs frameworks comme backends). La version Keras autonome multisupport a récemment été abandonnée, et vous ne pouvez plus utiliser Keras que dans le module Keras fourni avec TensorFlow (tf.keras).

Obtenir une copie de TensorFlow et de Keras

La copie de Python fournie avec Anaconda n’inclut pas de copie de TensorFlow avec Keras si bien que vous devez installer ce produit séparément. Pour éviter les problèmes d’intégration de TensorFlow avec les outils Anaconda, ne suivez pas les instructions d’installation du produit qui se trouvent à https://www.tensorflow.org/install.

Certains lecteurs ont remarqué que TensorFlow ne s’installe pas correctement sur certaines plateformes, notamment Windows 10. Si vous rencontrez des problèmes pour obtenir une bonne installation, vous pouvez toujours travailler sur la plupart des exemples en utilisant Google Colab, dont le fonctionnement est expliqué au Chapitre 6. Il est également expliqué dans ce chapitre les différences entre Notebook et Colab, ainsi que certaines limites de l’utilisation de Colab.

RÉDUIRE LES ERREURS DE CONDA ET DE PIP

Avant d’aller plus loin dans votre installation d’Anaconda, il est utile de mettre à jour Conda et PIP (Package Installer for Python) qui sont les deux applications que l’on utilise pour installer de nouvelles fonctionnalités dans l’environnement Python. L’utilisation d’une version obsolète de l’un ou l’autre gestionnaire de paquets pourrait vous causer des difficultés considérables lors de vos tentatives d’installations. En fait, lorsque vous installez des packages, on vous indique souvent que la copie du gestionnaire de packages que vous utilisez est obsolète. Pour mettre à jour Conda et PIP, on utilise ces commandes dans l’invite d’Anaconda :

   conda update conda
   python -m pip install
   --upgrade pip

Si vous avez des doutes sur l’état de l’un ou l’autre des gestionnaires de packages, effectuez une mise à jour avant d’essayer d’installer un nouveau package. Cette opération ne prend que quelques minutes et vous fera gagner un temps considérable par la suite.

Pour vous assurer que vos copies de TensorFlow et Keras sont disponibles avec Notebook, vous devez ouvrir une invite Anaconda, et non pas une invite de commande standard ou une fenêtre de terminal. Sinon, vous ne pourrez pas vous assurer que les chemins d’accès appropriés sont configurés. Les étapes suivantes vous permettront de commencer votre installation.

❶ À l’invite d’Anaconda, saisissez python --version et appuyez sur Entrée.

Vous voyez la version de Python actuellement installée, qui devrait être la version 3.8.3 pour ce livre, comme cela est illustré à la Figure 14.6. Le chemin que vous voyez dans la fenêtre dépend de votre système d’exploitation, qui est Windows dans cet exemple, mais vous pouvez voir un chemin différent lorsque vous utilisez l’invite Anaconda. Remarquez la partie (base) de l’invite. C’est un indicateur de l’environnement que vous utilisez, et (base) est l’environnement par défaut.

❷ Saisissez conda update --all et appuyez sur Entrée.

Avant d’essayer d’installer TensorFlow et Keras, assurez-vous que l’environnement Conda est à jour. Dans le cas contraire, vous pourriez découvrir que certaines dépendances sont obsolètes, ce qui empêchera votre installation de fonctionner. Conda effectuera une analyse de votre système et fournira ensuite une liste d’étapes à suivre. Lorsque l’on vous demande si vous voulez continuer, saisissez y et appuyez sur Entrée.

[picture]

Figure 14.6 Veillez à utiliser l’invite Anaconda pour l’installation et la vérification de la version de Python.

L’étape suivante consiste à créer un environnement dans lequel exécuter le code qui repose sur TensorFlow et Keras. L’avantage d’utiliser un environnement est que vous conservez un environnement vierge pour une utilisation ultérieure avec d’autres bibliothèques. On utilise Conda, plutôt qu’un autre produit d’environnement tel que virtualenv, pour s’assurer que le logiciel s’intègre aux outils Anaconda. Si vous utilisez un produit tel que virtualenv, l’installation fonctionnera, mais vous devrez effectuer de nombreuses autres étapes pour y accéder, et nous ne les décrivons pas dans cet ouvrage. Le nom de l’environnement pour ce livre est TF_env.

❸ Saisissez conda create -n TF_env python=3.7 tensorflow anaconda nb_conda et appuyez sur Entrée.

L’utilitaire Conda aura besoin d’un certain temps pour analyser l’environnement et rechercher les conflits. Il se peut que vous voyiez un message d’avertissement concernant la disponibilité d’une version plus récente de Conda. Ignorer ce message n’est généralement pas une bonne idée, en particulier lorsque vous travaillez avec un système Windows 10. Lorsque l’on vous demande si vous devez continuer, quittez le processus d’installation en saisissant N et en appuyant sur Entrée. Mettez à jour votre copie de Conda à l’aide des informations figurant dans l’encadré « Réduire les erreurs de Conda et de PIP » , puis relancez le processus d’installation. Si vous ne voyez pas de message d’erreur, saisissez y et appuyez sur Entrée lorsque l’on vous demande de continuer.

Notez que cette installation utilise en fait Python 3.7. La version 3.8.3 de Python utilisée pour les autres exemples du livre est incompatible avec la version 2.1 de TensorFlow. Cette étape prend beaucoup de temps, si bien que vous devez être patient. L’invite d’Anaconda réapparaît lorsque toutes les étapes requises sont terminées. Entre-temps, la lecture d’un bon article technique ou la prise d’un café peuvent vous aider à patienter.

❹ Saisissez conda activate TF_env et appuyez sur Entrée.

L’invite change pour afficher l’environnement TF_env à la place de l’environnement de base. Toutes les tâches que vous effectuez maintenant affecteront l’environnement TensorFlow au lieu de l’environnement de base original.

❺ Saisissez python -m pip install --upgrade pip et appuyez sur Entrée.

Notez que cette étape met à jour la copie de pip utilisée pour TF_env, au lieu de l’environnement de base. Si la copie de PIP est à jour dans cet environnement, la mise à jour se termine après avoir indiqué que PIP est à jour. Cette étape prend un peu de temps, mais pas autant que la création de l’environnement. Le but de cette étape est de s’assurer que vous avez installé la version la plus récente de PIP afin que les commandes ultérieures (dont certaines apparaissent dans le code du livre) n’échouent pas.

❻ Saisissez conda install pyarrow et appuyez sur Entrée.

Le code s’appuie sur PyArrow (http://arrow.apache.org/docs/python/install.html) pour lire les fichiers .feather utilisés pour stocker les jeux de données de ce livre. Vous devez l’installer dans chacun des environnements que vous créez.

❼ Saisissez conda deactivate et appuyez sur Entrée.

La désactivation d’un environnement vous ramène à l’environnement de base. Cette étape permet de vous assurer que vous terminez toujours une session dans l’environnement de base.

❽ Fermez l’invite Anaconda.

Vos installations de TensorFlow avec Keras sont maintenant prêtes à l’emploi.

Afin de travailler avec les exemples TensorFlow du livre, vous devez maintenant choisir un environnement différent dans Anaconda Navigator. Ouvrez une copie d’Anaconda Navigator et vous verrez une liste déroulante des environnements en haut à gauche de l’écran, intitulée Application on. Lorsque vous cliquez sur cette liste déroulante, vous voyez la liste des environnements disponibles, avec l’environnement actuel sélectionné, comme le montre la Figure 14.7. Cliquez sur l’entrée TF_env pour travailler avec les exemples TensorFlow. Le fait de changer d’environnement ne prend que quelques secondes, mais vous voyez une modification dans la configuration d’Anaconda Navigator qui montre que le nouvel environnement est en place. Une fois le changement d’environnement terminé, vous pouvez démarrer Jupyter Notebook.

[picture]

Figure 14.7 Utilisez l’environnement TF_env pour tous les exemples TensorFlow du livre.

Ouvrir la boîte noire

Il est temps de voir comment utiliser le framework TensorFlow pour résoudre un problème. Pour cet exemple, vous allez commencer par un problème bidimensionnel très simple inspiré du TensorFlow Neural Network Playground (vous trouverez ce site Web à http://playground.tensorflow.org), en essayant de diviser deux nuages de points ayant la forme de demi-lunes opposées et positionnées dans un plan cartésien :

import numpy as np
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
np.random.seed(0)
coord, cl = make_moons(500, noise=0.05)
X, Xt, y, yt = train_test_split(coord, cl,
                               test_size=0.30,
                               random_state=0)
cmap_set1 = plt.cm.Set1
fig, ax = plt.subplots(dpi=90)
ax.scatter(X[:,0], X[:,1], s=25, c=y, cmap=cmap_set1)
plt.show()

En regardant la Figure 14.8, vous pouvez voir que la tâche nécessite la formulation d’une séparation non linéaire parce qu’aucune ligne droite ne peut séparer exactement les deux nuages de points.

[picture]

Figure 14.8 Problème de la demi-lune bidimensionnelle.

En faisant vos propres expériences avec le TensorFlow Neural Network Playground, cela va aider à comprendre visuellement le fonctionnement d’un réseau de neurones et à comprendre comment les différents paramètres et hyperparamètres d’un réseau ont un impact sur les résultats générés.

Après avoir configuré les données de l’échantillon, le code crée une solution de réseau de neurones en définissant l’architecture du réseau qui traite les données :

import tensorflow.keras as keras

model = keras.models.Sequential()
model.add(keras.layers.Input(2,))
model.add(keras.layers.Dense(8, activation='relu'))
model.add(keras.layers.Dense(8, activation='relu'))
model.add(keras.layers.Dropout(0.2))
model.add(keras.layers.Dense(1, activation='sigmoid'))

model.compile(optimizer='adam',
             loss='binary_crossentropy',
             metrics=['acc'])
model.summary()

Dans certains cas, l’importation du package requis génère des messages d’avertissement que vous pouvez ignorer, car le code s’exécutera comme prévu. Les erreurs sont générées par le package et non pas par le code de l’exemple. Lorsque vous exécutez ce code, vous obtenez la sortie suivante, qui montre la composition du réseau de neurones :

 Model: "sequential"
 _____________________________________________________
 Layer (type) Output Shape Param #
 =====================================================
 dense (Dense) (None, 8) 24
 _____________________________________________________
 dense_1 (Dense) (None, 8) 72
 _____________________________________________________
 dropout (Dropout) (None, 8) 0
 _____________________________________________________
 dense_2 (Dense) (None, 1) 9
 =====================================================
 Total params: 105
 Trainable params: 105
 Non-trainable params: 0
 _____________________________________________________

Le code spécifie un modèle en utilisant la fonction Sequential() après avoir importé Keras à partir de TensorFlow. La fonction opère comme un pipeline de couches et d’opérations à exécuter de la première à la dernière. Pour définir ces couches, vous appelez add() pour définir l’opération suivante, comme vous le feriez avec des blocs de construction empilés les uns sur les autres. Vous commencez par la couche d’entrée (Input), qui alimente le réseau de neurones en données. La seule exigence de cette couche est de définir la forme attendue de vos données. Comme le réseau s’attend normalement à ce que les données soient divisées en plus petits morceaux, appelés lots, il ne s’intéresse qu’à d’autres dimensions que le nombre de lignes, à savoir la dimension des colonnes, mais il peut gérer autant de dimensions que nécessaire, ce qui le différencie des autres algorithmes d’apprentissage automatique qui n’acceptent en entrée que des matrices de cas par caractéristiques.

La couche Input traite l’entrée, sans appliquer de transformation. Étant donné que les réseaux de neurones reposent sur la descente de gradient, vous devez généralement standardiser (en soustrayant la moyenne et en divisant par l’écart-type) ou normaliser les entrées (en les forçant à se situer dans une plage de valeurs spécifique, par exemple entre 0 et +1 ou entre −1 et +1) au préalable. Vous devez également tester quelle procédure, que ce soit la standardisation ou la normalisation, fonctionne le mieux avec vos données et votre réseau.

Deux couches denses activées par ReLU suivent la couche d’entrée. Ici, les entrées se combinent et les fonctions d’activation reproduisent des correspondances non linéaires entre les entrées et les résultats. Une fonction ReLU (Rectified Linear Units), dont la fonction est (A(x) = max(0, x)), fournit une sortie comprise entre zéro et l’infini : lorsque l’entrée est supérieure à zéro, elle se comporte comme une fonction linéaire, à l’exception de la sortie composée de zéros lorsque les entrées sont nulles ou négatives. Dans sa simplicité, cette fonction d’activation est suffisante pour approximer même des fonctions non linéaires complexes si vous empilez suffisamment de couches de neurones activés par ReLU.

L’un des avantages de la fonction ReLU est qu’elle nécessite moins de puissance de traitement, car moins de neurones émettent un nombre non nul. Toutefois, cet avantage peut également devenir un inconvénient, car lorsqu’il y a trop de neurones inactifs, on obtient un problème appelé « ReLU mourant » . En fait, il est impossible de rétropropager une erreur vers un neurone dont la sortie est nulle, et le réseau cesse d’apprendre ou même de fonctionner s’il y a beaucoup de ReLU mourants.

Les deux couches denses sont suivies d’une couche de dropout (en anglais, dropout signifie abandonner), dont l’objectif est d’empêcher le surapprentissage en coupant de manière aléatoire certaines connexions entrantes entre les deux couches de traitement et le dernier neurone de sortie. Cette implémentation exclut aléatoirement 20 % des signaux entrants à chaque itération d’entraînement, avec le résultat illustré à la Figure 14.9.

Comme le montre la partie gauche de la Figure 14.9, les poids fonctionnent normalement en multipliant leurs entrées afin de produire des sorties pour les activations. Pour déconnecter l’activation, le code multiplie un masque composé d’un mélange aléatoire de un et de zéros avec les résultats. Si le neurone est multiplié par un, le réseau transmet son signal. Lorsqu’un neurone est multiplié par zéro, le réseau arrête son signal, ce qui oblige les autres neurones à ne pas compter sur lui.

Enfin, le réseau est complété par un neurone de sortie, activé par une fonction sigmoïde, qui produit des résultats compris entre 0 et 1 pouvant être interprétés comme des probabilités (si le résultat est supérieur à 0,5, la probabilité que l’échantillon appartienne à la classe positive est supérieure à 50 %). Le modèle est enfin compilé (dernière étape de préparation) à l’aide d’un optimiseur (dans cet exemple, l’optimiseur Adam, un optimiseur automatique générique basé sur la descente de gradient), d’une fonction de coût et d’une mesure d’erreur à surveiller.

[picture]

Figure 14.9 Le mécanisme de dropout exclut temporairement une partie des connexions de l’entraînement.

Même s’il semble y avoir trop de paramètres pour le nombre d’échantillons préparés pour l’entraînement (la règle empirique en statistique est d’avoir au moins 10 échantillons ou plus pour chaque paramètre du modèle), des techniques telles que le mécanisme de dropout permettent de contrôler le surapprentissage. D’une manière générale, il ne faut pas craindre d’utiliser des architectures complexes lors de l’exploitation de réseaux de neurones. On commence par des réseaux de taille moyenne et on détermine ensuite si la réduction ou l’augmentation de la complexité de l’architecture des couches améliore les résultats.

À ce stade, vous pouvez commencer l’entraînement en spécifiant le nombre d’itérations complètes traitant les données. Plus le nombre d’itérations est élevé, plus les neurones acquièrent d’informations à partir des données, ce qui risque d’engendrer un surapprentissage. Vous spécifiez également le lot, c’est-à-dire la taille des morceaux de données qui alimentent l’optimiseur, et vous décidez si vous souhaitez mélanger les données d’entrée à chaque itération afin d’injecter de la randomisation dans le processus, ce qui pourrait faciliter l’optimisation.

history = model.fit(X, y, epochs=1000, batch_size=64,
                   shuffle=True, verbose=0)

L’entraînement peut prendre un certain temps, mais il ne durera pas trop longtemps. Une fois qu’il est terminé, vous pouvez demander la précision du jeu de test et constater que le réseau de neurones fonctionnera parfaitement, car la précision (c’est-à-dire le ratio de classifications correctes) est de 1,0, ce qui signifie qu’il s’agit d’une adaptation parfaite.

from sklearn.metrics import accuracy_score
predictions = (model.predict(Xt)>=0.5).astype(int)
test_accuracy = accuracy_score(yt, predictions)
print(f"Accuracy on test set: {test_accuracy}")

Compte tenu de la simplicité du problème, qui consiste à cartographier des nuages de points sur un plan cartésien, vous pouvez également visualiser les limites de décision qui séparent un nuage de l’autre, telles qu’elles sont produites par le réseau de neurones (Figure 14.10).

[picture]

Figure 14.10 Les limites de décision montrent comment un réseau de neurones résout le problème de la demi-lune.

Pour produire le graphique de la Figure 14.10, qui représente la manière dont le réseau de neurones a cartographié une fonction qui coupe le plan, séparant les deux nuages de points, vous devez tester le réseau de neurones sur une plage de valeurs d’entrée possibles, puis représenter les résultats à l’aide de différentes couleurs :

steps = 100
x_min, x_max = coord[:,0].min(), coord[:,0].max()
y_min, y_max = coord[:,1].min(), coord[:,1].max()

x_range = np.linspace(x_min, x_max, steps)
y_range = np.linspace(y_min, y_max, steps)

xx, yy = np.meshgrid(x_range, y_range)
xy = np.stack([np.ravel(xx), np.ravel(yy)], axis=1)
zz = model.predict(xy)
zz = zz.reshape(xx.shape)

Les fonctions NumPy linspace() et meshgrid() vous aident à produire la plage des valeurs d’abscisse et d’ordonnée d’entrée et à les combiner minutieusement. Vous transmettez ensuite les combinaisons à l’évaluation du réseau de neurones et obtenez un vecteur de prédictions. Enfin, vous affichez les résultats sous la forme d’un tracé de contour, combiné à votre nuage de points original :

cmap_rb = plt.get_cmap('RdBu')
fig, ax = plt.subplots(dpi=90)
ax.contourf(xx, yy, zz, cmap=cmap_rb,
           alpha=0.3, levels=3)
ax.scatter(coord[:,0], coord[:,1],
          s=25, c=cl, cmap=cmap_set1)
plt.show()

Pour toutes vos expériences sur des jeux de données bidimensionnels, vous pouvez visualiser les limites de décision. Pour utiliser cette approche pour d’autres algorithmes d’apprentissage automatique, adaptez et utilisez les deux extraits de code précédents.

Introduction au deep learning

Après la rétropropagation, l’amélioration suivante des réseaux de neurones a conduit au deep learning. Les recherches se sont poursuivies malgré l’hiver de l’intelligence artificielle, et les réseaux de neurones ont commencé à tirer parti des progrès réalisés dans les CPU et les GPU (les processeurs graphiques plus connus pour leurs applications dans le domaine du jeu, mais qui sont en réalité de puissants outils pour le calcul matriciel et vectoriel). Grâce à ces technologies, l’entraînement de réseaux de neurones plus importants est devenu une tâche réalisable en moins de temps et accessible à un plus grand nombre de personnes. La recherche a également ouvert un monde de nouvelles applications. Les réseaux de neurones peuvent apprendre à partir d’énormes quantités de données, et, comme ils sont plus enclins à une variance élevée qu’aux biais, ils peuvent tirer profit du big data, créant des modèles qui s’améliorent continuellement, en fonction des quantités de données que vous leur fournissez. Les sections suivantes présentent une vue d’ensemble de l’apprentissage profond.

Comprendre quelques éléments essentiels de l’apprentissage profond

Si vous pensez que le deep learning implique uniquement l’utilisation de réseaux plus grands qu’auparavant, vous passez à côté d’un point important. L’apprentissage profond n’est pas simplement un relookage d’une ancienne technologie, car il fonctionne mieux que cette technologie en raison de la sophistication supplémentaire qu’il ajoute grâce à l’utilisation d’ordinateurs puissants et à la disponibilité de données de meilleure qualité (et pas seulement en plus grand nombre). Le deep learning implique également un changement qualitatif profond des capacités offertes par la technologie, ainsi que des applications nouvelles et étonnantes qui modernisent les anciens mais performants réseaux de neurones, en les transformant en quelque chose de nouveau.

Étant donné qu’il utilise de nombreuses couches, le deep learning peut cartographier des fonctions cibles plus complexes et résoudre des problèmes qui sont hors de portée des techniques d’apprentissage automatique préexistantes. Parmi ces problèmes, on peut citer la reconnaissance d’images, la traduction automatique et la reconnaissance vocale. Lorsqu’il ne comporte que quelques couches, un réseau de neurones est un parfait approximateur de fonctions universelles, c’est-à-dire un système capable de recréer n’importe quelle fonction mathématique possible. Lorsqu’il est doté d’un grand nombre de couches, un réseau de neurones devient capable de créer, à l’intérieur de sa chaîne interne de multiplications matricielles, non seulement des fonctions cibles sophistiquées, mais aussi un système évolué de représentations de caractéristiques permettant de résoudre des problèmes complexes.

L’une des premières réalisations en matière d’apprentissage profond qui a permis au public de prendre conscience de son potentiel est un programme capable d’identifier les chats. L’équipe Google Brain, dirigée à l’époque par Andrew Ng et Jeff Dean, a réuni 16 000 ordinateurs pour calculer un réseau de deep learning avec plus d’un milliard de poids, permettant ainsi un apprentissage non supervisé à partir de vidéos YouTube. Le réseau informatique pouvait même déterminer, sans intervention humaine, ce qu’est un chat, et les scientifiques de Google ont réussi à extraire du réseau une représentation de l’apparence d’un chat créée par le réseau lui-même (voir l’article de Wired à https://www.wired.com/2012/06/google-x-neural-network/). Cependant, il faut des réseaux complexes et de grande taille pour réussir certaines applications (pour apprendre des caractéristiques complexes, telles que les caractéristiques d’une série d’images). C’est pourquoi les réseaux de neurones précédents ont connu des problèmes tels que la disparition du gradient dès que l’on a essayé de les agrandir.

La disparition de gradient se produit lorsque vous essayez de transmettre un signal à travers un réseau de neurones et que le signal s’estompe rapidement jusqu’à des valeurs proches de zéro si bien qu’il ne peut pas passer à travers les fonctions d’activation. Cela se produit parce que les réseaux de neurones sont des multiplications en chaîne. Chaque multiplication en dessous de zéro diminue rapidement les valeurs entrantes, et les fonctions d’activation ont besoin de valeurs suffisamment élevées pour laisser passer le signal. Plus les couches de neurones sont éloignées de la sortie, plus elles risquent d’être exclues des mises à jour parce que les signaux sont trop faibles ; les fonctions d’activation les arrêteront. Par conséquent, votre réseau cesse d’apprendre dans son ensemble ou apprend à un rythme incroyablement lent.

En fait, lors de l’apprentissage d’un grand réseau, l’erreur se redistribue entre les neurones en favorisant les couches les plus proches de la couche de sortie. Les couches plus éloignées reçoivent des erreurs plus petites, parfois trop petites, ce qui rend l’entraînement lent, voire impossible. Aujourd’hui, les réseaux profonds sont possibles grâce aux études de chercheurs de l’université de Toronto au Canada, tels que Geoffrey Hinton³¹, qui a insisté pour travailler sur les réseaux de neurones même lorsqu’ils semblaient, pour la plupart, être une approche démodée de l’apprentissage automatique.

Le professeur Hinton, prix Nobel et vétéran du domaine des réseaux de neurones (il a contribué à la définition de l’algorithme de rétropropagation), et son équipe de Toronto ont mis au point quelques méthodes pour contourner le problème de la disparition du gradient. Il a ouvert le champ à de nouvelles solutions qui ont fait des réseaux de neurones un outil crucial pour l’apprentissage automatique et l’intelligence artificielle. Ce chapitre a déjà abordé certaines de ses idées, telles que l’activation ReLU et la couche de dropout. Il a également introduit des initialisations plus intelligentes et présenté l’idée du préentraînement, (une manière d’utiliser les poids calculés par un réseau de neurones entraîné pour un problème similaire) comme moyen d’engendrer une meilleure solution.

Le deep learning est une partie complexe et fascinante de l’apprentissage automatique qui a le vent en poupe. L’ouvrage de John Paul Mueller et Luca Massaron, Deep Learning For Dummies, examine en profondeur les concepts et les aspects pratiques de l’apprentissage profond.

La magie des convolutions

Étant donné les grandes capacités des réseaux de neurones pour les tâches de reconnaissance d’images, l’apprentissage profond a connu un grand essor grâce à un certain type de réseau de neurones, le réseau de neurones convolutif (abrégé en anglais en CNN pour convolutional neural network), que l’on désigne également sous le nom de ConvNet. Le chercheur français Yann Le Cun et d’autres scientifiques de renom ont conçu l’idée des CNN à la fin des années 1980, et ils ont pleinement développé leur technologie au cours des années 1990. Mais ce n’est que maintenant, environ 30 ans plus tard, que ces réseaux commencent à donner des résultats étonnants, atteignant même des performances supérieures à celles des humains dans certaines tâches de reconnaissance. Ce changement est dû au fait qu’il est possible de configurer ces réseaux en architectures complexes capables d’affiner leur apprentissage à partir d’un grand nombre de données utiles.

Pour comprendre l’idée qui sous-tend les CNN, il faut considérer les convolutions comme des filtres appliqués à une matrice de valeurs de pixels qui font partie d’une image plus grande. Le CNN transforme certaines parties de la matrice, en fait disparaître d’autres et en fait ressortir d’autres. Vous pouvez utiliser des filtres de convolution pour les bordures ou pour des formes spécifiques. Ces filtres sont également utiles pour trouver des détails dans les images qui déterminent ce que l’image montre. Les humains savent qu’une voiture est une voiture parce qu’elle a une certaine forme et certaines caractéristiques, et non pas parce qu’ils ont déjà vu tous les types de voitures possibles. Un réseau de neurones standard est lié à son entrée, et si l’entrée est une matrice de pixels, il reconnaît les formes et les caractéristiques en fonction de leur position sur la matrice. Les CNN peuvent élaborer des images mieux qu’un réseau de neurones standard pour les raisons suivantes :

• Le réseau spécialise des neurones particuliers pour reconnaître certaines formes (grâce aux convolutions), de sorte que la même capacité à reconnaître une forme n’a pas besoin d’apparaître dans différentes parties du réseau.

• En échantillonnant des parties d’une image en une seule valeur (une tâche appelée pooling, ou encore sous-échantillonnage), vous n’avez pas besoin de lier strictement les formes à une certaine position (ce qui rendrait impossible leur rotation). Le réseau de neurones peut reconnaître la forme dans chaque rotation ou distorsion, assurant ainsi une grande capacité de généralisation du réseau convolutif.

Cet exemple utilise le jeu fashion mnist³², un jeu de données d’articles vestimentaires provenant de Zalando, un magasin en ligne d’articles de mode. Le jeu de données comprend 60 000 échantillons d’entraînement et 10 000 échantillons de test. Chaque échantillon est une image de 28 × 28 pixels représentant un article de dix catégories de vêtements (Figure 14.11).

import tensorflow.keras as keras
loader = keras.datasets.fashion_mnist.load_data
(x_train, y_train), (x_test, y_test) = loader()

import matplotlib.pyplot as plt
class_names = {0: 'T-shirt/top', 1: 'Trouser',
              2: 'Pullover', 3: 'Dress', 4: 'Coat',
              5: 'Sandal', 6: 'Shirt', 7: 'Sneaker',
              8: 'Bag', 9: 'Ankle boot'}

plt.figure()
fig, ax = plt.subplots(5, 5, figsize=(10, 10), dpi=90)
for i in range(25):
    plt.subplot(5,5,i+1)
    plt.imshow(x_train[i], cmap=plt.cm.binary)
    plt.xticks([])
    plt.yticks([])
    plt.grid(False)
    plt.xlabel(class_names[y_train[i]])
plt.show()

Après avoir chargé le jeu de données et tracé quelques échantillons, le code divise les valeurs des pixels des images par 255, de sorte que les valeurs des pixels s’échelonnent désormais de 0 à 1. Le code remodèle ensuite le format de l’image de manière qu’elle ait un seul canal de couleur.

Les images ont généralement une forme matricielle 3D, dont la troisième dimension est un canal de couleur (en général, il y a un canal pour le rouge, le vert et le bleu dans le format RVB). Les CNN s’attendent à recevoir des images au format matriciel 3D, si bien que vous devez remodeler les matrices d’images 2D.

num_classes = 10
x_train = (x_train / 255.).reshape(-1, 28, 28, 1)
x_test = (x_test / 255.).reshape(-1, 28, 28, 1)

[picture]

Figure 14.11 Quelques images du jeu de données fashion mnist.

Après avoir préparé les données, le code instancie le modèle, qui contient deux couches de convolutions (l’une avec 32 et l’autre avec 64 filtres) qui analyseront les images à l’aide d’un noyau de 3 × 3 pixels. On s’attend à ce que le grand nombre de filtres et le petit noyau permettent d’identifier de nombreux détails de l’image. Ensuite, le code utilise une couche de pooling pour réduire de moitié la taille de l’entrée résultante avant de transmettre le tout à quelques couches denses.

model = keras.models.Sequential()
model.add(keras.layers.Input(shape=(28, 28, 1)))
model.add(keras.layers.Conv2D(filters=32,
                             kernel_size=(3, 3),
                             activation='relu'))
model.add(keras.layers.Conv2D(64, (3, 3),
         activation='relu'))
model.add(keras.layers.MaxPooling2D(pool_size=(2, 2)))
model.add(keras.layers.Flatten())
model.add(keras.layers.Dense(32, activation='relu'))
model.add(keras.layers.Dropout(0.2))
model.add(keras.layers.Dense(num_classes,
         activation='softmax'))

model.compile(optimizer='adam',
             loss='sparse_categorical_crossentropy',
             metrics=['acc'])

model.summary()

Vous pouvez voir l’organisation du modèle à partir de cette sortie :

 Model: "sequential_1"
 _____________________________________________________
 Layer (type)        Output Shape             Param #
 =====================================================
 conv2d (Conv2D)      (None, 26, 26, 32)       320
 _____________________________________________________
 conv2d_1 (Conv2D)    (None, 24, 24, 64)       18496
 _____________________________________________________
 max_pooling2d (MaxPooling2D)(None, 12, 12, 64) 0
 _____________________________________________________
flatten (Flatten)    (None, 9216)             0
 _____________________________________________________
 dense_3 (Dense)      (None, 32)               294944
 _____________________________________________________
 dropout_1 (Dropout)  (None, 32)               0
 _____________________________________________________
 dense_4 (Dense)      (None, 10)               330
 =====================================================
 Total params: 314,090
 Trainable params: 314,090
 Non-trainable params: 0
 _____________________________________________________

Le code utilise désormais l’entropie croisée catégorielle clairsemée comme fonction de perte, car elle accepte une cible multiclasse exprimée sous la forme de numéros de classe, sans autre transformation.

model.fit(x_train, y_train,
         batch_size=64,
         epochs=11,
         verbose=1,
         validation_split=0.2)

score = model.evaluate(x_test, y_test, verbose=0)
print(f'Accuracy on test set: {score[1]:0.3}')

Étant donné le grand nombre d’échantillons et les 11 itérations sur les données, le réseau de neurones a besoin de beaucoup de temps pour tout traiter. Le code a réservé 20 % des échantillons à des fins de validation pendant l’entraînement, ce qui permet de vérifier que la perte de validation et les mesures ne se détériorent jamais (signe de surapprentissage). Le code compare également la perte de validation à la perte d’entraînement (une grande différence entre la perte d’entraînement et la perte de validation est un autre signe, plus subtil, de surapprentissage).

Le processus devrait s’achever avec une précision d’entraînement d’environ 95 %, une précision de validation d’environ 92 % et une précision de test de 91,5 % (vous ne voyez la précision de test que dans la sortie affichée, qui peut varier quelque peu par rapport à 91,5 %). Comme les différences entre l’entraînement, la validation et le test sont faibles, vous avez la confirmation que le modèle, grâce aux couches de convolution implémentées et entraînées, est capable de deviner correctement neuf échantillons sur dix dans le jeu de données fashion mnist.

Réseaux de neurones récurrents

Les architectures de réseaux de neurones abordées jusqu’ici dans ce chapitre ne permettent pas de traiter simultanément une séquence d’éléments à l’aide d’une seule entrée. Par exemple, dans le cas d’une série de ventes mensuelles de produits, les chiffres de vente sont traités à l’aide de douze entrées, une pour chaque mois, et le réseau de neurones les analyse en une seule fois. Il s’ensuit que lorsque vous avez des séquences plus longues, vous devez les adapter en utilisant un plus grand nombre d’entrées, et votre réseau devient assez énorme, car chaque entrée doit être connectée à toutes les autres entrées de la couche suivante. Vous finissez par avoir un réseau caractérisé par un grand nombre de connexions (ce qui se traduit par un grand nombre de poids).

Les réseaux de neurones récurrents (RNN) sont apparus pour la première fois dans les années 1980, et différents chercheurs les ont améliorés jusqu’à ce qu’ils gagnent récemment en popularité grâce aux développements de l’apprentissage profond et de la puissance de calcul. L’idée qui sous-tend les RNN est simple : ils examinent une fois chaque élément de la séquence et en gardent la mémoire afin de pouvoir la réutiliser lors de l’examen de l’élément suivant de la séquence. Ce processus s’apparente au fonctionnement de l’esprit humain lorsqu’il lit un texte : une personne lit le texte lettre par lettre, mais comprend les mots en se souvenant de chaque lettre du mot. De la même manière, un RNN peut associer un mot à un résultat en se souvenant de la séquence de lettres qu’il reçoit, comme cela est illustré à la Figure 14.12.

Un RNN est représenté graphiquement par une unité neuronale (également appelée cellule) qui relie une entrée à une sortie, mais qui est également reliée à elle-même (voir la Figure 14.12). Cette autoconnexion représente le concept de récursivité, qui consiste à appliquer une fonction à elle-même jusqu’à ce qu’elle atteigne une sortie particulière. Le calcul d’une factorielle³³ est l’un des exemples de récursivité les plus courants. La figure montre un exemple spécifique de RNN utilisant une séquence de lettres pour créer le mot jazz. La partie droite de la figure montre une représentation du comportement de l’unité RNN recevant le mot jazz en entrée, mais il n’y a en fait qu’une seule unité, comme cela est indiqué sur la partie gauche du schéma. La figure montre une cellule récursive à gauche et la développe comme une série dépliée d’unités qui reçoivent les lettres individuelles du mot jazz à droite. Elle commence par j, qui est suivi des autres lettres. Au cours de ce processus, le RNN émet une sortie et modifie ses paramètres internes. En modifiant ses paramètres internes, l’unité apprend des données qu’elle reçoit et de la mémoire des données précédentes.

[picture]

Figure 14.12 Cellule de RNN repliée et dépliée traitant une entrée séquentielle.

Les RNN sont utiles pour les séquences et servent par conséquent pour apprendre des séquences de lettres ou de mots ainsi qu’une série temporelle de données économiques.

Cependant, les RNN ont d’abord souffert du problème de disparition du gradient jusqu’à ce que deux scientifiques se penchent sur la question et publient un article marquant en 1997 qui proposait une solution. Sepp Hochreiter, un informaticien qui a apporté de nombreuses contributions aux domaines de l’apprentissage automatique, de l’apprentissage profond et de la bio-informatique, et Jürgen Schmidhuber, un pionnier dans le domaine de l’intelligence artificielle, ont publié l’article « Long Short-Term Memory » dans la revue Neural Computation de MIT Press³⁴.

L’article introduisait un nouveau concept de cellule récurrente qui sert aujourd’hui de base à toutes les applications d’apprentissage profond utilisant des séquences. D’abord refusé parce qu’il était trop innovant (il était en fait en avance sur son temps), le nouveau concept de cellule proposé par l’article, appelé LSTM (abréviation de long short-term memory), est aujourd’hui utilisé pour effectuer plus de 4 milliards d’opérations neuronales par jour, selon le site Web de M. Schmidhuber (http://people.idsia.ch/∼juergen/). Les LSTM sont considérés aujourd’hui comme le standard de fait pour les applications de traduction automatique et les chatbots.

Google, Apple, Meta, Microsoft et Amazon ont tous développé des produits autour de la technologie LSTM conçue par Hochreiter et Schmidhuber. Des produits tels que les assistants vocaux intelligents et les traducteurs automatiques fonctionneraient différemment si le concept de LSTM n’avait pas été inventé.

L’exemple suivant montre les capacités et les performances des LSTM en utilisant un problème de série temporelle bien connu, les données sur les passagers des avions. Cet exemple est extrait d’un grand classique, l’ouvrage de Box, Jenkins et Reinsel (Wiley, 1976), Time Series Analysis, Forecasting and Control. Il est devenu populaire et figure dans tous les livres et logiciels consacrés aux séries temporelles.

Les données d’Air Passenger Data correspondent aux totaux mensuels des passagers aériens internationaux, de 1949 à 1960, d’une compagnie aérienne américaine. L’aspect intéressant de ces chiffres est qu’ils présentent une tendance à la hausse qui n’est pas linéaire et qu’ils affichent une certaine récurrence saisonnière (comme l’augmentation des voyages en été), ce que montre bien la Figure 14.13. Le code suivant charge et trace les données brutes :

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
filename = 'https://github.com/lmassaron/datasets/'
filename += 'releases/download/1.0/'
filename += 'air_passengers.feather'
passengers = pd.read_feather(filename)
passengers = passengers.set_index('Month')

passengers.plot()
plt.show()

Dans les problèmes de prévision, on essaye de trouver un modèle qui peut prédire l’avenir en connaissant le passé. Dans un premier temps, on décide de la date à laquelle on souhaite prédire l’avenir, puis de la date à laquelle on peut remonter dans le passé pour obtenir des informations. L’exemple s’efforce de prédire les passagers du mois suivant en utilisant les informations des douze mois précédents. La méthode .shift de pandas peut prendre la série originale et créer des versions décalées pour présenter les valeurs des passagers dans le passé. De cette manière, il est possible d’aligner la valeur cible avec ses 12 valeurs précédentes.

[picture]

Figure 14.13 Données issues de l’exemple Air Passenger Data.

lookback = 12
sequence = pd.DataFrame(
  {'shift_'+str(s): passengers['Passengers'].shift(s)
    for s in range(lookback+1)})[lookback:]

Les 12 premiers exemples sont incomplets (le jeu de données ne contient pas toutes les informations passées), si bien que le code les écarte. La manipulation des données accomplit les étapes suivantes :

❶ Séparer la variable cible des variables prédictives.

❷ Enregistrer la valeur maximale dans les données.

❸ Utiliser la valeur maximale pour transformer les données par une division (en veillant à ce que les valeurs des données ne dépassent pas 1,0).

❹ Remodeler les données car les LSTM s’attendent à des séquences de données ; leurs entrées doivent également être en 3D :

• la première dimension pour le cas ;

• la deuxième dimension pour le type de série (afin de pouvoir prendre en compte plus de séries) ;

• la dernière dimension pour les valeurs de la série.

Voici le code utilisé pour effectuer ces tâches.

y = sequence.iloc[:, 0]
X = sequence.iloc[:, 1:]

max_value = np.max(y)
y /= max_value
X /= max_value

shape = list(X.shape)
shape.insert(1, 1)
print(shape)

X = X.values.reshape(shape)

Ce code divise également les données en jeux d’entraînement et de test en sélectionnant les deux dernières années de données pour vérifier que le modèle fonctionne. La sélection de la dernière partie des données est une pratique courante lorsqu’il s’agit de séries temporelles (la dernière partie est appelée hold-out), car elle simule parfaitement ce qui se passe lorsque vous commencez à prédire l’avenir après avoir entraîné votre modèle sur une partie du passé.

N’utilisez jamais la validation croisée à moins d’employer la version fractionnée dans le temps : https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.TimeSeriesSplit.html. Lorsque vous entraînez votre modèle en utilisant des échantillons de la période que vous souhaitez prédire, vous pouvez en fait perdre certaines informations et générer un type de surapprentissage indésirable sur les données qui vous empêchera d’obtenir un prédicteur utile :

y_train = y[:-24]
y_test = y[-24:]

X_train = X[:-24]
X_test = X[-24:]

À ce stade, vous instanciez le modèle et utilisez le LSTM comme couche de traitement avant de transmettre les informations à des couches plus denses :

import tensorflow.keras as keras

model = keras.models.Sequential()
model.add(keras.layers.LSTM(units=4,
               input_shape=(1, lookback)))
model.add(keras.layers.Dense(4, activation='relu'))
model.add(keras.layers.Dense(1))
model.compile(loss='mean_squared_error',
             optimizer='adam')
model.summary()

L’étape suivante consiste à adapter le modèle, à transmettre les informations au réseau un nombre suffisant de fois et à mélanger les données à chaque itération :

model.fit(X_train, y_train,
         epochs=300,
         batch_size=8,
         shuffle=True,
         verbose=0)

Lorsque l’entraînement est terminé, vous pouvez tester votre modèle en effectuant une prédiction sur les données de test. Comme vous avez précédemment transformé les données en les divisant par la valeur maximale de la série, vous devez maintenant multiplier les résultats pour la même valeur maximale afin de rétablir les valeurs telles qu’elles devraient être, créant ainsi le résultat illustré à la Figure 14.14.

preds = model.predict(X_test) * max_value
real = y_test * max_value

from sklearn.metrics import mean_squared_error
rmse = np.sqrt(mean_squared_error(y_true=real, y_pred=preds))
print(f"RMSE for test set: {rmse:0.3}")
fig, ax = plt.subplots(dpi=90)
real.plot()
ax.plot(preds)
plt.show()

Vous pouvez examiner l’adéquation du modèle (la ligne orange plus claire, en bas) dans la Figure 14.14. La prédiction n’est pas parfaite (les prédictions ne peuvent jamais être totalement exactes, car l’avenir est intrinsèquement incertain). Les réseaux de neurones profonds alimentés par des RNN tels que les LSTM peuvent facilement identifier les modèles d’une séquence, en approximant des fonctions qui, en utilisant les approches de prévision classiques, nécessiteraient normalement des expériences et des essais approfondis.

Les couches convolutives et les réseaux de neurones récurrents constituent un sujet assez vaste qui nécessite encore plus d’exemples pour être bien compris. Dans la Partie 5 de ce livre, « Appliquer l’apprentissage à des problèmes du monde réel » , vous trouverez des exemples plus basiques de modèles d’apprentissage profond.

[picture]

Figure 14.14 Prévisions sur les deux dernières années des données de l’exemple Air Passenger Data.

28 Voir l’article à https://machinelearningmastery.com/exploding-gradients-in-neural-networks/ pour plus de détails.

29 Pour plus de détails, voir https://web.archive.org/web/20230325002700/https://www.techleer.com/articles/449-the-universal-approximation-theorem-for-neural-networks.

30 Voir l’article à https://towardsdatascience.com/understanding-the-mathematics-behind-gradient-descent-dde5dc9be06e pour plus de détails.

31 https://www.utoronto.ca/news/artificial-intelligence-u-t.

32 https://github.com/zalandoresearch/fashion-mnist.

33 https://www.geeksforgeeks.org/recursion.

34 https://web.archive.org/web/20220302125638/http://www.bioinf.at/publications/older/2604.pdf.

DANS CE CHAPITRE

Revoir le problème de la séparabilité non linéaire

•

Découvrir la formulation de base d’un SVM linéaire

•

Comprendre ce que sont les noyaux et ce qu’ils peuvent réaliser

•

S’entraîner au fonctionnement de l’implémentation Python des SVM

Chapitre 15

Aller plus loin avec les machines à vecteurs de support

Parfois les idées naissent d’un heureux hasard, et parfois de l’urgente nécessité de résoudre des problèmes pertinents. Aujourd’hui, la prochaine grande révolution en matière d’apprentissage automatique semble liée à l’apprentissage profond, bien qu’il ne soit pas encore possible d’en déterminer complètement l’orientation, car la technologie ne cesse d’évoluer. Les progrès réalisés dans la reconnaissance d’images et le traitement du langage naturel à l’aide de modèles de deep learning rendent la technologie fascinante et cela a attiré l’intérêt des chercheurs et l’argent des investisseurs. Toutefois, l’apprentissage profond n’est ni la première ni la dernière prochaine grande révolution à faire son apparition dans le domaine de l’apprentissage automatique.

Les machines à vecteurs de support (ou SVM, pour Support Vector Machines, également désignées en français par séparateurs à vaste marge pour conserver les initiales) étaient la « prochaine grande révolution » il y a deux décennies, et quand elles sont apparues, elles ont d’abord laissé perplexes de nombreux chercheurs qui se demandaient si cela allait vraiment fonctionner. Nombreux sont ceux qui se sont interrogés pour savoir si le type de représentation introduit par les SVM pourrait s’acquitter de tâches utiles. La représentation est la capacité de l’apprentissage automatique à approximer certains types de fonctions cibles attendues à la base d’un problème de données. Avoir un algorithme avec une bonne représentation d’un problème implique d’être capable de produire des prédictions fiables sur toute nouvelle donnée.

Les SVM démontrent non seulement une incroyable capacité de représentation, mais aussi une forte utilité, permettant à l’algorithme de trouver ses applications de niche dans le vaste (et croissant) monde des applications d’apprentissage automatique. On utilise aujourd’hui les SVM pour la classification linéaire et non linéaire, la régression et même la détection non supervisée des valeurs aberrantes. Ce chapitre vous aide à découvrir la polyvalence des SVM en vous présentant un algorithme époustouflant qui utilise des calculs complexes pour résoudre d’importants problèmes de reconnaissance d’images, de diagnostic médical et de classification textuelle.

Il n’est pas nécessaire de saisir manuellement le code source de ce chapitre. Il est beaucoup plus facile d’utiliser le code source téléchargeable de ce chapitre dans le dossier ML4D2E de la source téléchargeable pour le code Python et dans le dossier ML4D2ER pour le code R. Les fichiers d’exemple de ce chapitre commencent par ML4D2E ; 15 ; (pour Python) ou ML4D2E_R ; 15 ; (pour R). L’introduction fournit plus de détails sur la manière de trouver ces fichiers source.

Revisiter le problème de la séparation

Comme nous l’avons vu au Chapitre 10, lorsque l’on parle du perceptron, la non-séparabilité des classes peut être un problème lorsque l’on essaie de classifier les échantillons de deux classes d’une manière exacte, car il n’existe pas de ligne droite ou de surface plane qui trace une séparation précise entre les différents échantillons. L’ensemble des algorithmes et des techniques d’apprentissage automatique dont vous disposez offre quelques options dans de telles situations :

• K-plus proches voisins : s’adapte aux limites non linéaires entre les classes lorsqu’on utilise une petite valeur de k.

• Régression logistique : résout le problème en estimant une probabilité d’être dans une classe particulière, permettant ainsi une estimation même si la distinction entre les classes n’est pas possible en raison du chevauchement partiel.

• Transformations des caractéristiques : résout le problème en utilisant à la fois l’ingénierie des caractéristiques (en ajoutant la créativité humaine et les connaissances préalables au processus d’apprentissage) et le développement polynomial automatique (en créant des transformations de puissances et des interactions à partir des caractéristiques). Elle permet de trouver un nouvel ensemble de caractéristiques capables de distinguer les classes au moyen d’une ligne ou d’un plan de séparation.

Les arbres de décision (abordés au Chapitre 10) s’adaptent naturellement à la non-linéarité, car l’algorithme construit ses limites de classification à l’aide de multiples divisions sur des caractéristiques clés pour approximer des limites de séparation complexes et non linéaires. Il en va de même pour les réseaux de neurones évoqués précédemment, qui créent naturellement des transformations de caractéristiques et obtiennent des approximations non linéaires en connectant différentes strates de neurones (au prix d’une plus grande variabilité des estimations lorsque le processus d’apprentissage n’est pas géré avec soin).

Étant donné l’éventail déjà large des options possibles, on peut se demander pourquoi il a été nécessaire de créer un autre type d’algorithme d’apprentissage automatique, tel que le SVM, pour résoudre le problème de la non-séparabilité. L’absence de succès dans la recherche d’un algorithme maître capable d’apprendre la plupart des problèmes signifie que le théorème du « no-free-lunch » est réel et qu’il faut reconnaître qu’on ne peut pas considérer qu’un algorithme est meilleur qu’un autre. Le type de caractéristiques auxquelles vous avez affaire et le type de problème que vous devez résoudre sont les facteurs qui déterminent les algorithmes les plus efficaces. Avoir accès à une autre technique d’apprentissage efficace revient à disposer d’une arme supplémentaire pour résoudre des problèmes de données difficiles. En outre, les SVM présentent une série de caractéristiques qui peuvent rendre l’algorithme tout à fait intéressant pour de nombreux problèmes de données :

• une famille complète de techniques pour la classification binaire et multiclasse, la régression et la détection de données anomales ou nouvelles ;

• traitement robuste du surapprentissage, du bruit et des données aberrantes ;

• une capacité à gérer des situations avec de nombreuses caractéristiques (et les SVM sont toujours efficaces quand vous avez plus de caractéristiques que d’échantillons) ;

• manipulation simple et rapide d’environ 10 000 échantillons d’entraînement ;

• détection automatique de la non-linéarité dans les données, de sorte qu’il n’est pas nécessaire d’appliquer des transformations directement aux caractéristiques et à leur création.

Notez que le dernier point est efficace grâce à la disponibilité de fonctions spéciales, les fonctions noyau. La capacité spéciale des fonctions noyau est de transformer l’espace original des caractéristiques en un nouvel espace de caractéristiques reconstruit pour obtenir de meilleurs résultats de classification ou de régression. C’est similaire au principe du développement polynomial (une fonction noyau disponible fournit en fait le support pour le développement polynomial), mais les mathématiques qui le sous-tendent nécessitent moins de calculs, ce qui permet à l’algorithme de représenter des fonctions de réponse complexes en moins de temps et avec plus de précision.

Explication de l’algorithme

Les SVM, un type d’algorithme qui donne d’aussi bons résultats que les réseaux de neurones à de nombreuses occasions, sont la création du mathématicien Vladimir Vapnik et de certains de ses collègues (Boser, Guyon et Cortes) qui travaillaient dans les laboratoires d’AT&T dans les années 1990. Bien que de nombreux experts en apprentissage automatique aient d’abord été sceptiques à l’égard de l’algorithme parce qu’il ne ressemblait à aucune autre approche alors existante, les SVM ont rapidement gagné en popularité et en succès grâce à leurs performances dans de nombreux problèmes de reconnaissance d’images, comme la saisie manuscrite de chiffres, qui constituaient un défi pour la communauté d’apprentissage automatique de l’époque.

Aujourd’hui, les SVM sont largement utilisés par les spécialistes des données, qui les appliquent à un éventail incroyable de problèmes, du diagnostic médical à la reconnaissance d’images et à la classification textuelle. L’applicabilité de cette technique au big data est cependant quelque peu limitée en raison d’un manque d’extensibilité lorsque les échantillons et les caractéristiques sont trop nombreux.

Les mathématiques de l’algorithme sont un peu complexes, mais l’idée de départ est assez simple. C’est la raison pour laquelle ce chapitre utilise des exemples et des démonstrations faciles pour illustrer le fonctionnement de l’algorithme à l’aide d’intuitions mathématiques de base.

Commencez par examiner le problème de la séparation de deux groupes par une ligne. On voit rarement une situation similaire dans les données de la vie réelle, mais c’est un problème de classification de base de l’apprentissage automatique, et de nombreux algorithmes, comme le perceptron, sont construits à partir de celui-ci. En fonction de l’agencement des données, un problème de classification peut s’avérer difficile. Par exemple, sans transformer l’espace des caractéristiques d’une matrice de données hypothétique (composée de deux caractéristiques, x1 et x2), la Figure 15.1 montre comment résoudre le problème en utilisant (dans l’ordre des graphiques) un perceptron, une régression logistique et un SVM.

[picture]

Figure 15.1 Comparaison des différentes approches perceptron, régression logistique et SVM.

Fait intéressant, bien que le perceptron ne vise qu’à séparer les classes et que la masse des points influence clairement la régression logistique, la ligne de séparation tracée par un SVM a des caractéristiques claires et définies. En observant les différences entre les traits, vous pouvez voir que la meilleure ligne séparant les classes est celle avec le plus grand espace entre la ligne elle-même et les échantillons en marge des classes, ce qui est la solution du SVM. Selon la terminologie des SVM, la ligne de séparation est celle qui présente la marge la plus grande (l’espace vide entre les limites des classes). Le SVM place la ligne de séparation au milieu de la marge (souvent décrite dans les manuels comme marge maximale ou hyperplan de marge maximale) et les échantillons à proximité de la ligne (faisant ainsi partie des limites) sont dits des vecteurs de support.

Les vecteurs de support sont en fait des échantillons représentés sous forme de vecteurs dans l’espace des caractéristiques. En effet, pour définir la position d’un point dans un espace, vous devez décrire ses coordonnées dans chaque dimension comme une série de nombres, qui est un vecteur. De plus, vous pouvez les voir comme des vecteurs de support car l’hyperplan de marge maximale dépend d’eux – et en changer un fait muter la marge, et par conséquent l’hyperplan qui la définit.

Marge, hyperplan de séparation et vecteurs de support sont des termes clés utilisés pour définir le fonctionnement d’un SVM. Les vecteurs de support décident de la meilleure marge possible, tant dans le cas des classes séparables linéairement que dans celui des classes non séparables.

La stratégie d’un SVM est assez simple : en recherchant la plus grande marge de séparation, il tient compte du fait que l’algorithme dérive une fonction de classification à partir d’un échantillon d’observations, mais qu’il ne peut pas se fier à ces observations. Les valeurs échantillonnées changent, parfois même considérablement d’un échantillon à l’autre. Par conséquent, vous ne pouvez pas vous fier à une approche d’ajustement basée sur un seul échantillon pour comprendre toutes les variations de données possibles (comme le fait un perceptron, par exemple). Du fait que vous ne pouvez pas savoir si un futur échantillon sera similaire à celui utilisé pour l’apprentissage, garder la marge la plus grande possible offre au SVM une plus grande flexibilité dans l’espace des caractéristiques lors du travail avec des échantillons successifs. De plus, en regardant les limites des classes, le SVM n’est pas influencé par des points distants (contrairement à une régression linéaire). L’algorithme détermine la marge en utilisant uniquement les échantillons placés sur les limites.

Même s’ils sont créés pour la classification, les SVM peuvent aussi traiter des problèmes de régression. On utilise l’erreur de prédiction comme distance et les valeurs de surface de l’hyperplan de séparation pour déterminer la bonne valeur de prédiction pour chaque combinaison de valeurs de caractéristiques.

Éviter les pièges de l’inséparabilité

Vous devez tenir compte d’une dernière question concernant le processus d’optimisation des SVM décrit précédemment. Du fait que vous savez que les classes sont rarement séparables de manière linéaire, il n’est pas toujours possible pour tous les échantillons servant à l’entraînement de forcer la distance à égaler ou excéder M, la valeur seuil délimitant la marge vers le haut (+M) et vers le bas (−M). Dans ce cas, il est nécessaire de considérer l’expression suivante au cœur de l’optimisation des SVM :

y(x^Tw + b) \geq M(1 + \epsilon_i)

Cette formule de contrainte limitant la solution du SVM comporte ces différents éléments contributifs :

y : les valeurs de la réponse qui peuvent être évaluées à 1 ou −1 ;

xT : les caractéristiques transposées (les dimensions de la matrice sont modifiées) ;

w : les valeurs de pondération (un vecteur de coefficients) ;

b : le biais (une constante) ;

M : la marge (la valeur optimale pour la classification) ;

μi : la variable d’ajustement, une valeur de correction.

Dans le processus d’optimisation, la prédiction de chaque exemple est ajustée par une valeur epsilon spécifique (une valeur égale ou supérieure à zéro), de manière à corriger le plus grand nombre possible de classifications erronées. Si un exemple est déjà correctement classé, l’epsilon attribué est de 0. Les valeurs d’epsilon comprises entre 0 et 1 indiquent que l’exemple est toujours correct, mais qu’il se trouve dans la marge. Enfin, lorsque l’epsilon attribué est supérieur à 1, le cas est mal classé et se trouve du mauvais côté de l’hyperplan de séparation optimal.

Epsilon permet à l’algorithme de corriger les cas inadéquats afin que le processus d’optimisation ait plus de réussite. Vous pouvez définir l’étendue de cette correction en spécifiant que la somme de tous les epsilon doit être inférieure à une valeur, C, qui agit comme un moyen de créer une solution qui est plus affectée par un biais (si la valeur de C est petite) ou une variance (si la valeur de C est grande). La valeur de C indique approximativement le nombre d’inadéquations que le SVM peut ignorer au cours de l’apprentissage.

Le paramètre C est très important, sinon le paramètre le plus important de tous dans un SVM. Epsilon peut créer des exceptions dans la classification et permettre un ajustement plus réaliste de la formulation du SVM lorsque les données sont imprécises et bruyantes. Cependant, si vous introduisez trop de correction (donc, si C est trop grand), la recherche de la marge optimale acceptera trop d’exceptions, produisant ainsi une solution imprécise. En revanche, si C est trop bas, le SVM recherchera une séparation complète des points, ce qui peut aboutir à un hyperplan dont la marge est trop petite et qui, par conséquent, est moins généralisable à de nouvelles données.

Veillez à ne pas confondre la valeur C dans la formulation du SVM avec le paramètre C requis par les implémentations logicielles des SVM, car ils sont différents. Les implémentations Python des SVM acceptent un paramètre C comme coût d’une inadéquation qui correspond en fait à l’inverse du paramètre C, comme décrit précédemment. Par conséquent, en tant que paramètre logiciel, une valeur de C élevée conduit à une marge plus faible, car les coûts sont plus élevés lorsqu’un SVM classifie de manière incorrecte. D’autre part, un C faible implique une plus grande marge, davantage d’acceptation d’échantillons mal prédits, et par conséquent une variance accrue des estimations.

Appliquer la non-linéarité

Les SVM sont mathématiquement exigeants. Jusqu’à présent, vous avez vu quelques formulations qui vous aident à comprendre que les SVM sont un problème d’optimisation qui s’efforce de classifier tous les échantillons de deux classes. Lors de la résolution de l’optimisation dans les SVM, vous utilisez pour le partitionnement un hyperplan présentant la plus grande distance par rapport aux limites des classes. Si les classes ne sont pas linéairement séparables, la recherche de l’hyperplan de séparation optimal permet de gérer les erreurs (quantifiées par la valeur de C) afin de traiter le bruit.

En dépit d’un faible coût pour les erreurs, l’hyperplan linéaire du SVM ne peut pas récupérer les relations non linéaires entre les classes à moins que vous ne transformiez les caractéristiques de manière appropriée. Par exemple, vous ne pouvez classifier correctement qu’une partie des échantillons comme ceux illustrés à gauche de la Figure 15.2 si vous ne transformez pas les deux dimensions existantes en d’autres dimensions en utilisant la multiplication ou des puissances.

[picture]

Figure 15.2 Exemple où des points non séparables linéairement nécessitent une transformation de caractéristiques (à gauche) pour être séparés par une ligne (à droite).

En d’autres termes, vous mappez les caractéristiques existantes sur un espace de dimensionnalité supérieure dans l’espoir de trouver un moyen de séparer les classes de façon linéaire. C’est le même processus que celui décrit au Chapitre 13 lorsque vous essayez le développement polynomial (où vous avez découvert comment un modèle linéaire capture automatiquement des relations non linéaires entre caractéristiques). La création automatique de caractéristiques à l’aide du développement polynomial, qui est également possible pour les SVM, a certaines limites importantes :

• Le nombre de caractéristiques potentielles augmente de façon exponentielle, ce qui rend les calculs fastidieux et sature le jeu de données en mémoire (certains jeux de données ne peuvent pas être étendus au-delà d’une puissance 2 ou 3).

• Le développement crée de nombreuses fonctions redondantes, ce qui entraîne un surapprentissage.

• Il est difficile de déterminer le niveau de développement où les classes deviendront linéairement séparables, ce qui nécessite de nombreuses itérations de développement et de test.

En raison de ces limitations, les SVM ont adopté une méthode différente, appelée fonctions noyau, pour redéfinir l’espace des caractéristiques sans occuper plus de mémoire ou augmenter trop le nombre de calculs. Les fonctions noyau ne sont pas magiques : elles reposent sur des calculs algébriques, et constituent la partie la plus exigeante d’un SVM sur le plan mathématique. Pour comprendre leur fonctionnement, on pourrait dire que les fonctions noyau projettent les caractéristiques originales dans un espace de dimensions supérieures en les combinant d’une manière non linéaire. Elles le font de façon implicite parce qu’elles ne fournissent pas au SVM un nouvel espace de caractéristiques à utiliser pour l’apprentissage (comme le ferait un développement polynomial). Elles renvoient plutôt un vecteur de valeurs transformées que le SVM peut utiliser directement pour ajuster un hyperplan de séparation non linéaire sur les données d’origine.

Les fonctions noyau fournissent donc le résultat d’une combinaison de caractéristiques (précisément un produit scalaire, une multiplication entre vecteurs), sans calculer toutes les combinaisons impliquées dans ce résultat. Elles agissent comme un traducteur, convertissant implicitement les données d’origine dans un espace de dimension supérieure, trouvant un séparateur linéaire et renvoyant ensuite l’information de ce séparateur linéaire en dimensions supérieures dans l’espace d’origine (où elle est non linéaire). C’est ce qu’on appelle l’astuce du noyau.

Explication de l’astuce du noyau par l’exemple

Les fonctions noyau sont des fonctions de mappage pratiques qui permettent aux SVM d’obtenir un jeu de données transformé de taille limitée, ce qui équivaut à une transformation non linéaire plus complexe et plus intensive en quantité de données. Plus accessibles pour la plupart des ordinateurs en termes de traitement et de mémoire, les fonctions noyau permettent de résoudre automatiquement un problème de données à l’aide d’un hyperplan de séparation non linéaire, sans nécessiter d’intervention humaine pour la création des caractéristiques. Dans ses implémentations en Python, le SVM offre une gamme assez large de noyaux non linéaires. Voici une liste de tous les noyaux et de leurs paramètres :

• Linéaire : pas de paramètres supplémentaires.

• Fonction de base radiale : paramètres de forme : gamma.

• Polynômiale : paramètres de forme : gamma, degree et coef0.

• Sigmoïde : paramètres de forme : gamma et coef0.

• Noyaux personnalisés : dépend du noyau.

Bien que le choix de noyaux soit vaste (et encore plus vaste si vous concevez vos propres noyaux personnalisés), on utilise généralement la fonction de base radiale (RBF). La fonction RBF est le noyau le plus populaire parce qu’il est plus puissant que les autres noyaux, car il peut cartographier et approximer presque n’importe quelle fonction non linéaire si vous modifiez son paramètre de forme, gamma.

La fonction RBF fonctionne d’une manière simple mais intelligente. Elle crée une marge autour de chaque vecteur de support – dessinant des bulles dans l’espace des caractéristiques, comme le montre la Figure 15.3. Ensuite, en fonction de la valeur de l’hyperparamètre gamma, elle dilate ou restreint le volume des bulles afin qu’elles fusionnent les unes avec les autres et forment des zones de classification. La valeur de gamma définit le rayon de toutes les bulles que la fonction RBF crée. La marge résultante et l’hyperplan qui la traverse présenteront donc des limites très incurvées, démontrant qu’elle peut être très flexible, comme dans les exemples fournis sur la Figure 15.3.

[picture]

Figure 15.3 Un noyau RBF qui utilise divers hyperparamètres pour créer des solutions SVM uniques.

Le noyau RBF peut s’adapter à différentes stratégies d’apprentissage, résultant en un hyperplan courbé lorsque le paramètre C, par exemple dans l’implémentation Python scikit-learn, est élevé, et par la création d’une ligne de courbe plus lisse lorsque C est faible. Il peut également s’adapter à des formes complexes, comme une cible concentrique, lorsqu’une classe est placée à l’intérieur d’une autre. Toute cette souplesse se fait au détriment d’une plus grande variance, mais elle détecte également des règles de classification complexes que d’autres algorithmes peuvent échouer à trouver.

Lors de l’ajustement d’un noyau RBF, corrigez d’abord le paramètre C, parfois appelé terme de régularisation, pour définir un hyperplan de séparation courbé ; puis réglez gamma pour rendre la forme de la marge grossière et cassée lorsque l’hyperparamètre gamma est faible, ou régulier et fusionné en grandes zones en forme de bulles lorsqu’il est élevé.

Les noyaux polynomiaux et sigmoïdes ne sont pas aussi adaptables que la fonction RBF, montrant ainsi plus de biais, mais ils sont tous deux des transformations non linéaires de l’hyperplan de séparation. Si le sigmoïde comporte une seule courbe, la fonction polynomiale peut avoir autant de courbes ou d’hyperplans séparés que définis par son degré. Plus le degré est élevé, plus le calcul est long.

Vous avez plusieurs valeurs de forme à fixer lorsque vous utilisez les noyaux sigmoïdes et polynomiaux : gamma et coef0 pour les deux, et degree pour la version polynomiale. Comme il est difficile de déterminer les effets de différentes valeurs sur ces paramètres, vous devez tester différentes combinaisons de valeurs à l’aide d’une recherche par quadrillage, par exemple, et évaluer les résultats sur une base pratique. La Figure 15.4 fournit des représentations visuelles de ce que l’on peut faire avec les noyaux polynomiaux et RBF.

[picture]

Figure 15.4 Un noyau polynomial (à gauche) et un noyau RBF (à droite) appliqués aux mêmes données.

Malgré la possibilité de créer votre propre fonction noyau, la plupart des problèmes de données sont facilement résolus en utilisant la fonction RBF. Recherchez simplement la bonne combinaison pour C et gamma en essayant systématiquement différentes valeurs jusqu’à ce que vous obteniez le meilleur résultat pour votre jeu de validation ou votre procédure de validation croisée.

Classification et estimation avec les SVM

Bien que les SVM soient des algorithmes d’apprentissage complexes, ils sont polyvalents et faciles à utiliser lorsqu’ils sont mis en œuvre en Python. L’implémentation de scikit-learn s’appuie sur un package C++ externe, LIBSVM, développé à l’université nationale de Taïwan. Pour en savoir plus sur le LIBSVM partagé pour la classification et la régression SVM, consultez https://www.csie.ntu.edu.tw/∼cjlin/libsvm/. L’implémentation de scikit-learn utilise également le package C LIBLINEAR, spécialisé dans les problèmes de classification utilisant des méthodes linéaires sur des jeux de données volumineux et clairsemés. Ce package a été écrit par les mêmes auteurs de l’université nationale de Taïwan qui ont produit LIBSVM³⁵.

Pour illustrer la manière dont vous pouvez utiliser un SVM pour résoudre un problème complexe, cette section présente une tâche de reconnaissance de chiffres manuscrits et la résout à l’aide d’un noyau non linéaire, la fonction RBF. Il s’agit de l’une des premières applications réelles à utiliser un SVM, et cette application a suscité un regain d’intérêt pour cet algorithme. L’algorithme SVM apprend à partir du jeu de données des chiffres disponible dans le module datasets du package scikit-learn. Ce jeu de données contient une série d’images sous la forme de pixels en niveaux de gris de 8 × 8 représentant des chiffres manuscrits allant de 0 à 9. Le problème est assez simple comparé aux nombreux problèmes que les moteurs de reconnaissance d’images résolvent aujourd’hui au moyen de l’apprentissage profond, mais il permet de saisir le potentiel de l’approche d’apprentissage. Dans un premier temps, le code charge le jeu de données à partir de scikit-learn :

import numpy as np
from sklearn.datasets import load_digits

digits = load_digits()
X, y = digits.data, digits.target

Ce jeu de données ne contient que 1 797 échantillons. La variable y contient des étiquettes numériques comprises entre 0 et 9, la variable X contient un échantillon par ligne et chaque échantillon contient les pixels de chaque chiffre dans 64 caractéristiques (aplatissement de la matrice de 8 × 8 pixels). L’algorithme SVM apprend que si certains pixels s’activent ensemble dans les données, l’image représentée correspond à un nombre particulier.

Cet exemple illustre l’idée défendue par la tribu des Analogiseurs : certains modèles correspondent à des réponses basées sur une analogie. Les SVM ont tendance à cacher ce fait parce qu’ils semblent fournir une combinaison linéaire de caractéristiques pondérées, tout comme la régression linéaire. Cependant, lors de l’application des fonctions de noyau, l’optimisation est basée sur les produits scalaires des échantillons, exprimés sous forme de vecteurs de coordonnées. Les produits scalaires permettent d’estimer la distance entre les points afin de diviser l’espace des caractéristiques en partitions homogènes ayant la même classe à l’intérieur. La distance est un moyen d’établir des similitudes dans un espace composé de caractéristiques. En fait, les SVM travaillent sur la similarité, en associant des points et des motifs sur la base de la distance.

L’extrait de code suivant divise les données disponibles en deux parties, l’une pour l’entraînement et l’autre pour le test. Pour vérifier que les résultats du modèle sont corrects, le code extrait 20 % des échantillons pour les utiliser ultérieurement comme jeu de test (test hors échantillon) :

from sklearn.model_selection import train_test_split
from sklearn.model_selection import cross_val_score

X_train, X_test, y_train, y_test = train_test_split(X,
                y, test_size=0.2, random_state=42)

Dans un troisième temps, le code prépare l’algorithme SVM en tant que pipeline scikit-learn combinant la mise à l’échelle des données dans l’intervalle (−1, +1) et l’algorithme de classification par vecteur de support. Pour l’instant, le code ne règle aucun hyperparamètre, mais le paramètre gamma est réglé sur 'auto', un réglage qui utilise comme valeur le résultat d’une division par le nombre de caractéristiques de données utilisées.

from sklearn.preprocessing import MinMaxScaler
from sklearn.pipeline import make_pipeline
from sklearn.svm import SVC

svm = make_pipeline(MinMaxScaler(
                  feature_range=(-1, 1)),
                  SVC(gamma='auto'))

La mise à l’échelle est une action critique à effectuer avant d’introduire les données dans un SVM. La mise à l’échelle transforme toutes les valeurs dans la plage comprise entre −1 et +1 (ou entre 0 et 1, si vous préférez). La transformation d’échelle permet d’éviter que seules certaines caractéristiques influencent le processus d’optimisation de l’algorithme et contribue à rendre les calculs exacts, fluides et rapides.

La phase d’entraînement commence par une validation croisée (basée sur 10 plis en utilisant cv=10) des performances du SVM sur les données d’entraînement. Pour vérifier l’utilité de la représentation de l’algorithme, l’exemple utilise le score de précision (le pourcentage de suppositions correctes) comme mesure de la qualité du modèle. La précision obtenue est de 0,9847 sur la validation croisée et de 0,9806 sur l’échantillon de test hors échantillon.

cv_acc = cross_val_score(svm, X_train, y_train, cv=10)
test_acc = (svm.fit(X_train, y_train)
              .score(X_test, y_test))
print(f'Cross-validation accuracy:
    {np.mean(cv_acc):0.4f}')
print(f'Test accuracy: {test_acc:0.4f}')

Après avoir vérifié les résultats de la validation croisée et du test en utilisant les hyperparamètres par défaut, le code effectue une recherche systématique pour trouver de meilleurs paramètres susceptibles de fournir un plus grand nombre de réponses exactes. Au cours de cette recherche, le code teste différentes combinaisons de paramètres linéaires et RBF ainsi que les paramètres C et gamma (l’exécution de l’exemple peut prendre un certain temps).

from sklearn.model_selection import GridSearchCV
search_space = [{'svc__kernel': ['linear'],
                'svc__C': np.logspace(-3, 3, 7)},
               {'svc__kernel': ['rbf'],
                'svc__C':np.logspace(-3, 3, 7),
                'svc__gamma': np.logspace(-3, 2, 6)}]
gridsearch = GridSearchCV(svm,
                         param_grid=search_space,
                         refit=True, cv=10,
                         n_jobs=-2)
gridsearch.fit(X_train, y_train)
print(f'Best parameter: {gridsearch.best_params_}')
cv_acc = gridsearch.best_score_
test_acc = gridsearch.score(X_test, y_test)
print(f'Cross-validation accuracy:
        {np.mean(cv_acc):0.4f}')
print(f'Test accuracy: {test_acc:0.4f}')

Les calculs peuvent prendre quelques minutes, après quoi l’ordinateur indique les meilleurs paramètres kernel, C et gamma, ainsi qu’un score de validation croisée amélioré atteignant une précision de près de 99 %. La précision est élevée, ce qui indique que l’ordinateur peut presque distinguer toutes les différentes façons d’écrire les nombres de 0 à 9.

Le jeu de données fourni par scikit-learn n’est qu’une partie du jeu de données MNIST réel, disponible sur le site Web http://yann.lecun.com/exdb/mnist/. Le jeu de données complet se compose de 60 000 échantillons d’entraînement et de 10 000 échantillons de test. En utilisant le même algorithme SVC et le même code, le SVC peut également apprendre le jeu de données original, ce qui permet à votre solution de prédire n’importe quel nombre manuscrit que vous lui présentez.

35 https://www.csie.ntu.edu.tw/∼cjlin/liblinear.

DANS CE CHAPITRE

Découvrir pourquoi plusieurs suppositions valent mieux qu’une seule

•

Faire fonctionner ensemble des arbres non corrélés dans des forêts aléatoires

•

Apprendre à cartographier des fonctions cibles complexes, pièce par pièce, en utilisant le boosting

•

Obtenir de meilleures prédictions en calculant la moyenne des modèles

Chapitre 16

Recourir à des ensembles d’apprenants

Après avoir découvert tant d’algorithmes complexes et puissants, vous serez peut-être surpris de découvrir qu’une somme d’algorithmes d’apprentissage automatique plus simples peut souvent surpasser les solutions les plus sophistiquées. Tel est le pouvoir des ensembles, des groupes de modèles faits pour travailler de concert afin de produire de meilleures prédictions. Ce qui est étonnant avec les ensembles, c’est qu’ils sont constitués de groupes d’algorithmes qui n’obtiennent pas de bons résultats seuls.

Les ensembles ne fonctionnent pas très différemment de l’intelligence collective des foules, grâce à laquelle un ensemble de mauvaises réponses, si l’on en fait la moyenne, fournit la bonne réponse. Sir Francis Galton, le statisticien anglais de l’époque victorienne connu pour avoir formulé l’idée de corrélation, a raconté l’anecdote d’une foule dans une foire de comté qui pouvait deviner correctement le poids d’un bœuf après avoir fait la moyenne des réponses précédentes des gens. Vous pouvez trouver des exemples similaires partout et recréer facilement l’expérience en demandant à vos amis de deviner le nombre de bonbons dans un pot et de faire la moyenne de leurs réponses. Plus il y a d’amis qui participent au jeu, plus la réponse moyenne est précise.

Ce n’est pas la chance qui se cache derrière le résultat – c’est simplement la loi des grands nombres en action³⁶. Même si un individu a un peu plus de 50 % de chances d’obtenir la bonne réponse, une supposition est meilleure qu’un choix aléatoire. En cumulant de plus en plus de suppositions, la distribution globale des réponses finira par ressembler à cette faible chance d’obtenir la bonne réponse (vous obtenez une prévalence de bonnes réponses), ce qui est suffisant pour obtenir la bonne prédiction si vous choisissez le choix le plus fréquent ou si vous en faites la moyenne. Ce fait apparemment incroyable (mais fondé sur une théorie statistique solide) peut être utilisé de nombreuses manières pratiques, par exemple pour les prévisions consensuelles en économie et en sciences politiques. Il s’applique également à l’apprentissage automatique, où il permet d’obtenir des ensembles d’apprentissage efficaces tels que l’algorithme des forêts aléatoires.

Il n’est pas nécessaire de saisir manuellement le code source de ce chapitre. Il est beaucoup plus facile d’utiliser le code source téléchargeable de ce chapitre dans le dossier ML4D2E de la source téléchargeable pour le code Python et dans le dossier ML4D2ER pour le code R. Les fichiers d’exemple de ce chapitre commencent par ML4D2E ; 16 ; (pour Python) ou ML4D2E_R ; 16 ; (pour R). L’introduction fournit plus de détails sur la manière de trouver ces fichiers source.

S’appuyer sur les arbres de décision

Les ensembles sont basés sur une idée récente (formulée vers 1990), mais ils utilisent des outils plus anciens, comme les arbres de décision, qui font partie de l’apprentissage automatique depuis 1950. Comme nous l’avons vu au Chapitre 10, les arbres de décision semblaient à première vue très prometteurs et attrayants pour les praticiens en raison de leur facilité d’utilisation et de compréhension. Après tout, un arbre de décision peut facilement réaliser les choses suivantes :

• traiter des types mixtes de variables cibles et de prédicteurs, avec très peu ou pas de prétraitement des caractéristiques (les valeurs manquantes sont traitées presque automatiquement si vous les remplacez par une valeur anormalement élevée ou faible) ;

• ignorer les caractéristiques redondantes et ne sélectionner que les caractéristiques pertinentes ;

• fonctionner de façon autonome, sans hyperparamètres complexes à corriger et à ajuster ;

• visualiser le processus de prédiction comme un ensemble de règles récursives disposées dans un arbre avec des branches et des feuilles, offrant ainsi une facilité d’interprétation.

Étant donné l’éventail des caractéristiques positives, vous vous demandez peut-être pourquoi les praticiens ont commencé à se méfier petit à petit de cet algorithme au bout de quelques années. La principale raison en est que les modèles qui en résultent présentent souvent une variance élevée dans les estimations.

Pour mieux saisir le problème critique des arbres de décision, vous pouvez considérer ce problème visuellement. Pensez à la situation délicate du problème de « l’œil du taureau » (disons, une cible pour simplifier) qui nécessite un algorithme d’apprentissage automatique pour approximer des fonctions non linéaires (comme le font les réseaux de neurones) ou pour transformer l’espace des caractéristiques (comme lorsque vous utilisez un modèle linéaire avec développement polynomial ou des fonctions noyau dans des machines à vecteurs de support). La Figure 16.1 illustre le carré de la frontière de décision d’un seul arbre de décision (à gauche) par rapport à un ensemble d’arbres de décision (à droite).

[picture]

Figure 16.1 Comparaison entre un seul arbre de décision (à gauche) et un ensemble d’arbres de décision (à droite).

Les arbres de décision divisent l’espace des caractéristiques en zones délimitées, puis utilisent celles-ci à des fins de classification ou de régression. Lorsque la frontière de décision qui sépare les classes d’un problème de type cible est une ellipse, les arbres de décision peuvent l’approximer en combinant un certain nombre de zones.

Même si un arbre de décision donne d’excellents résultats dans la séparation de deux classes, il peut être facilement mis en défaut sur le jeu de test aux limites entre les deux classes. En effet, à proximité de la frontière, les choses sont souvent très différentes de ce qu’elles sont dans le jeu d’apprentissage. La frontière de décision de l’arbre est très imprécise et sa forme est extrêmement grossière et carrée. Le problème est clairement visible pour les problèmes bidimensionnels. La bidimensionnalité entraîne des estimations imprécises et un surapprentissage, qui s’aggravent de manière décisive à mesure que les dimensions des caractéristiques augmentent et en présence d’observations bruyantes (observations dispersées de manière aléatoire dans l’espace des caractéristiques). Il est toutefois possible d’améliorer les arbres de décision en utilisant certaines heuristiques intéressantes qui stabilisent les résultats des arbres :

• ne conserver que les cas correctement prédits pour entraîner à nouveau l’algorithme ;

• construire des arbres séparés pour les échantillons mal classifiés ;

• simplifier les arbres en élaguant les règles les moins efficaces.

En dehors de ces heuristiques, la meilleure astuce consiste à construire plusieurs arbres en utilisant des échantillons différents, puis de les comparer et de faire la moyenne de leurs résultats. L’exemple de la Figure 16.1, ci-dessus, montre que l’avantage est immédiatement visible. Au fur et à mesure que vous construisez un ensemble d’arbres, la frontière de décision devient plus lisse, ressemblant peu à peu à la forme cible hypothétique du séparateur de classes.

Cultiver une forêt d’arbres

Améliorer un arbre de décision en le reproduisant plusieurs fois et en faisant la moyenne des résultats pour obtenir une solution plus générale semblait être une si bonne idée qu’elle s’est répandue, et les universitaires comme les praticiens ont dérivé différentes solutions. Lorsque le problème est une régression, la technique fait la moyenne des résultats de l’ensemble. Cependant, lorsque les arbres se chargent d’une tâche de classification, la technique peut utiliser l’ensemble comme un système de vote, en choisissant la classe de réponse la plus fréquente comme sortie pour toutes ses répliques. Les sections suivantes expliquent comment l’utilisation d’arbres pour un ensemble permet d’obtenir une meilleure solution.

Création de l’ensemble de forêts aléatoires

Lorsque vous utilisez un ensemble pour la régression, l’écart-type, calculé à partir de toutes les estimations de l’ensemble pour un échantillon, peut vous fournir une estimation du degré de confiance que vous pouvez accorder à la prédiction. L’écart-type indique la qualité de la moyenne des estimations. Pour les problèmes de classification, le pourcentage d’arbres prédisant une certaine classe indique le niveau de confiance dans la prédiction, mais vous ne pouvez pas l’utiliser comme estimation de probabilité, car il s’agit du résultat d’un système de vote. Décider de la façon de calculer la solution d’un ensemble s’est fait rapidement. Trouver la meilleure façon de répliquer les arbres dans un ensemble a demandé plus de recherche et de réflexion. La première solution est le collage, c’est-à-dire l’échantillonnage d’une partie du jeu d’entraînement. Initialement proposée par Leo Breiman, cette technique réduit le nombre d’échantillons dans l’entraînement, ce qui peut devenir un problème pour apprendre à partir de données complexes, car vous avez moins d’échantillons à fournir à l’algorithme d’apprentissage. Elle montre son utilité en réduisant le bruit de l’échantillon d’apprentissage (moins d’échantillonnage réduit aussi le nombre de cas aberrants et anormaux). Le professeur Breiman a également testé ensuite les effets de l’échantillonnage dit bootstrap³⁷ (échantillonnage avec remplacement), qui non seulement laisse de côté une partie du bruit (avec le bootstrap, vous omettez en moyenne 37 % de votre échantillonnage initial), mais aussi, grâce à la répétition de l’échantillonnage, crée plus de variation dans les ensembles, améliorant ainsi les résultats. Cette technique est appelée bagging (pour bootstrap aggregation).

Le bootstrap apparaît dans le Chapitre 9 comme faisant partie des alternatives de validation. Avec cette méthode, vous échantillonnez le contenu d’un jeu pour créer un nouveau jeu, ce qui permet au code d’échantillonner les données plusieurs fois. De ce fait, vous pouvez trouver le même échantillon répété de une à plusieurs fois.

Breiman a remarqué que les résultats d’un ensemble d’arbres s’amélioraient lorsque les arbres diffèrent significativement les uns des autres (statistiquement, on dit qu’ils sont non corrélés), ce qui conduit à la dernière technique de transformation – la création d’ensembles d’arbres pour l’essentiel non corrélés utilisant différents sous-ensembles de caractéristiques.

La loi des grands nombres fonctionne parce que vous faites de nombreux essais indépendants d’un événement (par exemple, tester si une pièce de monnaie tombe du côté pile ou face) et lorsque vous comptez la distribution des essais, vous obtenez la distribution de probabilité correcte de l’événement. De la même manière, lorsque vous créez une forêt d’arbres de décision, si ceux-ci sont indépendants les uns des autres et ne commettent donc pas les mêmes erreurs, vous obtenez des estimations qui, mises bout à bout, sont plus correctes. Breiman a découvert que les arbres de décision deviennent indépendants les uns des autres si on les randomise en échantillonnant à la fois les jeux d’entraînement et les caractéristiques utilisées. Cette approche d’échantillonnage donne de meilleures prévisions que le bagging. La transformation ajuste à la fois les caractéristiques et les échantillons. Breiman, en collaboration avec Adele Cutler, a nommé le nouvel ensemble Random Forests (RF, soit forêts aléatoires).

Random Forests est une marque déposée par Leo Breiman et Adele Cutler. Pour cette raison, les implémentations open source ont souvent des noms différents, comme RandomForestClassifier dans scikit-learn de Python³⁸.

RF est un algorithme de classification (naturellement multiclasse) et de régression qui utilise un grand nombre de modèles d’arbres de décision construits sur différents jeux d’échantillons « bootstrappés » et de caractéristiques sous-échantillonnées. Ses créateurs se sont efforcés de rendre l’algorithme facile à utiliser (peu de prétraitement et peu d’hyperparamètres à tester) et compréhensible (la base de l’arbre de décision) afin de démocratiser l’accès à l’apprentissage automatique pour les non-experts. En d’autres termes, en raison de sa simplicité et de son utilisation immédiate, RF peut permettre à n’importe qui d’appliquer avec succès l’apprentissage automatique. L’algorithme fonctionne en quelques étapes répétées :

❶ Multiples passages de bootstrap sur le jeu d’entraînement. L’algorithme obtient un nouveau jeu servant à construire un seul arbre dans l’ensemble au cours de chaque bootstrap.

❷ Choix au hasard d’une sélection partielle de caractéristiques dans le jeu d’entraînement servant à trouver la meilleure variable de fractionnement à chaque fois que vous divisez l’échantillon dans un arbre.

❸ Création d’une arborescence complète à l’aide des échantillons bootstrappés. Évalue les nouvelles caractéristiques sous-échantillonnées à chaque fractionnement. L’expansion complète de l’arborescence n’est pas limitée pour permettre à l’algorithme de mieux fonctionner.

❹ Calcul de la performance de chaque arbre à l’aide d’échantillons que vous n’avez pas choisis lors de la phase de bootstrap (estimation out-of-bag, ou OOB, soit à peu près « en dehors du bootstrap » ). Les échantillons OOB fournissent des métriques de performance sans validation croisée ou à l’aide d’un jeu de test (équivalent au hors échantillon).

❺ Production de statistiques sur l’importance des caractéristiques et calcul de la manière dont les échantillons s’associent dans les nœuds terminaux de l’arborescence.

❻ Calcul d’une moyenne ou d’un vote sur de nouveaux échantillons lorsque tous les arbres de l’ensemble sont terminés. Déclare pour chacun d’eux l’estimation moyenne ou la classe gagnante en tant que prédiction.

Toutes ces étapes réduisent la variance des prédictions aux dépens d’une certaine augmentation du biais (parce que vous utilisez moins de caractéristiques simultanément). La solution construit chaque arbre jusqu’à son extension maximale possible, ce qui permet une approximation fine des fonctions cibles, même complexes. En outre, vous augmentez les chances que chaque arbre de la forêt soit différent des autres, car il est construit à partir d’échantillons différents. Il ne s’agit pas simplement de construire sur des jeux d’échantillons bootstrappés différents : chaque division prise par un arbre est fortement randomisée – la solution ne prend en compte qu’une caractéristique d’un ensemble défini par une sélection aléatoire. Par conséquent, même si une caractéristique importante domine les autres en termes de pouvoir prédictif, le fait qu’un arbre ne contienne pas la sélection permet à l’arbre de trouver différentes façons de développer ses branches et ses feuilles terminales de manière efficace.

La principale différence avec le bagging est cette opportunité de limiter le nombre de caractéristiques à prendre en compte lors du partage des branches de l’arbre. Si le nombre de caractéristiques sélectionnées est bas, l’arbre terminé sera différent des autres, ajoutant ainsi des arbres non corrélés à l’ensemble. En revanche, si la sélection est petite, le biais augmente, car la puissance d’ajustement de l’arbre est limitée. Comme toujours, pour déterminer le bon nombre de caractéristiques à prendre en compte pour le fractionnement, vous devez utiliser des résultats de validation croisée ou d’estimation OOB.

Il existe une variante de RF appelée Extremely Randomized Trees (ERT) qui est encore plus randomisée parce qu’elle ne choisit pas seulement de manière aléatoire les caractéristiques pour les divisions, mais décide également de manière aléatoire des divisions³⁹. Dans cette version, une plus grande variance est échangée contre un plus grand biais et, dans certains problèmes de données, elle peut donner de meilleurs résultats que la RF. En outre, elle est toujours plus rapide, car l’ERT nécessite moins de calculs. La croissance d’un grand nombre d’arbres dans l’ensemble ne pose aucun problème. Plus il y a d’arbres, plus vos estimations seront précises et stables. Vous devez cependant tenir compte du coût de l’effort de calcul ; compléter un grand ensemble prend beaucoup de temps. RF est un algorithme qui peut être exécuté en parallèle sur plusieurs processeurs.

Chaque arbre de l’ensemble RF est indépendant des autres (après tout, ils ne doivent pas être corrélés), ce qui signifie que vous pouvez construire chaque arbre en parallèle avec les autres. Étant donné que tous les ordinateurs modernes disposent d’une fonctionnalité multiprocesseur et multithread, ils peuvent effectuer des calculs sur de nombreux arbres en même temps, ce qui constitue un réel avantage de la RF par rapport à d’autres algorithmes d’apprentissage automatique.

Obtenir plus d’informations sur les forêts aléatoires

Vous trouverez de plus amples informations sur les forêts aléatoires en Python à https://scikit-learn.org/stable/modules/ensemble.html#forest. Pour une discussion générale sur l’algorithme, la meilleure ressource est le manuel de Leo Breiman et Adele Cutler à https://www.stat.berkeley.edu/∼breiman/RandomForests/.

Démonstration de l’algorithme RF

Une démonstration simple montre comment un algorithme RF peut résoudre un problème simple en utilisant un nombre croissant d’arbres. Cet exemple utilise le jeu de données sur la qualité du vin, qui modélise les préférences en matière de vin par la fouille de données sur les propriétés physiques et chimiques, en combinant à la fois les vins blancs et les vins rouges. Ce jeu de données est décrit dans « Modeling wine preferences by data mining from physicochemical properties » , par P. Cortez, A. Cerdeira, F. Almeida, T. Matos, et J. Reis, dans Decision Support Systems⁴⁰ (Elsevier, 47(4) : 547-553, 2009). Les données associent des tests physicochimiques (concentration d’alcool, densité, acidité, etc.) sur certains vins portugais à l’évaluation de la qualité par des experts. Le jeu de données offre la possibilité de le traiter à la fois comme un problème de régression et de classification. Dans cet exemple, il s’agit d’un problème de régression où l’on commence par charger les données et créer des jeux d’entraînement et de test.

import numpy as np
import pandas as pd

try:
    import matplotlib.pyplot as plt
    import seaborn as sns
    sns.set(style='whitegrid', palette='deep',
           font='sans-serif')
except:
    import matplotlib.pyplot as plt

filename = 'https://github.com/lmassaron/datasets/'
filename+='releases/download/1.0/wine_quality.feather'
wine = pd.read_feather(filename)

np.random.seed(42)
train = (wine.groupby('quality')
            .apply(lambda x: x.sample(frac=.7))
            .reset_index(drop=True))
test = wine[∼wine.index.isin(train.index)]
X_train = train.iloc[:,1:]
y_train = train.iloc[:,0]
X_test = test.iloc[:,1:]
y_test = test.iloc[:,0]

Après avoir chargé le jeu de données à partir du dépôt Internet du livre sur GitHub (voir l’introduction pour plus de détails), le code utilise pandas pour appliquer un échantillonnage stratifié (de sorte que la variable de réponse, quality, soit distribuée de manière égale dans les jeux d’entraînement et de test) afin de réserver 30 % des observations en tant que jeu de test.

Dans pandas, vous pouvez échantillonner à partir d’un DataFrame à l’aide de la méthode sample(), mais il s’agit simplement d’un échantillonnage aléatoire. Si vous devez stratifier en fonction d’une caractéristique pour vous assurer que vous échantillonnez cette caractéristique dans les bonnes proportions, vous devez d’abord regrouper les données en fonction de cette caractéristique à l’aide de la méthode groupby(), puis appliquer la méthode d’échantillonnage à l’aide de la méthode apply() et d’une fonction lambda. Voici, par exemple, le code pour échantillonner 80 % de df sur la base d’une caractéristique : df.groupby('feature').apply(lambda x : x.sample(0.8)). L’étape suivante consiste à modéliser le problème comme indiqué ci-dessous :

from sklearn.metrics import mean_absolute_error
from sklearn.ensemble import RandomForestRegressor

series = [10, 25, 50, 100, 150, 200, 250, 300, 500]
test_scores = list()
for param in series:
   rf = RandomForestRegressor(n_estimators=param,
          max_depth=14, random_state=42, jobs=-1)
   rf.fit(X_train, y_train)
   preds = rf.predict(X_test)
   test_scores.append(mean_absolute_error(y_test,
          preds))

L’exemple commence par importer des fonctions et des classes de scikit-learn : mean_absolute_error, pour la mesure, et RandomForestRegressor pour la modélisation du problème. Le dernier élément est l’implémentation par scikit-learn des forêts aléatoires pour les problèmes de régression. Après avoir défini quelques valeurs possibles pour le paramètre n_estimator, qui spécifie le nombre d’arbres de décision dans la RF, le code itère les valeurs et teste chacune d’entre elles en entraînant d’abord le régresseur, puis en évaluant le résultat sur le jeu de test.

Pour accélérer l’exécution de l’exemple, le code définit le paramètre n_jobs à -1, ce qui permet à l’algorithme d’utiliser toutes les ressources disponibles de l’unité centrale. Ce paramètre peut ne pas fonctionner sur certaines configurations d’ordinateur, car l’ordinateur utilise alors toutes les ressources pour entraîner le modèle. Vous pouvez également définir le paramètre à -2, ce qui permet de libérer un processeur pour d’autres tâches.

Après avoir effectué les calculs, le code produit un graphique qui révèle comment l’algorithme Random Forests converge vers une bonne exactitude après avoir créé quelques arbres, comme le montre la Figure 16.2. Il montre également que l’ajout d’arbres ne nuit pas aux résultats, car l’erreur tend à se stabiliser et même à s’améliorer légèrement après avoir atteint un certain nombre d’arbres.

import matplotlib.pyplot as plt

fig, ax = plt.subplots(dpi=120)
plt.plot(series, test_scores, '-o')
plt.xlabel('number of trees')
plt.ylabel('mean absolute error')
plt.show()

Comprendre les métriques d’importance

Les forêts aléatoires présentent ces avantages :

• Ajustement à des fonctions cibles complexes, mais avec peu de risques de surapprentissage.

[picture]

Figure 16.2 Visualisation de l’exactitude d’ensembles de différentes tailles.

• Sélection automatique des caractéristiques dont ils ont besoin (bien que le sous-échantillonnage aléatoire des caractéristiques dans le fractionnement des branches influence le processus).

• Facilité d’ajustement, car ils n’ont qu’un seul hyperparamètre, le nombre de caractéristiques sous-échantillonnées.

• Ils offrent une estimation des erreurs OOB, vous évitant ainsi de mettre en place une vérification par validation croisée ou par jeu de tests.

Un ensemble de forêts aléatoires peut également fournir des résultats supplémentaires susceptibles d’être utiles pour apprendre des données. Par exemple, il peut vous dire quelles caractéristiques sont plus importantes que d’autres. Vous pouvez construire des arbres en optimisant une mesure de pureté (entropie ou indice de Gini) afin que chaque division choisisse la caractéristique qui améliore le plus la mesure. Lorsque l’arborescence est terminée, on vérifie quelles caractéristiques l’algorithme utilise à chaque fractionnement, et on additionne les améliorations lorsque l’algorithme se sert d’une caractéristique plus d’une fois. Lorsque vous travaillez avec un ensemble d’arbres, faites simplement la moyenne des améliorations que chaque caractéristique apporte à tous les arbres. Le résultat montre le classement des caractéristiques prédictives les plus importantes.

Les praticiens appellent ce type d’évaluation de l’importance « importance de Gini » ou diminution moyenne de l’impureté. La méthode n’est pas sans défaut ; elle tend à favoriser les caractéristiques catégorielles ayant de nombreuses valeurs uniques (les caractéristiques à cardinalité élevée) parce qu’elles nécessitent de nombreuses divisions par rapport aux autres caractéristiques, ce qui implique qu’elles ont plus d’importance dans l’évaluation. L’importance de la permutation, que l’on obtient en utilisant la fonction permutation_importance() (https://scikitlearn.org/stable/modules/generated/sklearn.inspection.permutation_importance.html) sur un modèle RF ajusté, est une autre façon d’estimer l’importance des caractéristiques, qui est considérée comme plus juste. Dans ce cas, chaque caractéristique est permutée de manière aléatoire et la différence de performance sur un jeu de validation est considérée comme une mesure de l’importance de la caractéristique.

La documentation de scikit-learn contient une expérience intéressante qui illustre les limites de la méthode de diminution moyenne de l’impureté : une caractéristique aléatoire peut gagner en importance dans un modèle RF lorsqu’elle est choisie au hasard pour être divisée plus de fois au cours de l’entraînement. L’importance de la permutation peut fournir des estimations plus fiables dans des cas similaires⁴¹.

Afin de fournir une interprétation des mesures d’importance dérivées des forêts aléatoires, cet exemple teste la mise en œuvre de l’importance par permutation sur le jeu de données sur la qualité du vin testé précédemment en utilisant le nombre optimal d’arbres d’estimation. L’exemple commence par entraîner le régresseur RF avec 500 arbres.

from sklearn.inspection import permutation_importance

rf = RandomForestRegressor(n_estimators=500,
                          max_features='sqrt',
                          random_state=42)
rf.fit(X_train, y_train)
preds = rf.predict(X_test)
mae = mean_absolute_error(y_test, preds)
print(f"test mean absolute error for rf: {mae:0.3}")

L’erreur moyenne absolue obtenue est de 0,228, ce qui est un très bon résultat si l’on considère que le code estime des valeurs sur une échelle allant de 0 à 5. Après avoir entraîné la forêt aléatoire, vous pouvez tester l’importance de la caractéristique par permutation et tracer le résultat, comme cela est illustré à la Figure 16.3.

[picture]

Figure 16.3 Importance de la permutation des caractéristiques calculée sur le jeu de test.

result = permutation_importance(rf, X_test,
    y_test, n_repeats=10, random_state=42, n_jobs=-1)

sorted_idx = result.importances_mean.argsort()
fig, ax = plt.subplots(dpi=120)
ax.boxplot(result.importances[sorted_idx].T,
     vert=False, labels=X_test.columns[sorted_idx])
ax.set_title("Permutation Importances (test set)")
fig.tight_layout()
plt.show()

L’alcool est de loin la caractéristique la plus déterminante pour la qualité. L’acidité volatile et la densité sont également des aspects importants. Les autres caractéristiques contribuent presque de la même manière, mais la caractéristique binaire indiquant si le vin est rouge ou blanc n’est pas déterminante, compte tenu des autres caractéristiques physiques et chimiques.

Travailler avec des suppositions presque aléatoires

Grâce au bootstrap, le bagging⁴² produit une réduction de la variance en induisant certaines variations dans des prédicteurs par ailleurs similaires. Le bagging est plus efficace lorsque les modèles créés sont différents les uns des autres et, bien qu’il puisse fonctionner avec différents types de modèles, on l’emploie principalement avec des arbres de décision.

Le bagging et son évolution, les forêts aléatoires, ne sont pas les seuls moyens d’exploiter un ensemble. Au lieu de garder les éléments de l’ensemble indépendants, la stratégie opposée consiste à créer des ensembles interdépendants d’algorithmes simples d’apprentissage automatique pour résoudre des fonctions cibles complexes. Cette approche est appelée boosting, celui-ci fonctionnant en construisant des modèles séquentiellement et en entraînant chaque modèle à l’aide des informations fournies par le précédent.

Contrairement au bagging, qui préfère travailler avec des arbres totalement poussés, le boosting utilise des modèles biaisés, qui sont des modèles capables de bien prédire des fonctions cibles simples. Ces modèles plus simples comprennent des arbres de décision avec une seule branche divisée (ou stumps), des modèles linéaires, des perceptrons et des algorithmes bayésiens naïfs. Ces modèles peuvent ne pas bien fonctionner lorsque la fonction cible à deviner est complexe (ils sont également appelés apprenants faibles), mais ils peuvent être entraînés rapidement et fonctionner au moins un peu mieux qu’une conjecture aléatoire (ce qui signifie qu’ils peuvent modéliser une partie de la fonction cible).

Chaque algorithme de l’ensemble devine une partie de la fonction, de sorte qu’une fois additionnés, ils peuvent collectivement deviner la fonction entière. La situation n’est pas très différente de l’histoire des aveugles et de l’éléphant⁴³. Dans cette histoire, un groupe d’hommes aveugles doit découvrir la forme d’un éléphant, mais chaque homme ne peut sentir qu’une partie de l’animal. Un homme touche la défense, un autre les oreilles, un autre la trompe, un autre une patte, un autre le corps, et un autre la queue, qui sont des parties différentes de l’éléphant. Ce n’est qu’en rassemblant ce que chacun a appris séparément qu’ils peuvent déterminer la forme réelle de l’éléphant. Une telle approche présente un avantage important par rapport à la méthode du bagging : chaque modèle est dépendant des autres parce qu’il est spécialisé dans l’estimation des parties de la fonction cible où les autres modèles ont échoué, ce qui simplifie la tâche globale de l’ensemble.

Prédicteurs de bagging avec Adaboost

Adaboost (abréviation d’Adaptive Boosting) de Yoav Freund et Robert Schapire, inventé en 1995 est le premier algorithme de boosting. Voici sa formulation :

H(X) = signe \bigg(\sum_{m=1}^M\alpha_mH_m(X)\bigg)

Vous pouvez penser que la formulation d’Adaboost est assez compliquée à première vue, mais vous pouvez la simplifier en l’examinant pièce par pièce. La fonction H(X) représente la fonction de prédiction, qui transforme les caractéristiques, la matrice X, en prédictions. En tant qu’ensemble de modèles, la fonction de prédiction est une sommation pondérée des modèles, d’une manière qui ressemble aux modèles linéaires examinés dans le Chapitre 13.

La fonction H(X) fournit des résultats sous la forme d’un vecteur de signes (positifs ou négatifs) qui indiquent les classes dans une prédiction binaire (Adaboost est un algorithme de prédiction binaire). Les signes dérivent de la somme de M modèles, chacun pouvant être distingué par un indice m différent (le modèle générique est hm(X)). M est un nombre entier que l’on détermine lors de l’entraînement à partir des données. On détermine M en effectuant des tests sur un jeu de validation ou, mieux encore, en utilisant la validation croisée. Il est nécessaire de tester M car, en principe, chaque modèle ajuste une partie des données, et le fait d’avoir trop de modèles signifie qu’ils sont trop bien ajustés, ce qui est une sorte de mémorisation qui conduit à un surapprentissage, à une variance élevée des estimations et, par conséquent, à de mauvaises prédictions. Le nombre de modèles ajoutés est donc un hyperparamètre critique.

Notez que l’algorithme multiplie chaque modèle hm(X) par une valeur alpha, qui diffère pour chaque modèle. C’est le poids du modèle dans l’ensemble, et alpha est conçu de manière intelligente, car sa valeur est liée à la capacité du modèle à produire le moins d’erreurs de prédiction possible. On calcule alpha de la manière suivante :

\alpha_m = \frac{1}{2} * log((1 - err_m)\ /\ err_m)

Selon cette formulation, alpha prend une valeur plus grande quand l’erreur du modèle hm(X), indiquée par la notation errm, devient plus petite. L’algorithme multiplie les modèles comportant le moins d’erreurs par des valeurs alpha plus grandes, et ces modèles jouent donc un rôle plus important dans la sommation au cœur de l’algorithme Adaboost. Les modèles qui produisent plus d’erreurs de prévision sont moins pondérés.

Le rôle du coefficient alpha ne s’arrête pas à la pondération du modèle. Les erreurs produites par un modèle dans l’ensemble ne dictent pas simplement l’importance de ce modèle dans l’ensemble lui-même, mais elles modifient également la pertinence des échantillons d’entraînement utilisés pour l’apprentissage. Adaboost apprend la structure des données en employant un algorithme simple petit à petit ; la seule façon de focaliser l’ensemble sur différentes parties des données est d’attribuer des poids. L’attribution de poids indique à l’algorithme de compter un échantillon en fonction de son poids ; par conséquent, un seul échantillon peut « peser » autant que deux ou trois autres, voire plus. Vous pouvez aussi faire disparaître un échantillon du processus d’apprentissage en lui donnant de moins en moins d’importance. Lorsque l’on considère les poids, il devient plus facile de réduire la fonction de coût de la fonction d’apprentissage en travaillant sur les échantillons qui pèsent le plus (plus de poids = plus de réduction de la fonction de coût). L’utilisation de poids guide efficacement le processus d’apprentissage.

Dans un premier temps, les échantillons, comme dans tous les autres algorithmes d’apprentissage rencontrés jusqu’ici, ont la même contribution dans la construction du modèle. L’optimisation se déroule comme d’habitude. Après avoir créé le premier modèle et estimé une erreur totale, l’algorithme contrôle chaque échantillon pour déterminer si la prédiction est correcte. Si c’est le cas, rien ne se passe : le poids de chaque échantillon reste le même qu’avant. En revanche, en cas de classification erronée, chaque échantillon voit son poids augmenter et, lors de l’itération suivante, les échantillons ayant un poids plus grand influencent le modèle, mettant ainsi davantage l’accent sur la recherche d’une solution pour l’échantillon le plus grand.

À chaque itération, l’algorithme d’Adaboost est guidé par des poids pour travailler sur la partie des données qui est moins prévisible. En fait, vous n’avez pas besoin de travailler sur des données que l’algorithme peut prédire correctement. La pondération est une solution intelligente pour conditionner l’apprentissage, et les machines à gradient boosting, le prochain algorithme que vous allez explorer, affinent et améliorent le processus. Notez que la stratégie ici est différente de RF. Dans les forêts aléatoires, l’objectif est de créer des prédictions indépendantes ; ici, les prédicteurs sont chaînés les uns aux autres du fait que les prédicteurs antérieurs déterminent le fonctionnement des prédicteurs postérieurs. Comme les algorithmes de boosting reposent sur une chaîne de calculs, vous ne pouvez pas facilement paralléliser ceux-ci, si bien qu’ils sont plus lents. Vous pouvez ainsi exprimer la formulation de la mise à jour du poids de cette manière :

w_i = w_i * exp\bigg(\alpha_m * I(y_i \neq H_m(x_i))\bigg)

La fonction I(yi ≠ Hm(xi)) produit 0 si l’inégalité est fausse, et 1 si elle est vraie. Dans le second cas, le poids de l’échantillon précédent est multiplié par l’exponentielle d’alpha. L’algorithme modifie le vecteur w résultant en surpondérant les cas individuels mal classés par l’algorithme d’apprentissage le plus récent de l’ensemble. Au sens figuré, apprendre de cette façon, c’est comme faire un petit pas d’amélioration à chaque fois vers l’objectif d’un ensemble prédictif fonctionnel, et ce, sans regarder en arrière, puisque après avoir additionné les algorithmes d’apprentissage, on ne peut plus les changer.

Les types d’algorithmes d’apprentissage qui fonctionnent bien avec Adaboost sont des apprenants faibles, ce qui signifie qu’ils n’ont pas un grand pouvoir prédictif. Du fait qu’Adaboost réalise une approximation de fonctions complexes en utilisant un ensemble de ses parties, l’utilisation d’algorithmes d’apprentissage automatique qui s’entraînent rapidement et ont un certain biais est logique, de sorte que les parties sont simples. C’est comme dessiner un cercle en utilisant une série de lignes : même si la ligne est droite, tout ce que vous avez à faire, c’est de dessiner un polygone avec autant de côtés que possible pour approximer le cercle. Généralement, les stumps de décision sont l’apprenant faible préféré d’un ensemble Adaboost, mais vous pouvez aussi utiliser avec succès des modèles linéaires ou des algorithmes bayésiens naïfs. L’exemple suivant exploite la fonction de bagging fournie par scikit-learn pour déterminer si les arbres de décision, la régression ridge ou l’algorithme des k plus proches voisins (KNN) sont les plus appropriés pour effectuer des prédictions à partir du jeu de données sur la qualité du vin (cet exemple peut mettre un certain temps à s’exécuter).

import numpy as np
from sklearn.ensemble import AdaBoostRegressor
from sklearn.tree import DecisionTreeRegressor
from sklearn.linear_model import Ridge
from sklearn.neighbors import KNeighborsRegressor

dt = DecisionTreeRegressor(max_depth=14)
lr = Ridge(alpha=1.0, normalize=True)
knn = KNeighborsRegressor(n_neighbors=1)

model_labels = ['Decision trees', 'Linear regression', 'KNN']
models = [dt, lr, knn]

for label, model in zip(model_labels, models):
    ada_model = AdaBoostRegressor(model,
                  n_estimators=300, random_state=42)
ada_model.fit(X_train, y_train)
preds = ada_model.predict(X_test)
mae = mean_absolute_error(y_test, preds)
print(f"test mae for {label}: {mae:0.3}")

Vous pouvez améliorer la performance d’Adaboost en augmentant le nombre d’éléments dans l’ensemble jusqu’à ce que la validation croisée ne rapporte plus de résultats qui s’aggravent. Le paramètre que vous pouvez augmenter est n_estimators, et il est actuellement fixé à 300. Plus votre prédicteur est faible, plus votre ensemble doit être grand pour bien prédire.

Booster les prédicteurs intelligents

Adaboost, discuté plus haut dans ce chapitre, explique comment la procédure d’apprentissage crée une fonction après s’être déplacée systématiquement vers une cible, une analogie similaire à la descente de gradient décrite au Chapitre 8. Cette section s’intéresse à l’algorithme GBM (gradient boosting machines), qui utilise l’optimisation de la descente de gradient pour déterminer les bons poids pour l’apprentissage dans l’ensemble. Les performances qui en résultent sont impressionnantes, faisant de GBM l’un des outils prédictifs les plus puissants que vous puissiez apprendre à utiliser en apprentissage automatique. Voici la formulation à la base de l’algorithme GBM :

H(X) = \sum_{m=1}^M v * h_m(X ; w_m)

Comme dans Adaboost, on part de la formulation. Celle-ci exige que l’algorithme fasse une somme pondérée de multiples modèles de l’algorithme. En fait, ce qui change le plus, ce n’est pas le principe de fonctionnement du boosting, mais plutôt le processus d’optimisation pour obtenir les poids impliqués dans les fonctions additionnées.

Dans la formule précédente, M représente le nombre total de modèles et h la fonction finale, qui est la somme d’une série de M modèles. Chaque modèle est différent, d’où la notation hm, qui se traduit par h₁, h₂, etc. Les différences entre les fonctions d’apprentissage de la série se produisent parce que les modèles dépendent à la fois des caractéristiques X et des échantillons pondérés par les valeurs du vecteur w, qui change effectivement pour chaque modèle.

Où l’on retrouve la descente de gradient

Jusqu’à présent, les choses ne sont pas si différentes d’Adaboost. Notez cependant que l’algorithme pondère chaque modèle par un facteur constant, v, le facteur ou coefficient de rétrécissement (shrinkage factor). C’est ici que vous commencez à remarquer la première différence entre Adaboost et GBM. Le fait est que v est exactement comme alpha, bien que alpha change pour chaque modèle. Cependant, ici, il est fixé et force l’algorithme à apprendre dans tous les cas, quelle que soit la performance de la fonction d’apprentissage précédemment ajoutée. Compte tenu de cette différence, l’algorithme construit la chaîne en répétant la séquence d’opérations suivante :

H_m(X) = H_{m-1}(X) + v * h_m(X ; w_m)

Examinez la formule au fur et à mesure qu’elle se développe pendant l’entraînement. Après chaque itération m, l’algorithme additionne le résultat des modèles précédents avec un nouveau modèle construit sur les mêmes caractéristiques, mais sur une série d’échantillons pondérés différemment. Cela est représenté sous la forme de la fonction hm(X,w). La fonction montre l’autre différence principale par rapport à Adaboost : le vecteur w n’est pas déterminé par les erreurs de classification du modèle précédent, mais dérive plutôt d’une optimisation de la descente de gradient, qui attribue des poids par rapport à une fonction de coût, éventuellement de types différents.

GBM peut prendre en charge différents problèmes : régression, classification et classement (pour ordonner des échantillons), chaque problème utilisant une fonction de coût particulière. La descente de gradient aide à découvrir l’ensemble des valeurs vectorielles w qui réduisent la fonction de coût. Ce calcul équivaut à choisir les meilleurs échantillons à utiliser pour obtenir une prédiction améliorée. L’algorithme calcule le vecteur w de multiples fois lorsque la fonction h l’utilise, et à chaque fois, l’algorithme ajoute la fonction résultante aux précédentes. Le secret des performances de l’algorithme GBM réside dans les poids optimisés par la descente de gradient, ainsi que dans ces trois astuces intelligentes :

• Rétrécissement (shrinkage) : agit comme un taux d’apprentissage dans l’ensemble. Comme dans la descente de gradient, il faut fixer un taux d’apprentissage adéquat pour éviter de trop s’éloigner de la solution, qui est la même qu’en GBM. De faibles valeurs de rétrécissement conduisent à de meilleures prévisions.

• Sous-échantillonnage : émule l’approche par collage. Si chaque arbre suivant s’appuie sur un sous-échantillon des données d’entraînement, le résultat est une descente de gradient stochastique. Pour de nombreux problèmes, l’approche du collage permet de réduire le bruit et l’influence des valeurs aberrantes, améliorant ainsi les résultats.

• Arbres de taille fixée : fixer la profondeur de l’arbre utilisée dans le boosting, c’est comme fixer une limite de complexité aux fonctions d’apprentissage que vous placez dans l’ensemble, en vous appuyant sur des arbres plus sophistiqués que les « souches » (stumps) utilisées dans Adaboost. La profondeur agit comme une puissance dans un développement polynomial : plus l’arbre est profond, plus le développement est important, ce qui augmente à la fois la capacité à intercepter des fonctions cibles complexes et le risque de surapprentissage.

scikit-learn implémente GBM en utilisant toutes les caractéristiques décrites jusqu’ici dans ce chapitre. Vous trouverez des informations sur GBM à https://scikit-learn.org/stable/modules/ensemble.html#gradient-boosting. L’exemple suivant poursuit le test précédent, mais cette fois-ci, vous créez un classificateur GBM pour le jeu de données sur la qualité du vin et testez ses performances (cet exemple peut également prendre un certain temps à s’exécuter) :

from sklearn.ensemble import GradientBoostingRegressor

gbm = GradientBoostingRegressor(n_estimators=500,
                               subsample=0.9,
                               max_depth=12,
                               learning_rate=0.1,
                               random_state=42)
gbm.fit(X_train, y_train)
preds = gbm.predict(X_test)
mae = mean_absolute_error(y_test, preds)
print(f"test mean absolute error for gbm: {mae:0.3}")

État de l’art en matière de données tabulaires

L’implémentation scikit-learn du gradient boosting n’est pas la seule disponible pour créer des solutions plus performantes pour vos modèles de données. Le gradient boosting est un algorithme que vous pouvez perfectionner en améliorant la façon dont les arbres de décision sont construits, la façon dont leurs divisions sont effectuées et la façon dont l’algorithme traite les données manquantes et les caractéristiques catégorielles.

Le premier package alternatif de gradient boosting est XGBoost (https://github.com/dmlc/xgboost), une solution qui étonne par sa vitesse et ses performances accrues par rapport à la version existante de scikit-learn. En particulier, XGBoost exploite les capacités de calcul multicœur des ordinateurs existants, malgré le fait que GBM soit un algorithme séquentiel. XGBoost y parvient en trouvant les meilleures divisions à l’intérieur des arbres de décision. Il parallélise ensuite les opérations de l’algorithme et les rend distribuables sur plusieurs ordinateurs.

Ensuite, Microsoft a créé LightGBM (https://github.com/Microsoft/LightGBM), qui est encore plus rapide que XGBoost (parce qu’il utilise des approximations intelligentes) et donne de meilleurs résultats sur certains problèmes parce qu’il construit ses arbres de décision d’une manière différente. LightGBM utilise une stratégie de recherche en profondeur, c’est-à-dire qu’il construit des arbres très profonds, devinant mieux certains exemples qu’une approche en largeur, dans laquelle vous construisez des arbres larges pour essayer de faire correspondre autant d’échantillons que possible.

CatBoost (https://catboost.ai/) est une autre solution du moteur de recherche russe Yandex. CatBoost est le meilleur de sa catégorie lorsqu’il s’agit de traiter des données catégorielles complexes telles que des adresses géographiques ou des identifiants de produits. En outre, les développeurs de scikit-learn, comme Olivier Grisel, ont conçu une version expérimentale de GBM inspirée de LightGBM (vous pouvez écouter Grisel parler du travail de ces développeurs sur l’algorithme à EuroPython 2019 : https://www.youtube.com/watch?v=urVUlKbQfQ4).

Faire la moyenne de différents prédicteurs

Jusqu’à cette section, ce chapitre a traité des ensembles composés du même type d’algorithmes d’apprentissage automatique. Mais les systèmes de calcul de la moyenne et de vote peuvent également fonctionner tout à fait correctement lorsque vous utilisez un mélange d’algorithmes d’apprentissage automatique différents. Il s’agit de l’approche de calcul de la moyenne, qui est largement utilisée lorsque vous ne pouvez pas réduire autrement la variance de l’estimation.

Lorsque vous essayez d’apprendre à partir de données, vous essayez différentes solutions, modélisant ainsi vos données à l’aide de différentes solutions d’apprentissage automatique. C’est une bonne pratique de comparer les solutions en recherchant le meilleur ajustement de validation, mais vous pouvez également tirer parti du travail effectué en vérifiant si vous pouvez placer certaines de ces solutions avec succès dans des ensembles en utilisant des moyennes de prédiction ou en comptant les classes prédites. Le principe est le même que pour le bagging de prédictions non corrélées, lorsque des modèles mélangés peuvent produire des prédictions moins affectées par la variance. Pour obtenir une moyenne efficace, vous devez :

❶ Diviser vos données en jeux d’entraînement, de validation et de test.

❷ Utiliser les données d’entraînement avec différents algorithmes d’apprentissage automatique.

❸ Enregistrer les prédictions de chaque algorithme et évaluer la viabilité du résultat à l’aide du jeu de validation.

Faites la moyenne des résultats pour une tâche de régression (en utilisant également différentes pondérations) ou choisissez le résultat le plus fréquent pour la classification. Cette moyenne est en quelque sorte un modèle de second niveau. Une fois que vous avez produit vos prédictions finales, vous pouvez encore les vérifier sur le jeu de validation avant d’essayer le jeu de test.

Mélanger les solutions

La moyenne de différentes solutions est également appelée mélange, et le processus que l’on utilise pour les mélanger est souvent appelé mélangeur. scikit-learn fournit les classes VotingClassifier et VotingRegressor qui peuvent organiser le mélange pour vous. Cet exemple mélange le meilleur modèle précédent, le régresseur de gradient booting, avec un modèle complètement différent, un régresseur à vecteur de support, parce que vous avez généralement plus de succès lorsque vous mélangez différents modèles. Après avoir exécuté le code, on constate que la combinaison de GBM et de SVR donne les meilleurs résultats pour les données relatives à la qualité du vin.

from sklearn.ensemble import VotingRegressor
from sklearn.svm import SVR

gbm = GradientBoostingRegressor(n_estimators=500,
               subsample=0.9, max_depth=12,
               learning_rate=0.1,random_state=42)

svr = SVR(C=10.0, gamma=0.3, epsilon=0.0001)

blender = VotingRegressor(
   estimators=[('gbm', gbm), ('svm', svr)], n_jobs=-2)

blender.fit(X_train, y_train)
preds = blender.predict(X_test)
mae = mean_absolute_error(y_test, preds)
print(f"test mean absolute error for blender: {mae:0.3}")

Le mélange peut donner de meilleurs résultats que les éléments mélangés lorsque vous mélangez différents types de modèles qui ont des performances presque égales. Le calcul de la moyenne ne fait qu’ajuster certaines de leurs faiblesses tout en maintenant leurs performances.

Avec VotingRegressor et VotingClassifier, vous pouvez décider d’effectuer une moyenne pondérée en indiquant les poids dans le paramètre weights. En outre, VotingClassifier permet de compter les classifications majoritaires (voting='hard') ou de mélanger les probabilités des différentes classes (voting='soft').

Empiler différentes solutions

Le mélange n’est pas la seule approche possible pour intégrer davantage de modèles et réduire ainsi la variance des prédictions. Une autre approche consiste à utiliser les prédictions de vos modèles comme caractéristiques en plus d’un autre modèle. Il s’agit de l’empilement (stacking), qui nécessite de s’adapter à des préparations assez complexes. scikit-learn a facilité le processus en fournissant les classes StackingRegressor et StackingClassifier qui gèrent tous les processus à votre place. Vous fournissez une approche de validation croisée correcte, vos estimateurs et un modèle final, comme dans l’exemple. Dans ce cas, le résultat n’est pas le meilleur pour le problème, mais l’empilement de plusieurs solutions permet souvent d’obtenir les meilleurs résultats.

from sklearn.ensemble import StackingRegressor
from sklearn.model_selection import StratifiedKFold

gbm = GradientBoostingRegressor(n_estimators=300,
              subsample=0.9, max_depth=16,
              learning_rate=0.1, random_state=42)

svr = SVR(C=10.0, gamma=0.4, epsilon=0.0001)

sfk = StratifiedKFold(n_splits=10, shuffle=True,
                     random_state=42)

stacker = StackingRegressor(estimators=[('svm', svr)],
              final_estimator=gbm, passthrough=True,
              cv=sfk, n_jobs=-2)
stacker.fit(X_train, y_train)
preds = stacker.predict(X_test)
mae = mean_absolute_error(y_test, preds)
print(f"test mean absolute error for stacker: {mae:0.3}")

L’empilement de plusieurs modèles et l’empilement ou le mélange des résultats de plusieurs empilements est la meilleure pratique pour gagner les compétitions d’apprentissage automatique. Dans la pratique, il faut toujours garder à l’esprit que la dette technique (c’est-à-dire le compromis entre une complexité inutile et un minuscule supplément de performance) devra être remboursée un jour ou l’autre.

D’une manière générale, n’adoptez le mélange et l’empilement que lorsque la différence par rapport à l’utilisation de solutions plus simples en vaut vraiment la peine.

36 Voir https://fr.wikipedia.org/wiki/Loi_des_grands_nombres.

37 Voir par exemple https://fr.wikipedia.org/wiki/Bootstrap_(statistiques).

38 https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html.

39 https://scikit-learn.org/stable/modules/ensemble.html#extremely-randomized-trees.

40 https://www.sciencedirect.com/science/article/abs/pii/S0167923609001377?via%3Dihub.

41 https://scikit-learn.org/stable/auto_examples/inspection/plot_permutation_importance.html#sphx-glr-auto-examples-inspection-plot-permutation-importance-py.

42 Rappelons que le terme bagging, c’est-à-dire littéralement ensachage, est en fait un diminutif pour bootstrap aggregation.

43 https://americanliterature.com/author/james-baldwin/short-story/the-blind-men-and-the-elephant.

Livre 2

Python

Partie 1

Débuter avec Python

Dans cette partie…

Définir l’association entre Python et les applications.

Utiliser Google Colab pour travailler avec Python.

Exécuter des tâches essentielles à l’aide de Python.

Créer votre première application.

Réaliser des exploits magiques.

DANS CE CHAPITRE

Comprendre pourquoi vous voulez parler à votre ordinateur.

•

Comprendre qu’une application est une forme de communication.

•

Comprendre ce que font l es applications, et pourquoi vous voulez en créer.

•

Pourquoi utiliser Python comme langage de programmation ?

Chapitre 1

Parler à votre ordinateur

Avoir une conversation avec son ordinateur peut encore de nos jours paraître relever de la science-fiction. Après tout, les membres de l’équipage du vaisseau Enterprise dans Star Trek parlaient régulièrement à leurs ordinateurs. Mais avec l’avènement de logiciels interactifs comme Siri (https://www.apple.com/siri/), Alexa (https://www.amazon.com/dp/B07XKF5RM3/) ou encore d’autres logiciels conversationnels (https://windowsreport.com/talking-pc-software/), finalement rien de tout cela n’est plus inconcevable.

Demander vocalement à un ordinateur ou à un appareil portable comme un smartphone ou une tablette de donner certaines informations est une chose. Mais fournir des instructions à la machine en est une autre. Ce chapitre (comme évidemment tout ce livre) considère que vous voulez donner des instructions à votre ordinateur et en obtenir des résultats. Vous y découvrirez également qu’il est nécessaire de faire appel à un langage spécial pour effectuer ce type de communication, et pourquoi vous devez utiliser le langage Python pour y arriver. Mais l’idée principale à retirer de ce chapitre, c’est que la programmation est simplement une forme de communication, qui vient prendre sa place parmi les autres formes de communication dont vous vous servez déjà avec votre ordinateur.

Comprendre pourquoi vous voulez parler à votre ordinateur

Parler (j’emploie ce verbe au sens large) à une machine peut sembler un peu bizarre au premier abord (même si les gens parlent déjà aux chats, aux chiens, aux voitures, aux grille-pain et à d’autres objets), mais c’est nécessaire, car l’ordinateur ne peut pas lire dans votre cerveau. Du moins, pas encore. Les ordinateurs capables de lire dans les pensées s’en rapprochent, comme cela est décrit dans l’article disponible à https://www.psychnewsdaily.com/this-computer-can-read-your-mind-and-render-your-thoughts-as-pictures/. Et même si une machine était capable de déchiffrer vos pensées, elle ne ferait que continuer à communiquer avec vous. Rien ne peut se produire sans cet échange d’informations entre la machine et vous. Des activités telles que celles-ci :

• lire vos messages électroniques ;

• écrire quelque chose à propos de vos vacances ;

• trouver le plus beau cadeau du monde…

sont toutes des exemples de communication entre vous et l’ordinateur ; et le fait que, pour atteindre les buts que vous lui assignez, il doive pousser plus loin l’interaction avec d’autres machines, ou d’autres personnes, pour leur transmettre vos requêtes n’y change rien. Il s’agit tout simplement d’une extension de l’idée de base, à savoir que communiquer est une condition nécessaire pour obtenir n’importe quel résultat.

Dans la plupart des cas, tout cela se déroule de manière pratiquement invisible pour vous, à moins que vous n’y réfléchissiez d’un peu plus près. Par exemple, lorsque vous vous rendez dans un salon de discussion (https://workspaceupdates.googleblog.com/2021/09/google-chat-rooms-are-now-spaces.html) en ligne, vous pouvez imaginer que vous communiquez avec une autre personne. Cependant, vous communiquez avec votre ordinateur, votre ordinateur communique avec l’ordinateur de l’autre personne par le biais du salon de discussion (quel qu’il soit), et l’ordinateur de l’autre personne communique avec cette personne (ou éventuellement une intelligence artificielle). La Figure 1.1 illustre ce concept.

[picture]

Figure 1.1 Les communications entre personnes et ordinateurs s’effectuent de manière invisible, transparente.

Remarquez l’espèce de nuage au centre de la Figure 1.1. Il pourrait contenir à peu près n’importe quoi, mais vous savez qu’on peut au moins y trouver d’autres ordinateurs qui exécutent certaines applications. C’est grâce à eux que vous et votre correspondant(e) pouvez dialoguer. De votre point de vue, c’est quelque chose de très facile, voire d’évident. Toute cette chaîne travaille à votre service en arrière-plan, de façon invisible, et vous avez simplement l’impression de converser comme si vous vous trouviez dans la même pièce (enfin presque).

Oui, une application est une forme de communication

La communication avec les ordinateurs s’établit dès lors que vous vous servez d’applications. Par exemple, vous utilisez une application pour répondre à vos messages, une autre pour acheter quelque chose, et une autre encore pour créer un diaporama. Une application (certains l’appellent maintenant une app) fournit le moyen d’exprimer des idées humaines devant un ordinateur, tout en permettant à celui-ci de les comprendre et en définissant les outils grâce auxquels ce dernier est capable de gérer ces processus de communication. Les données utilisées pour exprimer le contenu d’une présentation sont évidemment différentes de celles qui concernent un cadeau pour votre maman. Par conséquent, les manières dont vous les visualisez, les utilisez et les comprenez sont différentes pour chaque tâche. C’est pourquoi vous devez utiliser des applications diverses de manière à interagir avec toutes ces données pour que l’ordinateur et vous arriviez à vous comprendre.

Il est possible de trouver des applications qui répondent à vos besoins quotidiens. En fait, il en existe vraisemblablement des tas qui sont capables de le faire, sans que vous le sachiez. Les programmeurs ont travaillé dur pour créer des millions et des millions d’applications de tous types depuis de nombreuses années. Mais cela ne répond pas forcément à votre question. La réponse tient dans une réflexion sur vos données et sur la manière dont vous voulez interagir avec elles. Certaines de ces données peuvent être tellement spécifiques qu’aucun programmeur ne s’est penché dessus. À moins que vous n’ayez besoin d’un format de données totalement nouveau. Dans ce cas, le dialogue avec l’ordinateur ne peut pas s’établir, à moins que vous ne définissiez vous-même une application capable de traiter vos données. Les sections qui suivent décrivent cette notion d’application du point de vue de vos données, avec les traitements dont vous avez besoin.

Des procédures pour tous les jours

Une procédure, c’est simplement une série d’étapes à exécuter pour effectuer une certaine tâche. Pour griller du pain, par exemple, vous devez suivre une procédure comme celle-ci :

• Vous sortez le pain et le beurre du réfrigérateur.

• Vous ouvrez le sachet du pain et vous sortez deux tartines.

• Vous ouvrez le couvercle du grille-pain (s’il en a un).

• Vous insérez vos deux tranches de pain dans l’appareil.

• Vous appuyez sur le bouton ad hoc du grille-pain pour lancer la cuisson.

• Vous attendez que le grille-pain vous rende les tartines.

• Vous retirez les tartines du grille-pain.

• Vous placez les tartines sur un plateau.

• Vous beurrez les tartines.

D’accord, il peut y avoir des variations, et chacun prépare son petit-déjeuner à sa manière. Mais il y a un point qui met tout le monde d’accord : vous ne beurrez pas vos tartines avant de les mettre dans le grille-pain. Si ? Excusez-moi. À chacun ses préférences. Mais, dans tous les cas, reconnaissons que la plupart des gens effectuent tous ces gestes mécaniquement, sans y réfléchir. Pourtant, ils répondent bel et bien à une procédure qui ne varie pas d’une journée à l’autre (enfin, sauf si vous êtes en retard pour aller travailler).

Les ordinateurs sont incapables d’exécuter des tâches en l’absence de procédures. Autrement dit, vous devez leur expliquer quelles étapes ils doivent effectuer, dans quel ordre, et quels sont les cas particuliers (les exceptions) auxquels ils doivent répondre. Toutes ces informations (et plus encore) sont définies à l’intérieur d’une application. Pour faire court, une application est simplement une procédure écrite que vous utilisez pour expliquer à l’ordinateur ce qu’il doit faire, quand il doit le faire, et comment il doit le faire. Du fait que vous utilisez quotidiennement de très nombreuses procédures, tout ce que vous avez à faire, c’est d’appliquer des connaissances que vous possédez déjà pour expliquer à un ordinateur ce qu’il doit réaliser. D’accord, c’est un peu simpliste, mais vous voyez l’idée.

Écrire des procédures

Lorsque j’étais écolier, notre institutrice nous demandait régulièrement d’écrire sur un papier une série d’étapes, par exemple pour faire griller du pain comme dans l’exemple ci-dessus. Chaque papier était lu, et on expérimentait ce qu’il décrivait. D’accord, on faisait parfois des erreurs. Je me souviens d’une fois où j’avais oublié de mentionner qu’il fallait enlever les tranches du grille-pain avant de les tartiner. Je n’ose même pas vous raconter le résultat… Mais ces leçons m’ont été très utiles. Il est vrai qu’écrire des procédures n’est pas un long fleuve tranquille, et que souvent nous oublions de préciser telle ou telle étape en supposant que les autres vont comprendre ce que nous voulons faire. Mais c’est une mauvaise hypothèse.

Cependant, il ne suffit pas de mettre les procédures par écrit. Vous devez également tester la procédure en demandant à quelqu’un qui n’est pas familier avec la tâche de l’exécuter en utilisant votre procédure. Lorsque vous travaillez avec des ordinateurs, l’ordinateur est votre meilleur cobaye.

Les applications sont des procédures comme les autres

Lorsque vous écrivez une application, vous écrivez en réalité une procédure qui définit une série d’étapes que l’ordinateur devrait exécuter pour accomplir certaines tâches, ce que vous voulez lui voir faire. Si vous manquez une étape, vous n’obtiendrez pas les résultats escomptés. L’ordinateur ne sait pas ce que vous voulez dire, ou ce que vous avez l’intention de lui faire réaliser. La seule chose qu’il comprenne, c’est que vous lui avez fourni une procédure spécifique, et qu’il doit suivre les étapes de celle-ci.

Faire faire de drôles de choses à votre ordinateur

Il y a sans doute des personnes qui suivent des procédures que vous avez créées. Elles compensent automatiquement les déficiences de votre procédure en fonction de leur propre expérience, ou prennent des notes pour signaler ce qui ne va pas. En d’autres termes, les êtres humains sont capables d’interpréter vos intentions pour en combler les lacunes.

Lorsque vous commencez à écrire des programmes pour ordinateur, vous ressentez généralement de la frustration, car la machine effectue les tâches qui lui sont dévolues d’une manière totalement précise, et elle prend vos instructions au pied de la lettre. Par exemple, si vous dites à l’ordinateur qu’une certaine valeur devrait être égale à 5, l’ordinateur recherche une valeur strictement égale à 5. Point. Un humain pourrait voir 4,95, et comprendre que cette valeur répond en fait à votre demande. Pour l’ordinateur, 4,95 et 5 sont deux choses bien différentes. En bref, les ordinateurs sont inflexibles, pas du tout intuitifs et manquent totalement d’imagination. Lorsque vous écrivez une procédure informatique, l’ordinateur doit faire ce que vous demandez avec une précision totale, sans jamais varier dans le temps, et sans jamais modifier ou interpréter votre procédure. Dans certains cas (peu nombreux), les résultats peuvent même être assez drôles (comme la fois où l’ordinateur a commencé à réciter un poème que vous vouliez garder secret). Quand vous programmez, il est utile d’avoir le sens de l’humour.

Définir ce qu’est une application

Comme je l’ai déjà mentionné, les applications fournissent des moyens pour définir des idées humaines avec précision, et d’une manière telle qu’elle soit compréhensible par un ordinateur. Pour atteindre cet objectif, l’application s’appuie sur une ou plusieurs procédures qui décrivent comment manipuler certaines données, et comment les présenter. Par exemple, vous pouvez voir sur votre écran quelque chose qui est affiché par votre traitement de texte. Mais, pour cela, l’ordinateur a besoin de procédures permettant de retrouver les données qui forment ce texte sur le disque, leur donner une mise en forme compréhensible par vous, puis vous présenter le résultat. Les sections qui suivent définissent les spécificités d’une application plus en détail.

Comprendre que les ordinateurs utilisent un langage spécial

Les langues naturelles sont complexes et difficiles à comprendre. Même des applications telles que Siri, Google Assistant, Alexa ou Cortana ont de sérieuses limites quant à la compréhension de ce que vous dites. Au fil des ans, les ordinateurs ont acquis la capacité de saisir la parole humaine sous forme de données et d’interpréter certains mots prononcés comme des commandes, mais les ordinateurs ne comprennent toujours pas la parole humaine. Ce que fait l’ordinateur, c’est faire correspondre des modèles de voix à des données qu’il comprend, puis faire correspondre ces données à des commandes spécifiques.

De leur côté, les ordinateurs sont, par essence, incapables de s’appuyer uniquement sur votre langage pour comprendre les procédures que vous écrivez. Comme je l’ai dit plus haut, ils prennent celles-ci au pied de la lettre. Si vous écriviez des applications comme vous parlez, les résultats seraient totalement imprévisibles. C’est pourquoi vous devez utiliser un langage spécial, appelé un langage de programmation, pour communiquer avec les ordinateurs. Ces langages spéciaux rendent possible l’écriture de procédures qui sont à la fois spécifiques et totalement compréhensibles aussi bien par les humains que par les ordinateurs.

En fait, les ordinateurs ne parlent aucun langage. Ils utilisent des codes binaires pour modifier en interne des sortes de bascules et effectuer des tas de calculs mathématiques. Ils ne comprennent même pas les lettres, rien que les nombres. Une application particulière transforme le langage spécifique que vous utilisez pour communiquer avec l’ordinateur en codes binaires (des 0 et des 1, et c’est tout). Évidemment, c’est là un sujet que vous n’avez en aucun cas besoin d’approfondir pour lire ce livre. Mais il est intéressant de savoir que les ordinateurs ne connaissent que les chiffres, et donc ne parlent aucune langue.

Aider les humains à parler à l’ordinateur

Il est important de ne jamais perdre de vue l’objectif que vous poursuivez lorsque vous écrivez une application. Celle-ci sert à aider les humains à dialoguer avec l’ordinateur d’une certaine manière. Toute application travaille avec certains types de données qui sont entrées, stockées, manipulées et « recrachées » pour fournir les résultats désirés. Que cette application soit un jeu ou un tableur, l’idée de base est exactement la même. Les ordinateurs travaillent avec des données fournies par des êtres humains pour délivrer un certain résultat.

Lorsque vous créez une application, vous fournissez une nouvelle méthode pour le dialogue entre l’homme et la machine. La nouvelle approche que vous construisez permettra à d’autres humains de voir des données d’une nouvelle façon. La communication homme/machine devrait être suffisamment simple pour que l’application semble disparaître de la vue. Réfléchissez à votre propre expérience d’utilisateur. Les meilleures applications sont bien celles qui vous permettent de vous concentrer uniquement sur les données avec lesquelles vous interagissez. Par exemple, un jeu ne sera véritablement « immersif » que si vous n’avez à vous occuper que de la planète que vous essayez de sauver, ou des ennemis que vous devez combattre, et pas du tout de l’application qui vous permet de faire tout cela.

Une des meilleures façons de commencer à réfléchir à une application que vous voudriez créer, c’est de regarder d’autres applications. Écrire ce que vous aimez ou pas dans d’autres applications est un bon point départ pour définir ce que vous voulez faire, comment vous voulez le faire, et comment tout cela devrait se présenter aux yeux du monde. Voici quelques questions que vous pouvez vous poser au cours de ce processus mental :

• Qu’est-ce qui m’a gêné dans l’application ?

• Quelles fonctions étaient faciles à utiliser ?

• Quelles fonctions étaient difficiles à utiliser ?

• Est-ce qu’il était facile ou pas d’interagir avec mes données ?

• Comment serait-il possible d’améliorer ces interactions ?

• Qu’est-ce que je pourrais faire pour améliorer une telle application ?

Les développeurs professionnels se posent bien d’autres questions lorsqu’ils créent des applications. Mais celles-ci constituent un bon point de départ, car elles vous aident à mieux percevoir les applications comme des moyens d’aider les êtres humains à communiquer avec les ordinateurs. Si vous vous êtes déjà senti frustré en utilisant une application, vous pouvez comprendre ce que vos utilisateurs pourront ressentir si vous ne vous êtes pas posé les bonnes questions au cours de votre travail de développement. La communication est l’élément le plus important de toute application que vous êtes appelé à créer.

Vous pouvez aussi commencer à réfléchir à la manière dont vous travaillez. Commencez à écrire des procédures pour des choses que vous faites. Une bonne idée consiste à prendre le processus étape par étape, en écrivant à chaque fois toutes vos réflexions. Lorsque vous avez terminé, demandez à quelqu’un d’autre de tester votre procédure afin de voir si elle marche effectivement. Vous pourriez être surpris de découvrir que, même en fournissant de gros efforts, vous pouvez facilement oublier d’inclure certaines étapes.

La pire application du monde débute généralement par le fait que le programmeur ne sait pas exactement ce que cette application est censée faire, en quoi elle est spéciale, ce qu’elle doit apporter, et à qui elle est destinée. Lorsque vous décidez de créer une application, assurez-vous que vous savez pourquoi vous le faites, et quel est l’objectif que vous voulez atteindre. Mettre un plan de bataille en place aide beaucoup à bien programmer, et à y prendre du plaisir (ce qui a son importance). Vous pourrez ainsi travailler posément sur votre nouvelle application et voir les étapes se réaliser les unes après les autres jusqu’à ce que tout soit opérationnel. Après quoi, vos amis à qui vous demanderez de tester votre application trouveront qu’elle est vraiment cool, et vous aussi.

Comprendre pourquoi Python est si cool

Il existe de nombreux langages de programmation. En fait, un étudiant pourrait passer tout un semestre à étudier des langages de programmation, et ne même pas entendre parler de certains d’entre eux. Vous pourriez penser que les développeurs sont parfaitement à l’aise avec tout cela, et qu’ils en choisissent simplement un pour communiquer avec l’ordinateur. Mais ce n’est voir que par le petit bout de la lorgnette.

Il y a de bonnes raisons qui justifient la création de nouveaux langages. Chaque langage a quelque chose de particulier à offrir, quelque chose qu’il fait particulièrement bien. De plus, la technologie évolue sans cesse, et donc les langages de programmation doivent eux aussi progresser. Du fait que créer une application est une affaire de communication efficace, de nombreux programmeurs connaissent plusieurs langages de manière à pouvoir choisir celui qui convient le mieux pour réaliser une tâche particulière. Par exemple, un certain langage sera meilleur pour gérer une base de données, tandis qu’un autre sera particulièrement adapté pour créer une interface utilisateur.

Comme tous les langages de programmation, Python fait certaines choses exceptionnellement bien ; vous devez donc savoir quels sont ses principaux champs de compétence avant de commencer à l’utiliser. Vous pourriez bien être étonné par les choses réellement cool que vous pouvez réaliser avec Python. Connaître les forces et les faiblesses d’un langage de programmation vous aide non seulement à en tirer le meilleur parti, mais aussi à éviter les frustrations dues à l’emploi d’un langage pour des choses qu’il ne fait pas très bien. Les sections qui suivent devraient vous aider à prendre ce genre de décision.

Quelques bonnes raisons de choisir Python

Lorsque Guido van Rossum (https://gvanrossum.github.io/) a décidé de créer Python, l’objectif principal était de développer un langage de programmation qui rendrait les programmeurs efficaces et productifs. Dans cette optique, voici les raisons pour lesquelles il est souhaitable d’utiliser Python lors de la création d’une application :

• Moins de temps de développement : le code Python est généralement deux à trois fois plus court que l’équivalent écrit en C/C++ ou en Java. Vous passez donc moins de temps à écrire votre application, et plus de temps à l’utiliser.

• Facilité de lecture : un langage de programmation est comme tout autre langage, vous devez être capable de le lire pour comprendre ce qu’il fait. Le code Python tend à être plus facile à lire que d’autres, ce qui signifie que vous passez moins de temps à l’interpréter, et plus de temps à y apporter les modifications essentielles.

• Temps d’apprentissage réduit : les créateurs de Python ont voulu fabriquer un langage de programmation avec le moins de règles complexes possible pouvant rendre difficile son apprentissage. Après tout, les programmeurs veulent créer des applications, pas apprendre de nouvelles langues obscures et tarabiscotées.

Bien que Python soit un langage populaire, ce n’est pas toujours le langage le plus populaire (cela dépend en fait du site que vous utilisez pour la comparaison). Cependant, il occupe actuellement la première place sur des sites tels que TIOBE (https://www.tiobe.com/tiobe-index), une organisation qui répertorie les statistiques d’utilisation (entre autres choses), ou bien encore. Statistics Times (https://statisticstimes.com/tech/top-computer-languages.php).

Si vous recherchez un langage dans le seul but d’obtenir un emploi, Python est un excellent choix, mais Java, C/C++ ou C# pourraient être de meilleurs choix, selon le type d’emploi que vous recherchez. Visual Basic est également un excellent choix, même s’il n’est actuellement pas aussi populaire que Python. Assurez-vous de choisir un langage que vous aimez et qui répondra à vos besoins en matière de développement d’applications, mais choisissez également en fonction de ce que vous avez l’intention d’accomplir. Vous serez peut-être surpris d’apprendre que de nombreuses universités utilisent Python pour enseigner le codage, et qu’il est devenu le langage le plus populaire dans ce domaine (voir https://www.pythoncentral.io/how-is-python-used-in-education/ pour plus de détails).

Décider comment tirer un bénéfice personnel de Python

En dernier ressort, vous pouvez utiliser n’importe quel langage de programmation pour écrire toutes sortes d’applications. Mais si vous vous trompez dans votre choix, le processus risque d’être lent, avec de gros risques d’erreurs ou de bogues (de fautes, si vous voulez), et vous allez détester ce langage. Mais vous pouvez quand même arriver au bout de vos peines. Bien entendu, personne n’a envie de vivre une telle expérience, et il est donc important de savoir quels types d’applications sont généralement créés avec Python. En voici quelques exemples (évidemment non exhaustifs) :

• Créer des exemples de « maquettes » d’applications : les développeurs ont souvent besoin de produire un prototype, disons comme une maquette d’application, avant d’obtenir les ressources qui leur permettront de créer l’application elle-même. Python est un outil adapté à la productivité, ce qui permet de fabriquer rapidement de tels prototypes.

• Créer des scripts pour des applications Web : même si JavaScript est probablement le langage de programmation le plus populaire dans ce domaine, Python est un deuxième tout proche de la première place. Il offre en particulier des fonctionnalités que ne propose pas JavaScript (voir la comparaison sur https://www.educba.com/python-vs-javascript/ pour plus de détails), et sa grande efficacité accélère la création de ce type d’applications (ce qui est un plus dans notre monde en accélération permanente).

• Concevoir des applications mathématiques, scientifiques ou d’ingénierie : il est intéressant de savoir que Python donne accès à des bibliothèques qui facilitent grandement la création de telles applications. Les deux bibliothèques les plus populaires sont NumPy (numpy.org) et SciPy (scipy.org). Elles permettent de réduire considérablement le temps passé à écrire des applications à caractère scientifique.

• Travailler avec XML : XML (pour eXtensible Markup Language, ou langage de balises étendu) est la base utilisée pour le stockage de la plupart des données sur l’Internet, ainsi que par de nombreuses applications du bureau. Contrairement à la plupart des langages, pour qui XML est bien souvent comme une pièce rapportée, Python en fait un citoyen de première classe. Si vous avez besoin de travailler avec un service Web, ce qui est la principale méthode pour échanger des informations sur l’Internet (ou toute autre application faisant un usage intensif du XML), Python est un excellent choix.

• Interagir avec des bases de données : l’industrie et les services reposent très largement de nos jours sur la gestion de bases de données. Python n’est pas un langage de requête, comme SQL, mais il est très bon dans l’interaction avec les bases de données. Il rend la création de connexions et de manipulation des données relativement aisée.

• Créer des applications d’apprentissage automatique et de deep learning : Python est devenu au fil des années incontournable dans les développements de l’intelligence artificielle, notamment grâce à des bibliothèques telles que Scikit-learn, TensorFlow, pandas, Keras, Theno, etc.

• Développer des interfaces utilisateur : Python ne ressemble pas à d’autres langages comme C#, où vous disposez d’un outil de conception intégré pour réaliser une interface utilisateur et où vous pouvez simplement déposer des éléments par glisser-déposer à partir d’une boîte à outils pour créer une interface utilisateur. Cependant, il possède de multiples extensions qui rendent ce genre d’interface facile à fabriquer (voyez l’adresse https://wiki.python.org/moin/GuiProgramming pour plus de détails). En fait, Python n’est pas lié à tel ou tel type d’interface. Vous pouvez utiliser la méthode qui convient le mieux à vos besoins.

Quelles organisations utilisent Python ?

Python est très bien adapté aux tâches pour lesquelles il a été conçu. Cela va sans dire, mais encore mieux en le disant… En fait, c’est la raison pour laquelle de nombreuses organisations se servent de Python pour des prototypes de développement de leurs propres applications (et bien entendu plus si affinité). Vous avez donc besoin de connaître un tel langage de programmation, car ces grandes organisations ont tendance à investir dans son développement pour le rendre meilleur.

Citons donc quelques noms connus :

• Go.com (go.com) : application Web.

• Google (www.google.com) : moteur de recherche.

• NASA (www.nasa.gov) : applications scientifiques.

• Nyse – New York Stock Exchange (nyse.nyx.com) : applications Web.

• Redhat (www.redhat.com) : outils d’installation Linux.

• Yahoo! (www.yahoo.com) : des parties de la messagerie Yahoo!.

• YouTube (www.youtube.com) : moteur graphique.

Cette liste ne constitue évidemment qu’une toute petite partie des organisations qui utilisent Python d’une manière extensive. Pour en savoir plus à ce sujet, voyez l’adresse www.python.org/about/success/. Le nombre de ces success stories est devenu si important que cette liste est probablement très incomplète et qu’il faudrait créer de nouvelles catégories pour mieux prendre conscience de cette réussite.

Trouver des applications Python utiles

Vous pouvez très bien avoir une application Python en pleine activité sur votre ordinateur sans que vous en ayez même conscience. Python est en effet utilisé de nos jours dans de multiples types d’applications. Cela va des utilitaires qui fonctionnent dans une console (une fenêtre de commandes, si vous préférez) à des suites dédiées à la Conception Assistée par Ordinateur (la CAO). Certaines applications sont dédiées aux appareils portables, tandis que d’autres sont réservées à des services pour de grandes entreprises. En résumé, aucun domaine n’échappe aux applications écrites en Python. Pour autant, rien ne remplace l’expérience que vous pouvez retirer de celle d’autres utilisateurs. Vous pouvez trouver de multiples références en ligne, mais le meilleur point de départ se trouve à l’adresse wiki.python.org/moin/Applications.

Désolé, mais vous aurez besoin de réviser votre anglais pour accéder à toutes ces ressources !

En tant que programmeur Python, vous avez aussi envie de savoir que nombre d’outils de développement sont facilement accessibles afin de vous faciliter l’existence. Un outil de développement fournit certains niveaux d’automation lorsque vous écrivez des procédures qui indiquent à l’ordinateur ce qu’il doit faire. Plus il y a d’outils de développement, et moins vous avez à travailler pour créer une application parfaitement fonctionnelle. Les développeurs adorent partager leurs listes d’outils préférés. Mais, en tout état de cause, vous devriez pouvoir trouver votre bonheur (ou une bonne partie de celui-ci) en consultant la page www.python.org/about/apps/.

Comparer Python avec d’autres langages

Comparer un langage avec un autre est un exercice assez dangereux, car il s’agit à la fois d’une question de préférences personnelles et de besoins à assurer, comme d’ailleurs dans toute démarche structurée et scientifique. Il est donc important de bien comprendre que Python n’est pas le langage universel dont tout le monde peut rêver en vain. Il n’y a pas de meilleur langage de programmation. Il y a simplement des langages qui sont mieux adaptés pour tel ou tel type d’application. Cela étant posé, vous pouvez également vous référer aux comparaisons consultables sur la page wiki.python.org/moin/LanguageComparisons.

C#

Des tas de gens clament que Microsoft a simplement copié Java pour créer C#. Cela étant dit, il est vrai que C# a certains avantages (ou désavantages) comparativement à Java. La première remarque (incontestable) est que C# est plus facile à apprendre et à utiliser. En revanche, et puisque nous parlons ici de Python, celui-ci a plusieurs avantages :

• Il est incontestablement plus facile à apprendre.

• Son code est plus concis.

• Il est totalement gratuit (ou open source comme on dit).

• Il supporte mieux les plateformes multiples.

• Il permet d’utiliser facilement des environnements de développement variés.

• Il est facile à étendre via des composants Java ou C/C++.

• Il supporte mieux les développements scientifiques.

Java

Depuis bien longtemps, les développeurs recherchent un langage qui leur permettrait d’écrire une application une fois, et de la faire exécuter en tout temps et partout. Java est conçu pour être utilisé sur de multiples plateformes. Il repose pour cela sur certains « trucs » que vous découvrirez plus tard dans ce livre afin d’accomplir cette « magie » . Pour l’instant, et pour ce qui nous concerne ici, il vous suffit de savoir que Java a rencontré un tel succès qu’il a été largement copié (avec un niveau de succès très inégal). Mais Python conserve un certain nombre d’avantages :

• Il est nettement plus facile à apprendre.

• Son code est bien plus concis.

• Les variables de Python (ces choses qui sont stockées dans la mémoire de votre ordinateur) peuvent contenir des données non seulement extrêmement variées, mais aussi totalement dynamiques.

• Les développements sont bien plus rapides.

Pas mal, non ?

Perl

Perl est l’acronyme de Practical Extraction and Report Language ( « langage d’extraction et de rapport pratique » , ce qui n’est pas très joli, non ?). Le point fort de Perl, c’est l’extraction d’informations depuis des bases de données et la présentation de celles-ci. Bien entendu, Perl s’est développé bien au-delà de ses sources. Vous pouvez donc l’utiliser pour écrire toutes sortes d’applications. Mais, comparativement à Perl, Python conserve là encore un certain nombre d’avantages :

• Il est plus simple à apprendre.

• Il est plus simple à lire.

• Il protège mieux les données.

• Il s’intègre mieux avec Java.

• Il est moins dépendant des diverses plateformes.

R

Les spécialistes des données ont souvent du mal à choisir entre R et Python, car les deux langages sont adaptés à l’analyse statistique et aux graphiques dont les data scientists ont besoin pour comprendre les modèles de données. Les deux langages sont également open source et prennent en charge un large éventail de plateformes. Toutefois, R est un peu plus spécialisé que Python et tend à s’adresser au marché universitaire. Par conséquent, Python a les avantages suivants par rapport à R:

• L’accent est mis sur la productivité et la lisibilité du code.

• Il est conçu pour être utilisé par les entreprises.

• Il offre un débogage plus facile.

• Il emploie des techniques de codage cohérentes.

• Il a une plus grande flexibilité.

• Il est plus facile à apprendre.

Haskell

Les langages fonctionnels purs, tels que Haskell, présentent certains avantages significatifs, car ils marchent extrêmement bien dans les applications multithreads en raison du fait que les fonctions pures renvoient toujours la même valeur pour les mêmes entrées. Il n’y a pas d’effets secondaires. De nombreux développeurs considèrent également que les langages fonctionnels sont plus faciles à déboguer et moins susceptibles de produire des erreurs. Python est un langage fonctionnel impur, ce qui signifie qu’il n’adhère pas strictement à toutes les exigences d’un langage fonctionnel. Cependant, Python présente ces avantages par rapport à Haskell :

• Il est plus facile à apprendre.

• Il y a un plus grand choix de bibliothèques.

• Il est facile à utiliser pour le prototypage et la modélisation.

• Il est considéré comme un meilleur langage côté serveur.

• Il a une plus grande flexibilité.

DANS CE CHAPITRE

Utiliser des commandes pour effectuer des tâches.

•

Obtenir de l’aide sur Python.

•

Découvrir les fonctions et les objets.

•

Employer l’inspecteur.

Chapitre 2

Interagir avec Python

En dernier ressort, toute application que vous créez interagit avec l’ordinateur et les données qu’il contient. Le point central, ce sont les données. Sans données, il n’y a effectivement aucune raison valable d’avoir une application. Et toute application que vous utilisez (même un petit jeu de solitaire) manipule des données d’une certaine manière. En fait, tout cela pourrait se résumer en quelques mots :

• créer ;

• lire ;

• actualiser ;

• supprimer.

Soit en abrégé CLAS. Si vous vous rappelez cet acronyme, vous aurez présent à l’esprit le résumé de ce que la plupart des applications font avec les données contenues dans l’ordinateur. Cependant, avant que votre application puisse accéder à cet ordinateur, vous devez vous-même interagir avec un langage de programmation qui va créer une liste de tâches à effectuer dans un autre langage, celui que l’ordinateur comprend. Tel est l’objet de ce chapitre. Ici, vous allez commencer à interagir avec Python. C’est lui qui va être chargé de prendre la liste des étapes que vous voulez réaliser avec les données, et les traduire dans la langue extrêmement binaire de la machine. Ce chapitre vous aide à comprendre comment obtenir de l’aide sur les méthodes de manipulation des données, travailler avec des fonctions et des objets qui interagissent avec les données, et utiliser le module inspect de Python pour effectuer un travail de détective sur vos données.

Vous n’avez pas à saisir manuellement le code source de ce chapitre. En fait, l’utilisation de la source téléchargeable est beaucoup plus facile. Vous pouvez trouver la source de ce chapitre dans le fichier Chapitre 03 Travailler avec les commandes.ipynb. Consultez l’introduction pour savoir comment trouver ces fichiers source.

Comprendre l’importance du fichier README

De nombreuses applications sont accompagnées d’un fichier appelé README (« Lisez-moi », mais qui ne s’appelle pas forcément README). Celui-ci fournit généralement des informations mises à jour sur une nouvelle version, et qui n’ont pas pu être intégrées à la documentation ou à l’aide de l’application. Malheureusement, la plupart des gens ignorent ce fichier README, et d’autres ne savent même pas qu’il existe. Conséquence : des tas d’utilisateurs passent à côté de renseignements intéressants sur leur nouveau produit.

La version de Python que vous utilisez possède un fichier Release Notes (Notes sur la version, tout simplement parce que les développeurs n’ont pas voulu l’appeler README) qui contient des informations essentielles sur Python. Si vous téléchargez et installez une copie de Python sur votre machine en local, ce fichier s’appelle NEWS.txt et il apparaît dans le dossier principal de Python. Si vous utilisez Colab et ne téléchargez rien, vous pouvez trouver les mêmes informations en ligne. Par exemple, les notes sur la version de Python 3.10.3 apparaissent à l’adresse https://docs.python.org/release/3.10.3/whatsnew/changelog.html. Vous trouverez des liens vers les notes pour d’autres versions de Python à l’adresse https://www.python.org/downloads/. Lorsque vous ouvrez ce fichier, vous trouvez toutes sortes d’informations vraiment intéressantes, dont la plupart sont centrées sur les mises à jour de Python que vous devez absolument connaître.

Ouvrir et lire le fichier Release Notes (appelé NEWS.txt ou changelog.html parce que les gens ignoraient apparemment l’autre fichier) vous aidera à devenir un génie de Python. Vous épaterez votre entourage, qui viendra vous poser des tas de questions. Et cela vous permettra peut-être aussi d’obtenir une ascension fulgurante dans votre carrière (quoique je refuse totalement de m’engager sur ce point). Mais, bien entendu, vous pouvez aussi tout simplement passer à autre chose en vous disant que la lecture de ces fichiers, c’est juste trop d’efforts.

Saisir une commande

Lorsque vous dressez votre lion (ou votre chat ou votre chien), vous lui apprenez des ordres tels que « assis » , « pas bouger » et « ne me mange pas » . Chaque commande ordonne à l’animal d’exécuter une tâche ou de faire autre chose d’intéressant. De la même manière, l’utilisation des commandes en Python permet d’effectuer des tâches dans des applications, de tester les idées que vous avez pour écrire votre application et d’en découvrir davantage sur Python. L’utilisation des commandes permet d’acquérir une expérience pratique du fonctionnement réel de Python.

Dire à l’ordinateur ce qu’il doit faire

Python, comme tout autre langage de programmation, est basé sur des commandes. Une commande est simplement une étape dans une procédure. Dans le Chapitre 1, par exemple, j’ai décrit les étapes d’une procédure permettant d’obtenir du pain grillé et beurré. Lorsque vous travaillez avec Python, une commande telle que print() répond exactement au même principe : une étape dans une procédure.

Pour dire à l’ordinateur ce qu’il doit faire, vous exécutez une ou plusieurs commandes dans un langage que Python comprend. Celui-ci traduit ensuite ces commandes dans des instructions que l’ordinateur reconnaît, et vous voyez ensuite le résultat de cette traduction. Une commande comme print() peut afficher de tels résultats à l’écran. Cependant, Python prend en charge toutes sortes de commandes, dont la plupart réalisent des tâches importantes sans afficher pour autant quoi que ce soit.

Au fur et à mesure de votre progression dans ce livre, vous allez utiliser des commandes pour effectuer toutes sortes d’actions. Chacune d’entre elles vous permettra d’atteindre un certain but, exactement comme les étapes d’une procédure. Si vous avez par moments la sensation que toutes ces commandes deviennent bien trop complexes, souvenez-vous qu’il faut les prendre comme des étapes dans une chaîne d’actions. Même les procédures purement humaines sont parfois d’une extrême complexité. Mais, si vous les prenez étape par étape, vous comprenez mieux de quoi il s’agit et comment tout cela fonctionne. Il en va exactement de même pour les commandes de Python. Ne vous laissez pas déborder par elles, mais concentrez-vous plutôt sur la succession des étapes qui conduisent à l’objectif fixé.

Dire à l’ordinateur que vous avez terminé

À un certain moment, la procédure que vous avez créée se termine (sinon, il y a un sérieux problème…). Dans notre exemple du Chapitre 1, la procédure est finie une fois que le pain est grillé et beurré. Il y a un point de départ et un point d’arrivée. Lorsque vous saisissez des commandes, une étape particulière se termine quand vous appuyez sur la touche Entrée.

Appuyer sur Entrée n’exécute pas réellement la commande ; vous devez cliquer sur Exécuter la cellule dans Colab ou sur Exécuter dans Jupyter Notebook. Vous pouvez également saisir une seule commande dans une cellule de code, puis appuyer sur Ctrl + Entrée pour l’exécuter.

Dans ce livre, vous allez constater que Python fournit diverses manières de signifier qu’une étape, un groupe d’étapes, ou même une application tout entière se termine. Quelles que soient les tâches réalisées, les programmes d’ordinateur ont toujours un début et une fin distincts.

Voir les résultats

Vous savez maintenant qu’une commande est une étape dans une procédure, et que chaque commande a un point de départ et un point d’arrivée. Et il en va de même pour des groupes de commandes ainsi que pour les applications.

Pour passer de la théorie à la pratique, suivez ces étapes :

❶ Démarrez Google Colab ou Jupyter Notebook, puis créez un nouveau notebook Python 3 comme cela est décrit dans la section « Créer un nouveau notebook » du Chapitre 2.

Vous voyez une cellule de code où vous pouvez saisir une commande.

❷ Saisissez print("Ceci est une ligne de texte.") dans la cellule de code.

Notez qu’il ne se passe rien. Oui, vous avez bien saisi une commande, mais vous n’avez pas indiqué que la commande est terminée.

❸ Appuyez sur Ctrl + Entrée.

La commande est terminée, et vous voyez un résultat comme celui qui est illustré à la Figure 2.1.

Ce petit exercice vous montre comment les choses fonctionnent à l’intérieur de Python. Chaque commande que vous saisissez effectue une certaine tâche, mais uniquement après que vous avez indiqué à Python que la commande est terminée. Dans cet exemple, vous avez utilisé la commande print() en fournissant un certain texte à afficher. Notez que le résultat apparaît immédiatement après l’appui sur les touches Ctrl + Entrée, car il s’agit d’un environnement interactif, où les commandes sont donc exécutées immédiatement. Plus tard, vous commencerez à créer des applications et vous constaterez alors que les résultats n’apparaissent pas forcément tout de suite. Une application, en effet, n’est pas un environnement interactif… Mais chaque commande n’en est pas moins exécutée immédiatement, une fois que l’application (et non plus vous) a indiqué à Python qu’elle était terminée.

Styles de codage de Python

La plupart des langages de programmation sont dédiés à l’utilisation d’un seul style de codage, ce qui réduit la flexibilité pour le programmeur, mais Python est différent. Vous pouvez utiliser un certain nombre de styles de codage pour obtenir différents effets avec Python. Voici les quatre styles de codage Python couramment utilisés :

• Fonctionnel : chaque instruction est une sorte d’équation mathématique. Ce style se prête bien à une utilisation dans des activités de traitement parallèle.

• Impératif : les calculs sont réalisés sous la forme de changements de l’état du programme. Ce style est le plus utilisé pour la manipulation des structures de données.

• Orienté objet : il s’agit du style couramment utilisé avec d’autres langages pour simplifier l’environnement de codage en utilisant des objets pour modéliser le monde réel. Python n’implémente pas complètement ce style de codage, car il ne prend pas en charge des fonctionnalités telles que le masquage des données, mais vous pouvez tout de même majoritairement utiliser cette approche. Vous verrez l’emploi de ce style plus tard dans le livre.

• Procédural : tout le code que vous avez écrit jusqu’à présent (et une grande partie du code initial de ce livre) est procédural, ce qui signifie que les tâches se déroulent étape par étape. Ce style est surtout utilisé pour l’itération, les séquences, la sélection et la modularisation. C’est la forme la plus simple de codage que vous pouvez utiliser.

Même si ce livre ne couvre pas tous ces styles de codage (et d’autres que Python prend en charge), il est utile de savoir que vous n’êtes pas prisonnier d’un style de codage particulier. Comme Python prend en charge plusieurs styles de codage et que vous pouvez mélanger ces styles dans une même application, vous avez l’avantage de pouvoir utiliser Python de la manière qui convient le mieux à un besoin particulier. Vous pouvez en apprendre plus sur les styles de codage à l’adresse https://newrelic.com/blog/nerd-life/python-programming-styles.

[picture]

Figure 2.1 L’exécution de commandes indique à Python ce qu’il doit demander à l’ordinateur de faire.

Obtenir de l’aide de Python

Python est un langage informatique, pas un langage naturel. Vous ne pouvez donc pas discuter facilement avec lui (du moins pas encore). Vous devez donc découvrir les commandes de Python pas à pas, exactement comme si vous appreniez une langue étrangère. Si votre langue maternelle est le français et que vous essayiez de dire quelque chose en allemand, vous avez besoin d’une sorte de guide pour vous y aider. Sinon, vous ne sortirez qu’un charabia incompréhensible, et les gens vous regarderont bizarrement. Et même si ce que vous dites a un sens, ce n’est peut-être pas celui que vous vouliez donner à votre phrase.

C’est exactement le même principe avec Python. Vous avez besoin d’un guide pour vous aider à parler correctement dans sa langue. Heureusement, Python est assez accommodant de ce point de vue, et il est toujours prêt à vous aider.

Vous l’avez compris depuis longtemps. Le dialogue avec Python passe par une troisième langue : l’anglais. Désolé, mais il n’est pas possible d’y échapper !

L’aide fournie par Python se situe à deux niveaux :

• Le mode Aide, dans lequel vous pouvez parcourir la liste des commandes disponibles.

• L’aide directe, qui vous permet de demander des informations sur une commande spécifique.

Il n’y a pas une méthode meilleure que l’autre. Tout dépend de ce dont vous avez besoin à un moment donné. Les sections suivantes détaillent tout cela.

Entrer dans le mode Aide

Jupyter Notebook a un avantage par rapport à Google Colab, car il fournit une bonne référence sur le langage par le biais du menu Help. En fait, vous pouvez également trouver des références pour NumPy, SciPy, MatPlotLib, SymPy et pandas. Cependant, il est également important de prendre conscience que vous avez accès à d’autres formes d’aide qui sont prises en charge à la fois par Jupyter Notebook et Colab sous la forme des quatre commandes énumérées dans la liste suivante :

• copyright()

• credits()

• license()

• help()

Ces quatre commandes fournissent une aide sur Python. Par exemple, la commande copyright() vous indique qui détient le droit de copier, d’accorder une licence ou de distribuer Python. La commande credits() vous indique qui a créé Python. La commande license() décrit le contrat d’utilisation que vous passez avec le détenteur du droit d’auteur. Contrairement à copyright() et credits(), license() exige que vous appuyiez sur la touche Entrée pour obtenir davantage d’informations sur la licence ou que vous saisissiez q et appuyiez sur Entrée pour quitter l’écran de licence, comme cela est illustré à la Figure 2.2, lorsque vous travaillez avec Colab ou Jupyter Notebook. Notez que l’icône Exécuter la cellule à gauche de la cellule reste active tant qu’il y a des informations à afficher.

La commande que vous utiliserez le plus est tout simplement help(). Pour activer le mode Aide, saisissez help() et cliquez sur Exécuter la cellule. Vous noterez que vous devez inclure les parenthèses après la commande. Chaque commande Python est associée à des parenthèses. Après avoir saisi cette commande, Python passe en mode d’aide et vous voyez un affichage similaire à celui illustré à la Figure 2.3.

Vous pouvez toujours savoir que vous êtes en mode Aide grâce à l’invite help> que vous voyez dans la cellule et parce que l’icône Exécuter la cellule reste active.

[picture]

Figure 2.2 Certaines commandes nécessitent une saisie supplémentaire de votre part pour qu’elles se terminent.

Demander de l’aide

Pour obtenir de l’aide, vous devez savoir quelle question poser. L’affichage illustré sur la Figure 2.3 vous fournit quelques pistes. Pour explorer Python, les quatre sujets de base sont :

• modules

• keywords (mots-clés)

[picture]

Figure 2.3 Vous pouvez poser des questions à Python sur d’autres commandes en mode Aide.

• symbols

• topics (sujets)

Les deux premières rubriques ne vous apprendront pas grand-chose pour l’instant. Vous n’aurez pas besoin de la rubrique sur les modules avant le Chapitre 5. Le sujet des mots-clés commencera à se révéler utile au Chapitre 5. Cependant, les mots-clés symbols et topics sont déjà utiles, car ils vous aident à comprendre où commencer votre aventure Python. Lorsque vous saisissez symbols et appuyez sur Entrée, vous obtenez une liste des symboles utilisés en Python. Pour voir quels sujets sont disponibles, saisissez topics et appuyez sur Entrée. Vous obtenez une liste de rubriques semblables à celles illustrées à la Figure 2.4.

Le Chapitre 5 commence la discussion sur les symboles lorsque vous explorez l’utilisation des opérateurs en Python. Lorsque vous voyez une rubrique qui vous plaît, comme FUNCTIONS, saisissez simplement FUNCTIONS et appuyez sur Entrée (vous devez saisir le mot en majuscules ; ne vous inquiétez pas, Python ne pensera pas que vous criez). Vous voyez apparaître des informations d’aide semblables à celles illustrées à la Figure 2.5.

Au fur et à mesure que vous travaillez sur les exemples du livre, vous utilisez des commandes qui semblent intéressantes, et vous pourriez vouloir plus d’informations à leur sujet. Par exemple, dans la section « Voir les résultats » de ce chapitre, vous employez la commande print(). Pour obtenir plus d’informations sur cette commande, saisissez print et appuyez sur Entrée (vous noterez que vous n’avez pas à inclure cette fois-ci les parenthèses, car vous demandez de l’aide sur print(), sans utiliser réellement la commande). La Figure 2.6 présente l’aide pour la commande print().

[picture]

Figure 2.4 La rubrique d’aide sur les topics (sujets) fournit un point de départ pour votre aventure Python.

[picture]

Figure 2.5 Vous devez utiliser des majuscules pour demander des informations sur un sujet.

[picture]

Figure 2.6 Demandez des informations sur les commandes en saisissant le nom de la commande en minuscules ou en majuscules.

Malheureusement, la lecture des informations d’aide ne vous est probablement pas encore d’un grand secours, car vous ne connaissez pas suffisamment bien Python pour le moment. Vous pouvez cependant demander de l’aide sur des notions qui ne sont pas des commandes, mais qui figurent dans les rubriques d’aide. Par exemple, vous pouvez vous demander ce que signifie sys.stdout, car l’aide sur la commande print() ne dit rien à ce sujet. Saisissez sys.stdout et appuyez sur Entrée. Vous voyez apparaître les informations d’aide illustrées à la Figure 2.7.

Il se peut que vous ne trouviez toujours pas les informations aussi utiles que vous le souhaitez, mais au moins vous en savez un peu plus. Dans le cas de l’aide sur sys.stdout, il y a beaucoup de contenu et tout ne peut pas tenir sur un seul écran (la Figure 2.7 n’affiche qu’une partie de l’aide). Pour voir davantage d’informations, vous devez faire défiler la cellule vers le haut ou vers le bas.

Quitter le mode Aide

À un moment donné, vous devez quitter le mode Aide pour effectuer un travail utile. Il suffit d’appuyer sur Entrée sans rien saisir. Lorsque vous appuyez sur la touche Entrée, un message vous indique que vous avez quitté l’aide, puis l’icône de la cellule d’exécution devient l’invite Python standard, comme cela est illustré à la Figure 2.8.

[picture]

Figure 2.7 Vous pouvez demander de l’aide sur des éléments de l’aide qui est affichée.

[picture]

Figure 2.8 Quittez le mode d’aide en appuyant sur Entrée sans rien saisir.

Obtenir de l’aide directement

Entrer dans le mode d’aide n’est pas nécessaire, sauf si vous voulez parcourir l’aide, ce qui est toujours une bonne idée, ou si vous ne savez pas vraiment ce que vous recherchez. Si vous avez une bonne idée de ce dont vous avez besoin, vous pouvez demander de l’aide directement, ce qui est une chouette fonctionnalité en Python. Ainsi, au lieu de manipuler le mode d’aide, vous pouvez simplement saisir le mot help, suivi d’une parenthèse gauche et d’un guillemet simple, de ce que vous voulez trouver, d’un autre guillemet simple et de la parenthèse droite. Par exemple, si vous voulez en savoir plus sur la commande print(), vous saisissez help('print') et cliquez sur Exécutez la cellule. La Figure 2.9 illustre une sortie typique lorsque vous accédez à l’aide de cette façon-là.

[picture]

Figure 2.9 Python vous permet d’obtenir de l’aide à chaque fois que vous en avez besoin, sans quitter l’invite Python.

Vous pouvez également parcourir l’aide directement depuis une cellule. Par exemple, lorsque vous saisissez help('topics') et que vous appuyez sur Entrée, vous obtenez une liste de rubriques comme celle qui apparaît à la Figure 2.10. Vous pouvez comparer cette liste avec celle de la Figure 2.4. Les deux listes sont identiques, même si vous avez saisi l’une en mode d’aide et l’autre dans l’invite Python.

Vous vous demandez peut-être pourquoi Python dispose d’un mode d’aide si vous pouvez obtenir les mêmes résultats directement grâce à une commande saisie dans une cellule. La réponse est d’ordre pratique. Il est plus facile de naviguer dans le mode d’aide. De plus, même si vous n’avez pas à saisir beaucoup de texte sous la forme d’une commande dans une cellule séparée, vous en saisirez moins dans le mode d’aide. Le mode d’aide fournit également des aides supplémentaires, par exemple en listant les sujets que vous pouvez saisir, comme cela est illustré à la Figure 2.4. Vous avez donc toutes sortes de bonnes raisons d’entrer dans le mode d’aide lorsque vous prévoyez de poser à Python de nombreuses questions.

Quel que soit l’endroit où vous demandez de l’aide, vous devez respecter la casse (la différence entre les majuscules et les minuscules) des rubriques d’aide. Par exemple, si vous souhaitez obtenir des informations générales sur les fonctions, vous devez saisir help('FUNCTIONS') et non pas help('Functions') ou help('functions'). Lorsque vous ne respectez la graphie correcte, Python indique qu’il ne sait pas ce que vous voulez dire ou qu’il n’a pas pu trouver la rubrique d’aide. Il ne saura pas vous dire que vous n’avez pas utilisé la bonne casse. Un jour, les ordinateurs sauront ce que vous vouliez saisir, mais ce n’est pas encore le cas.

[picture]

Figure 2.10 Vous pouvez parcourir l’aide à partir de l’invite Python si vous le souhaitez vraiment.

En savoir plus sur les fonctions et les objets

La fonction d’aide de Python offre un aperçu des différentes fonctions et objets présents dans une application. Cependant, vous avez souvent besoin d’en savoir plus que ce que l’aide fournit. Dans ce cas, vous pouvez utiliser un type d’approche alternative à l’aide, comme je le décris dans les sections suivantes.

Crier « Bonjour » ne sert à rien : utilisez plutôt dir()

Il est nécessaire d’avoir une idée des besoins d’une application sans déployer trop d’efforts. Bien sûr, vous pourriez essayer de demander simplement, en commençant par « Bonjour » en guise de salutation, mais des tests approfondis montrent que parler à votre ordinateur ne fonctionnera pas. La fonction dir() fournit un moyen de découvrir de nouvelles choses sur les fonctions et les objets en Python. Elle offre également plus d’informations à certains égards que help() et elle est plus flexible. L’utilisation de la fonction dir() seule affiche une liste de fonctions, de classes et d’objets qui sont actuellement disponibles et qui sont similaires à cette liste (votre liste peut varier en fonction de ce que vous avez chargé dans Python) :

['In', 'Out', '_', '__', '___', '__builtin__',
'__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__', '_dh', '_i', '_i1', '_ih',
'_ii', '_iii', '_oh', '_sh', 'exit', 'get_ipython',
'quit']

Pour gagner de la place dans le livre, nous avons imprimé cette liste dans un format horizontal. Vous la verrez en fait dans un format vertical qui peut être un peu difficile à lire.

Au fur et à mesure que vous importez des paquets, des classes et des fonctions et que vous exécutez du code, la liste change pour refléter les nouveaux ajouts. Par exemple, lorsque vous exécutez le code suivant :

 monNum = 2056
 dir(monNum)

vous voyez une liste des fonctions que vous pouvez utiliser avec monNum :

['__abs__', '__add__', '__and__', '__bool__',
'__ceil__', '__class__', '__delattr__', '__dir__',
'__divmod__', '__doc__', '__eq__', '__float__',
'__floor__', '__floordiv__', '__format__', '__ge__',
'__getattribute__', '__getnewargs__', '__gt__',
'__hash__', '__index__', '__init__',
'__init_subclass__', '__int__', '__invert__', '__le__',
'__lshift__', '__lt__', '__mod__', '__mul__', '__ne__',
'__neg__', '__new__', '__or__', '__pos__', '__pow__',
'__radd__', '__rand__', '__rdivmod__', '__reduce__',
'__reduce_ex__', '__repr__', '__rfloordiv__',
'__rlshift__', '__rmod__', '__rmul__', '__ror__',
'__round__', '__rpow__', '__rrshift__', '__rshift__',
'__rsub__', '__rtruediv__', '__rxor__', '__setattr__',
'__sizeof__', '__str__', '__sub__', '__subclasshook__',
'__truediv__', '__trunc__', '__xor__',
'as_integer_ratio', 'bit_length', 'conjugate',
'denominator', 'from_bytes', 'imag', 'numerator',
'real', 'to_bytes']

C’est une sacrée liste de choses que vous pouvez faire avec monNum ! Cette liste indique les détails concernant monNum, un objet que vous avez créé. Ainsi, vous pouvez saisir monNum.bit_length() et appuyer sur Entrée pour voir que la longueur de cette variable particulière exprimée en bits est de 12. L’utilisation de dir() peut vous rafraîchir la mémoire ou vous faire découvrir de nouvelles fonctionnalités dont vous n’aviez peut-être pas connaissance.

Que sont ces doubles barres de soulignement ?

Python ne dispose pas du concept de variables privées, classes privées, fonctions privées, etc. Certains développeurs estiment que les fonctionnalités de programmation orientée objet (POO) de Python sont par conséquent incomplètes (pour une discussion approfondie sur le sujet, voir https://www.geeksforgeeks.org/data-hiding-in-python/). Le créateur de Python, Guido van Rossum, a évoqué ce problème en ces termes : « Nous sommes tous des adultes. N’hésitez pas à vous tirer une balle dans le pied si cela est nécessaire. » Ainsi, Python ne dispose pas d’un véritable masquage des données, ni des avantages que cela procure et des problèmes que cela pose. Il y a à la place le double soulignement (__), qui indique que le membre est privé et que vous ne devriez pas l’utiliser, mais de nombreux développeurs le font quand même.

Certaines implémentations Python considèrent le double soulignement comme privé, même si cette fonctionnalité ne fait pas partie de la spécification. Par exemple, monNum.__eq__(2056) renvoie True si monNum contient effectivement la valeur 2056, mais la fonction __eq__() est répertoriée comme privée, de telle sorte que ce que vous devriez utiliser dans votre code est plutôt monNum == 2056.

Exploiter les classes

Dans toute implémentation de la POO, les classes héritent d’autres classes, de telle sorte que vous vous retrouvez avec une hiérarchie d’objets. L’examen approfondi de cette hiérarchie peut vous en apprendre beaucoup sur la façon d’interagir avec les objets Python d’une manière que vous n’auriez peut-être pas envisagée par le passé. Par exemple, lorsque vous travaillez avec monNum, vous pouvez utiliser l’appel monNum.to_bytes(4, byteorder = 'big') pour afficher monNum sous la forme d’une série d’octets : b'\x00\x00\x08\x08'. La sortie est un autre objet. Par conséquent, vous pouvez utiliser l’appel dir(monNum. to_bytes(4, byteorder = 'big')) pour en savoir plus sur la façon dont vous pouvez utiliser ce nouvel objet :

['__add__', '__class__', '__contains__', '__delattr__',
'__dir__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__getitem__', '__getnewargs__',
'__gt__', '__hash__', '__init__', '__init_subclass__',
'__iter__', '__le__', '__len__', '__lt__', '__mod__',
'__mul__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__rmod__', '__rmul__',
'__setattr__', '__sizeof__', '__str__',
'__subclasshook__', 'capitalize', 'center', 'count',
'decode', 'endswith', 'expandtabs', 'find', 'fromhex',
'hex', 'index', 'isalnum', 'isalpha', 'isascii',
'isdigit', 'islower', 'isspace', 'istitle', 'isupper',
'join', 'ljust', 'lower', 'lstrip', 'maketrans',
'partition', 'replace', 'rfind', 'rindex', 'rjust',
'rpartition', 'rsplit', 'rstrip', 'split',
'splitlines', 'startswith', 'strip', 'swapcase',
'title', 'translate', 'upper', 'zfill']

Curieusement, vous pouvez remplacer certains des octets de la valeur par des octets différents en utilisant monNum.to_bytes(4, byteorder = 'big').replace(b'\x00', b'\x01'), ce qui donne comme résultat : b'\ x01\x01\x08\x08'. Cependant, vous ne saviez peut-être pas que vous pouviez faire cela sans analyser en profondeur l’objet original monNum à l’aide de dir().

Si vous voulez vraiment vous faire plaisir, vous pouvez commencer à combiner les bribes de connaissances que vous obtenez en vous plongeant dans l’arborescence des objets. Par exemple, monNum.from_ bytes(monNum.to_bytes(4, byteorder = 'big').replace(b'\x00', b'\ x01'), 'big') produit une nouvelle valeur monNum de 16844808. C’est un très grand nombre, mais cela montre également que vous pouvez utiliser Python pour produire un large assortiment d’effets intéressants en jouant simplement avec dir().

Jouer au détective

Le module inspect (voir https://docs.python.org/fr/3/library/inspect.html) permet d’aller encore plus en profondeur que la fonction dir() en jouant le rôle d’un détective. Vous pouvez interroger tout objet, classe ou variable de manière détaillée. Les sections suivantes vous donnent un bref aperçu de ce que vous pouvez faire, car ce module peut devenir très complexe. Comme avec tous les outils, vous devez passer du temps avec inspect avant de maîtriser son emploi.

Accéder au module inspect

Le module inspect n’est pas inclus dans la configuration standard de Python, et vous devez donc l’importer dans l’environnement afin de pouvoir l’utiliser en ajoutant la ligne de code suivante :

import inspect

Ce code importe l’intégralité du module inspect. Si vous souhaitez utiliser un nombre limité de fonctionnalités, vous pouvez également utiliser d’autres méthodes d’importation Python, telles que

from inspect import isfunction

Au lieu de tout importer, cette commande importe uniquement la méthode isfunction().

Utiliser le module inspect

Il existe de nombreuses façons différentes d’utiliser inspect. Par exemple, vous pouvez l’utiliser pour poser des questions directes telles que inspect.ismodule(inspect), qui renvoie True, car inspect est un module. Il est également possible d’utiliser inspect sur n’importe quel objet, y compris ceux que vous créez. La requête inspect.getmembers(-monNum) renvoie une quantité considérable d’informations (nous n’imprimons qu’un extrait, car la liste complète est beaucoup plus longue) :

[('__abs__', <method-wrapper '__abs__' of int object at
0x7f71e52ea4b0>),
('__add__', <method-wrapper '__add__' of int object at
0x7f71e52ea4b0>),
('__and__', <method-wrapper '__and__' of int object at
0x7f71e52ea4b0>),
('__bool__', <method-wrapper '__bool__' of int object
at 0x7f71e52ea4b0>),
('__ceil__', <function int.__ceil__>),
('__class__', int),
('__delattr__',
<method-wrapper '__delattr__' of int object at
0x7f71e52ea4b0>),
('__dir__', <function int.__dir__>),
('__divmod__', <method-wrapper '__divmod__' of int
object at 0x7f71e52ea4b0>),
...
('bit_length', <function int.bit_length>),
('conjugate', <function int.conjugate>),
('denominator', 1),
('from_bytes', <function int.from_bytes>),
('imag', 0),
('numerator', 2056),
('real', 2056),
('to_bytes', <function int.to_bytes>)]

La plupart des informations indiquent ce qu’est un membre particulier, comme une méthode ou une fonction. Dans certains cas, vous obtenez également la valeur du membre. Par exemple, la sortie de la fonction denominator() est 1.

Il vous faudra peut-être un certain temps pour passer en revue toutes les fonctionnalités d’inspect, mais le jeu en vaut la chandelle. Par exemple, vous pouvez rapidement vérifier si une fonction est intégrée. Un appel à inspect.isbuiltin(dir) renvoie True, car la fonction dir() fait partie des fonctionnalités Python par défaut.

La réflexion consiste-t-elle vraiment à se regarder dans un miroir ?

Le processus utilisé par le module inspect pour obtenir des informations détaillées sur votre code est appelé réflexion, mais il n’a rien à voir avec l’activité de se regarder dans un miroir. Au lieu de cela, la réflexion analyse le code compilé dans le but de reconstruire les informations de l’élément original. Le terme introspection signifie essentiellement la même chose lorsqu’il s’agit de visualiser du code, et certaines personnes disent que le module inspect effectue une introspection.

Outre l’affichage des informations sur les éléments, la réflexion peut également vous permettre de modifier les objets sous-jacents, mais il s’agit d’une technique extrêmement avancée dont la plupart des développeurs n’ont pas vraiment besoin. Dans le cadre de ce livre, j’appelle réflexion l’introspection du code pour en découvrir davantage, afin de garder les choses simples (et pour que la terminologie soit conforme à celle utilisée par les autres langages). Vous pouvez en savoir plus sur les différences entre introspection et réflexion à https://betterprogramming.pub/python-reflection-and-introspection-97b348be54d8 et voir quelques utilisations avancées de la réflexion à l’adresse https://www.geeksforgeeks.org/reflection-in-python/.

DANS CE CHAPITRE

Écrire et exécuter votre première application.

•

Formater votre code.

•

Utiliser efficacement les commentaires.

•

Développer des applications gérables.

Chapitre 3

Écrire votre première application

Nombreux sont les gens qui voient le développement d’applications comme une sorte de magie pratiquée par des sorciers appelés geeks qui envoient des ondes bizarres à leurs claviers pour produire toutes sortes de programmes. En fait, la vérité est bien plus prosaïque.

Le développement d’une application suit un certain nombre de processus très stricts, qui n’ont absolument rien à voir avec quelque magie que ce soit. Ce chapitre a pour but d’effacer toute part d’imaginaire pour rentrer dans le concret, autrement dit la technologie. Lorsque vous l’aurez terminé, vous devriez être à même de développer une application, certes simple, sans avoir besoin d’invoquer je ne sais quel sort.

Comme pour toute autre tâche, les gens utilisent des outils pour écrire des applications. Dans le cas de Python, vous n’êtes pas obligé d’utiliser un outil, mais l’emploi d’un outil rend la tâche tellement plus facile que vous aurez vraiment envie de vous en servir. Dans ce chapitre, vous allez utiliser un environnement de développement intégré (IDE) très populaire, appelé Google Colab (abordé au Chapitre 2). Un IDE est une application d’un genre particulier qui facilite considérablement l’écriture, le test et le débogage du code. Vous pouvez également utiliser Jupyter Notebook pour le développement de bureau, qui fait partie de la collection d’outils Anaconda (https://www.anaconda.com/products/distribution) ou vous pouvez obtenir Jupyter Notebook sous forme de téléchargement séparé à l’adresse https://jupyter.org/install.

Un grand nombre d’autres outils sont disponibles pour écrire des applications Python. Ce livre ne s’attarde pas sur ces outils, car Conda (un outil en ligne de commande fourni avec Jupyter Notebook) effectue toutes les tâches nécessaires et il est disponible gratuitement. Cependant, au fur et à mesure que vos compétences augmentent, vous pourriez trouver les fonctionnalités d’autres outils tels que Komodo Edit (https://www.activestate.com/products/komodo-ide/downloads/edit/) plus à votre goût. Vous trouverez une excellente liste de ces outils sur https://wiki.python.org/moin/IntegratedDevelopmentEnvironments.

Vous n’avez pas à saisir manuellement le code source de ce chapitre. En fait, l’utilisation de la source téléchargeable est beaucoup plus facile. Vous pouvez ainsi trouver la source téléchargeable de ce chapitre dans les fichiers Chapitre 04 Commentaires.ipynb, Chapitre 04 Indentation.ipynb, et Chapitre 04 Exemples.ipynb. Consultez l’introduction pour savoir comment trouver ces fichiers source.

Comprendre pourquoi les IDE sont importants

Vous vous posez sans doute la question de savoir pourquoi vous avez besoin d’un IDE pour travailler avec Python. Python est livré avec un outil en ligne de commande que vous pouvez ouvrir en saisissant python et en appuyant sur Entrée dans une fenêtre d’invite de commandes ou de terminal sur un ordinateur Mac ou PC (vous pouvez vous faciliter la tâche en vous assurant que le dossier dans lequel Python est installé figure dans la variable d’environnement PATH de votre système ou en installant Anaconda, qui est livré avec sa propre invite Anaconda). D’ailleurs, Python est livré avec un IDE limité appelé Integrated Development and Learning Environment (IDLE) qui apparaît dans le dossier Lib\idlelib de votre installation Python (encore une fois, pour les configurations sur des ordinateurs). De nombreuses personnes s’interrogent probablement sur le développement d’applications autonomes.

Malheureusement, les outils fournis avec Python sont intéressants et même utiles pour démarrer, mais ils ne vous aideront pas à créer facilement des applications utiles. De plus, l’accès à ces outils implique la création d’une configuration de bureau qui n’offrira peut-être pas la flexibilité dont vous avez besoin, car vous ne pourrez plus programmer sur votre tablette (ou votre smartphone, si vous êtes vraiment motivé). Si vous choisissez de travailler avec Python à long terme, vous avez vraiment besoin d’un meilleur outil pour les raisons décrites dans les sections suivantes.

Créer un meilleur code

Un bon IDE contient une certaine dose d’intelligence. Par exemple, l’IDE peut suggérer des alternatives lorsque vous saisissez un mot-clé erroné, ou il peut vous dire qu’une certaine ligne de code ne fonctionnera tout simplement pas telle qu’elle est écrite. Plus l’IDE est intelligent, moins vous devez travailler dur pour écrire un meilleur code. Écrire un meilleur code est essentiel car personne ne veut passer des heures à chercher des erreurs, appelées bogues.

Les IDE varient considérablement dans le niveau et le type d’intelligence qu’ils fournissent, et c’est la raison pour laquelle il en existe tant. Vous pouvez trouver le niveau d’aide obtenu d’un IDE insuffisant par rapport à vos besoins, alors qu’un autre IDE veillera sur vous comme une mère poule. Chaque développeur a des besoins différents et, par conséquent, des exigences différentes en matière d’IDE. L’essentiel est d’obtenir un IDE qui vous aide à écrire un code propre et efficace rapidement et facilement.

Fonctionnalité de débogage

Trouver les bogues (erreurs) dans votre code est un processus appelé débogage. Même le développeur le plus expert du monde passe du temps à déboguer. Écrire un code parfait du premier coup est presque impossible, surtout si vous vous arrachez les yeux en développant sur un smartphone. Lorsque vous y parvenez, il faut s’en réjouir, car cela n’arrivera pas souvent. Par conséquent, les capacités de débogage de votre IDE sont essentielles. Malheureusement, il existe très peu de fonctionnalités de débogage dans les outils Python natifs. Si vous passez un peu de temps à déboguer avec ces outils, vous les trouverez rapidement inefficaces, car ils ne vous disent pas grand-chose sur votre code.

Les meilleurs IDE font également office d’outils de formation. S’il dispose de suffisamment de fonctionnalités, un IDE peut vous aider à explorer le code écrit par de véritables experts. Suivre l’exécution des lignes de code d’une application à l’aide d’un débogueur est une méthode traditionnelle d’apprentissage de nouvelles compétences et de perfectionnement. Une augmentation apparemment minime de vos connaissances peut souvent apporter un énorme gain de temps par la suite. Lorsque vous recherchez un IDE, ne prenez pas uniquement en compte les fonctions de débogage comme un moyen d’éliminer les erreurs, mais pensez aussi aux possibilités d’apprendre de nouvelles choses sur Python.

Comprendre pourquoi les notebooks sont utiles

La plupart des IDE ressemblent à des éditeurs de texte améliorés, et c’est précisément ce qu’ils sont. Oui, vous disposez de toutes sortes de fonctions intelligentes, de conseils, d’astuces, de coloration du code, etc., mais au bout du compte, ce ne sont que des éditeurs de texte. Je n’ai absolument rien contre les éditeurs de texte, et vous ne trouverez aucun reproche sur ces outils dans ce chapitre. Cependant, étant donné que les développeurs Python se concentrent souvent sur des applications scientifiques qui nécessitent quelque chose de plus élaboré qu’une présentation purement textuelle, l’emploi alternatif de notebooks peut se révéler utile.

Un notebook diffère d’un éditeur de texte en ce qu’il se concentre sur une technique avancée que Donald Knuth, célèbre informaticien de Stanford, appelle literate programming (mot à mot programmation lettrée). On utilise ce type de programmation afin de créer une sorte de présentation du code, des notes, des équations mathématiques et des graphiques. En bref, vous vous retrouvez avec un carnet de notes de scientifique rempli de tout ce qui est nécessaire pour comprendre complètement le code. Vous voyez souvent les techniques de literate programming employées dans des progiciels coûteux tels que Mathematica et MATLAB. Le développement à l’aide de notebooks produit d’excellents résultats dans les domaines suivants :

• démonstration ;

• collaboration ;

• recherche ;

• objectifs pédagogiques ;

• présentation.

Ce livre utilise Colab et Jupyter Notebook parce que ces IDE vous offrent une excellente expérience de codage en Python, et vous aident à découvrir l’énorme potentiel des techniques de literate programming. Si vous passez beaucoup de temps à effectuer des tâches scientifiques, Colab et les produits similaires sont indispensables. De plus, Colab et Jupyter Notebook étant tous deux gratuits, vous bénéficiez des avantages du style literate programming sans le coût d’autres progiciels.

Dans ce livre, Colab est utilisé pour faire référence à Google Colab, Jupyter Notebook pour faire référence à Jupyter Notebook, et Notebook (employé tout seul) pour faire référence à Colab ainsi qu’à Jupyter Notebook de manière générique. Il existe des différences entre les deux produits que vous devez connaître, et je les signale quand c’est nécessaire.

Créer l’application

Vous avez en fait créé votre première application Python en suivant les étapes de la section « Voir les résultats » du Chapitre 3. La méthode print() n’a peut-être pas l’air de grand-chose, mais vous allez l’utiliser assez souvent. Cependant, l’approche de la literate programming proposée par Colab requiert un peu plus de connaissances que celles que vous possédez actuellement. Les sections suivantes ne vous dévoilent pas tout sur cette approche, mais elles vous aident à comprendre ce que ce type de programmation peut apporter en termes de fonctionnalités. Toutefois, avant de commencer, assurez-vous d’avoir le fichier Chapitre 04 Exemples.ipynb ouvert et prêt à l’emploi, car vous en aurez besoin pour explorer Notebook.

Développer le code

Les applications réelles font autre chose que d’afficher un message à l’aide de la méthode print(). À partir de la deuxième partie du livre, vous aurez les compétences requises pour réaliser des choses vraiment intéressantes. Mais pour l’instant, il est temps de faire quelque chose d’un peu plus utile que d’afficher un message, même si vous ne comprenez pas tout ce que fait le code (vous le comprendrez certainement plus tard au fur et à mesure de votre progression dans cet ouvrage). Créez un nouveau notebook (consultez la section « Créer un nouveau notebook » du Chapitre 2 si vous avez besoin d’aide) et saisissez le code suivant dans une cellule de code :

def ajouteQuelquechose(valeur1, valeur2):
    return valeur1 + valeur2
print(ajouteQuelquechose(1, 2))

Lorsque vous cliquez sur l’icône Exécuter la cellule (ou sur le bouton Exécuter dans Jupyter Notebook), vous voyez le résultat 3 apparaître comme par magie. Bien sûr, le plaisir de voir le code que vous avez saisi s’exécuter est momentané et vous pourriez, dans un second temps, penser qu’il s’agit d’un groupe de lettres formant un charabia qui ne signifie pas grand-chose pour la plupart des humains. C’est donc le moment de décomposer un peu ce code et d’y ajouter des commentaires pour que vous puissiez vous souvenir de ce qu’il fait. Ajoutez les commentaires suivants (en gras) :

# Définit une fonction pour encapsuler le code d'une tâche qui
# accepte deux informations dans des contenants appelés
# variables dont le nom est valeur1 et valeur2.
def ajouteQuelquechose(valeur1, valeur2):

    # Additionne les deux valeurs et retourne le résultat
    # au programme appelant.
    return valeur1 + valeur2

# Appelle la fonction ajouteQuelquechose() avec deux nombres
# pour les additionner puis utiliser print() pour afficher le
résultat.
print(ajouteQuelquechose(1, 2))

Les commentaires commencent toujours par un signe dièse (#) afin que l’interpréteur Python ne les confonde pas avec du code. C’est incroyable la différence que peuvent faire quelques commentaires. Bien sûr, il se peut que vous ne compreniez toujours pas complètement le code, mais vous avez une meilleure idée de son fonctionnement, et c’est pourquoi il faut toujours commenter votre code.

Ajout de cellules de documentation

Les cellules se présentent sous plusieurs formes. Ce livre ne les utilise pas toutes, mais savoir comment utiliser les cellules de documentation peut s’avérer utile. Sélectionnez la première cellule (celle qui contient le code que vous venez de créer). Cliquez sur le bouton +Texte dans Colab ou choisissez Insert → Insert Cell Above dans Jupyter Notebook. Une nouvelle cellule est ajoutée au notebook. Lorsque vous travaillez avec Jupyter Notebook, vous devez sélectionner Markdown dans la liste déroulante qui contient actuellement Code. Saisissez # Création de l’application car il s’agit du titre de premier niveau de cette section. Le symbole # est appelé balisage, et il s’agit d’une méthode pour indiquer au notebook comment formater le texte (la section « Travailler avec des cellules de texte » de ce chapitre détaille l’utilisation du balisage). Sélectionnez la cellule suivante dans Colab ou cliquez sur Run dans Jupyter Notebook. Vous voyez le texte se transformer en un titre. Cliquez sur le bouton Déplacer la cellule vers le haut dans Colab si nécessaire pour placer le titre correctement, comme cela est illustré à la Figure 3.1.

Si vous pensez que ces cellules spéciales se comportent comme des pages HTML, vous avez raison. Utilisez le même processus que vous avez employé pour le premier titre pour ajouter un deuxième titre en saisissant ## Développer le code. Comme vous pouvez le voir à la Figure 3.2, le nombre de dièses (#) que vous ajoutez au texte affecte le niveau du titre, mais les symboles dièse n’apparaissent pas dans le titre.

[picture]

Figure 3.1 L’ajout de titres vous aide à séparer et à documenter votre code.

[picture]

Figure 3.2 L’emploi de niveaux de titres permet de mettre en valeur le contenu des cellules.

Vous noterez que vous avez également documenté précisément l’endroit où le code apparaît dans le livre. Les autres exemples de code source du livre suivent cette même convention afin que vous puissiez facilement faire correspondre le code source à la section qui le décrit.

Cellules ayant un contenu différent

Ce chapitre (et ce livre) ne présente pas tous les types de contenu de cellules que vous pouvez employer avec Colab et Jupyter Notebook. Cependant, vous pouvez également ajouter des éléments tels que des graphiques à vos notebooks. Le moment venu, vous pouvez afficher votre notebook sous forme de rapport et l’utiliser dans des présentations de toutes sortes. La technique de literate programming est différente de celle que vous avez peut-être utilisée par le passé, mais elle présente des avantages certains, comme vous le verrez dans les prochains chapitres.

Tester les cellules de code bloc-notes

Il y a des moments où vous voulez expérimenter du code sans l’intégrer dans votre application. Vous pouvez également avoir besoin d’accéder à un shell tout en travaillant avec Colab. Par exemple, la commande ls de Linux (https://linuxize.com/post/how-to-list-files-in-linux-using-the-ls-command/) affiche une liste des fichiers et des répertoires trouvés à l’emplacement où vous exécutez le code Colab. Dans ce cas, vous pouvez créer une cellule de code bloc-notes, comme celle illustrée à la Figure 3.3, en choisissant Insérer → Cellule de code bloc-notes. Cette fonctionnalité est disponible uniquement dans Colab et non pas dans Jupyter Notebook.

[picture]

Figure 3.3 Une cellule de code bloc-notes fournit un endroit pour expérimenter et exécuter des commandes shell.

Un clic sur les points de suspension dans le coin supérieur droit affiche les tâches que vous pouvez effectuer avec la cellule de code bloc-notes. Voici quelques-unes des options les plus importantes :

• Copier la cellule : copie le contenu de la cellule dans le Presse-papiers où vous pouvez coller les informations dans une autre application.

• Supprimer l’élément de sortie : efface la sortie de la cellule avant que vous n’exécutiez à nouveau le code.

• Afficher les éléments de sortie en mode plein écran : affiche les données de la cellule en plein écran afin que vous puissiez les voir toutes en même temps en dehors des volets de Colab. Appuyez sur Echap pour quitter le mode plein écran.

• Déplacer l’onglet vers le volet suivant : place la cellule de code bloc-notes dans le prochain volet disponible. Normalement, une cellule de code bloc-notes s’ouvre dans son propre volet, ce qui peut dans certains cas prendre trop de place à l’écran.

• Déplacer l’onglet vers le volet précédent : place la cellule de code bloc-notes dans le volet précédent, qui peut être le volet initialement utilisé pour son contenu.

• Fermer l’onglet : ferme l’onglet de la cellule de code bloc-notes sélectionnée sans affecter les autres onglets.

• Fermer tous les onglets : ferme tous les onglets de la cellule de code bloc-notes sans affecter les autres cellules Colab. Notez que vous pouvez également voir des options pour fermer tous les onglets à gauche de l’onglet actuel et tous les onglets à droite de l’onglet actuel.

• Modifier la mise en page : détermine l’apparence de Colab par rapport aux cellules de code bloc-notes. La valeur par défaut est l’utilisation de deux colonnes, mais vous pouvez également employer une vue d’une seule colonne où les onglets des cellules de code bloc-notes apparaissent en haut avec un onglet séparé intitulé Notebook qui contient votre code. Il est également possible de placer les cellules de code bloc-notes sous la dernière ligne du volet Notebook actuel.

Vous pouvez avoir plusieurs cellules de code bloc-notes à la fois. Chaque fois que vous choisissez Insérer → Cellule de code bloc-notes, Colab crée un autre onglet de cellule de code bloc-notes. Vous pouvez fermer les onglets dans l’ordre que vous souhaitez.

Interagir avec les champs de formulaire

Les champs de formulaire permettent à l’utilisateur de saisir des valeurs qu’un programme peut ensuite utiliser comme entrée. De la même manière que Jupyter Notebook ne fournit pas de cellules de code bloc-notes, il ne fournit pas non plus d’accès aux champs de formulaire. Ainsi, même si l’ajout de champs de formulaire à votre notebook Colab n’empêche pas le code de s’exécuter également dans Jupyter Notebook, vous ne bénéficierez pas dans Jupyter Notebook de la fonctionnalité que les champs de formulaire offrent.

L’utilisation de champs de formulaire crée une incompatibilité. À moins que vous n’ayez une bonne raison d’ajouter des éléments d’interface graphique à votre notebook, il est généralement préférable de ne pas utiliser de champs de formulaire et d’employer plutôt des méthodes d’entrée de données Python standard, comme la fonction input() présentée ici :

nom = input("Saisissez votre nom : ")
print(f "Votre nom est : {nom}")

Choisir un type de champ de formulaire

Si vous décidez néanmoins d’employer des champs de formulaire dans votre programme, choisissez Insertion → Ajouter un champ de formulaire. La boîte de dialogue Ajouter un champ de formulaire s’affiche (Figure 3.4). Le champ Type du champ de formulaire contient une liste d’éléments d’interface utilisateur graphique que vous pouvez ajouter (dropdown, input, slider et markdown).

[picture]

Figure 3.4 La boîte de dialogue Ajouter un champ de formulaire permet d’ajouter des entrées d’interface graphique à votre bloc-notes.

Le type de champ de formulaire que vous choisissez détermine la façon dont la boîte de dialogue Ajouter un champ de formulaire apparaît. Par exemple, si vous choisissez dropdown, vous devez ajouter des options d’une liste déroulante. Pour le type slider, vous devez ajouter des valeurs minimales et maximales, ainsi que la taille de l’incrément entre les valeurs.

L’option markdown est intéressante, car elle permet d’ajouter ce qui revient à une cellule de texte à l’intérieur d’une cellule de code. Il suffit de saisir Colab markdown (https://colab.research.google.com/notebooks/markdown_guide.ipynb) dans le champ Markdown. Cela signifie que vous pouvez vraiment mélanger le code et la syntaxe markdown afin d’améliorer la présentation de votre notebook.

Définition du type et du nom de la variable

Pour utiliser un champ de formulaire, il vous suffit de remplir les champs vides. Accomplissez les étapes suivantes pour voir un exemple :

❶ Sélectionnez input dans le champ Type du champ de formulaire.

La boîte de dialogue illustrée à la Figure 3.4 s’affiche.

❷ Sélectionnez string dans le champ Type de variable.

❸ Saisissez VotreNom dans le champ Nom de la variable, puis cliquez sur Enregistrer.

Vous voyez la combinaison de code et d’interface graphique illustrée à la Figure 3.5.

Afficher à la fois le code et l’interface graphique peut sembler contre-productif, car cela n’améliore pas la présentation de votre notebook. Pour vous débarrasser du code, cliquez sur les points de suspension dans la barre d’outils de la cellule, puis sélectionnez Format → Masquer le code dans le menu.

[picture]

Figure 3.5 L’ajout d’un champ de formulaire à votre notebook affiche une combinaison de code et d’interface graphique.

Utiliser le champ de formulaire

L’un des aspects intéressants des champs de formulaire est qu’ils font partie d’une cellule de code. Vous pouvez donc ajouter du code directement sous la saisie du champ de formulaire afin d’interagir avec ce dernier. Saisissez print(f"Votre nom est : {VotreNom}") directement sous l’entrée VotreNom = "" #@param {type : "string"} dans la cellule. Saisissez un nom dans le champ du formulaire, et vous verrez que le code est également modifié. À présent, cliquez sur Exécuter la cellule et vous constaterez que le code interagit avec le champ de formulaire, comme cela est illustré à la Figure 3.6.

[picture]

Figure 3.6 Concevez du code pour interagir avec les champs de formulaire que vous créez.

En reliant un certain nombre de champs de formulaire dans la même cellule, vous pouvez créer l’apparence d’un formulaire utilisé dans une autre application. L’utilisateur remplit le formulaire et clique sur Exécuter la cellule pour voir le résultat. Lorsque vous masquez le code, l’interface graphique semble transparente, et comme elle réside dans un notebook, vous avez également accès aux fonctions de présentation qu’un notebook peut offrir.

Exécution de l’application

En général, une application est constituée du notebook actuel et de toutes les ressources liées. Ce n’est pas une application au sens traditionnel du terme, car vous ne pouvez exécuter qu’une seule cellule à la fois, mais il est utile de considérer l’application comme étant l’ensemble du notebook.

Voir le résultat

Jusqu’à présent, vous avez vu des applications où l’on n’exécutait qu’une seule cellule à la fois. Pour voir un résultat complet sans avoir à cliquer dans chaque cellule sur la commande Exécuter la cellule, ce qui est particulièrement agaçant, vous pouvez utiliser les options suivantes :

• Exécution → Tout exécuter : exécute toutes les cellules sans effacer la sortie au préalable.

• Exécution → Exécuter avant : exécute toutes les cellules situées au-dessus de la cellule qui a actuellement le focus.

• Sélection de cellules : sélectionnez plusieurs cellules à l’aide des touches Maj + Flèche Haut ou Maj + Flèche Bas, puis choisissez Exécuter → Exécuter le code sélectionné pour n’exécuter que les cellules sélectionnées.

• Exécution → Exécuter à la suite : exécute toutes les cellules situées sous la cellule qui a actuellement le focus.

• Exécution → Redémarrer et tout exécuter : exécute toutes les cellules après avoir redémarré l’exécution et effacé la sortie.

Les options de Jupyter Notebook sont similaires, mais suffisamment différentes pour justifier des explications séparées.

• Sélection de cellules : sélectionnez plusieurs cellules à l’aide des touches Maj + Flèche Haut ou Maj + Flèche Bas, puis choisissez Cell → Run Cells pour n’exécuter que les cellules sélectionnées. Vous pouvez également choisir Cell → Run Cells and Select Below pour exécuter les cellules sélectionnées et choisir la cellule suivante comme celle ayant le focus. S’il n’y a pas de cellule suivante, Jupyter Notebook en crée une. En choisissant Cell → Run Cells and Insert Below, vous exécutez les cellules sélectionnées, puis vous insérez une nouvelle cellule comme cellule ayant le focus, même s’il y a une cellule existante à utiliser.

Lorsque vous utilisez cette méthode, assurez-vous que vous avez sélectionné la cellule et non pas le texte à l’intérieur de la cellule. Jupyter Notebook met en évidence les cellules sélectionnées en les affichant dans une couleur claire.

• Cell → un All : exécute toutes les cellules sans effacer la sortie au préalable.

• Cell → Run All Above : exécute toutes les cellules au-dessus de la cellule qui a actuellement le focus.

• Cell → Run All Below : exécute toutes les cellules situées sous la cellule qui a actuellement le focus.

• Kernel → Restart & Clear Output : efface la sortie des exécutions précédentes sans exécuter le code. Il s’agit d’une fonctionnalité importante si vous souhaitez n’exécuter que certaines des cellules sans que les résultats précédents n’interfèrent avec le test en cours.

• Kernel → Restart & Run All : exécute toutes les cellules après avoir redémarré l’exécution et effacé la sortie.

Visualiser l’historique du code exécuté

Il est utile de voir l’historique du code que vous avez exécuté lors de la création d’une application. Cela peut vous aider à repérer des erreurs de logique ou même dans l’ordre d’exécution des commandes. Parfois, l’affichage de l’historique du code vous rappelle simplement qu’il faut redémarrer l’exécution ou le noyau parce que c’est le bazar avec des variables qui sont toujours présentes alors que vous n’en avez plus besoin et qui causent des problèmes. Pour voir l’historique du code dans Colab, choisissez Affichage → Historique d’exécution. La Figure 3.7 présente un historique d’exécution des exemples illustrant les sections précédentes.

[picture]

Figure 3.7 L’affichage de l’historique du code exécuté peut vous en apprendre beaucoup sur votre code.

Le haut de chaque entrée contient l’heure à laquelle vous avez exécuté le code. À côté de l’horodatage se trouve une icône sur laquelle vous pouvez cliquer pour copier le code dans une cellule de code bloc-notes afin de l’analyser de manière plus approfondie. La dernière icône de la ligne d’horodatage fait défiler l’affichage jusqu’au code en question et le sélectionne afin que vous n’ayez pas à le rechercher manuellement.

Au bas de la fenêtre se trouve une cellule où vous pouvez tester des idées quand vous consultez le code exécuté. Elle fait office de cellule de code bloc-notes, et on l’utilise comme n’importe quelle autre cellule.

Comprendre l’utilisation de l’indentation

Lorsque vous travaillez sur les exemples de ce livre, vous constatez que certaines lignes sont indentées. En fait, les exemples fournissent également une bonne quantité d’espace blanc (c’est-à-dire des lignes supplémentaires entre les lignes de code). L’indentation est également fonctionnelle. Python utilise l’indentation pour définir des zones de code telles que le code associé aux fonctions et aux boucles. L’emploi de l’indentation est donc un élément important d’un programme Python.

Les différentes utilisations de l’indentation deviendront plus familières au fur et à mesure que vous travaillerez sur les exemples du livre. Cependant, vous devez savoir dès le départ pourquoi l’indentation est utilisée et comment on la met en œuvre. Pour ce faire, nous allons prendre un exemple. Les étapes suivantes vous aident à créer un nouvel exemple qui utilise l’indentation afin de rendre la relation entre les éléments de l’application beaucoup plus apparente et plus facile à comprendre par la suite.

❶ Ouvrez un nouveau notebook en sélectionnant Nouveau notebook dans Colab, et New → Python3 dans Jupyter Notebook, ou en choisissant File → New Notebook (en choisissant éventuellement le bon type de notebook) si vous avez déjà un notebook ouvert.

Colab ou Jupyter Notebook crée un nouveau notebook.

❷ Saisissez print("Ceci est une très longue ligne de texte qui va" +.

Vous voyez le texte s’afficher normalement à l’écran, comme vous vous y attendez. Le signe plus (+) indique à Python qu’il y a du texte supplémentaire à afficher (n’ajoutez pas le point à la fin de la phrase). L’ajout de texte sur plusieurs lignes en un seul long morceau de texte est appelé concaténation. Vous apprendrez à utiliser cette fonctionnalité plus tard dans le livre, et vous n’avez donc pas besoin de vous en préoccuper pour le moment.

❸ Appuyez sur la touche Entrée.

Le point d’insertion ne retourne pas au début de la ligne, comme vous pourriez vous y attendre. Au lieu de cela, il se retrouve directement sous le premier guillemet double, comme cela est illustré à la Figure 3.8. Cette fonction s’appelle l’indentation automatique et c’est l’une des caractéristiques qui différencient un éditeur de texte ordinaire d’un éditeur conçu pour écrire du code.

[picture]

Figure 3.8 La fenêtre Édition indente automatiquement certains types de texte.

❹ Saisissez " apparaître sur plusieurs lignes dans le fichier de code source.") et appuyez sur Entrée.

Remarquez que le point d’insertion revient au début de la ligne. Lorsque Notebook détecte que vous avez atteint la fin du code, il n’indente plus automatiquement le texte qui revient à sa position initiale.

❺ Cliquez sur Exécuter.

Vous voyez la sortie illustrée à la Figure 3.9. Bien que le texte apparaisse sur plusieurs lignes dans le fichier de code source, il apparaît sur une seule ligne dans le résultat.

[picture]

Figure 3.9 Utilisez la concaténation pour faire apparaître plusieurs lignes de texte sur une seule ligne dans la sortie.

Ajouter des commentaires

Les gens créent des notes tout le temps. Lorsque vous devez faire des courses, vous regardez dans vos placards, déterminez ce dont vous avez besoin en l’écrivant sur une liste ou en dictant à votre téléphone. Lorsque vous arrivez au magasin, vous relisez votre liste pour vous rappeler ce dont vous avez besoin. L’emploi de notes est utile pour toutes sortes de besoins, comme suivre le cours d’une conversation entre partenaires commerciaux ou se souvenir des points essentiels d’une conférence. Les humains ont besoin de notes pour se rafraîchir la mémoire. Les commentaires dans le code source constituent une autre forme de note. On les ajoute au code afin de pouvoir se souvenir plus tard de la tâche effectuée par le code. Les sections suivantes décrivent les commentaires de manière plus détaillée.

Titres versus commentaires

Au début, vous serez peut-être un peu dérouté par les titres et les commentaires. Les titres apparaissent dans des cellules séparées, alors que les commentaires sont intégrés au code source. Les titres et les commentaires servent des objectifs différents. Les titres ont pour but de vous informer sur la totalité d’un groupe de code, tandis que les commentaires vous renseignent sur des étapes de code particulières, voire des lignes de code individuelles. Même si vous utilisez les titres et les commentaires pour la documentation, chacun d’entre eux a un but unique. Les commentaires sont généralement plus détaillés que les titres.

Comprendre les commentaires

Les ordinateurs ont besoin d’un moyen pour déterminer que le texte que vous écrivez est un commentaire et non pas du code à exécuter. Python fournit deux méthodes pour définir qu’un texte est un commentaire et non pas du code. La première méthode s’applique aux commentaires qui ne sont que sur une seule ligne. Elle utilise le signe dièse (#) de la manière suivante :

# Ceci est un commentaire.
print("Bonjour !") #Ceci est également un commentaire.

Un commentaire d’une seule ligne peut apparaître sur une ligne à part ou après le code. On utilise généralement un commentaire d’une seule ligne pour un texte descriptif court, comme une explication d’un bout de code particulier. Notebook affiche les commentaires dans une couleur distinctive (généralement bleue ou verte) et en italique.

Créer des commentaires multilignes

Python ne prend pas directement en charge les commentaires multilignes, mais vous pouvez en créer un à l’aide d’une chaîne encadrée par trois guillemets (appelée docstring). Un commentaire multiligne commence et se termine par trois guillemets doubles (""") ou trois guillemets simples (''') comme dans l’exemple suivant :

"""
    Application : Commentaires.py
    Écrit par : John
    Objectif : Montre comment utiliser les commentaires.
"""

Contrairement aux commentaires sur une seule ligne, les docstrings multilignes utilisées sous la forme de commentaires apparaissent normalement en rouge et en caractères romains. L’interpréteur les lit, mais vous pouvez toujours les utiliser comme commentaires, car ces lignes ne sont pas exécutées. Python n’affiche pas de message d’erreur lorsqu’elles apparaissent dans votre code. Cependant, Notebook les traite différemment, comme cela est illustré à la Figure 3.10.

[picture]

Figure 3.10 Les commentaires multilignes fonctionnent, mais ils fournissent également des résultats.

Notez que les véritables commentaires Python, ceux qui sont précédés d’un dièse (#), ne génèrent aucune sortie. En revanche, les docstrings génèrent une sortie. Si vous prévoyez d’éditer votre notebook sous forme de rapport, vous devez éviter d’utiliser des docstrings (certains IDE, comme IDLE, ignorent complètement les docstrings).

Vous pouvez également affecter une docstring à une variable, ce qui n’est pas possible avec un commentaire. Le code et la sortie présentés à la Figure 3.11 montrent à quoi ressemble une docstring lorsqu’elle est affectée à une variable. Vous noterez que la docstring n’est pas affichée dans ce cas, car elle apparaît comme une affectation de variable. Le formatage de la docstring reste cependant effectif, contrairement à l’exemple de concaténation présenté dans la section « Comprendre l’utilisation de l’indentation » , plus haut dans ce chapitre.

[picture]

Figure 3.11 Utilisez les docstrings pour les chaînes de caractères qui nécessitent plusieurs lignes.

On utilise généralement les docstrings multilignes pour des explications plus longues afin de détailler les raisons de la création d’une application et les tâches qu’elle effectue. Bien sûr, il n’y a pas de règles strictes sur la manière précise d’utiliser les commentaires, l’objectif principal étant d’indiquer précisément à l’ordinateur ce qui est un commentaire et ce qui ne l’est pas.

Utiliser les commentaires à titre de pense-bête

Beaucoup de gens ne comprennent pas vraiment les commentaires et ils ne savent pas quoi en faire dans le code. Vous ne devez pas perdre de vue que vous pouvez écrire un bout de code aujourd’hui et ne pas y revenir avant plusieurs années. Vous avez besoin de notes pour vous rafraîchir la mémoire afin de vous rappeler la tâche effectuée par le code et les raisons pour lesquelles vous l’avez écrit. En fait, voici quelques arguments courants justifiant l’emploi de commentaires dans votre code :

• Permettre de vous souvenir de ce que fait le code et pourquoi vous l’avez écrit.

• Enregistrer qui a écrit le code lorsqu’il s’agit d’un projet impliquant plusieurs personnes (avec notamment des informations sur la façon de contacter ces personnes).

• Dire aux autres comment maintenir votre code.

• Rendre votre code accessible à d’autres développeurs.

• Lister les idées pour les futures mises à jour.

• Fournir une liste des documents que vous avez utilisés pour écrire le code.

• Maintenir une liste des améliorations que vous avez apportées.

Vous pouvez également utiliser les commentaires de bien d’autres façons, mais ce sont les plus courantes. Observez la manière dont les commentaires sont utilisés dans les exemples du livre, en particulier dans les derniers chapitres où le code devient plus complexe. À mesure que votre code devient plus compliqué, vous devez ajouter plus de commentaires et faire en sorte qu’ils soient pertinents et se concentrent sur l’essentiel et sur ce que vous devez retenir.

Utiliser des commentaires pour empêcher l’exécution du code

Les développeurs utilisent aussi parfois la fonction de commentaire pour empêcher l’exécution de lignes de code (on parle alors de ligne de code mise en commentaire). Vous pouvez être amené à le faire afin de déterminer si une ligne de code est à l’origine de l’échec de votre application. Comme pour tout autre commentaire, vous pouvez utiliser un commentaire à ligne unique ou un commentaire multiligne. Cependant, lorsque vous utilisez le commentaire multiligne, vous voyez le code qui ne s’exécute pas dans la sortie (et il peut être utile de voir où le code affecte la sortie).

Rendre votre notebook informatif et descriptif

Après avoir consacré beaucoup de temps et d’efforts à la création de titres et de lignes de code, ce n’est pas le moment de faiblir et vous devez soigner les finitions afin que votre notebook se démarque véritablement de la concurrence. Heureusement, vous n’avez pas besoin d’un diplôme en communication pour rendre les notebooks intéressants et même agréables à regarder. Il va vous falloir un peu de temps pour que votre notebook soit visuellement attrayant, mais le jeu en vaut la chandelle lorsque vous faites une présentation. Les sections suivantes proposent quelques idées sur la façon d’améliorer l’aspect de vos notebooks.

Travailler avec des cellules de texte

Jusqu’à présent, vous avez travaillé avec des cellules de texte contenant des titres. Lorsque vous ouvrez une cellule de texte dans Colab, vous voyez une barre d’outils contenant toutes sortes d’options intéressantes, comme celles illustrées à la Figure 3.12.

[picture]

Figure 3.12 La barre d’outils d’une cellule de texte contient une variété d’options intéressantes.

Voici le rôle de ces boutons :

• Activer/désactiver l’en-tête : fait défiler les différents niveaux de titre afin que vous puissiez voir comment le texte apparaît dans différentes tailles.

• Gras : fait apparaître le texte sélectionné en caractères gras.

• Italique : fait apparaître le texte sélectionné en italique.

• Format code : configure le texte sélectionné pour qu’il apparaisse comme un code plutôt que comme du texte normal. Vous pouvez utiliser cette option pour mettre en forme des choses comme une syntaxe d’appel ou des exemples d’utilisation d’une fonction.

• Insérer un lien : ajoute une URL à la cellule de texte afin que l’utilisateur puisse cliquer dessus et voir d’autres ressources en ligne. C’est également un moyen pratique de créer des références au fur et à mesure que vous écrivez votre programme afin que vous puissiez simplement revenir au site Web qui vous a inspiré une bonne idée de codage.

• Insérer une image : place une image à la position actuelle du curseur, ce qui permet d’ajouter une documentation graphique à votre notebook en plus du texte.

• Augmenter le retrait : ajoute un retrait au texte actuel. Vous pouvez également utiliser cette fonction pour créer des listes à puces et des listes numérotées à plusieurs niveaux.

• Ajouter une liste numérotée : crée une nouvelle liste numérotée à la position actuelle du curseur.

Vous pouvez ajouter des sous-listes à puces à une liste numérotée et vice versa en utilisant simplement la fonction d’augmentation du retrait. Par exemple, la Figure 3.13 illustre à quoi ressemble une liste imbriquée. Vous noterez l’utilisation du symbole > pour augmenter le niveau de retrait. Remarquez également que le type de puce change en fonction du niveau de retrait.

[picture]

Figure 3.13 Utilisez les retraits pour créer des listes imbriquées pour votre documentation.

• Ajouter une liste à puces : crée une nouvelle liste à puces à la position actuelle du curseur.

• Ajouter une règle horizontale : place une ligne en travers de la cellule de texte pour séparer un groupe textuel d’un autre.

• LaTeX : permet l’utilisation du balisage LaTeX (https://www.latex-project.org/) dans la cellule de texte.

• Insérer un emoji : place l’emoji sélectionné à l’emplacement actuel du curseur. Lorsque vous cliquez sur cette option, vous voyez une liste d’emojis standard. Les emojis fonctionnent avec Colab, mais vous devrez peut-être les installer sur Jupyter Notebook (https://sawanrai777.medium.com/have-fun-with-emojis-in-jupyter-notebook-f74db7f45210). Notez que vous pouvez copier et coller des emojis à partir de sites Web et d’autres emplacements dans votre code, mais vous devez les placer dans une chaîne de caractères pour garantir une interprétation correcte par l’IDE.

Ajouter des en-têtes de section

Un en-tête de section est simplement une cellule de texte dans laquelle un titre est déjà défini. Vous pouvez modifier le niveau de titre de la section en faisant varier le nombre de caractères # devant le titre. La seule différence est que Colab fait une partie du travail à votre place. Ajoutez un nouvel en-tête de section à votre notebook en choisissant Insérer → Cellule d’en-tête de section.

Interagir avec la table des matières

La table des matières est une fonctionnalité prise en charge par Colab, mais pas par Jupyter Notebook. Pour voir la table des matières, choisissez Affichage → Sommaire ou cliquez sur le bouton Sommaire dans la barre d’outils de gauche. La Figure 3.14 illustre l’affichage du sommaire de ce chapitre dans les exemples de code.

[picture]

Figure 3.14 La fonction Sommaire fournit un aperçu de votre notebook.

Pour utiliser la table des matières, il suffit de cliquer sur la rubrique qui contient les informations que vous voulez voir. Le titre actuellement sélectionné apparaît en gras pour que vous sachiez où vous vous trouvez dans le notebook.

La Figure 3.14 souligne la nécessité d’utiliser des cellules de texte distinctes pour chaque niveau d’en-tête de votre notebook, plutôt que de combiner tous les en-têtes d’une zone particulière dans une seule cellule de texte (même si cela semble acceptable dans le document). Si vous ne placez pas chaque niveau d’en-tête dans sa propre cellule de texte, la table des matières ne fonctionnera pas comme prévu.

Renommer un notebook

Les notebooks que vous créez doivent avoir des noms descriptifs, sinon ils deviennent trop difficiles à localiser quand vous en avez accumulé un certain nombre. C’est un peu comme quand on a un placard rempli du sol au plafond de boîtes non identifiées (ou pire encore, de boîtes dont le contenu est mal indiqué). La seule façon de trouver quelque chose est de sortir les boîtes une par une et de les ouvrir pour voir ce qu’elles contiennent. Pour renommer un notebook, il suffit de cliquer sur le nom de fichier en haut du notebook, par exemple Chapitre 04 Commentaires.ipynb, et de saisir un nouveau nom.

Fermer et arrêter un notebook

Il n’est pas nécessaire (ni possible) de fermer et d’arrêter les notebooks lorsque vous travaillez avec Colab. Cependant, lorsque vous travaillez avec Jupyter Notebook, vous disposez d’un serveur local (appelé kernel, c’est-à-dire noyau), et vous devez interagir avec ce serveur pour vous assurer que toutes les modifications que vous apportez à un notebook sont enregistrées. La liste suivante contient les options de menu de Jupyter Notebook que vous utilisez pour interagir avec le serveur afin de vous assurer que vos modifications sont enregistrées et que le serveur lui-même est arrêté lorsque vous voulez mettre fin à une session.

• File → Save and Checkpoint : enregistre les modifications que vous avez apportées sur le disque et crée un point de contrôle afin que vous puissiez restaurer une version précédente de votre fichier. Cependant, Jupyter Notebook ne sauvegarde qu’une seule version précédente, vos options d’annulation sont donc quelque peu limitées.

• File → Revert to Checkpoint : présente une liste de fichiers de point de contrôle pour ce notebook. Il n’y a généralement qu’un seul de ces fichiers, et le sélectionner restaurera le fichier du point de contrôle précédent.

• File → Close and Halt : ferme le fichier et arrête le serveur pour ce notebook. La fonctionnalité globale du serveur n’est pas affectée. Pour arrêter complètement le serveur, vous devez cliquer sur Quit dans l’écran principal du notebook Jupyter (celui où vous sélectionnez le fichier à ouvrir).

• Kernel → Reconnect : permet la récupération après un problème logiciel si le serveur rompt la connexion avec le notebook actuel. Vous pouvez savoir que la connexion est rompue, car vous voyez un message No Kernel sur le côté droit de la barre de menu.

• Kernel → Shutdown : arrête le noyau sans fermer le fichier. Un message No Kernel s’affiche sur le côté droit de la barre de menu lorsque l’arrêt est complet.

• Kernel → Change Kernel : sélectionne un autre noyau à utiliser lors de l’exécution du notebook actuel. Vous pouvez installer plusieurs versions de Python, par exemple, et avoir besoin de tester le code avec chaque version. La section « Voir le résultat » de ce chapitre parle de diverses options de redémarrage que vous pouvez employer pour vous assurer que vous avez un environnement propre à utiliser entre les tests.

Partie 2

Apprendre la langue de Python

Dans cette partie…

Exploiter différents types de données.

Modifier la façon dont Python perçoit les données.

Choisir des chemins dans les applications.

Effectuer des tâches répétitives.

Gérer les situations exceptionnelles.

DANS CE CHAPITRE

Comprendre le stockage des données.

•

Comprendre les différents types de stockage des données.

•

Ajouter des dates et des heures aux applications.

Chapitre 4

Enregistrer et modifier des informations

Souvenez-vous que le début du Chapitre 2 vous proposait de retenir l’acronyme CLAS (Créer, Lire, Actualiser, Supprimer). C’est une méthode simple pour se souvenir des tâches que l’ordinateur effectue à partir des informations que vous voulez gérer. Bien entendu, les geeks se servent d’un autre mot pour désigner ces informations. Ils les appellent des données, mais informations ou données, le sens est le même.

Pour que vos informations soient utiles, vous devez disposer de certaines méthodes pour qu’elles soient conservées de manière permanente. Sinon, vous devriez tout reprendre à zéro chaque fois que l’ordinateur se mettrait en marche. De plus, Python doit fournir des règles pour traiter toutes ces informations. Ce chapitre est donc consacré au contrôle des informations : comment les stocker de manière permanente, et comment les manipuler dans vos applications.

Vous n’avez pas à saisir manuellement le code source de ce chapitre. En fait, l’utilisation de la source téléchargeable est beaucoup plus facile. Vous trouverez ainsi le code source de ce chapitre dans le fichier Chapitre 06 Stockage et modification des informations.ipynb. Consultez l’introduction pour savoir comment trouver ces fichiers source.

Enregistrer des informations

Une application doit pouvoir accéder rapidement aux informations. Sinon, elles pourraient mettre pas mal de temps à réaliser les tâches qui leur sont dévolues. Elles stockent donc ces informations (ou données) en mémoire. Mais cette mémoire n’est que temporaire. Lorsque vous éteignez la machine, il est indispensable que vos données soient enregistrées sous une forme permanente, qu’il s’agisse de votre disque dur, d’un disque ou d’une clé USB, d’une carte mémoire, d’un disque réseau, ou encore d’une solution fondée sur le cloud. De plus, vous devez tenir compte de la nature de ces données, par exemple des nombres ou du texte. Les sections qui suivent discutent de ces sujets plus en détail.

Considérer les variables comme des boîtes de rangement

Lorsque vous travaillez avec des applications, vous enregistrez des informations (ou des données) dans des variables. Une variable, c’est comme une sorte de boîte de rangement. Lorsque vous voulez accéder à une certaine information, vous utilisez la variable qui la contient. Si vous avez de nouvelles informations à enregistrer, vous les placez dans une variable. Changer des informations implique d’accéder d’abord à la bonne variable (la bonne boîte de rangement), puis à y enregistrer les nouvelles valeurs. Et vous pouvez bien sûr aussi vider une boîte de rangement, et donc également une variable. Bref, les variables, c’est comme dans la vraie vie.

Les ordinateurs sont des machines plutôt soigneuses quant au rangement des affaires. Chaque variable contient uniquement un objet, ou plutôt une pièce d’information. Ceci permet de retrouver beaucoup plus facilement cette pièce d’information particulière, sans avoir à fouiller dans les tréfonds d’une gigantesque malle en osier. Vous n’avez pas eu à rencontrer de variables dans les précédents chapitres, mais sachez que la plupart des applications reposent sur une utilisation intensive de boîtes de rangement électroniques.

Utiliser la bonne boîte pour enregistrer les bonnes données

Trop de gens ont tendance à ranger les choses dans le mauvais type de boîte. Il n’est pas si rare par exemple de retrouver des chaussures dans un sac à vêtements, et un foulard dans une boîte à chaussures. Python, lui, aime que tout soit au bon endroit. C’est pourquoi vous trouvez des nombres qui sont enregistrés dans une sorte de variable, et des textes qui sont enregistrés dans une autre sorte de variables. Bien sûr, il s’agit dans tous les cas de variables, mais il y en a différents types pour stocker des informations de nature différente. Cela s’appelle ne pas mélanger les torchons avec les serviettes.

Utiliser des variables spécialisées permet de travailler avec leur contenu en utilisant des outils spécifiques. Pour l’instant, vous n’avez pas à vous soucier des détails. Retenez simplement que chaque type d’information est enregistré dans un type spécial de variable. Si Python sait tenir sa maison, ce n’est en fait pas le cas des ordinateurs qui n’ont aucune notion sur la nature des informations (donc des données). Tout ce qu’un ordinateur connaît, ce sont des suites gigantesques de 0 et de 1, qui correspondent à la présence ou à l’absence d’un certain voltage. À un niveau plus élevé, les ordinateurs sont certes capables de travailler avec des nombres, mais ce n’est au final qu’une extension de cette affaire de 0 et de 1. Les nombres, les lettres, les dates, les heures, et tout ce à quoi vous pouvez penser en termes d’informations, se traduisent en définitive par des 0 et des 1 dans un système informatique. On est bien peu de choses, tout de même. Ainsi, la lettre A majuscule est en réalité enregistrée sous la forme 01000001, qui est numérique et se traduit dans le système décimal par la valeur 65. En réalité, l’ordinateur n’a aucune idée en général, et en particulier de ce que peut bien être la lettre A, pas plus de ce qu’est une date comme le 29/03/2023.

Python et ses principaux types de données

Chaque langage de programmation définit des variables destinées à contenir différents types d’informations, et Python ne fait pas exception à la règle. Une variété particulière de variable est appelée un type de donnée. Connaître le type de donnée d’une variable est important, puisque c’est lui qui vous indique quel genre d’information celle-ci contient. De plus, pour enregistrer une information dans une variable, il faut en choisir une qui possède le bon type de donnée. Ainsi, Python ne vous permet pas d’enregistrer du texte dans une variable conçue pour contenir des données numériques. D’abord, votre texte risquerait d’être transformé en une bouillie de chiffres, et ensuite votre application risquerait tout bonnement de ne pas fonctionner. Vous pouvez en gros classifier les types de données de Python en trois catégories principales : les nombres, les textes et les valeurs logiques (ou booléennes). Mais il n’y a pas vraiment de limites quant à la manière dont tout cela peut se présenter du point de vue d’un humain. Les sections qui suivent décrivent ces différents types de données standards.

Placer des informations dans des variables

Pour placer une valeur dans une variable, vous vous servez du signe égal (=), que l’on appelle l’opérateur d’affectation (autrement dit, vous affectez une valeur à une variable). Vous retrouverez la cohorte des opérateurs Python de base dans le Chapitre 5. Mais vous avez besoin dès maintenant de vous familiariser avec celui-ci. Supposons donc simplement que vous vouliez stocker le nombre 5 en mémoire, c’est-à-dire dans une boîte, et donc dans une variable. Il faut que vous donniez un nom à celle-ci, sans quoi personne ne pourrait savoir de quoi on parle. Disons que ce nom sera maVar. En faisant appel à l’opérateur d’affectation, vous saisissez donc maVar = 5 dans la fenêtre de Python. Même si Python engloutit cette ligne sans rien dire, il a compris de quoi il s’agissait. Pour le voir, saisissez le nom de votre variable et cliquez sur Exécutez la cellule. Vous pouvez aussi utiliser une fonction print() comme cela est illustré à la Figure 4.1. Python va afficher sa valeur, donc le contenu de cette boîte particulière.

[picture]

Figure 4.1 Utilisez l’opérateur d’affectation pour placer une information dans une variable.

Les noms des variables font la différence entre les majuscules et les minuscules. Si vous avez nommé votre variable maVar, le fait de saisir mavar, qui est une autre variable totalement différente du point de vue de Python, provoquera une erreur puisque cette dernière n’a jamais été définie.

Comprendre les types numériques

Les humains ont tendance à penser aux nombres en termes généraux (d’ailleurs, beaucoup ne font même pas la différence entre chiffres et nombres). Nous voyons par exemple 1 et 1.0, ou 1,0, comme étant le même nombre. C’est la même valeur unitaire, mais la seconde forme contient en plus un séparateur décimal. Pour ce qui nous concerne, nous, humains, ces deux nombres sont égaux et parfaitement interchangeables. Mais Python a un avis très différent. Pour lui, il s’agit de deux types de nombres différents, et chaque format nécessite un mode de traitement spécifique. Les sections qui suivent décrivent les types numériques entiers, en virgule flottante, ainsi que les nombres complexes supportés par Python.

Entiers

Un entier, c’est... comment dire… un nombre entier. Par exemple, 1 est un entier. D’un autre côté, 1.0 n’est pas un entier. C’est un nombre décimal. Point. Dans Python, les entiers sont représentés par un type de donnée appelé int (pour integer en anglais).

Contrairement à de nombreux langages de programmation, Python n’a pas de limite de capacité pour les int (vous pouvez créer un int de n’importe quelle taille). Au cas où vous voudriez comprendre comment cela fonctionne, le site https://jakevdp.github.io/PythonDataScienceHandbook/02.01-understanding-data-types.html fournit des détails supplémentaires, mais vous n’avez vraiment pas besoin de savoir comment cela marche sous le capot pour travailler avec Python. Cette fonctionnalité de taille illimitée peut poser des problèmes lorsque vous travaillez avec des bibliothèques tierces, telles que NumPy (https://numpy.org/doc/stable/user/basics.types.html), où il existe différents types de données int avec des tailles spécifiques. Vous devez donc être conscient de ce problème lorsque vous écrivez une application utilisant des bibliothèques tierces.

Lorsque vous travaillez avec le type int, vous avez accès à nombre de fonctionnalités intéressantes. Nous retrouverons la plupart d’entre elles le moment venu, mais une de ces fonctionnalités est la capacité de ce type d’utiliser différentes bases numériques :

• Base 2 : utilise uniquement des 0 et des 1.

• Base 8 : utilise des chiffres de 0 à 7.

• Base 10 : c’est notre système décimal de tous les jours.

• Base 16 : c’est le système hexadécimal, qui utilise les chiffres de 0 à 9 ainsi que les lettres de A à F pour obtenir seize « chiffres » différents.

Pour indiquer à Python quelle base vous voulez utiliser (en dehors de la base 10), vous ajoutez au nombre un 0 suivi d’une lettre spéciale. Par exemple, la syntaxe 0b100 est la valeur un-zéro-zéro en base 2, soit 4 dans le système décimal. Ces lettres spéciales sont les suivantes :

• b : base 2

• o : base 8

• x : base 16

Il est aussi possible de convertir des valeurs numériques d’une base vers une autre en utilisant les fonctions bin(), oct() et hex(). La Figure 4.2 vous en propose quelques exemples. Essayez-les vous-même en variant les valeurs pour mieux voir comment tout cela fonctionne.

[picture]

Figure 4.2 Le type int peut contenir des valeurs dans plusieurs bases numériques.

Utiliser une base différente rend en fait les choses plus faciles dans de nombreuses situations. Vous en trouverez quelques-unes plus loin dans ce livre. Pour l’instant, retenez simplement que le type int est capable de supporter des valeurs entières dans plusieurs bases numériques.

Valeurs en virgule flottante

Tout nombre qui comprend une partie décimale est dit en virgule flottante. C’est par exemple le cas de 1.0. La différence entre un entier et un nombre en virgule flottante est donc très facile à repérer : il y a un point ou pas. Python enregistre les nombres en virgule flottante dans le type de donnée float.

Les valeurs en virgule flottante ont un avantage sur les entiers purs et durs : vous pouvez y enregistrer des valeurs comportant une partie fractionnaire. Contrairement aux entiers, les variables à virgule flottante ont une capacité de stockage en raison de la manière dont elles sont stockées selon le format IEEE 754 (voir https://www.geeksforgeeks.org/ieee-standard-754-floating-point-numbers/ pour plus de détails). Dans leur cas, la plus grande valeur possible est ±1.7976931348623157 × 10³⁰⁸ et la plus petite est ±2.2250738585072014 × 10⁻³⁰⁸, du moins sur la plupart des plateformes.

Lorsque vous travaillez avec des nombres en virgule flottante, vous pouvez affecter leur valeur à des variables de différentes manières. Les deux méthodes les plus courantes consistent à saisir directement le nombre, ou à utiliser la notation scientifique. Dans ce dernier cas, un e sépare le nombre de son exposant. La Figure 4.3 illustre ces deux techniques. Remarquez qu’un exposant négatif se traduit par une valeur fractionnaire.

[picture]

Figure 4.3 Les valeurs en virgule flottante peuvent être affectées de plusieurs manières.

Nombres complexes

Peut-être avez-vous étudié les nombres complexes sur les bancs du lycée ou de l’université. Un nombre complexe est formé par l’association entre un nombre réel et un nombre imaginaire (https://www.mathsisfun.com/numbers/complex-numbers.html). Ces nombres étranges sont utilisés dans différents domaines :

• ingénierie électrique ;

• dynamique des fluides ;

• mécanique quantique ;

• graphismes sur ordinateur ;

• systèmes dynamiques.

Ils ont encore d’autres usages, mais cette liste vous donne une idée générale. Si vous n’êtes pas concerné par une de ces disciplines, il est vraiment peu probable que vous ayez un jour à les rencontrer. Cependant, Python est l’un des rares langages qui supportent en natif ces nombres complexes. Comme vous le découvrirez au fil de ce livre, Python est très doué pour tout ce qui est scientifique et technique.

En Python, la partie imaginaire d’un nombre complexe est suivie de la lettre j. Si vous voulez par exemple créer un nombre complexe dont la partie réelle est 3 et la partie imaginaire 4, vous pourrez saisir une affectation telle que celle-ci :

monComplexe = 3 + 4j

Pour voir uniquement la partie réelle, saisissez alors simplement print(monComplexe.real). De même, pour afficher la partie imaginaire, vous saisissez print(monComplexe.imag).

Mais pourquoi autant de types de données numériques ?

De nombreux programmeurs, y compris parmi les plus anciens, ont du mal à comprendre pourquoi il faut plusieurs types de données numériques. Après tout, les êtres humains n’ont pas besoin de tout cela. Pour mieux appréhender cette notion, vous avez besoin d’en savoir un peu plus sur la manière dont un ordinateur traite les nombres.

Un entier est enregistré simplement sous la forme de bits que l’ordinateur est capable d’interpréter directement. Par exemple, une valeur binaire égale à 100 se traduit par le chiffre 4 dans le système décimal. Mais, d’un autre côté, les nombres qui contiennent un point décimal (dans le langage des ordinateurs, ou une virgule décimale dans notre propre système) ont besoin d’être enregistrés d’une manière totalement différente. Souvenez-vous un peu de vos années d’école, et vous comprendrez mieux ce qui se passe. Un nombre décimal (ou en virgule flottante) doit être enregistré avec un signe plus ou moins (ce qui ne demande en fait qu’un bit), suivi de sa mantisse (la partie fractionnaire du nombre) et d’un exposant (une puissance de 2). Pour obtenir une valeur en virgule flottante, vous utilisez donc une équation du style :

Valeur = Bit de signe,
Mantisse * 2^exposant

À une certaine époque, les ordinateurs utilisaient tous des représentations différentes de la virgule flottante, mais ils emploient tous aujourd’hui la norme IEEE-754 (https://standards.ieee.org/ieee/754/6210/). Vous en apprendrez plus sur cette norme à https://www.geeksforgeeks.org/ieee-standard-754-floating-point-numbers/. Une explication complète du fonctionnement précis des nombres à virgule flottante sort du cadre de ce livre, mais vous pouvez lire une description assez compréhensible à https://www.cprogramming.com/tutorial/floating_point/understanding_floating_point_representation.html. Il n’y a rien de mieux pour vous aider à comprendre un concept que de tester des valeurs. Vous trouverez un convertisseur de nombres à virgule flottante vraiment intéressant à https://www.h-schmidt.net/FloatConverter/IEEE754.html, où vous pouvez cliquer sur les bits individuels (pour les désactiver ou les activer) et voir le nombre à virgule flottante qui en résulte.

Comme vous pouvez l’imaginer, les nombres à virgule flottante ont tendance à consommer plus d’espace en mémoire en raison de leur complexité. En outre, ils utilisent une zone entièrement différente du processeur (une zone qui fonctionne plus lentement que la partie utilisée pour les calculs des nombres entiers). Enfin, les nombres entiers sont précis, contrairement aux nombres à virgule flottante, qui ne peuvent pas représenter précisément certains nombres, si bien que vous obtenez une approximation à la place. En résumé, les décimales sont inévitables dans le monde réel, et c’est pourquoi vous avez besoin de nombres à virgule flottante. Les systèmes informatiques doivent faire de nombreux compromis, et celui-ci est inévitable.

Comprendre les valeurs booléennes

Cela peut paraître un peu étrange, mais les ordinateurs vous donnent toujours une réponse franche et directe. Un ordinateur ne vous dira jamais « Peut-être que oui, peut-être que non, va savoir » . Toute réponse que vous obtenez de sa part se décline en deux catégories : c’est vrai (true en anglais) ou c’est faux (false en anglais). True ou false, là est la question…

En fait, il existe toute une branche des mathématiques appelée algèbre booléenne (https://www.tutorialspoint.com/computer_logical_organization/boolean_algebra.htm). Elle doit son origine au mathématicien anglais George Boole (https://plato.stanford.edu/entries/boole/), un super geek de cette époque, grâce auquel, même s’il vivait au 19e siècle, les ordinateurs sont capables de prendre des décisions. Et, pour dire vrai, l’algèbre booléenne existe depuis 1854, donc bien avant l’invention des ordinateurs. Et si l’on dit souvent que l’invention de ces extraordinaires machines doit tout à Alan Turing (1912 – 1954), elles ne seraient rien sans George Boole. Comme quoi…

Lorsque vous utilisez une valeur booléenne dans Python, vous faites appel au type bool. Une variable de ce type ne peut contenir que deux valeurs : vrai (true) ou faux (false). Vous pouvez définir une valeur en utilisant un de ces deux mots-clés, ou créer une expression qui définit une idée logique valant ou bien true, ou bien false.

Par exemple, vous pourriez dire monBooleen = 1 > 2. Mais vous comprenez alors que 1 étant plus petit que 2, cette expression sera toujours fausse. Vous trouverez dans ce livre nombre d’exemples utilisant le type bool. Retenez donc pour l’instant uniquement ce concept sans chercher à aller plus loin.

Déterminer le type d’une variable

Vous aurez parfois besoin de déterminer le type d’une variable. Il peut s’agir d’un cas où vous n’êtes pas certain de la réponse, ou d’une situation dans laquelle vous avez récupéré le code de quelqu’un d’autre dont le code source n’est pas accessible ou insuffisamment documenté.

Dans tous les cas, vous pouvez utiliser la fonction type(). Si vous placez par exemple la valeur 5 dans la variable monInt (en saisissant monInt = 5), et que vous oubliiez plus tard de quoi il s’agit, vous pourrez retrouver cette information en saisissant type(monInt). La réponse sera <class ‘int’>, ce qui vous informera que la variable monInt contient une valeur int.

Comprendre les chaînes de caractères

De tous les types de données, les chaînes de caractères sont les plus faciles à comprendre par des êtres humains comme vous et moi, et parmi les plus difficiles à comprendre pour les ordinateurs. Si vous avez lu les précédents chapitres de ce livre, vous avez déjà fait connaissance avec ces chaînes de caractères (un texte quelconque, si vous préférez). Une chaîne de caractères est simplement un regroupement de caractères que vous placez entre des guillemets doubles ou simples. Par exemple, tout le code proposé dans le Chapitre 5 repose sur des chaînes de caractères. Ainsi, l’affectation maChaine = " Python est un grand langage " affecte une chaîne de caractères (Python est un grand langage) à une variable appelée maChaine.

De son côté, l’ordinateur n’a aucune notion de ce que sont des lettres (ou des chiffres, ou des signes de ponctuation, et ainsi de suite). Pour lui, seuls comptent les nombres en mémoire. Par exemple, le caractère A majuscule a un code numérique, soit 65 en valeur décimale. Pour en avoir le cœur net, saisissez print(ord("A")) et cliquez sur Exécutez la cellule pour voir la réponse qui est renvoyée. Elle va bien indiquer 65. Il est donc possible, au passage, de convertir une lettre quelconque en une valeur numérique grâce à cette fonction ord().

Même si l’ordinateur ne comprend rien aux chaînes de caractères, ces dernières sont très utiles dans pratiquement toutes les applications. Vous aurez donc aussi besoin d’en convertir certaines en valeurs numériques. Vous aurez pour cela besoin des fonctions int() et float(). Comme vous vous en doutez certainement, la première effectue une conversion vers une valeur entière, et la seconde vers une valeur en virgule flottante. Si vous saisissez par exemple monInt = int("123") suivi d’un clic sur Exécutez la cellule, vous allez créer une variable de type int qui contient la valeur 123. La Figure 4.4 illustre ce mécanisme.

[picture]

Figure 4.4 Convertir une chaîne en nombre est facile grâce aux fonctions int() et float().

Vous pouvez effectuer la conversion inverse en vous servant de la fonction str(). Si vous saisissez par exemple maChaine = str(1234.56) (et cliquez sur Exécuter la cellule), vous allez créer une chaîne de caractères contenant la valeur "1234.56" que vous assignez à la variable chaîne maChaine. La Figure 4.5 illustre cette technique. L’important est que vous pouvez alterner ainsi entre chaînes et nombres avec une grande facilité. Les chapitres ultérieurs vous en diront plus à ce sujet.

[picture]

Figure 4.5 Convertir des nombres en chaînes, c’est aussi possible.

Travailler avec des dates et des heures

Les dates et les heures sont des données couramment utilisées. Qu’il s’agisse d’un rendez-vous, d’un événement quelconque, d’un anniversaire ou tout simplement de se réveiller le matin, ce type de donnée rythme notre existence. Du fait de cette familiarité avec notre vie, il est utile, et même nécessaire, de savoir comment Python traite tout cela. Et, comme les ordinateurs ne connaissent que des nombres, les dates et les heures n’ont pas plus de sens pour eux que les chaînes de caractères.

Pour travailler avec des dates et des heures, vous devez opérer une tâche particulière dans Python. Celui-ci est un peu comme un ordinateur, et il ne sait pas de quoi on parle tant que vous n’avez pas importé (import) un module particulier appelé datetime. Mais voilà un sujet sur lequel nous reviendrons dans le Chapitre 5. Ne vous souciez donc pas pour l’instant des tenants et des aboutissants de cette fonction. Tout ce que vous voulez dans l’immédiat, c’est pouvoir faire quelque chose avec les dates et les heures.

Les ordinateurs contiennent des horloges. Mais celles-ci servent surtout aux humains qui utilisent l’ordinateur. Tournez le problème dans tous les sens, et vous arriverez à la même conclusion… Pour obtenir l’heure courante, vous pouvez saisir print(datetime.datetime.now()) suivi d’un clic sur Exécuter la cellule. Vous allez alors voir s’afficher des informations extrêmement précises sur le jour, l’heure et même des fractions de seconde (voir la Figure 4.6).

[picture]

Figure 4.6 Pour obtenir la date et l’heure courantes, utilisez la fonction now().

Certes, ces informations sont un peu dures à lire dans le format tel qu’il est affiché. Supposons donc que vous vouliez uniquement récupérer la date du jour courant dans un format lisible. Pour accomplir cette tâche, vous allez devoir combiner plusieurs choses qui ont été vues dans les sections précédentes. Saisissez print(str(datetime.datetime.now(). date())) et cliquez sur Exécuter la cellule. La Figure 4.7 illustre le résultat obtenu, tout en vous montrant que la réponse est affichée au format anglo-saxon (autrement dit, dans le sens année-mois-jour).

[picture]

Figure 4.7 La fonction str() permet d’afficher le jour courant dans un format plus lisible.

De la même manière, Python dispose aussi d’une fonction time() qui permet d’obtenir l’heure courante (https://docs.python.org/fr/3/library/time.html). Il est aussi possible de séparer toutes ces informations en faisant appel aux fonctions day (jour), month (mois), year (année), hour (heure), minute, second et microsecond, comme dans :

datetime.datetime.now().hour

Les chapitres qui suivent vous aideront à mieux comprendre comment utiliser ces fonctions de date et d’heure afin de permettre aux utilisateurs de vos applications de rester informés à chaque instant.

DANS CE CHAPITRE

Comprendre la manière dont Python voit les données.

•

Utiliser des opérateurs pour affecter, modifier et comparer des données.

•

Organiser le code à l’aide de fonctions.

•

Interagir avec l’utilisateur.

Chapitre 5

Gérer l’information

Que vous utilisiez l’un des termes information ou données pour faire référence aux contenus gérés par votre application, cela ne fait aucune différence. En fait, j’utilise ces mots de manière interchangeable dans ce livre. Mais, dans tous les cas, vous devez fournir certaines procédures pour travailler avec ces informations, ou ces données. Sinon, votre application ne servirait strictement à rien. Vous avez donc besoin de méthodes qui vous permettent d’affecter des valeurs à des variables, de modifier le contenu de ces variables afin d’atteindre des buts spécifiques, et de comparer ces contenus avec des résultats attendus.

Il est aussi essentiel de commencer à travailler avec des méthodes vous permettant d’écrire un code compréhensible. Certes, vous pourriez vous contenter d’écrire ce code comme une (très) longue procédure, mais essayer alors de s’y retrouver serait une tâche incroyablement ardue, et vous devriez en plus répéter certaines étapes devant être exécutées plusieurs fois. Les fonctions sont un des outils dont vous disposez pour « empaqueter » votre code afin qu’il soit plus facile à lire, à comprendre et à réutiliser.

Les applications ont également besoin d’interagir avec l’utilisateur. Bien sûr, ce n’est pas une totale obligation. Mais le cas contraire est rare. Pour être à même d’offrir à l’utilisateur un certain service, les applications doivent donc être capables de communiquer avec l’utilisateur pour savoir comment il veut gérer les données.

Vous n’avez pas à saisir manuellement le code source de ce chapitre. En fait, l’utilisation de la source téléchargeable est beaucoup plus facile. Vous trouverez ainsi le code source de ce chapitre dans le fichier Chapitre 07 Gérer l’information.ipynb. Consultez l’introduction pour savoir comment trouver ces fichiers source.

Contrôler la manière dont Python voit les données

Comme vous l’explique le Chapitre 4, toutes les données contenues dans votre ordinateur sont enregistrées sous la forme de 0 et de 1. L’ordinateur ne comprend pas les concepts de lettres, de données booléennes, de dates, d’heures, ou tout autre type d’information à l’exception des nombres. De plus, la capacité que possède l’ordinateur à travailler avec des nombres est à la fois inflexible et relativement simpliste. Lorsque vous manipulez une chaîne dans Python, vous dépendez de la manière dont Python traduit le concept de chaîne de caractères dans une forme compréhensible par l’ordinateur. La manière dont Python « voit » les données est différente de votre propre perception ou du mode de fonctionnement de l’ordinateur. Python fonctionne comme un intermédiaire chargé de rendre vos applications fonctionnelles.

Pour gérer les données dans une application, celle-ci doit contrôler la manière dont Python voit les données. Pour atteindre cet objectif, vous avez notamment à votre disposition les opérateurs, des méthodes telles que les fonctions, ainsi que des outils permettant d’interagir avec l’utilisateur. Toutes ces techniques reposent en partie sur la notion de comparaison. Déterminer ce qu’il faut faire ensuite signifie comprendre l’état actuel des données par comparaison avec un autre état.

Faire des comparaisons

Python emploie des opérateurs pour effectuer des comparaisons. Plus généralement, les opérateurs jouent un rôle majeur dans la manipulation des données. Pour en savoir plus à leur sujet, voyez plus loin dans ce chapitre la section « Travailler avec les opérateurs » . Et les chapitres qui suivent feront un usage extensif de ces opérateurs afin de créer des applications capables de prendre des décisions, d’effectuer des tâches répétitives et d’interagir avec l’utilisateur.

Dans certains cas, vous utiliserez des voies un peu détournées pour effectuer des comparaisons (la mesure stricte d’une valeur ou d’un résultat par rapport à un autre) dans une application. Par exemple, vous pouvez comparer la sortie produite par deux fonctions (voyez la section « Comparer les sorties de fonctions » , plus loin dans ce chapitre). Avec Python, il est possible d’effectuer des comparaisons selon différents niveaux afin de gérer vos données sans problème. Ces techniques masquent les détails de ces processus de manière à ce que vous puissiez vous concentrer sur l’essentiel : effectuer des comparaisons et définir les réactions qui en découlent. Les choix que vous effectuez parmi ces techniques affectent la manière dont Python voit les données, et déterminent également les sortes de choses que vous pouvez faire afin de gérer les données une fois ces comparaisons effectuées. Les applications ont besoin de comparaisons pour interagir correctement avec les données.

Comprendre comment les ordinateurs effectuent des comparaisons

Les ordinateurs sont incapables de comprendre ce que sont par exemple les fonctions, ou toute autre structure que vous créez avec Python. Toutes ces choses n’existent que pour votre propre bénéfice, pas celui de l’ordinateur. Cependant, les ordinateurs supportent directement le concept d’opérateur au niveau du processeur. De ce fait, la plupart des opérateurs de Python ont un corollaire direct avec une commande compréhensible par l’ordinateur. Par exemple, lorsque vous avez besoin de savoir si un nombre est plus grand qu’un autre, l’ordinateur est capable d’effectuer cette comparaison directement, en utilisant son processeur.

Certaines comparaisons ne sont pas directes. Les ordinateurs ne travaillent qu’avec des nombres. Si vous demandez à Python de comparer deux chaînes de caractères, ce qu’il fait en réalité, c’est de comparer la valeur numérique interne associée à chaque caractère de la chaîne. Par exemple, la lettre A est stockée dans la mémoire de l’ordinateur sous la forme d’une valeur numérique égale à 65 dans le système décimal. Par contre, le a minuscule a une autre valeur numérique, en l’occurrence 97. De ce fait, même si vous considérez que ABC et abc sont identiques dans le sens où elles sont formées des trois premières lettres de l’alphabet, l’ordinateur a un point de vue radicalement différent. Il vous dira que abc est plus grand que ABC, puisque 97 est plus grand que 65.

Travailler avec les opérateurs

Les opérateurs sont à la base du contrôle et de la gestion des données dans les applications. Vous utilisez des opérateurs pour définir comment un morceau de donnée est comparé avec un autre, ainsi que pour modifier l’information contenue dans une variable. Les opérateurs jouent donc un rôle essentiel dans tout calcul ainsi que dans l’affectation de données.

Lorsque vous utilisez un opérateur, vous devez fournir soit une variable, soit une expression. Vous savez déjà qu’une variable est une sorte de boîte de rangement servant à contenir des données. Une expression est une équation ou une formule qui fournit une description d’un concept mathématique. Dans la plupart des cas, le résultat renvoyé par l’évaluation d’une expression est une valeur booléenne (vraie ou fausse). Les sections qui suivent décrivent en détail les opérateurs.

Comprendre l’opérateur ternaire de Python

Un opérateur ternaire nécessite trois éléments. Python supporte un seul opérateur de ce type que vous pouvez utiliser pour déterminer la valeur vraie d’une expression. Cet opérateur prend la forme suivante :

ValeurVrai if Expression
else ValeurFaux

Si Expression est vraie, l’opérateur renvoie la valeur ValeurVrai. Sinon, il retourne ValeurFaux. Par exemple, si vous tapez :

«Bonjour» if True else «Au
revoir»

l’opérateur va retourner Bonjour. Et si vous tapez :

«Bonjour» if False else
«Au revoir»

il retournera Au revoir.

Cet opérateur est commode dans des situations où vous avez besoin de prendre rapidement une décision et que vous ne voulez pas écrire un tas de code pour y arriver.

L’un des avantages de Python, c’est qu’il offre normalement plusieurs manières d’arriver à un certain résultat. En l’occurrence, il offre une version plus courte de cet opérateur, sous la forme :

   (ValeurFaux, ValeurVrai)
   [Expression]
L’exemple  ci-dessus  pourrait
doncs’écrire:
   («Au revoir», «Bonjour»)
   [True]
ou encore
   («Au revoir», «Bonjour»)
   [False]
aveclesm êmesrésultats.

Simplement, la première forme est plus claire, et la seconde plus courte.

Définir les opérateurs

Un opérateur accepte une ou plusieurs entrées sous la forme de variables ou d’expressions, il effectue une certaine tâche (comme une comparaison ou une addition), et il renvoie une sortie en fonction du résultat obtenu. La classification des opérateurs se fait pour partie selon leur effet, et pour partie selon le nombre d’éléments qu’ils réclament. Par exemple, un opérateur unaire n’a besoin que d’une seule variable ou expression, tandis qu’un opérateur binaire en a besoin de deux. Python ne prend pas en charge les opérateurs supérieurs à trois, car cela serait vraiment ridicule (et peut-être déroutant).

Les éléments passés à un opérateur sont appelés des opérandes. Dans le cas d’un opérateur binaire, vous avez donc un opérande gauche (à gauche), et un opérande droit (à droite). Les opérateurs ternaires sont spéciaux, comme vous le découvrirez dans l’encadré « Comprendre l’opérateur ternaire de Python » , alors ne vous en préoccupez pas pour l’instant. Python offre plusieurs catégories d’opérateurs :

• unaire ;

• arithmétique ;

• relationnel ;

• logique ;

• au niveau du bit ;

• affectation ;

• membre ;

• identité.

Chacune de ces catégories réalise une tâche spécifique. Par exemple, les opérateurs arithmétiques effectuent des calculs mathématiques, tandis que les opérateurs relationnels servent aux comparaisons. Voyons donc cela plus en détail.

Unaire

Un opérateur unaire n’a besoin que d’une seule variable ou expression. On s’en sert souvent dans des processus de décision (par exemple pour déterminer si une chose n’est pas semblable à une autre). Le Tableau 5.1 décrit les opérateurs unaires.

Arithmétique

Les tâches mathématiques complexes exécutées par Python sont souvent basées sur des tâches mathématiques beaucoup plus simples, comme l’addition (voir le Tableau 5.2). Vous pouvez toujours créer vos propres bibliothèques de fonctions mathématiques en utilisant également ces opérateurs simples.

Tableau 5.1

Les opérateurs unaires de Python.

Opérateur | Description | Exemple
∼ | Inverse les bits d’un nombre (les 0 deviennent 1 et vice versa) | ∼4 renvoie comme valeur −5.
- | Inverse la valeur positive ou négative d’un nombre | − (−4) donne 4, et −4 donne −4.
+ | Renvoie simplement la valeur en entrée | +4 donne 4.

Tableau 5.2

Les opérateurs arithmétiques de Python.

Opérateur | Description | Exemple
+ | Ajoute deux valeurs | 5+2=7
− | Soustrait l’opérande droit de l’opérande gauche | 5–2=3
* | Multiplie deux valeurs | 5 * 2 = 10
/ | Divise l’opérande gauche par l’opérande droit | 5 / 2 = 2.5
% | Divise l’opérande gauche par l’opérande droit et retourne le reste (opérateur modulo) | 5%2=1
** | Calcule la valeur exponentielle de l’opérande gauche par l’opérande droit | 5 ** 2 = 25
// | Divise l’opérande gauche par l’opérande droit et retourne la partie entière | 5 // 2 = 2

Relationnel

Les opérateurs relationnels comparent une valeur avec une autre et vous disent si la relation fournie est vraie (voir le Tableau 5.3). Par exemple, 1 est plus petit que 2, mais 1 n’est jamais plus grand que 2.

Logique

Les opérateurs logiques combinent la valeur vraie ou fausse de variables ou d’expressions pour pouvoir déterminer la valeur vraie résultante (voir le Tableau 5.4). Ils servent à créer des expressions booléennes qui vous aident à savoir si une certaine tâche doit ou non être réalisée.

Tableau 5.3

Les opérateurs relationnels de Python.

Opérateur | Description | Exemple
== | Détermine si deux valeurs sont égales. Notez que cet opérateur contient deux signes d’égalité. L’oubli d’un de ces signes est une erreur courante, qui se traduit par une simple affectation. | 1 == 2 renvoie False (faux).
!= | Détermine si deux valeurs sont différentes. L’emploi par inadvertance d’un opérateur tel que <> provoque une erreur dans les versions actuelles de Python. | 1 ! = 2 renvoie True (vrai).
> | Vérifie que l’opérande gauche est plus grand que l’opérande droit | 1 > 2 renvoie False (faux).
< | Vérifie que l’opérande gauche est plus petit que l’opérande droit | 1 < 2 renvoie True (vrai).
>= | Vérifie que l’opérande gauche est supérieur ou égal à l’opérande droit | 1 >= 2 renvoie False (faux).
<= | Vérifie que l’opérande gauche est inférieur ou égal à l’opérande droit | 1 <= 2 renvoie True (vrai).

Tableau 5.4

Les opérateurs logiques de Python.

Opérateur | Description | Exemple

and (et)

Détermine si les deux opérandes sont vrais.

True and True renvoie True (vrai)

True and False renvoie False (faux)

False and True renvoie False (faux)

False and False renvoie False (faux)

or (ou)

Détermine si l’un des deux opérandes est vrai.

True and True renvoie True (vrai)

True and False renvoie True (vrai)

False and True renvoie True (vrai)

False and False renvoie False (faux)

not (non) | Inverse la valeur logique de l’opérande. | not False renvoie True (vrai)

Opérateurs au niveau du bit

Les opérateurs au niveau du bit interagissent avec les bits (0 ou 1) individuels d’un nombre (voir le Tableau 5.5). Par exemple, le chiffre 6 s’écrit 0b0110 dans le système binaire. Avec ce type d’opérateur, une valeur de 0 est comptée comme fausse (False), et une valeur de 1 comme vraie (True).

Tableau 5.5

Les opérateurs au niveau de bit de Python.

Opérateur | Description | Exemple
& (et) | Détermine si les bits individuels des deux opérateurs sont vrais, et renvoie 1 dans ce cas ou 0 sinon. | 0b1100 & 0b0110 = 0b0100
| (ou) | Détermine si les bits individuels des deux opérateurs sont ou non vrais, et renvoie 1 dans ce cas ou 0 sinon. | 0b1100 & 0b0110 = 0b1110
^ (ou exclusif) | Détermine si un seul des bits individuels des deux opérateurs est vrai, et renvoie 1 dans ce cas. Si les deux bits sont vrais ou tous les deux faux, le résultat est faux. | 0b1100 & 0b0110 = 0b1010
∼ (complément de un) | Calcule le complément à un de la valeur. | ∼0b1100 = −0b1101 ∼0b0110 = −0b0111
<< (décalage à gauche) | Décale vers la gauche les bits de l’opérande gauche selon le rang déterminé par l’opérande droit. Les bits entrants sont mis à zéro, et les bits sortants sont perdus. | 0b00110011 << 2 = 0b11001100
>> (décalage à droite) | Décale vers la droite les bits de l’opérande gauche selon le rang déterminé par l’opérande droit. Les bits entrants sont mis à zéro, et les bits sortants sont perdus. | 0b00110011 >> 2 = 0b00001100

Affectation

Les opérateurs d’affectation placent une valeur dans une variable, MaVar dont la valeur de départ est égale à 5 (voir le Tableau 5.6). Nous avons déjà vu le cas le plus simple (=), mais Python offre bien d’autres opérateurs (composés) de ce type, afin notamment de réaliser une opération arithmétique en même temps que l’affectation.

Tableau 5.6

Les opérateurs d’affectation de Python.

Opérateur | Description | Exemple
= | Affecte la valeur de l’opérande droit à l’opérande gauche. | MaVar = 2 affecte la valeur 2 à la variable MaVar.
+= | Ajoute la valeur de l’opérande droit à l’opérande gauche et place le résultat dans l’opérande gauche. | MaVar += 2 affecte la valeur 7 à MaVar.
−= | Soustrait la valeur de l’opérande droit de l’opérande gauche et place le résultat dans l’opérande gauche. | MaVar −= 2 affecte la valeur 3 à MaVar.
*= | Multiplie la valeur de l’opérande droit par celle de l’opérande gauche et place le résultat dans l’opérande gauche. | MaVar *= 2 affecte la valeur 7 à MaVar.
/= | Divise la valeur de l’opérande gauche par celle de l’opérande droit et place le résultat dans l’opérande gauche. | MaVar /= 2 affecte la valeur 2.5 à MaVar.
/= | Divise la valeur de l’opérande gauche par celle de l’opérande droit et place le reste dans l’opérande gauche. | MaVar %= 2 affecte la valeur 1 à MaVar.
**= | Calcule la valeur exponentielle de l’opérande gauche par celle de l’opérande droit et place le résultat dans l’opérande gauche. | MaVar **= 2 affecte la valeur 25 à MaVar.
//= | Divise la valeur de l’opérande gauche par celle de l’opérande droit et place le résultat entier dans l’opérande gauche. | MaVar //= 2 affecte la valeur 2 à MaVar.

:=

Cet opérateur, une nouveauté de la version 3.8, permet d’affecter des valeurs aux variables dans le cadre d’une expression plus large. O n le surnomme également opérateur de morse, du fait de l’apparence du signe. Il s’utilise en particulier dans des structures de type if ou while afin de raccourcir le bloc de code correspondant

Voir à ce sujet les Chapitres 7 et 8.

Membre

Ces opérateurs détectent l’apparence d’une valeur dans une liste ou une séquence, et retournent la valeur vraie de cette apparence (voir le Tableau 5.7). Par exemple, vous entrez une valeur dont vous pensez qu’elle devrait se trouver dans une base de données, et la routine de recherche vous indique si cette valeur existe ou non dans la base.

Tableau 5.7

Les opérateurs "membre de" de Python.

Opérateur | Description | Exemple
in | Détermine si la valeur de l’opérande gauche apparaît dans la séquence fournie par l’opérande droit. | "Bonjour" in "Bonjour et au revoir" est vrai (True)
not in | Détermine si la valeur de l’opérande gauche manque dans la séquence fournie par l’opérande droit. | "Bonjour" not in "Bonjour et au revoir" est faux (False)

Identité

Ces opérateurs déterminent si une valeur ou une expression est d’un certain type ou classe (voir le Tableau 5.8). Ils permettent de s’assurer qu’une information est bien du type que vous pensez qu’elle possède.

Tableau 5.8

Les opérateurs d’identité de Python.

Opérateur | Description | Exemple
is | Renvoie vrai ou faux selon que les deux opérandes sont ou non de même type | type(2) is int renvoie vrai (True)
is not | Renvoie vrai ou faux selon que les deux opérandes ne sont pas ou sont de même type | type(2) is not int renvoie faux (False)

Comprendre l’ordre de priorité des opérateurs

Lorsque vous créez des instructions simples ne contenant qu’un seul opérateur, la structure de cet opérateur détermine directement la sortie. En revanche, lorsque vous commencez à travailler avec de multiples opérateurs, il devient nécessaire de savoir lequel est évalué en premier. Par exemple, l’expression 1 + 2 * 3 peut renvoyer la valeur 7 (si la multiplication est effectuée en premier) ou 9 (si l’addition est effectuée en premier). L’ordre de priorité des opérateurs vous indique que la bonne réponse est 7, à moins d’utiliser des parenthèses pour modifier l’ordre par défaut. Ainsi, (1 + 2) * 3 donnera comme résultat 9, car les parenthèses ont un ordre de priorité plus élevé que celui de la multiplication. Le Tableau 5.9 décrit l’ordre de priorité des opérateurs sous Python.

Tableau 5.9

Ordre de priorité des opérateurs de Python.

Opérateur | Description
() | Les parenthèses regroupent des expressions afin de modifier l’ordre des priorités des opérateurs qu’elles contiennent, afin par exemple d’évaluer une addition avant une multiplication.
** | Exponentiation (opérande gauche puissance opérande droit).
∼+− | Les opérateurs unaires interagissent avec une seule variable ou expression.
* / % // | Multiplication, division, modulo et reste de la division.
>> << | Décalage droit et gauche des bits.
& | Opérateur logique ET au niveau des bits.
^| | Opérateur logique OU au niveau des bits ou standard.
<= < > >= | Opérateurs de comparaison.
= %= /= //= −= += *= **= | Opérateurs d’affectation.
is, is not | Opérateurs d’identité
in, not in | Opérateurs de membre.
Not or and | Opérateurs logiques.

Créer et utiliser des fonctions

Afin de gérer correctement les informations, vous avez besoin d’organiser les outils utilisés pour effectuer les tâches demandées. Chacune des lignes de code que vous écrivez réalise une certaine tâche, et vous combinez ces lignes de code afin d’atteindre le résultat escompté. Parfois, vous devez répéter des instructions avec différentes données, ce qui risque au bout du compte de rendre votre code si long qu’il devient presque impossible de déterminer ce que fait chaque partie. Les fonctions servent précisément à organiser les choses pour que votre code reste propre et rangé à la perfection. De plus, les fonctions permettent de réutiliser les instructions qu’elles contiennent autant de fois qu’il est nécessaire avec des données différentes. Ces notions sont développées dans les sections qui suivent.

Voir les fonctions comme des boîtes de rangement pour le code

Vous vous dirigez vers votre placard, vous ouvrez la porte, et tout dégringole. C’est une véritable avalanche qui manque de vous emporter. La boule de bowling qui se trouvait sur l’étagère du haut aurait pu provoquer de sérieux dommages ! Cependant, vous disposez d’une batterie de boîtes de rangement, et vous avez tôt fait d’y mettre les objets de l’armoire dans un ordre précis. Quand vous avez terminé, vous pouvez ensuite retrouver tout ce dont vous avez besoin rapidement et sans prendre des risques inutiles. Les fonctions jouent exactement le même rôle que ces boîtes : elles permettent de regrouper proprement des éléments de code, et d’en faciliter ensuite la lecture, la compréhension ainsi que l’exécution.

Comprendre la réutilisabilité du code

Revenons à notre armoire. Vous y rangez par exemple vos vêtements. Quand vous en avez besoin, vous les sortez et vous les mettez. Quand ils sont sales, vous les lavez, puis vous les remettez en place lorsqu’ils sont secs. Bref, vos vêtements sont réutilisables (enfin, du moins, généralement…). Et il en va de même pour les fonctions. Personne n’a envie de répéter encore et encore la même tâche. Cela deviendrait vite fastidieux et ennuyeux. Lorsque vous créez une fonction, vous définissez quelque chose comme une « boîte » de code que vous pouvez ensuite utiliser chaque fois que vous devez répéter la même tâche. Il suffit pour cela d’indiquer qu’elle doit faire appel à votre fonction, et la manière dont cet appel doit être réalisé.

Le code qui a besoin des services d’une fonction est souvent décrit comme l’appelant. L’appelant doit fournir des informations à la fonction (qui est donc appelée et qui agit comme une sorte de boîte noire), et celle-ci retourne également des informations à l’appelant.

Vous pouvez créer des fonctions pour faire tout ce que vous voulez. La réutilisabilité du code est pratiquement toujours une nécessité, notamment pour :

• Réduire le temps de développement

• Réduire les erreurs de programmation

• Améliorer la fiabilité des applications

• Permettre à d’autres personnes de profiter du travail d’un programmeur

• Rendre le code plus facile à comprendre

• Améliorer l’efficacité des applications

Définir une fonction

Créer une fonction ne demande pas beaucoup de travail. En voici la preuve :

❶ Créez un nouveau notebook dans Notebook.

Le livre utilise le nom de fichier Chapitre 07 Gérer l’information.ipynb, où vous trouverez tout le code source de ce chapitre. Consultez l’introduction pour obtenir des informations sur l’utilisation de la source téléchargeable.

❷ Saisissez def Hello() : et appuyez sur Entrée.

Cette étape demande à Python de définir une fonction appelée Hello. Les parenthèses sont importantes, car elles servent à définir les informations dont la fonction a besoin (même si elles restent vides dans ce cas). Les deux-points qui suivent le nom de la fonction indiquent à Python que vous avez fini de définir la manière dont il est possible d’accéder à la fonction. Remarquez que le point d’insertion est maintenant indenté, comme cela est illustré sur la Figure 5.1. Cette indentation est là pour vous rappeler que vous devez ensuite expliquer à la fonction ce qu’elle a à faire.

[picture]

Figure 5.1 Définissez le nom de votre fonction.

❸ Saisissez print("Ceci est ma première fonction Python ! ") et appuyez sur Entrée.

Vous devriez remarquer quelque chose de spécial. Le pointeur d’insertion est toujours en retrait car Notebook attend que vous fournissiez l’étape suivante de la fonction.

❹ Cliquez sur Exécuter la cellule.

La fonction est maintenant terminée.

Même si cet exemple est très simple, il illustre la trame générale de création d’une fonction sous Python. Vous commencez par le mot-clé def, vous définissez ensuite le nom de la fonction, vous fournissez entre parenthèses les éléments nécessaires à son exécution (aucun dans ce cas), et vous spécifiez ensuite les étapes à accomplir. Une fonction se termine lorsque le point d’insertion se trouve à gauche ou que vous passez à la cellule suivante.

Accéder aux fonctions

Une fois que vous avez défini une fonction, vous allez bien entendu vouloir l’utiliser, et donc y accéder. Dans la précédente section, vous avez créé une fonction appelée Hello(). Pour y faire appel, il vous suffit de taper Hello() et de cliquer sur Exécuter la cellule. La Figure 5.2 illustre le résultat de l’exécution de cette fonction.

[picture]

Figure 5.2 Chaque fois que vous saisissez le nom d’une fonction, elle retourne le résultat pour lequel elle est programmée.

Chaque fonction que vous créez offre un modèle d’utilisation similaire. Vous saisissez le nom de la fonction, une parenthèse ouvrante, toute entrée requise, et une parenthèse fermante ; puis vous cliquez sur Exécuter la cellule. Dans cet exemple, vous n’avez pas d’entrée si bien que vous saisissez simplement Hello().

Transmettre des informations aux fonctions

L’exemple Hello() de la section précédente est intéressant, car vous n’avez pas besoin de saisir cette longue chaîne de caractères à chaque fois que vous voulez exécuter le code de la fonction. Cependant, il est également assez limité, car vous ne pouvez l’utiliser que pour dire une seule chose. Les fonctions doivent vous permettre de faire plusieurs choses, sinon vous finirez par écrire un grand nombre de fonctions qui varient en fonction des données qu’elles utilisent plutôt que de la fonctionnalité qu’elles fournissent. L’utilisation d’arguments vous aide à créer des fonctions qui sont flexibles et peuvent utiliser une multitude de données.

Comprendre les arguments

Le mot argument signifie que vous devez fournir des informations à la fonction pour que celle-ci puisse les traiter. Même si ce nom, argument, n’est pas nécessairement idéal, ce concept est relativement évident. Un argument rend possible la transmission de données à la fonction afin qu’elle puisse réaliser certaines tâches avec elles. Ceci augmente donc la souplesse et la puissance de vos fonctions.

En l’état, la fonction Hello() est totalement rigide, puisqu’elle ne fait qu’afficher encore et toujours la même chaîne de caractères. Ajouter un argument à cette fonction la rendra plus flexible, puisqu’elle pourra alors afficher un message quelconque. Pour cela, ouvrez une nouvelle fenêtre de Python en mode Shell (ou ouvrez le fichier Arguments01. py dans le dossier des exemples de ce chapitre). Définissez ainsi une variante appelée Hello2() :

def Hello2(Salutation):
    print(Salutation)

Remarquez que les parenthèses ne sont plus vides. Elles contiennent un mot, Salutation, qui est l’argument de la fonction Hello2(). Il s’agit en fait d’une variable que vous pouvez passer à la fonction print() de manière à afficher un message à l’écran.

Assurez-vous de cliquer sur Exécuter la cellule après avoir créé Hello2() ou vous obtiendrez un message NameError lorsque vous essaierez d’utiliser la fonction dans la section suivante.

Transmettre les arguments requis

Vous avez maintenant une nouvelle fonction appelée Hello2(). Elle nécessite cette fois le passage d’un argument. Pour le vérifier, saisissez simplement Hello2() et cliquez sur Exécuter la cellule. Vous allez voir s’afficher un message d’erreur qui vous rappelle tout simplement que la fonction réclame un argument (voir la Figure 5.3).

Non seulement Python vous indique que l’argument est manquant, mais il vous indique également le nom de l’argument. Créer une fonction comme vous l’avez fait jusqu’à présent signifie que vous devez fournir un argument. Saisissez maintenant par exemple Hello2("Ceci est une fonction intéressante") et cliquez sur Exécuter la cellule (notez que si vous utilisez Jupyter Notebook, vous passez automatiquement à la cellule suivante après avoir cliqué sur Run Cell). Cette fois, vous voyez bien le texte spécifié. Cependant, vous ne savez toujours pas si Hello2() est suffisamment flexible pour afficher plusieurs messages. Appuyez sur la touche Entrée (si nécessaire) pour commencer une autre ligne, et essayez ensuite avec Hello2("Un autre message…") et cliquez sur Exécuter la cellule. Vous voyez à nouveau la sortie attendue, ce qui signifie que Hello2() est effectivement une amélioration par rapport à Hello().

[picture]

Figure 5.3 Si vous ne fournissez pas d’argument, un message d’erreur s’affiche.

Mais la souplesse de la fonction Hello2() va au-delà de l’affichage de chaînes de caractères. Créez une nouvelle cellule (si nécessaire). Saisissez par exemple Hello2(1234) et cliquez sur Exécuter la cellule. Vous obtenez comme réponse 1234. Plus fort encore : saisissez Hello2(5+4) et cliquez sur Exécuter la cellule. La fonction vous donne alors le résultat de l’opération, 9.

Transmettre des arguments par nom

Progressivement, vos fonctions vont devenir de plus en plus complexes, de même que les méthodes permettant de les utiliser. Vous aurez donc besoin de davantage de contrôle pour préciser comment appeler une fonction et comment lui transmettre des arguments. Jusqu’ici, vous n’avez utilisé que des arguments positionnels, c’est-à-dire dans l’ordre où ils apparaissent dans la définition de la fonction. Cependant, Python permet aussi de passer des arguments par nom. Pour cela, vous fournissez le nom de l’argument, suivi d’un signe égal, et de la valeur à transmettre. Pour voir comment cela fonctionne, tapez la fonction suivante dans un notebook et exécutez-la :

def AddIt(Valeur1, Valeur2):
   print(Valeur1, " + ", Valeur2, " = ", (Valeur1 + Valeur2))

Notez que la fonction print() reçoit une liste d’arguments séparés par une virgule. De plus, ces arguments peuvent être de différents types, ce qui est une autre des facilités offertes par Python.

Pour tester cette fonction, commencez par saisir AddIt(2, 3) dans une nouvelle cellule et cliquez sur Exécuter la cellule. C’est un passage d’arguments par position. La réponse 2 + 3 = 5 va s’afficher. Saisissez ensuite AddIt(Valeur2 = 3, Valeur1 = 2) et cliquez sur Exécuter la cellule. Cette fois, il s’agit d’un passage d’arguments par nom. Vous recevez encore la même réponse, même si la position des deux arguments a été inversée.

Donner aux arguments une valeur par défaut

Quelle que soit la méthode de passage utilisée (par position ou par nom), les fonctions précédentes nécessitent la fourniture d’une ou plusieurs valeurs. Cependant, une fonction peut parfois utiliser une valeur par défaut lorsqu’une valeur simple ou courante est disponible. Ceci permet de simplifier l’utilisation des fonctions et de limiter les risques d’erreurs. Pour définir une valeur par défaut, il suffit de faire suivre le nom de l’argument d’un signe égal et de ladite valeur.

Pour voir comment cela fonctionne, saisissez la fonction suivante dans une nouvelle cellule d’un notebook et cliquez sur Exécuter la cellule :

def Hello3(Salutation = "Aucune valeur fournie"):
    print(Salutation)

C’est une autre version des fonctions Hello() et Hello2(), mais qui compense cette fois automatiquement les oublis qui pourraient se produire. Si quelqu’un essaie d’exécuter Hello3() sans passer d’argument, cette fonction ne provoque pas d’erreur. Au lieu de cela, elle affiche le message par défaut. Pour le vérifier, saisissez Hello3() et cliquez sur Exécuter la cellule. Tapez ensuite quelque chose comme Hello3("Ceci est une chaîne") et cliquez sur Exécuter la cellule pour vérifier le comportement normal de la fonction. Vous pouvez également essayer les variations Hello3(5) ou encore Hello3(2+7) pour constater qu’elle affiche bien la valeur ou le résultat du calcul. La Figure 5.4 illustre ces différents tests.

Créer des fonctions avec un nombre variable d’arguments

Dans la plupart des cas, vous connaissez exactement le nombre d’arguments dont votre fonction a besoin. En fait, c’est l’objectif que vous devriez vous fixer, car les fonctions ayant un nombre déterminé d’arguments sont plus faciles à corriger plus tard. Cependant, cela n’est quand même pas toujours possible. Supposons par exemple que vous deviez créer une application qui fonctionne en mode ligne de commandes. Dans ce cas, l’utilisateur pourrait fort bien ne fournir aucun argument, le nombre maximum des arguments possibles, ou encore toute combinaison intermédiaire d’arguments.

[picture]

Figure 5.4 Fournir des arguments par défaut lorsque c’est possible rend votre fonction plus facile à utiliser.

Heureusement, Python fournit une technique permettant de transmettre un nombre variable d’arguments à une fonction. Il vous suffit pour cela de placer un astérisque (*) devant le nom de l’argument, comme dans *VarArgs. La méthode habituelle consiste à spécifier un second argument qui contient le nombre d’arguments passés à la fonction. Toutefois, vous pouvez éliminer le deuxième argument et utiliser simplement la fonction len() pour obtenir le nombre d’arguments transmis à la fonction. Voici un exemple d’une fonction qui peut afficher un nombre variable d’éléments (ne vous inquiétez pas trop si vous ne comprenez pas tout maintenant – vous n’avez pas encore appris certaines de ces techniques).

def Hello4(ArgCount, *VarArgs):
   print("Vous avez passé ", ArgCount, " argument(s).")
   for Arg in VarArgs:
      print(Arg)

Cet exemple utilise quelque chose que l’on appelle une boucle for (décrite au Chapitre 5). Pour l’instant, il vous suffit de savoir que cette fonction récupère un argument à la fois depuis VarArgs, en plaçant chaque élément dans la variable Arg, et en affichant tout simplement la valeur de ces arguments via la fonction print(). Après avoir saisi le code, cliquez sur Exécuter la cellule.

Dans une nouvelle cellule, saisissez par exemple Hello4(1, "Chaîne test") et cliquez sur Exécuter la cellule. Vous devriez voir s’afficher le nombre d’arguments ainsi que la chaîne de test. Ressayez maintenant avec Hello4(3, "Un", "Deux", "Trois") et cliquez sur Exécuter la cellule. Comme l’illustre la Figure 5.5, la fonction gère le nombre variable d’arguments sans aucun problème.

[picture]

Figure 5.5 Les fonctions avec un nombre variable d’arguments peuvent rendre vos applications plus souples.

Renvoyer des informations depuis une fonction

Les fonctions peuvent afficher directement des résultats, ou bien encore retourner des données à l’application de manière à ce que la procédure appelante puisse les traiter d’une façon ou d’une autre. En fait, les deux sont possibles simultanément, mais c’est une situation moins courante.

La manière dont ces fonctions font leur travail dépend du type de tâche qu’elles sont supposées effectuer. Par exemple, une fonction chargée de réaliser des calculs mathématiques est plus susceptible de renvoyer des données à la procédure appelante qu’une autre.

Pour renvoyer des données, une fonction doit contenir le mot-clé return, suivi des informations à retourner. Il n’y a pas de limites quant aux données retournées à la procédure appelante. Voici certains types courants dans de tels cas :

• Valeurs : toute valeur est acceptable. Il peut s’agir de nombres, de chaînes de caractères, ou encore de valeurs booléennes.

• Variables : une variable quelconque se comporte comme une valeur directe. L’appelant reçoit ce que contient cette variable.

• Expressions : de nombreux développeurs utilisent des expressions en tant que raccourcis. Par exemple, vous pouvez parfaitement renvoyer A + B plutôt que d’effectuer l’opération, placer le résultat dans une variable, puis retourner cette dernière à la procédure appelante. Utiliser une expression est plus rapide et accomplit le même travail.

• Résultats d’autres fonctions : vous pouvez aussi renvoyer le résultat produit par d’autres fonctions.

Il est temps de voir comment fonctionnent les valeurs de retour. Saisissez le code suivant dans un notebook et cliquez sur Exécuter la cellule :

def DoAdd(Valeur1, Valeur2):
   return Valeur1 + Valeur2

Cette fonction prend deux valeurs en entrée, et retourne leur somme. Certes, cette tâche pourrait être effectuée sans faire appel à une fonction, mais c’est aussi le point de départ de nombreuses fonctions. Pour la tester, saisissez print("La somme de 3 + 4 est", DoAdd(3, 4)) dans une nouvelle cellule et cliquez sur Exécuter la cellule. Vous voyez La somme de 3 + 4 est 7.

Comparer les sorties de fonctions

Vous pouvez utiliser des fonctions qui retournent des données de multiples manières. Par exemple, la section précédente de ce chapitre vous montre l’emploi d’une fonction pour fournir une entrée dans une autre fonction. L’une des applications possibles de ce mécanisme est la comparaison entre données, afin notamment de créer des expressions définissant une valeur logique.

Pour mieux voir comment tout cela s’articule, vous allez utiliser la fonction DoAdd() de la section précédente. Saisissez print("3 + 4 égale 2 + 5 est", (DoAdd(3, 4) == DoAdd(2, 5))) et cliquez sur Exécuter la cellule. Vous devriez voir s’afficher la valeur True (vrai), comme l’illustre la Figure 5.6.

[picture]

Figure 5.6 Les fonctions permettent d’effectuer toutes sortes de tâches.

Récupérer la saisie de l’utilisateur

Très peu d’applications vivent uniquement dans leur propre monde, autrement dit sans interaction avec l’utilisateur. En fait, c’est cette interaction qui est essentielle, notamment du fait que les ordinateurs (ou les smartphones et les tablettes qui en sont les descendants) sont conçus pour servir les besoins des utilisateurs. Pour cela, une application doit fournir à l’utilisateur des moyens lui permettant de communiquer avec elle. En fait, la technique la plus courante est relativement facile à mettre en œuvre. Vous avez simplement besoin de faire appel à la fonction input() pour obtenir ce résultat.

La fonction input() retourne toujours une chaîne de caractères, même si l’utilisateur saisit un nombre. Ceci signifie que, dans le cas où vous attendez effectivement une valeur numérique, vous devrez convertir la chaîne renvoyée pour obtenir le résultat escompté. Cette fonction vous permet également de définir un message demandant une réponse à l’utilisateur, ce qui permet de préciser ce que l’application attend de lui. L’exemple suivant montre comment utiliser la fonction input() de manière simple :

Nom = input("Quel est votre nom : ")
print("Bonjour ", Nom)

Dans cet exemple, la fonction input() demande un nom à l’utilisateur. Une fois que l’utilisateur a saisi son nom et appuyé sur Entrée, l’exemple lui envoie un message d’accueil personnalisé. Essayez d’exécuter cet exemple. La Figure 5.7 illustre des résultats typiques lorsque vous saisissez John comme nom d’utilisateur.

[picture]

Figure 5.7 Vous donnez un nom d’utilisateur, et l’application vous salue.

Vous pouvez utiliser input() pour obtenir d’autres types de données. Tout ce dont vous avez ensuite besoin, c’est de la bonne fonction de conversion. Le code suivant propose un exemple de cette technique afin de demander une entrée numérique :

ANumber = float(input("Tapez un nombre : "))
print("Vous avez tapé : ", ANumber)

Quand vous exécutez cet exemple, l’application vous demande une entrée numérique. L’appel à la fonction float() convertit la saisie en nombre. Il suffit ensuite d’appeler print() pour afficher le résultat. Lorsque vous exécutez l’exemple en utilisant une valeur telle que 5.5, vous obtenez le résultat souhaité.

Il est important de comprendre que la conversion de données n’est pas sans risques. Si vous essayez de taper autre chose qu’un nombre dans cet exemple, Python va renvoyer un message d’erreur, comme cela est illustré à la Figure 5.8. Le Chapitre 4 vous aidera à comprendre comment détecter et réparer de telles erreurs avant qu’elles ne fassent planter le programme.

[picture]

Figure 5.8 Une conversion de donnée change le type de la saisie utilisateur en fonction de vos besoins, mais elle peut aussi provoquer des erreurs.

DANS CE CHAPITRE

Utiliser l’instruction if pour prendre une décision simple.

•

Prendre des décisions plus complexes avec l’instruction if...else.

•

Créer des niveaux de décision multiples en imbriquant les instructions.

Chapitre 6

Prendre des décisions

La possibilité de prendre des décisions pour suivre un chemin ou bien un autre est quelque chose d’essentiel dans toute activité. Les mathématiques donnent à l’ordinateur la capacité d’obtenir des informations utiles. Et ces informations permettent ensuite d’effectuer telle ou telle action. Sans cela, un ordinateur ne servirait strictement à rien. Tout langage de programmation doit offrir des fonctionnalités pour pouvoir prendre des décisions d’une certaine manière. Ce chapitre explore les techniques que Python utilise dans ce domaine.

Réfléchissez au processus que vous suivez lorsque vous prenez des décisions. Vous obtenez la valeur de quelque chose, vous la comparez avec une valeur attendue, et vous agissez en conséquence. Prenons un exemple : vous voyez un signal lumineux et vous voyez qu’il est rouge. Vous le comparez au signal que vous attendez, le vert. Vous constatez donc que le signal n’est pas au vert. Action : vous vous arrêtez au feu. La plupart des gens ne prennent pas le temps de réfléchir à ce genre de processus, tout simplement, car c’est devenu un automatisme. La plupart des décisions quotidiennes se prennent naturellement, mais les ordinateurs, eux, doivent effectuer à chaque fois les tâches suivantes :

❶ Obtenir la valeur réelle ou actuelle de quelque chose.

❷ Comparer cette valeur réelle ou actuelle avec une valeur attendue.

❸ Effectuer l’action voulue en fonction du résultat de la comparaison.

Vous n’avez pas à saisir manuellement le code source de ce chapitre. En fait, l’utilisation de la source téléchargeable est beaucoup plus facile. Vous trouverez ainsi le code source de ce chapitre dans le fichier Chapitre 08 Prendre des décisions.ipynb. Consultez l’introduction pour savoir comment trouver ces fichiers source.

Prendre des décisions simples avec l’instruction if

Dans Python, l’instruction if fournit la méthode la plus simple pour prendre des décisions. Si cette instruction constate que quelque chose est vrai, cela signifie que Python devrait exécuter les étapes qui suivent. Les prochaines sections vous expliquent comment utiliser l’instruction if pour prendre différentes décisions. Vous pourriez être surpris de découvrir tout ce que ces deux petites lettres peuvent faire pour vous.

Comprendre l’instruction if

Sans le savoir, vous utilisez couramment des instructions if dans la vie de tous les jours. Vous vous dites par exemple : « Si c’est vendredi, je vais aller nager à la piscine. » L’instruction if de Python est moins bavarde, mais elle suit le même schéma. Supposons que vous créez une variable appelée TestMe et que vous y placez la valeur 6, comme ceci :

TestMe = 6

Vous demandez ensuite à l’ordinateur de comparer la valeur de TestMe à6:

if TestMe == 6:
  print("TestMe est égal à 6 !")

Cette séquence débute par le mot if (si). Lorsque Python voit ce mot, il sait que vous voulez prendre une décision. À sa suite vient une condition. Une condition indique tout simplement le type de comparaison que vous voulez demander à Python d’effectuer. Dans ce cas, il s’agit de déterminer si TestMe contient bien la valeur 6.

Remarquez que la condition utilise l’opérateur relationnel d’égalité, ==, et non l’opérateur =, qui définit une affectation. Oublier un des deux signes d’égalité est une erreur courante. Les opérateurs relationnels (et les autres) sont décrits dans le Chapitre 5.

La condition se termine toujours par un deux-points ( : ). Si vous l’oubliez, Python ne saura pas que la condition se termine, et il continuera à rechercher sa suite avant de prendre une décision. Une fois ce caractère entré, vous placez les tâches que Python devrait effectuer si la condition est vérifiée (vraie). Ici, vous demandez simplement à afficher un message qui confirme que TestMe est bien égal à 6.

Utiliser l’instruction if dans une application

Dans Python, il est possible d’utiliser l’instruction if de diverses manières. Cependant, vous avez immédiatement besoin d’en connaître trois :

• Utiliser une condition unique pour exécuter une seule instruction quand cette condition est vraie.

• Utiliser une condition unique pour exécuter plusieurs instructions quand cette condition est vraie.

• Combiner plusieurs instructions en une seule et exécuter une ou plusieurs instructions que la condition combinée est vraie.

Les sections qui suivent explorent ces trois possibilités et fournissent des exemples d’utilisation. Vous en retrouverez bien d’autres dans ce livre, car if est une méthode importante dès lors qu’il s’agit de prendre des décisions.

Travailler avec les opérateurs relationnels

Un opérateur relationnel détermine la manière dont une valeur placée sur le côté gauche de cet opérateur se compare avec celle qui est placée du côté droit de l’expression. Une fois le résultat déterminé, il produit une réponse True ou False qui reflète la valeur vraie de l’expression. Par exemple, 6 == 6 est vrai, tandis que 5 == 6 est faux. Le Tableau 5.3 du Chapitre 5 décrit les opérateurs relationnels. Les étapes qui suivent montrent comment créer et utiliser une instruction if.

❶ Ouvrez un nouveau notebook.

Vous pouvez également utiliser le fichier source téléchargeable, Chapitre 08 Prendre des décisions.ipynb.

❷ Tapez TestMe = 6 et appuyez sur Entrée.

Cette étape assigne la valeur 6 à la variable TestMe. Remarquez qu’elle utilise l’opérateur d’affectation (=), et non l’opérateur d’égalité (==).

❸ Tapez if TestMe == 6 : et appuyez sur Entrée.

Cette étape crée une instruction if qui teste la valeur de TestMe en utilisant l’opérateur d’égalité. À ce stade, vous pouvez noter deux choses dans Notebook :

• Le mot if est affiché dans une couleur différente de celle du reste de l’instruction.

• La ligne suivante est automatiquement indentée.

❹ Tapez print("TestMe est égal à 6 ! ").

Notez que le mot print apparaît dans une couleur particulière, car il s’agit d’un nom de fonction. La chaîne de caractères possède également son propre mode de couleur. Ce mode d’affichage rend plus facile la lecture du code Python.

❺ Cliquez sur Exécuter la cellule.

Notebook exécute l’instruction if, comme cela est illustré à la Figure 6.1. Comme TestMe contient la valeur 6, l’instruction if fonctionne comme prévu.

[picture]

Figure 6.1 Des instructions if simples peuvent aider vos applications à savoir ce qu’elles doivent faire dans certaines conditions.

Effectuer des tâches multiples

Après avoir pris une décision, vous avez très souvent non pas une, mais plusieurs tâches à accomplir. Python se base sur l’indentation pour déterminer le moment où il doit arrêter d’exécuter les tâches qui sont associées à l’instruction if. Tant que le texte est indenté, cela signifie qu’il fait partie de l’instruction if. Dès que cette indentation se termine, la ligne qui suit devient la première qui est extérieure à cette instruction if. Un bloc de code consiste en une instruction plus les tâches associées à celle-ci. Cette notion est extrêmement générale, mais, en l’occurrence, elle s’applique à une instruction if qui fait partie d’un bloc de code.

❶ Tapez le code suivant dans une nouvelle cellule de notebook, en appuyant sur Entrée après chaque ligne :

TestMe = 6
if TestMe == 6:
   print("TestMe est égal à 6 !")
   print("Terminé !")

Notez que Python continue à indenter les lignes tant que vous continuez à saisir du code. Toutes les lignes que vous tapez ainsi font partie du bloc de code associé à l’instruction if courante.

❷ Cliquez sur Exécuter la cellule.

Python va exécuter tout le bloc de code. La sortie est illustrée sur la Figure 6.2.

[picture]

Figure 6.2 Un bloc de code peut contenir de multiples lignes, une pour chaque tâche.

Effectuer des comparaisons multiples en utilisant des opérateurs logiques

Jusqu’ici, les exemples proposés n’utilisaient qu’une seule comparaison. Mais, dans la vraie vie, il est fréquent d’avoir à traiter une série de comparaisons pour répondre à divers besoins. Si vous faites par exemple cuire des sablés, si le minuteur est tombé à zéro et que les bords des gâteaux ont bruni, alors il est temps de les sortir du four.

Pour effectuer des comparaisons multiples, vous utilisez des opérateurs relationnels que vous combinez ensuite à l’aide d’opérateurs logiques (reportez-vous au Tableau 5.4 du Chapitre 5). Un opérateur logique décrit la manière de combiner des conditions. Par exemple, vous pourriez dire que x == 6 et y == 7 sont deux conditions permettant si elles sont vérifiées d’effectuer une ou plusieurs tâches. Le mot-clé et (and dans le langage de Python) est un opérateur logique qui demande à ce que les conditions soient vraies pour être validé.

L’un des usages les plus courants des comparaisons multiples consiste à déterminer si une valeur appartient à un certain intervalle, autrement dit se trouve entre deux valeurs. En fait, il s’agit de quelque chose d’important pour rendre vos applications à la fois plus sûres et mieux adaptées aux besoins des utilisateurs. Prenons un exemple. Il s’agit ici de créer un fichier vous permettant d’exécuter la même application de multiples fois.

❶ Tapez le code suivant dans une nouvelle cellule de notebook, en appuyant sur Entrée après chaque ligne :

 Value = int(input("Tapez un nombre entre 1 et 10: "))

if (Value > 0) and (Value <= 10):
  print("Vous avez tapé : ", Value)

La première ligne demande à l’utilisateur de taper un nombre entre 1 ET 10. Vous n’avez alors aucune idée de ce que l’utilisateur a réellement saisi, ou même s’il s’agit d’une valeur quelconque. L’emploi de la fonction int() signifie que l’utilisateur doit saisir un nombre entier (donc sans point décimal). Sinon, l’application va générer une exception (autrement dit, une indication d’erreur, sujet sur lequel nous reviendrons dans le Chapitre 6). Ce premier contrôle permet de vérifier si l’entrée possède le type demandé.

L’instruction if contient deux conditions. La première vérifie que la valeur est strictement supérieure à 0 (et donc supérieure ou égale à 1, s’agissant d’un entier). La seconde fait la même de l’autre côté de l’intervalle pour s’assurer que la valeur est inférieure ou égale à 10. Ce n’est que lorsque ces deux conditions sont réunies que if peut autoriser l’instruction suivante à afficher la valeur saisie par l’utilisateur.

❷ Cliquez sur Exécuter la cellule.

Python vous demande de taper un nombre entre 1 et 10.

❸ Tapez 5 et appuyez sur Entrée.

L’application détermine que cette valeur appartient à l’intervalle demandé, et elle affiche le message illustré à la Figure 6.3.

❹ Sélectionnez à nouveau la cellule et répétez les Étapes 2 et 3, mais saisissez cette fois 22 au lieu de 5.

L’application n’affiche rien, puisque le nombre n’est pas compris entre 1 et 10. Dans ce cas, la ou les instructions qui font partie du bloc if ne sont pas exécutées.

[picture]

Figure 6.3 L’application vérifie que la valeur saisie appartient au bon intervalle, et elle affiche dans ce cas un message.

❺ Sélectionnez à nouveau la cellule, puis répétez les Étapes 2 et 3, et tapez cette fois 5.5.

Python va afficher le message d’erreur illustré à la Figure 6.4. Bien entendu, vous pensez que la valeur 5.5 est comprise entre 1 et 10, et que 5.5 est bien une valeur numérique. Mais Python de son côté, sait que 5 est un nombre entier, et 5.5 un nombre décimal, et par conséquent non entier.

❻ Répétez encore une fois les Étapes 2 et 3, en tapant cette fois le mot Bonjour.

[picture]

Figure 6.4 Un mauvais type d’information se traduit par l’affichage d’un message d’erreur.

Python affiche le même message d’erreur que ci-dessus. Il ne veut même pas déterminer quel est le type erroné de l’entrée. Il sait seulement que ce type est incorrect, et donc inutilisable.

Les meilleures applications utilisent diverses sortes de tests d’intervalles pour s’assurer qu’elles se comportent d’une manière prévisible. Plus une application devient prévisible, moins l’utilisateur a besoin de penser à ce qu’on lui demande, et plus celui-ci peut passer de temps à travailler de manière réellement efficace. Essayez toujours de vous placer du point de vue de l’utilisateur, et non du point de vue du programmeur !

Le nouvel opérateur d’affectation de Python 3.8 (l’opérateur de morse) peut aussi aider à raccourcir des blocs de code if dans certaines situations où cela ne risque pas de rendre ceux-ci plus confus Par exemple, le test qui suit évite de faire appel deux fois à la fonction len(), qui retourne la longueur d’un objet :

if (n := len(objet)) > 10:
   print(f"La liste est trop longue:({n} éléments, <= 10 atten-
du)")

Choisir entre plusieurs alternatives avec l’instruction if…else

La plupart des décisions que vous avez à prendre dans une application consistent à choisir une ou deux options basées sur certaines conditions. Par exemple, face à un feu routier, vous avez deux options : appuyer sur le frein pour stopper, ou sur l’accélérateur pour continuer. Le bon choix dépend des conditions que vous pouvez observer, donc en l’occurrence de la couleur du feu. Avec Python, vous n’allez pas freiner, mais traiter des alternatives comparables.

Comprendre l’instruction if…else

Avec Python, vous choisissez une des alternatives possibles en utilisant la clause else de l’instruction if. Une clause est un ajout à un bloc de code qui modifie la manière dont celui-ci fonctionne. La plupart des blocs de code supportent de multiples clauses. Dans ce cas, la clause else vous permet d’effectuer une tâche alternative lorsque l’instruction if n’est pas vérifiée, ce qui accroît l’utilité de celle-ci. En règle générale, on fait référence à la clause else de l’instruction if en l’appelant l’instruction if…else, les trois points indiquant qu’il se passe quelque chose entre ces deux mots.

N’oubliez jamais que la clause else n’est exécutée que si l’instruction if n’est pas vérifiée. Il est important de bien réfléchir à ce qu’implique le fait d’exécuter certaines tâches lorsqu’une ou plusieurs conditions sont fausses. Ceci peut parfois conduire à des conséquences inattendues.

Utiliser l’instruction if…else dans une application

L’exemple de la section précédente est moins utile qu’il pourrait l’être dans le cas où l’utilisateur entre une valeur qui se trouve en dehors de l’intervalle demandé. Le fait de taper une donnée qui n’est pas du bon type provoque une erreur, mais saisir une valeur du bon type en dehors de l’intervalle spécifié ne fournit pas non plus une réponse très explicite. Dans l’exemple qui suit, vous allez découvrir un moyen de corriger ce problème en faisant appel à une clause else afin de fournir une alternative à l’instruction if si la condition qu’elle teste est fausse :

❶ Tapez le code suivant dans une nouvelle cellule de notebook, en appuyant sur Entrée après chaque ligne :

Value = int(input("Tapez un nombre entre 1 et 10 : "))

if (Value > 0) and (Value <= 10):
   print("Vous avez tapé : ", Value)
else:
   print("Vous avez tapé une valeur incorrecte !")

Comme dans l’exemple précédent, cette application demande une valeur à l’utilisateur, et elle détermine ensuite si cette entrée est un entier qui appartient à l’intervalle spécifié. Cependant, dans ce cas, la clause else fournit en réponse un message alternatif lorsque l’utilisateur a débordé de l’intervalle voulu.

Remarquez que la clause else se termine par un deux-points, exactement comme l’instruction if. Ceci permet à Python de savoir où la clause se termine. Nombre de messages d’erreur apparaissent tout simplement parce qu’on a oublié d’ajouter ce caractère. Pensez-y !

❷ Cliquez sur Exécuter la cellule.

Python vous demande de taper un nombre entre 1 et 10.

❸ Tapez 5 et appuyez sur Entrée.

L’application détermine que cette valeur appartient à l’intervalle demandé, et elle affiche le message illustré précédemment à la Figure 6.3.

❹ Répétez les Étapes 2 et 3, mais tapez maintenant 22 au lieu de 5.

Cette fois, l’application affiche le message illustré à la Figure 6.5. L’utilisateur sait maintenant qu’il a entré une valeur ne respectant pas l’intervalle indiqué. Il pourra donc relancer l’application pour corriger son erreur.

[picture]

Figure 6.5 Il est toujours bon de fournir une explication lorsque l’utilisateur a effectué une saisie incorrecte.

Utiliser l’instruction if…elif dans une application

Vous prenez votre petit-déjeuner à l’hôtel. Vous constatez en lisant le menu que vous pouvez avoir des œufs, des crêpes, des gaufres ou encore des céréales. Une fois que vous avez choisi ce que vous voulez dans le menu, le serveur vous amène votre commande. Créer un menu dans lequel l’utilisateur peut effectuer une sélection nécessite bien une instruction dans le genre if…else, mais avec un petit quelque chose en plus. Ce petit quelque chose s’appelle ici la clause elif. Celle-ci sert à créer un autre jeu de conditions (en fait, son nom est juste une contraction de else et de if).

Les étapes suivantes décrivent comment utiliser l’instruction if... elif pour créer un menu.

❶ Tapez le code suivant dans une nouvelle cellule de notebook, en appuyant sur Entrée après chaque ligne :

print("1. Rouge")
print("2. Orange")
print("3. Jaune")
print("4. Vert")
print("5. Bleu")
print("6. Pourpre")
Choice = int(input("Choisissez votre couleur favorite : "))

if (Choice == 1):
   print("Vous avez choisi Rouge !")
elif (Choice == 2):
   print("Vous avez choisi Orange !")
elif (Choice == 3):
   print("Vous avez choisi Jaune !")
elif (Choice == 4):
   print("Vous avez choisi Vert !")
elif (Choice == 5):
   print("Vous avez choisi Bleu !")
elif (Choice == 6):
   print("Vous avez choisi Pourpre !")
else:
   print("Vous avez fait un choix invalide !")

Cet exemple commence par afficher un menu. L’utilisateur voit une liste de choix, puis l’application lui demande de taper un nombre entier (avec l’utilisation de la fonction int()) qui est placé dans la variable Choice afin de déterminer sa couleur préférée.

Une fois que l’utilisateur a effectué son choix, l’application recherche dans la liste les valeurs potentielles. Dans chaque cas, la valeur de Choice est comparée avec une certaine réponse afin de créer une condition pour chaque situation. Si, par exemple, l’utilisateur tape 1, l’application indique que la valeur choisie est le rouge. Si aucune des options choisies n’est correcte, la clause else est exécutée par défaut.

❷ Cliquez sur Exécuter la cellule.

Python affiche un message et l’application vous demande de choisir votre couleur préférée.

❸ Tapez 1 et appuyez sur Entrée.

L’application affiche le message illustré à la Figure 6.6.

❹ Répétez les Étapes 2 et 3, mais tapez cette fois 5 au lieu de 1.

L’application affiche un message de sortie différent (celui qui est associé à la couleur demandée).

❺ Répétez les Étapes 2 et 3, mais tapez cette fois 8 au lieu de 1.

L’application vous dit que vous avez fait un choix invalide.

❻ Répétez les Étapes 2 et 3, mais tapez cette fois Rouge au lieu de 1.

[picture]

Figure 6.6 Les menus vous permettent de choisir une option dans une liste.

Vous n’obtenez alors qu’un message d’erreur (Figure 6.7). Toutes les applications que vous avez à créer devraient être capables de détecter les erreurs et les entrées incorrectes. Le Chapitre 6 vous montrera comment gérer ces erreurs.

Utiliser des décisions imbriquées

Le processus de prise décision implique souvent plusieurs niveaux. Revenons à notre exemple d’hôtel-restaurant. Vous choisissez des œufs pour le petit déjeuner, ce qui constitue votre premier niveau de décision. Mais, maintenant, le serveur vous demande avec quoi vous voulez vos œufs. Il ne vous demanderait rien si vous aviez par exemple opté pour des céréales. Dans le cas des œufs, vous devez donc prendre une décision à un second niveau. Et selon ce que vous choisissez maintenant, il pourrait y avoir un troisième niveau, ou encore une quatrième, et ainsi de suite. Voire aucun…

[picture]

Figure 6.7 Toutes vos applications devraient pouvoir détecter les erreurs de saisie.

Pas d’instruction switch ?

Si vous avez travaillé avec d’autres langages, vous avez peut-être remarqué que Python ne possède pas d’instruction switch. Les développeurs utilisent couramment l’instruction switch dans d’autres langages pour créer des applications basées sur des menus. L’instruction if...elif est généralement utilisée dans le même but en Python.

Cependant, l’instruction if... elif n’offre pas tout à fait la même fonctionnalité qu’une instruction switch, car elle n’impose pas l’utilisation d’une seule variable à des fins de comparaison. Par conséquent, certains développeurs font appel à la fonctionnalité de dictionnaire de Python pour remplacer l’instruction switch. Le Chapitre 6 décrit comment travailler avec les dictionnaires. Vous pouvez également utiliser une approche de classe pour remplacer l’instruction switch (https://favtutor.com/blogs/python-switch-case). La raison pour laquelle l’approche par classe n’est pas traitée dans ce livre est qu’elle est plus difficile à mettre en œuvre, à lire et à déboguer que l’approche par dictionnaire.

Ce type de processus, où un choix dépend d’un autre, est appelé une imbrication. Les développeurs se servent souvent de cette technique pour créer des applications capables de prendre des décisions complexes à partir de saisies utilisateur variées. Les sections qui suivent décrivent plusieurs types d’imbrications permettant de demander à Python de prendre des décisions complexes.

Utiliser des instructions if ou if…else multiples

La technique la plus courante consiste à utiliser des combinaisons d’instructions if et if…else. Cette forme est souvent appelée un arbre de sélection, du fait de ses ressemblances avec les branches d’un arbre. Dans ce cas, vous suivez un certain chemin pour obtenir le résultat escompté. Les étapes suivantes montrent comment créer un arbre de sélection :

❶ Tapez le code suivant dans un notebook, en appuyant sur la touche Entrée après chaque ligne :

One = int(input("Tapez un nombre entre 1 et 10 : "))
Two = int(input("Tapez un nombre entre 1 et 10 : "))

if (One >= 1) and (One <= 10):
  if (Two >= 1) and (Two <= 10):
     print("Votre nombre secret est : ", One * Two)
  else:
     print("La seconde valeur est incorrecte !")
else:
  print("La première valeur est incorrecte !")

Il s’agit simplement d’une extension de l’application que vous avez déjà rencontrée plus haut dans la section « Utiliser l’instruction if…else dans une application » . Remarquez cependant que l’indentation est différente. La seconde instruction if…else est imbriquée à l’intérieur de la première. Cette indentation indique à Python qu’il y a un second niveau d’imbrication.

❷ Cliquez sur Exécuter la cellule.

Une fenêtre Python en mode Shell va s’ouvrir. Un message vous demande de choisir un nombre compris entre 1 et 10.

❸ Tapez 5 et appuyez sur Entrée.

L’application vous demande une autre valeur comprise entre 1 et 10.

❹ Tapez 2 et appuyez sur Entrée. La combinaison (le produit) des deux nombres va s’afficher (voir la Figure 6.8).

[picture]

Figure 6.8 Plusieurs niveaux d’imbrication vous permettent d’effectuer des tâches plus complexes.

Dans cet exemple, les entrées sont les mêmes que dans l’exemple précédent if...else. Ainsi, vous obtiendrez un message d’erreur si vous entrez une valeur qui ne se trouve pas dans l’intervalle indiqué. Ce message est adapté au rang de la saisie utilisateur (la première ou la seconde) de manière à ce que celui-ci sache quelle valeur est incorrecte.

Fournir des messages d’erreur spécifiques est toujours utile, car, dans le cas contraire, les utilisateurs risquent de se sentir frustrés et de ne pas comprendre ce qui se passe. De plus, cela vous aide aussi à localiser plus facilement la source des problèmes.

Combiner d’autres types de décisions

Il est possible d’utiliser n’importe quelle combinaison d’instructions if, if…else et if…elif pour atteindre le but désiré. Vous pouvez imbriquer autant de niveaux de blocs de code que vous le souhaitez afin d’effectuer les contrôles voulus. Par exemple, le Listing 6.1 vous propose de faire votre choix dans le menu du petit déjeuner d’un hôtel-restaurant.

Listing 6.1 Créer un menu pour le petit déjeuner.

print(«1. Oeufs»)
print(«2. Crêpes»)
print(«3. Gaufres»)
print(«4. Céréales»)
MainChoice = int(input(«Que voulez-vous au petit déjeuner ? “))

if (MainChoice == 2):
   Meal = “Crêpes”
elif (MainChoice == 3):
   Meal = “Gaufres”

if (MainChoice == 1):
   print(“1. Galette au froment»)
   print(«2. Beignet»)
   print(«3. Galette de seigle»)
   print(«4. Crêpe nature»)
   Pancake = int(input(«Choisissez un type de crêpe : «))

   if (Pancake == 1):
      print(«Vous avez choisi des œufs avec une galette au froment.»)
   elif (Pancake == 2):
      print(«Vous avez choisi des œufs avec des beignets.»)
   elif (Pancake == 3):
      print(«Vous avez choisi des œufs avec une galette de seigle.»)
   elif (Pancake == 4):
      print(«Vous avez choisi des œufs avec une crêpe nature.»)
   else:
      print(«Nous avons des œufs, mais pas ce type de crêpe.»)

elif (MainChoice == 2) or (MainChoice == 3):
   print(“1. Sirop»)
   print(«2. Confiture de fraises»)
   print(«3. Sucre en poudre»)
   Topping = int(input(«Choisissez une garniture : «))

   if (Topping == 1):
      print («Vous avez choisi « + Meal + « au sirop.»)
   elif (Topping == 2):
      print («Vous avez choisi « + Meal + « à la confiture de fraises.»)
   elif (Topping == 3):
      print («Vous avez choisi « + Meal + « avec du sucre en poudre.»)
   else:
      print («Nous avons des « + Meal + «, mais pas cette garniture.»)

elif (MainChoice == 4):
   print(«Vous avez choisi des céréales.»)

else:
   print(«Désolé, nous n’avons pas cela dans notre menu !»)

Cet exemple propose quelques fonctionnalités intéressantes. Vous pourriez par exemple croire qu’une instruction if…else nécessite toujours une clause else. Or, cet exemple montre une situation dans laquelle cette clause n’a pas lieu d’être. Vous pouvez ici utiliser une instruction if…elif pour vous assurer que la variable Meal contient la valeur correcte, sans autre option à considérer.

La technique de sélection est en fait la même que celle que vous avez vue dans les exemples précédents. Un utilisateur entre un nombre appartenant à l’intervalle spécifié de manière à obtenir le résultat voulu. Certaines des sélections nécessitent un choix secondaire, et un menu adapté à une telle décision vous est proposé. Par exemple, les œufs sont accompagnés d’un certain autre plat, ce qui n’est pas le cas par exemple des céréales.

Remarquez aussi que cet exemple combine des variables et du texte d’une manière spécifique. Du fait qu’une garniture peut concerner aussi bien les crêpes que les gaufres, vous avez besoin d’une méthode permettant de spécifier de quoi il s’agit. La réponse est basée sur la valeur de la variable Meal qui a été définie à un niveau précédent de décision.

La meilleure façon de comprendre cet exemple est de faire différents tests. Essayez diverses combinaisons de menus pour voir de plus près comment tout cela se déroule.

DANS CE CHAPITRE

Effectuer une tâche un certain nombre de fois.

•

Effectuer une tâche jusqu’à ce qu’elle soit terminée.

•

Imbriquer les boucles.

Chapitre 7

Effectuer des tâches répétitives

Jusqu’ici, tous les exercices de ce livre effectuaient une série d’étapes juste une fois avant de se terminer. Mais le monde réel ne fonctionne pas de cette manière. La plupart des tâches effectuées par des êtres humains sont répétitives (et parfois aussi fastidieuses). Par exemple, votre docteur trouve que vous avez besoin de faire plus d’exercices physiques, et il vous dit de faire cinquante pompes tous les jours. Si vous vous arrêtez à une seule pompe, votre état de santé ne va pas s’améliorer, et vous n’aurez définitivement pas suivi les conseils du docteur. Bien entendu, comme vous savez exactement la quantité de pompes à faire, vous pouvez effectuer cette tâche médicale un nombre connu de fois. Python ne fait pas de pompes, mais il est tout à fait capable de réaliser le même genre de répétition grâce à l’instruction for.

Malheureusement, vous ne savez pas toujours combien de fois une certaine tâche doit être réalisée. Supposons par exemple que, en tant que numismate averti, vous avez devant vous une pile de pièces réputées être rares. Vous prenez la pièce du dessus, vous l’examinez, et vous déterminez si cette pièce est ou non la plus belle du lot. Mais, quelle que soit à ce stade votre réponse, cela ne suffit pas à terminer la tâche. Vous devez examiner toutes les pièces une par une en les prenant chaque fois sur le dessus de la pile. Et ce n’est qu’une fois arrivé à la dernière que vous pourrez décider que votre tâche est terminée. Mais, comme vous ne savez pas au départ combien la pile contient de pièces, vous ne savez pas non plus combien de fois vous allez devoir pratiquer cette « dépilation » . Vous n’obtenez la réponse que quand la pile est vide. Python, lui aussi, sait effectuer ce type de travail répétitif en utilisant l’instruction while.

La plupart des langages de programmation appellent cette sorte de séquence répétitive une boucle. L’idée consiste à décrire la répétition comme une sorte de mouvement circulaire, dans lequel le code exécute une série de tâches encore et encore, et cela jusqu’à ce que la boucle se termine. Ces boucles sont un élément essentiel de multiples parties d’une application, notamment les menus. En fait, écrire aujourd’hui une application sans utiliser des boucles est virtuellement impossible.

Parfois, vous devez même créer des boucles qui se trouvent dans d’autres boucles. C’est par exemple le cas pour produire une table de multiplication. La boucle intérieure s’occupe des valeurs en colonne, et la boucle extérieure se charge des lignes (ou le contraire). Vous verrez un tel exemple plus tard dans ce chapitre, alors ne vous inquiétez pas trop de comprendre précisément comment de telles choses fonctionnent pour l’instant.

Vous n’avez pas à saisir manuellement le code source de ce chapitre. En fait, l’utilisation de la source téléchargeable est beaucoup plus facile. Vous trouverez ainsi le code source de ce chapitre dans le fichier Chapitre 09 Accomplir des tâches répétitives.ipynb. Consultez l’introduction pour savoir comment trouver ces fichiers source.

Traiter des données en utilisant l’instruction for

Le premier bloc de code que rencontrent les programmeurs dès qu’il est question de boucle est l’instruction for. Il est d’ailleurs très difficile d’imaginer un langage de programmation qui serait dépourvu de cette instruction. En l’occurrence, la boucle est exécutée un nombre fixé de fois, et vous savez quel est ce nombre avant même que la boucle ne débute. Du fait que tout est connu à l’avance, for est certainement le type de boucle le plus simple à utiliser. Les sections suivantes décrivent la boucle for de manière plus détaillée.

Récursivité : une autre façon d’exécuter des tâches de manière répétitive

La récursivité fournit une méthode de répétition des tâches (un type d’implémentation de boucle) en utilisant une fonction qui s’appelle elle-même jusqu’à ce qu’elle résolve un problème particulier en simplifiant le problème original. Dans une approche simplifiée, c’est un peu comme si vous vous parliez à vous-même jusqu’à ce que vous ayez démêlé un problème. Les langages de programmation fonctionnels tels que Haskell se passent très bien des boucles for ou while en utilisant la récursivité (https://sarakhandaker.medium.com/how-to-code-with-no-loops-8ed815624aae). Python prend également en charge la récursivité, mais il s’agit là d’une technique avancée qui dépasse le cadre de ce livre ; vous pourrez en apprendre plus sur le sujet à https://realpython.com/python-recursion/. Dans la plupart des cas, lorsque vous travaillez avec Python, il est préférable d’employer les boucles for et while afin de garder les choses simples et faciles à déboguer.

Comprendre l’instruction for

Une boucle for (pour) débute par l’instruction qui porte ce nom. Celle-ci décrit le comportement de la boucle. Peu importe que cette description soit une série de lettres dans une chaîne, ou des éléments numériques dans une collection. Vous pouvez même spécifier une plage de valeurs en faisant appel à la fonction range(). En voici un exemple très simple :

for Letter in "Bonjour !":

La ligne d’instruction débute par le mot-clé for. L’élément suivant est une variable appelée Letter qui, dans ce cas, contient un unique élément. Le mot-clé in indique à Python que la séquence continue. Ici, cette séquence prend la forme d’une chaîne de caractères, " Bonjour ! ". L’instruction for se termine par un deux-points, exactement comme les instructions de prise de décision décrites dans le Chapitre 2.

Sous cette instruction for, vous pouvez suivre les tâches répétitives à effectuer dans la boucle. Python considère, comme d’habitude, que toutes les lignes indentées qui viennent après forment un bloc de code qui définit le contenu de la boucle. Là encore, le principe est le même que pour les instructions de prise de décision étudiées au Chapitre 2.

Créer une boucle for simple

La meilleure manière de voir comment fonctionne une boucle for, c’est encore d’en créer une. Dans cet exemple, la séquence va être basée sur une chaîne de caractères. La boucle va traiter chaque caractère un par un jusqu’à ce qu’il n’y en ait plus.

❶ Ouvrez un nouveau notebook.

Vous pouvez également utiliser le fichier source téléchargeable, Chapitre 09 Accomplir des tâches répétitives.ipynb.

❷ Tapez le code suivant dans un notebook, en appuyant sur Entrée à la fin de chaque ligne :

LetterNum = 1
for Letter in "Bonjour !":
   print("La lettre ", LetterNum, " est ", Letter)
LetterNum+=1

L’exemple commence par créer une variable, LetterNum, servant à assurer le suivi du nombre de lettres déjà traité. Chaque fois qu’un passage dans la boucle se termine, la variable LetterNum est mise à jour (incrémentée) d’une unité.

L’instruction for parcourt la séquence de lettres de la chaîne "Bonjour ! ". Chaque lettre trouvée est placée dans la variable Letter. L’instruction qui suit affiche sous une forme lisible le contenu de nos deux variables, LetterNum et Letter.

❸ Cliquez sur Exécuter la cellule.

L’application affiche la liste des lettres de la chaîne, ainsi que leur rang (voir la Figure 7.1).

[picture]

Figure 7.1 Une boucle for permet par exemple de traiter l’une après l’autre tous les caractères d’une chaîne.

Contrôler l’exécution avec l’instruction break

La vie est souvent faite d’exception à une règle. Par exemple, vous êtes chargé de mettre en place une ligne de production de montres connectées. Mais, à un certain moment, une certaine pièce vient à manquer dans l’approvisionnement de la ligne. Si cette pièce est indisponible, la production doit s’arrêter au milieu du cycle de fabrication. Le travail n’est pas terminé, mais il faut attendre que la pièce soit à nouveau réapprovisionnée.

Les ordinateurs sont également concernés par ce genre de problème. Supposons que vous traitiez des données provenant d’une ressource en ligne. À un certain moment, le réseau rencontre un problème et la connexion est interrompue. Le flux de données cesse temporairement, et donc votre application n’a plus rien à faire, même si le nombre de tâches à réaliser n’a pas été accompli.

La clause break rend possible une rupture dans une boucle. Cependant, vous ne devez pas l’utiliser telle quelle. Il faut l’entourer d’une instruction if qui détermine la condition générant ce break. Cette instruction doit dire quelque chose comme ceci : si le flux des données n’arrive plus, alors quitter la boucle.

Dans l’exemple qui suit, vous allez voir ce qui se passe lorsque le compteur de la boucle atteint un certain niveau lors du traitement d’une chaîne de caractères. Certes, il n’est pas très complexe, et ce pour rester simple et compréhensible, mais il reflète ce qui se passe dans le monde réel lorsque le traitement d’une certaine donnée devient trop long (ce qui peut indiquer une condition d’erreur).

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne :

Value = input("Tapez un mot d’au plus 8 caractères : ")
LetterNum = 1

for Letter in Value:
   print("La lettre ", LetterNum, " est ", Letter)
   LetterNum+=1
   if LetterNum > 8:
     print("La chaîne est trop longue !")
     break

Cet exemple prolonge celui de la section précédente. Cependant, il permet cette fois à l’utilisateur de saisir une chaîne de caractères de longueur quelconque. Si cette chaîne contient plus de huit caractères, l’application cesse l’exécution de la boucle.

L’instruction if contient le code de la condition d’arrêt. Si LetterNum devient plus grand que 8, cela signifie que la chaîne est trop longue. Remarquez aussi le niveau d’indentation supplémentaire utilisé pour l’instruction if. Dans ce cas, l’utilisateur voit un message d’erreur lui indiquant que la chaîne est trop longue, et le code exécute une instruction break pour terminer la boucle.

❷ Cliquez sur Exécuter la cellule.

Python demande à l’utilisateur de taper quelque chose.

❸ Tapez Bonjour et appuyez sur Entrée.

L’application liste chaque caractère de la chaîne.

❹ Répétez les Étapes 2 et 3, mais tapez cette fois la chaîne Je suis trop longue.

L’application affiche le message d’erreur attendu, puis elle stoppe la boucle une fois les 8 premiers caractères affichés (voir la Figure 7.2).

[picture]

Figure 7.2 Les chaînes longues sont tronquées pour s’assurer qu’elles ne dépassent pas la longueur autorisée.

Cet exemple ajoute un contrôle de la longueur de la chaîne à votre panoplie de traitement des erreurs potentielles. Le Chapitre 2 vous a montré comment s’assurer qu’une valeur appartient bien à un certain intervalle. Ici, ce contrôle est nécessaire pour s’assurer que des données, en l’occurrence une chaîne, ne dépassent pas la taille autorisée pour les champs d’un formulaire ou d’une base de données. De plus, limiter le format des saisies rend votre système moins sensible à certains modes de piratage (tels que l’injection de script, décrite à https://www.stackhawk.com/blog/command-injection-python/), et donc plus sûr.

Contrôler l’exécution avec l’instruction continue

Parfois, vous pouvez avoir besoin de contrôler chaque élément d’une séquence, mais sans traiter certains d’entre eux. Supposons par exemple que vous deviez traiter toutes les informations sur des voitures dont les caractéristiques sont enregistrées dans une base de données, sauf les voitures bleues. Après tout, peut-être n’avez-vous pas besoin de vous attarder sur cette couleur particulière. La clause break, qui ne fait que terminer la boucle, ne convient pas ici. Sinon, vous ne verriez pas les autres véhicules de la base de données dont vous ne connaissez bien sûr pas à l’avance la couleur.

L’alternative la plus courante à la clause break est la clause continue. Comme break, elle apparaît à l’intérieur d’une instruction if. En revanche, le déroulement de l’application se poursuit alors avec l’élément suivant de la séquence, au lieu de terminer totalement celle-ci.

Les étapes qui suivent vous montrent la différence entre continue et break. Ici, le code refuse de traiter la lettre j, mais il accepte toutes les autres lettres de l’alphabet.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne :

LetterNum = 1
for Letter in "Bonjour !":
   if Letter == "j":
     continue
     print("La lettre j rencontrée, traitement interrompu.")
   print("La lettre ", LetterNum, " est ", Letter)
   LetterNum+=1

Ce code part d’un exemple étudié plus haut dans ce chapitre, mais il ajoute une instruction if avec la clause continue dans le bloc de code if.

Remarquez que l’indentation indique bien que la fonction print() qui suit appartient bien à ce bloc de code. En fait, vous ne verrez jamais ce message, car l’itération courante dans la boucle se termine immédiatement (autrement dit, à la différence de break), et la boucle continue à s’exécuter en passant directement à l’itération suivante. Ce type de code inaccessible est souvent appelé code mort par les développeurs, car il ne fait rien du tout. Évitez cela, car ça complique le processus de débogage et rend le code plus difficile à lire et à comprendre.

❷ Cliquez sur Exécuter la cellule.

Python affiche la suite des lettres de la chaîne, ainsi que leur rang, mais en omettant la lettre j qui est traitée au niveau de la clause continue (voir la Figure 7.3).

[picture]

Figure 7.3 La clause continue permet d’éviter le traitement de certains éléments.

Ne rien faire avec l’instruction pass

Le langage Python comprend quelque chose que l’on ne trouve pas souvent dans d’autres langages : une clause pass. Cette clause indique à Python de ne rien faire. Vous vous demandez peut-être pourquoi un langage inclut une clause qui ne fait rien du tout. La plupart du temps, la clause pass est utilisée lorsque vous développez la logique de votre application et que vous ne savez pas vraiment ce que vous allez faire à l’intérieur d’une instruction if, for ou while qui n’est pas complète et qui ne comporte aucun code à exécuter. La clause pass permet d’exécuter votre code afin que vous puissiez voir la logique fonctionner sans avoir à gérer les messages d’erreur. Les étapes suivantes utilisent un exemple similaire à celui de la section précédente, « Contrôler l’exécution avec l’instruction continue » , sauf que l’on emploie à la place une clause pass.

❶ Tapez le code suivant dans une nouvelle cellule de notebook, en appuyant sur Entrée après chaque ligne :

LetterNum = 1
for Letter in "Bonjour !":
   if Letter == "j":
     pass # Ajouter du code plus tard ici pour traiter j
     print("La lettre j rencontrée, non traitée.")
   print("La lettre ", LetterNum, " est ", Letter)
   LetterNum+=1

❷ Cliquez sur Exécuter la cellule.

Le résultat est similaire à celui de la Figure 9.1. La clause pass permet simplement de compléter l’instruction if dans cet exemple.

Remarquez comment la clause pass permet à cet exemple de s’exécuter même si l’instruction if devrait normalement être considérée comme incomplète. L’utilisation de la clause pass offre les avantages suivants pendant le développement :

• Elle permet de travailler d’abord sur l’application globale, sans se soucier des détails.

• Elle permet d’exécuter le code en parcourant toute la structure de l’arborescence afin de vérifier son fonctionnement global.

• Elle réduit le temps de développement, car vous n’êtes pas impliqué trop tôt dans le détail de l’écriture de l’application.

Valider la saisie avec la clause else

Python dispose encore d’une autre clause que l’on ne trouve pas dans tous les langages : else (sinon). Cette clause rend possible l’exécution de code même si vous n’avez aucun élément à traiter dans une séquence. Par exemple, vous pourriez tout simplement indiquer à l’utilisateur qu’il n’y a plus rien à faire. La clause else est toujours associée à une instruction if faisant partie de la boucle et assortie d’une clause break dans ce cas, il s’agit donc d’une extension de l’exemple de la section « Contrôler l’exécution avec l’instruction break » de ce chapitre. En fait, c’est ce que propose le code qui suit.

❶ Tapez le code suivant dans un notebook, en appuyant sur la touche Entrée après chaque ligne :

Vous pouvez par exemple lancer Python en mode Shell, puis choisir dans le menu File la commande New File.

❷ Tapez le code suivant en appuyant sur Entrée à la fin de chaque ligne :

Value = input("Tapez moins de 8 caractères : ")
LetterNum = 1

for Letter in Value:
  print ("La lettre ", LetterNum, " est ", Letter)
  LetterNum+=1
  if (Letter == "" or Letter == Value[-1]):
     break
else:
  print("La chaîne est vide.")

Remarquez les niveaux d’indentation dans cet exemple. L’instruction if est indentée sous l’instruction for, mais la clause else apparaît au même niveau d’indentation que l’instruction for. L’utilisation de l’indentation correcte est essentielle dans cette situation.

Dans cet exemple, l’instruction if doit également vérifier deux conditions :

La chaîne de caractères est vide, car l’utilisateur a appuyé sur Entrée.

Le traitement a atteint la fin de la chaîne et il est terminé. On trouve la fin d’une chaîne de caractères en utilisant Value[-1] dans cet exemple ; la section « Sélectionner des caractères individuels » du Chapitre 2 vous en dit plus sur les indices de chaînes de caractères.

❸ Cliquez sur Exécuter la cellule.

Python demande à l’utilisateur de saisir quelque chose.

❹ Tapez Bonjour et appuyez sur Entrée.

L’application liste tous les caractères de la chaîne, comme sur la Figure 9.1.

❺ Répétez les Étapes 3 et 4, mais ne tapez aucun texte. Appuyez simplement sur Entrée.

Le message associé à la clause else indique maintenant que votre chaîne est vide (voir la Figure 7.4).

Il est facile de faire une mauvaise utilisation de la clause else, car une séquence vide ne signifie pas forcément l’absence d’une saisie. Cela peut également être le signal d’une erreur, ou du fait que d’autres conditions ont besoin d’un traitement particulier. Assurez-vous que vous comprenez comment l’application gère les données pour vous assurer que la clause else ne va pas en fait masquer une condition d’erreur, plutôt que de la rendre visible afin de pouvoir y remédier.

[picture]

Figure 7.4 La clause else rend possible l’exécution de tâches basées sur une séquence vide.

Traiter des données avec l’instruction while

L’instruction while (tant que) trouve son intérêt dans des situations où vous n’êtes pas sûr du nombre de données qu’une application doit traiter en boucle. Elle demande à Python de continuer à répéter le bloc de code tant qu’il trouve des éléments. Ce type de boucle est très utile, par exemple si vous devez effectuer des tâches comme télécharger des fichiers dont la taille n’est pas connue à l’avance, ou encore diffuser des informations à partir d’une source comme une radio Internet. Toute application dans laquelle vous ne pouvez pas dire à l’avance quel est le volume de données à traiter est une bonne candidate à l’emploi de l’instruction while. Voyons cela de plus près.

Comprendre l’instruction while

L’instruction while travaille avec une condition au lieu d’une séquence. Cette condition indique que l’instruction while doit effectuer une certaine tâche, tant que cette condition reste vraie (ou jusqu’à ce qu’elle devienne fausse). Prenons l’exemple d’une file d’attente devant la caisse d’un grand magasin. La caissière s’occupe des clients l’un après l’autre, et ce jusqu’à ce que la file d’attente soit vide. Bien entendu, de nouveaux clients peuvent se présenter à tout moment à la caisse, et il est donc impossible de savoir à l’avance combien la caissière en traitera. Mais supposons maintenant que, au bout de 25 clients, la caissière ait le droit de prendre une pause. Voici à peu près comment cela pourrait s’écrire :

while numClients <= 25:

Cette séquence débute par le mot-clé while. Elle pose alors une condition, ici une variable NumClients dont la valeur doit être inférieure ou égale à 25 pour continuer. En l’état, rien ne précise la valeur initiale de la variable NumClients, pas plus que le code ne définit la manière dont celle-ci évolue. La seule chose qui est connue au début de cette procédure, c’est que NumClients doit être inférieur ou égal à 25 pour que la boucle continue à être effectuée. Comme vous en avez maintenant l’habitude, l’instruction se termine par un deux-points, et les lignes de code qui vont suivre seront indentées.

Du fait que l’instruction while n’effectue pas une série de tâches un nombre déterminé de fois, il est possible de produire une boucle sans fin, autrement dit qui ne se termine jamais. Reprenons notre exemple. Si la valeur de NumClients est égale à zéro lorsque l’application rencontre l’instruction while, et que la valeur de la variable n’augmente jamais, la boucle sera répétée sans cesse jusqu’à la fin des temps (… ou jusqu’à ce que l’ordinateur soit éteint). Les boucles sans fin peuvent provoquer toutes sortes de problèmes bizarres, comme de gros ralentissements, voire un gel complet du système. Il faut donc à tout prix éviter de se retrouver dans une telle situation. En d’autres termes, vous devez toujours fournir une méthode pour que la boucle while se termine (à la différence d’une boucle for, dont on connaît à l’avance le nombre d’itérations). Il faut donc vous préoccuper de trois choses :

❶ Créer l’environnement de la condition (par exemple, affecter la valeur 0 à NumClients).

❷ Tester la condition à l’intérieur de l’instruction while (par exemple, NumClients <= 25).

❸ Mettre la condition à jour de manière à s’assurer qu’elle ne sera plus vérifiée au bout d’un temps fini (par exemple en incrémentant NumClients dans le bloc de code while).

Comme pour la boucle for, vous avez la possibilité de modifier le comportement par défaut de l’instruction while. En fait, vous avez accès aux mêmes quatre clauses que celles déjà étudiées avec for :

• break : termine la boucle courante.

• continue : termine immédiatement le processus pour l’élément courant, mais continue avec l’élément suivant.

• pass : sert d’emplacement réservé pour le futur code.

• else : fournit une technique alternative lorsque les conditions de la boucle ne sont pas remplies.

Le nouvel opérateur d’affectation de Python 3.8 (l’opérateur de morse) peut aussi aider à raccourcir des blocs de code while dans certaines situations où cela ne risque pas de rendre ceux-ci plus confus Par exemple, la boucle qui suit se charge de la lecture et du traitement du contenu d’un fichier, f, par blocs d’une longueur fixée de 256 caractères :

while (bloc := f.read(256)) != '':
    traiter(bloc)

Utiliser l’instruction while dans une application

Vous pouvez utiliser l’instruction while de multiples manières. Mais ici, nous allons nous en tenir à un exemple simple qui vous montrera le mode de fonctionnement de while. Nous allons afficher un compteur basé sur l’état d’une variable appelée Sum. Celle-ci est initialisée avec la valeur zéro, et le compteur s’arrête lorsque la variable atteint la valeur 5. Les étapes suivantes vous aident à créer et à tester le code de l’exemple.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne :

Sum = 0
while Sum < 5:
   print(Sum)
   Sum+=1

Ce code met en œuvre d’une manière très simple les trois tâches que vous devez effectuer lorsque vous travaillez avec une boucle while. Elle commence par initialiser la variable Sum avec la valeur 0, ce qui est la première étape à franchir. La condition elle-même apparaît sur la ligne de l’instruction while. Et la fin du code accomplit la troisième étape, c’est-à-dire actualiser la valeur de Sum. Pour que tout cela apparaisse clairement, le code affiche la valeur courante de la variable à chaque passage dans la boucle.

Une instruction while offre davantage de souplesse que la boucle for. La méthode employée pour actualiser la valeur de Sum est relativement évidente. Cependant, vous pourriez utiliser n’importe quelle autre méthode nécessitée par l’application pour atteindre vos objectifs. Rien ne dit que vous devriez mettre à jour la variable Sum d’une façon spécifique. Par exemple, vous pouvez tester l’état de trois ou quatre variables si vous le désirez. Bien entendu, plus la condition sera complexe, et plus vous risquerez d’entrer dans une boucle sans fin. Vous devez donc bien réfléchir à la manière de quitter votre boucle while.

❷ Cliquez sur Exécuter la cellule.

Python exécute la boucle while et affiche la séquence numérique illustrée à la Figure 7.5.

[picture]

Figure 7.5 Cette simple boucle while affiche une suite de nombres.

Imbriquer des boucles

Dans certains cas, il est possible d’utiliser au choix une boucle for ou une boucle while pour obtenir le même effet. Dans l’exemple qui suit, vous allez créer un générateur de table de multiplication en imbriquant une boucle while à l’intérieur d’une boucle for. Comme vous voulez que la sortie produite soit bien présentée, vous allez également la mettre en forme (nous reviendrons sur ce sujet dans le Chapitre 2).

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne :

X = 1
Y = 1
print ('{:>4}'.format(' '), end= ' ')
for X in range(1, 11):
  print('{:>4}'.format(X), end=' ')
print()
for X in range(1,11):
  print('{:>4}'.format(X), end=' ')
  while Y <= 10:
     print('{:>4}'.format(X * Y), end=' ')
     Y += 1
  print()
  Y=1

Cet exemple commence par créer deux variables, X et Y, chargées de contenir respectivement la valeur en ligne et celle en colonne de la table.

Pour que cette table soit lisible, le code doit créer un en-tête en haut de chaque colonne et à gauche de chaque ligne. Lorsque l’utilisateur voit la valeur 3 en haut, et la valeur 4 à gauche, il doit simplement les suivre pour savoir où les rangées correspondantes se coupent, et donc où se trouve leur produit.

La première fonction print() ajoute un espace (puisque rien n’apparaît dans le coin supérieur gauche de la table, comme l’illustre un peu plus loin la Figure 9.6). Toutes les autres instructions de formatage créent un espacement de quatre caractères de large. La partie { : >4} du code détermine la taille de la colonne. La fonction format(' ') définit ce qui est affiché dans cet espace. L’attribut end de la fonction print() change le caractère de fin de ligne pour remplacer le retour chariot par une espace.

La première boucle for affiche les nombres de 1 à 10 en haut de la table. La fonction range() permet de spécifier facilement cet intervalle. Vous devez lui passer la valeur de départ, donc 1 dans ce cas, et la valeur finale augmentée d’une unité, soit 11 ici.

Arrivé là, le curseur attend patiemment à la fin de la ligne d’en-tête. Pour le renvoyer à la ligne suivante, un simple appel à la fonction print(), sans aucun argument, suffit à faire le travail.

Même si ce morceau de code peut paraître un peu complexe, il suffit de le suivre ligne par ligne pour en comprendre le mécanisme. La table de multiplication calcule des valeurs allant de 1 * 1 à 10 * 10, et donc vous avez besoin de dix lignes et de dix colonnes pour afficher tous les résultats de ces opérations. L’instruction for demande donc à Python de créer dix lignes.

Jetez maintenant un coup d’œil à la Figure 9.6. À l’intérieur de la boucle, le premier appel à la fonction print() affiche l’en-tête de la ligne courante. Bien entendu, vous devez formater cette présentation, et c’est pourquoi le code utilise un espacement de quatre caractères se terminant par une espace (au lieu d’un retour chariot) pour pouvoir afficher les résultats des calculs sur la même ligne.

C’est maintenant qu’arrive la boucle while. Celle-ci gère l’affichage d’une colonne donnée, et ce pour la ligne courante. Les valeurs à placer dans les colonnes sont à chaque fois le produit de X par Y. Là encore, la sortie est formatée pour respecter la règle des quatre espaces (bien entendu, vous pourriez en choisir une autre, dès lors que le résultat est lisible et cohérent). La boucle while se termine une fois que Y a atteint sa valeur limite, 10, grâce à l’incrémentation Y + = 1.

Vous revenez alors à la boucle for. La fonction print() termine la ligne courante en envoyant un retour chariot. De plus, Y doit être réinitialisé à la valeur 1 afin qu’il soit prêt pour la ligne suivante. Et ainsi de suite…

❷ Cliquez sur Exécuter la cellule.

Python affiche la table de multiplication illustrée à la Figure 7.6.

[picture]

Figure 7.6 Grâce au formatage, cette table de multiplication est plaisante à lire.

DANS CE CHAPITRE

Comprendre la cause des erreurs.

•

Gérer les conditions d’erreur.

•

Spécifier qu’une erreur s’est produite.

•

Développer vos propres indicateurs d’erreur.

•

Effectuer des tâches après une erreur.

Chapitre 8

Les erreurs ? Quelles erreurs ?

La plupart du temps, un code d’une certaine complexité comporte des erreurs. Si votre application se bloque soudain sans raison apparente, il y a une erreur. Voir apparaître un message obscur est un autre type d’erreur. Cependant, nombre d’erreurs peuvent aussi se produire sans que vous ne receviez la moindre notification. Par exemple, une application pourrait effectuer les mauvais calculs sur une série de valeurs qui lui ont été fournies, ce qui donnerait un résultat erroné. Dans ce cas, vous pourriez ne jamais vous en apercevoir, jusqu’à ce que quelqu’un vous en avertisse ou que vous soyez pris d’un sérieux doute. Les erreurs ont aussi besoin de cohérence. Vous pourriez en déclencher une dans certaines circonstances, disons par exemple quand le réseau est surchargé, et pas dans d’autres. En bref, des erreurs peuvent survenir dans toutes sortes de situations et pour toutes sortes de raisons. Ce qu’il faut retenir, c’est que l’ordinateur ne vous a pas personnellement visé et n’a rien contre vous, car les ordinateurs n’ont pas d’émotions. Ce chapitre est conçu pour atténuer toute angoisse liée à l’informatique que vous pourriez avoir à propos des erreurs.

Il n’y a rien de surprenant à ce que des erreurs surgissent. Les applications sont écrites par des humains, et les humains font des erreurs. La plupart des développeurs appellent ces erreurs des exceptions, dans le sens où il s’agit d’exception à la règle. Puisque des exceptions se produisent dans les applications, vous devez les détecter et y réagir dès que possible. Détecter et traiter une exception relève de ce que l’on appelle un gestionnaire d’erreur, ou encore un gestionnaire d’exception (on parle aussi de système de gestion d’exception, ou d’erreur). Pour détecter correctement des erreurs, vous avez besoin de savoir quelle peut en être la cause, et pourquoi elles se produisent. Pour cela, vous devez intercepter l’exception, autrement dit l’examiner et si possible en faire quelque chose. Nous découvrirons donc aussi comment gérer des exceptions dans vos propres applications.

Parfois, votre code détecte une erreur dans l’application. Lorsque cela se produit, vous devez lever (raise) ou propager (throw) une exception. Vous pouvez rencontrer ces deux termes (avec des variantes) pour désigner à peu près la même chose : votre code a rencontré une erreur qu’il ne peut pas gérer, et il passe une information sur cette erreur à une autre pièce de code, le gestionnaire d’exception, qui va interpréter, traiter et avec de la chance réparer cette erreur. Même si Python possède des tas de messages génériques qui couvrent la plupart des situations, certaines sont tout à fait spéciales, ce qui peut vous obliger à utiliser des messages d’erreur personnalisés (sans compter que ceux de Python sont tous en anglais). Par exemple, vous pourriez avoir besoin de fournir un support particulier pour une application de base de données, chose que Python ne couvre pas par défaut. Il est important de savoir quand il est nécessaire de traiter les exceptions localement, quand les envoyer au code appelant, et quand créer des exceptions spéciales, de manière à ce que chaque partie de l’application sache comment gérer l’exception. Nous reviendrons sur tous ces sujets dans ce chapitre.

Vous n’avez pas à saisir manuellement le code source de ce chapitre. En fait, l’utilisation de la source téléchargeable est beaucoup plus facile. Vous trouverez ainsi le code source de ce chapitre dans le fichier Chapitre 8 Gérer les erreurs.ipynb. Consultez l’introduction pour savoir comment trouver ces fichiers source.

Savoir pourquoi Python ne vous comprend pas

Les langages de programmation comme les ordinateurs sont des choses inanimées. Ils n’ont aucun état d’âme, aucun désir, aucune volonté personnelle. De plus, ils ne pensent pas. Ils acceptent à peu près littéralement ce que les programmeurs leur disent de faire. Et c’est tout. Et c’est en même temps parfois très frustrant quand on a soudain l’impression (fausse) que le langage de programmation ou l’ordinateur semble vaquer à ses propres occupations sans se soucier du reste…

Ni Python ni l’ordinateur ne sont capables de comprendre ce que vous voulez faire lorsque vous tapez du code. Ils suivent vos instructions en les prenant au pied de la lettre. Par exemple, vous n’avez pas demandé à Python de supprimer un fichier de données, à moins qu’une certaine situation absurde se produise. Mais, si vous n’avez pas très clairement explicité les choses, il n’est pas impossible que Python supprime le fichier de données, situation absurde ou pas. Lorsque ce genre de chose se produit, on dit généralement que l’application bogue, ou qu’elle a un bug. Mais, en réalité, il s’agit simplement d’erreurs de codage qui pourraient être détectées et corrigées en faisant appel à un débogueur (un outil qui permet de stopper ou de mettre en pause l’exécution d’une application, d’examiner le contenu des variables, et plus généralement de disséquer l’application pour voir où se trouve le problème).

Des erreurs se produisent bien souvent dans des situations où le développeur fait certaines suppositions qui se révèlent fausses. Bien entendu, ceci comprend les réactions des utilisateurs, qui ne se soucient probablement pas du tout de l’extrême niveau de soin que vous avez pris pour écrire une application parfaite. L’utilisateur entre une mauvaise donnée. Python, qui n’est en rien concerné par ces problématiques, traite cette donnée, même si votre intention initiale était tout autre. Python ne comprend rien à ces questions existentielles. Il effectue son travail en fonction des règles que vous avez définies. Ce qui implique qu’il vous appartient, et uniquement à vous, de créer des règles qui protègent les utilisateurs d’eux-mêmes.

Python n’est ni proactif ni créatif. Ce sont des qualités qui n’existent que chez le développeur. Si une erreur réseau se produit, ou si l’utilisateur fait quelque chose d’inattendu, Python est incapable de créer une solution afin de résoudre ce problème. Il ne fait qu’exécuter du code. Si vous n’avez rien prévu pour gérer une telle situation, il est probable que l’application plantera sans autre forme de procès (et éventuellement les données de l’utilisateur avec). Bien entendu, un développeur ne peut pas anticiper tous les cas possibles. C’est pourquoi la plupart des applications complexes comportent des erreurs (en l’occurrence, il s’agirait d’une erreur par omission).

Ne vous bercez d’aucune illusion : écrire un code parfaitement à l’épreuve des balles est juste une idée absurde. Vous devez être totalement convaincu qu’un certain nombre de bogues se produiront dans la durée de vie de vos applications, que la nature et les utilisateurs continueront à effectuer des actions auxquelles personne n’avait pensé, et que même le meilleur programmeur du monde ne peut pas anticiper toutes les conditions d’erreur possible. En d’autres termes, supposez toujours que votre application est sujette à des erreurs qui vont provoquer des exceptions. C’est en raisonnant ainsi que vous pourrez créer des applications non pas parfaites, mais plus efficaces et plus fiables. Gardez à l’esprit que la loi de Murphy ( « Si quelque chose peut mal tourner, cela arrivera » ) vous aidera plus que vous ne le pensez. Pour en savoir plus sur les lois de Murphy, consultez le site https://people.howstuffworks.com/murphys-law.htm. En outre, vous devez considérer que certains événements dont la probabilité est faible peuvent avoir un impact élevé sur votre logiciel. On les appelle les événements « black-swan » (cygne noir ; voir https://betterprogramming.pub/the-black-swan-events-in-distributed-systems-d6a5d51adddf), et vous devez également vous y préparer.

Prendre en considération les sources d’erreurs

Vous pourriez peut-être deviner les sources d’erreurs potentielles de votre application en lisant dans le marc de café, mais vous vous doutez bien que cela risque de ne pas être très efficace. En fait, les erreurs se répartissent en catégories bien définies qui vous aident, du moins dans une certaine mesure, à mieux comprendre quand et où elles sont susceptibles de se produire. Les deux catégories principales sont les suivantes :

• les erreurs qui surgissent à un moment spécifique ;

• les erreurs qui sont d’un type spécifique.

Les sections qui suivent explicitent ces notions. Le concept général, c’est que vous avez besoin de réfléchir à la classification des erreurs de manière à commencer à les trouver et les réparer dans vos applications avant même qu’elles ne deviennent un problème.

Erreurs surgissant à un moment spécifique

Ce type d’erreur se décompose en deux catégories principales :

• au moment de la compilation ;

• au moment de l’exécution.

Dans tous les cas, votre application va mal. Voyons donc cela de plus près.

Erreur de compilation

Une telle erreur se produit au moment où vous demandez à Python d’exécuter votre application. Avant même de commencer, il doit interpréter le code et le transcrire dans une forme compréhensible par l’ordinateur. L’ordinateur, de son côté, ne connaît qu’une langue extrêmement binaire, et de surcroît liée à son processeur et à son architecture. Si les instructions que vous avez saisies sont mal formulées, ou si des informations indispensables manquent, Python ne peut pas effectuer la conversion dans le langage de la machine. Il renvoie donc une erreur que vous devez réparer pour que l’application puisse être lancée.

Fort heureusement, les erreurs de ce type sont les plus faciles à localiser et à fixer. Du fait que l’application ne peut même pas se lancer, l’utilisateur ne voit jamais ce genre d’erreur. C’est à vous de résoudre le problème en corrigeant le code erroné.

L’apparence d’une erreur de compilation devrait attirer votre attention sur le fait qu’il existe d’autres fautes de frappe ou d’autres omissions dans le code. Il est toujours bénéfique de contrôler les lignes de code environnantes afin de s’assurer qu’il n’y a pas d’autres problèmes potentiels qui pourraient ne se révéler qu’un moment de l’exécution.

Prendre en compte les effets des mises à jour des bibliothèques et des packages sur votre code

Python s’appuie sur un grand nombre de bibliothèques tierces pour effectuer des tâches. Une bibliothèque est un code que quelqu’un d’autre écrit et que vous utilisez ensuite avec votre application afin de ne pas avoir à rédiger autant de code. Ces bibliothèques sont organisées sous une forme pratique appelée package, et il s’agit d’une sorte de cadeau qu’un programmeur offre à un autre. À l’heure où j’écris ces lignes, 3 409 411 packages sont répertoriés sur https://pypi.org/ et sont prêts à être employés avec le langage de programmation Python. Rien que pour les projets de messagerie électronique, 659 packages sont disponibles. Ainsi, avant d’élaborer un code complexe pour effectuer une tâche spécifique, pensez au fait que quelqu’un d’autre a probablement déjà écrit ce code pour vous.

Les bibliothèques sont généralement un bon ajout à votre boîte à outils de programmeur, car quelqu’un d’autre les débogue à votre place, et vous aurez donc moins d’erreurs à traquer. Cependant, c’est toute cette histoire de débogage qui pose quelques problèmes. Le code qui fonctionnait parfaitement avec la version 1.0 de la bibliothèque peut ne pas fonctionner avec la version 2.0, car la bibliothèque mise à jour a corrigé des problèmes dont dépend le fonctionnement de votre code. Un correctif de ce type est appelé un changement de rupture (breaking change), car il casse le code existant et provoque des erreurs. Par conséquent, votre code peut fonctionner un jour, mais pas le lendemain, lorsque quelque chose que vous installez met à jour les bibliothèques et installe la nouvelle version à votre insu. Oui, d’autres programmeurs peuvent vous gâcher votre journée en faisant ce qu’il faut et en corrigeant les bogues dans leur code. Le Chapitre 5 vous explique comment importer des packages (bibliothèques) dans votre code et les utiliser.

Certaines erreurs de mise à jour se produisent également dans le langage Python lui-même. Par exemple, lorsque vous travaillez avec Python 2, vous pouvez créer du code qui utilise l’instruction print de la manière suivante : print "Hello World". Dans Python 3, vous devez utiliser la fonction print() comme ceci : print("Hello World"). Les informations concernant les mises à jour de Python sont accessibles à https://docs.python.org/fr/3/whatsnew/3.0.html.

Erreur d’exécution

C’est fait. Python a compilé votre code et lancé l’application. Et c’est à partir de là que peut se produire une erreur d’exécution. De telles erreurs ont des causes variées, et certaines sont plus difficiles à cerner que d’autres. Vous savez qu’une erreur d’exécution s’est produite si l’application cesse brusquement de fonctionner en affichant une exception, ou encore si l’utilisateur se plaint de recevoir une sortie erronée, ou bien encore de l’instabilité de l’application.

Toutes les erreurs d’exécution ne produisent pas une exception. Certaines peuvent se traduire par un gel du fonctionnement, par une sortie qui semble erratique, ou encore par des données endommagées. Certaines risquent aussi d’affecter d’autres applications, voire même la plateforme sur laquelle le code est exécuté. Votre responsabilité est donc engagée en tant que développeur !

De nombreuses erreurs d’exécution sont provoquées par des fautes de frappe, voire des oublis dans la saisie. Par exemple, mal orthographier le nom d’une variable empêchera Python de placer la bonne valeur au bon endroit (de ce point de vue, la confusion entre majuscule et minuscule est rédhibitoire). Oublier un argument facultatif (il n’est pas absolument nécessaire dans la plupart des cas) mais nécessaire (il est requis dans ce cas précis) dans l’appel à une méthode peut aussi provoquer des problèmes. En général, il est possible de localiser de telles erreurs en faisant appel aux services d’un débogueur, ou plus simplement en relisant votre code ligne par ligne.

Les erreurs d’exécution peuvent également être provoquées par des événements extérieurs non associés à votre code (voir l’encadré « Prendre en compte les effets des mises à jour des bibliothèques et des packages sur votre code » pour des informations complémentaires sur le code des bibliothèques tierces). Par exemple, l’utilisateur entre une information incorrecte et l’application ne sait pas la traiter, ce qui provoque une exception. Un problème sur le réseau peut tout aussi bien rendre des données inaccessibles. Parfois aussi, c’est le matériel de l’ordinateur qui a une défaillance, ce qui provoque une erreur non repérable. Tous ces exemples forment des erreurs par omission, que votre application pourrait être en mesure de récupérer si votre application contient le code nécessaire à cela. Il est important de prendre en compte ces différents cas lorsque vous vous concevez votre application.

Distinguer les types d’erreurs

Connaître les types d’erreurs vous aide à comprendre où rechercher d’éventuels problèmes dans une application. Les exceptions fonctionnent comme de nombreuses autres choses de la vie. Par exemple, vous savez que les appareils électroniques ne marchent pas sans une source d’alimentation électrique. Si vous essayez d’allumer votre téléviseur, et qu’il ne se passe rien, la première chose à faire est de vérifier que son cordon d’alimentation est correctement branché dans la prise de courant.

Comprendre les types d’erreurs vous aide à localiser celles-ci plus vite, plus tôt et plus efficacement. Et donc aussi à commettre moins d’erreurs avec les erreurs. Les meilleurs développeurs savent bien qu’il est toujours plus facile de corriger les problèmes pendant le développement d’une application que lorsqu’elle est en service, car les utilisateurs sont par essence impatients, et ils veulent que les problèmes soient réparés immédiatement et correctement. De plus, prendre cette question à bras le corps dès le début du cycle de développement limite la quantité de code à vérifier et à corriger.

Toute l’astuce consiste à savoir où regarder. Python (comme la plupart des autres langages de programmation) distingue ces types d’erreurs :

• syntaxiques ;

• sémantiques ;

• logiques.

Les sections qui suivent vont préciser cela en procédant par ordre de difficulté croissante.

Erreurs de syntaxe

Chaque fois que vous faites une faute de frappe quelconque, vous commettez une erreur de syntaxe. Bien souvent, celles-ci sont plutôt faciles à détecter, car le code ne s’exécute tout simplement pas. L’interpréteur peut même vous guider en affichant un message d’erreur qui vous signale l’emplacement de l’erreur. Cependant, certaines erreurs de syntaxe sont très délicates à découvrir. Ainsi, Python est extrêmement sensible à la capitalisation des caractères. Il suffit donc de mettre une majuscule à la place d’une minuscule (ou l’inverse) dans une seule occurrence d’une variable pour que celle-ci ne fonctionne pas comme elle le devrait. Localiser ce genre de faute peut parfois être un véritable challenge.

La plupart des erreurs de syntaxe sont déclenchées lors de la compilation, et l’interpréteur les pointe pour vous. Dans ce cas, la correction est facilitée, puisque l’interpréteur vous dit généralement ce qu’il faut modifier, et ce avec une précision considérable. Même si ce n’est pas le cas, les erreurs de syntaxe empêchent l’application de fonctionner correctement, et vous les retrouvez au cours des phases de test. Peu d’erreurs de ce type devraient persister au-delà, du moins si vous avez testé complètement votre application.

Erreurs sémantiques

Lorsque vous créez une boucle qui s’exécute plus de fois qu’il n’est nécessaire, vous ne recevez en général aucune information de l’application vous signalant qu’il y a une erreur. Du point de vue de l’application, tout semble normal, mais des boucles supplémentaires peuvent provoquer toutes sortes d’erreurs dans les données. Vous n’avez pas fait de faute de frappe, vous avez provoqué sans le vouloir une erreur sémantique.

Les erreurs sémantiques sont dues au fait que l’idée sous-jacente à une série d’étapes est erronée. Le résultat est incorrect, même si le code semble se dérouler normalement. De telles erreurs sont parfois ardues à localiser, et un bon débogueur est bien souvent votre meilleur allié. Vous pouvez également trouver des articles sur le débogage sur mon blog à l’adresse http://blog.johnmuellerbooks.com.

Erreurs de logique

Certains ne font pas la distinction entre erreurs sémantiques et erreurs de logique, mais ils ont tort. Une erreur sémantique se produit lorsque le code est pour l’essentiel correct, mais que l’implémentation est mauvaise (comme une boucle qui s’exécute une fois de trop). Les erreurs de logique sont dues à un défaut de raisonnement du développeur. Cela se produit souvent lors de l’emploi incorrect d’un opérateur relationnel ou logique. Mais elles peuvent aussi être dues à bien d’autres causes. Par exemple, un programmeur pense que les données qu’il traite sont toujours stockées sur le disque dur local. Et l’application risque donc fort de se comporter d’une manière inhabituelle si elle essaie de charger des données à partir d’un disque réseau.

Les erreurs logiques sont assez difficiles à corriger, puisque le problème ne se trouve pas dans le code, mais dans la conception qui a amené à l’écrire. C’est le processus mental de développement qui est fautif. Et donc la personne qui est responsable de cette erreur est la moins bien placée pour la localiser. Avoir une seconde paire d’yeux (disons, un partenaire avisé – une partenaire avisée est tout aussi parfait) est donc toujours une bonne idée. De même, formaliser avec précision les spécifications d’une application aidera beaucoup, car cela permettra de mieux suivre la logique cachée derrière les tâches que cette application doit réaliser.

Intercepter les exceptions

D’un point de vue général, un utilisateur ne devrait jamais voir un message ou une boîte de dialogue dont l’affichage est provoqué par une exception. L’application devrait toujours intercepter cette exception, et la traiter avant qu’elle n’arrive jusqu’à l’utilisateur. Bien entendu, le monde réel n’est pas aussi simple que cela, et certaines exceptions arrivent à passer à travers les mailles du filet. Pour autant, le but est d’essayer d’intercepter toute exception potentielle lors du développement d’une application. C’est le sujet des sections qui suivent.

Gérer les exceptions de base

Pour gérer les exceptions, vous devez indiquer votre intention à Python puis fournir le code qui va effectuer ce genre de tâche. Vous avez pour cela de nombreuses méthodes à votre disposition. Nous allons dans ce qui suit commencer par le plus simple pour aller vers des méthodes plus complexes, mais aussi plus souples et plus puissantes.

Gérer une unique exception

Dans le Chapitre 8, les différents exemples avaient une manière bien à eux de traiter le cas où un utilisateur tapait une valeur non conforme. Une partie de la solution consistait à contrôler une plage de valeurs. Mais cela n’aidait pas à résoudre une situation dans laquelle l’utilisateur saisissait du texte au lieu d’une valeur numérique. Un gestionnaire d’exception fournit une solution plus élaborée à ce problème, comme le montrent les étapes qui suivent. Vous pouvez également retrouver directement ce code dans le fichier téléchargeable BasicException1.py.

Comprendre les exceptions intégrées à Python

Python est accompagné d’une grande quantité d’exceptions prédéfinies, bien plus que vous ne pourriez l’imaginer. Vous pouvez en voir la liste en consultant l’adresse Web https://docs.python.org/fr/3.10/library/exceptions.html. La documentation partage la liste des exceptions en plusieurs catégories (voir la hiérarchie des exceptions à https://docs.python.org/fr/3.10/library/exceptions.html#exception-hierarchy). En voici un rapide aperçu :

• Classes de base : elles fournissent les blocs de construction essentiels pour les autres exceptions (par exemple, l’exception judicieusement appelée Exception). Vous pouvez cependant voir directement certaines d’entre elles lorsque vous travaillez avec une application (par exemple, ArithmeticError).

• Exceptions dites concrètes : certaines applications peuvent rencontrer des erreurs difficiles à résoudre parce qu’il n’existe pas de bon procédé pour les traiter, ou parce qu’elles signalent un événement que l’application doit gérer elle-même. Par exemple, si un système vient à manquer de mémoire, Python génère une exception MemoryError. Résoudre ce problème est difficile, car il n’est pas toujours possible de libérer de la mémoire occupée par d’autres programmes. Lorsque l’utilisateur appuie sur une touche d’interruption (comme Ctrl + C ou Suppr), Python génère une exception KeyboardInterrupt. C’est bien à l’application de réagir face à cette situation avant de passer à d’autres tâches.

• Exceptions de l’OS : le système d’exploitation peut générer des erreurs que Python passe ensuite à votre application. Si, par exemple, celle-ci tente d’ouvrir un fichier qui n’existe pas, le système génère une erreur FileNotFoundError.

• Avertissements : Python essaie aussi parfois de vous avertir d’un événement inattendu ou d’actions qui pourraient par la suite engendrer des erreurs. Par exemple, une tentative inappropriée d’utiliser une certaine ressource, comme une icône, va déclencher l’exception ResourceWarning. Il s’agit bien d’un simple avertissement, pas d’une erreur. Vous n’êtes donc pas verbalisé. Vous pouvez l’ignorer, mais vous devez savoir qu’une erreur peut survenir plus tard.

❶ Ouvrez un nouveau notebook.

Vous pouvez également utiliser le fichier source téléchargeable, Chapitre 8 Gérer les erreurs.ipynb.

❷ Tapez le code suivant dans le notebook, en appuyant sur Entrée à la fin de chaque ligne :

try:
   Value = int(input("Tapez un nombre entre 1 et 10 : "))
except ValueError:
   print("Vous devez taper un nombre entre 1 et 10 !")
else:
   if (Value > 0) and (Value <= 10):
     print("Vous avez tapé : ", Value)
   else:
     print("La valeur que vous avez tapée est incorrecte !")

Le code qui est placé à l’intérieur du bloc try gère sa propre exception ValueError. Dans ce cas, cette gestion signifie obtenir de l’utilisateur une saisie via l’appel à int(input()). Si une exception se produit à l’extérieur de ce bloc, elle ne sera pas gérée par celui-ci. Bien entendu, du coup, la tentation pourrait être grande de placer tout le code dans un seul bloc try. Mais ce serait en fait une très mauvaise idée. Un bon gestionnaire d’exception doit être petit et spécifique à un certain problème potentiel pour qu’il soit plus facile de localiser et donc de corriger celui-ci.

Le bloc except recherche une exception spécifique, dans ce cas ValueError. Si l’utilisateur provoque une exception ValueError en tapant par exemple Bonjour au lieu d’une valeur numérique, ce bloc d’exception particulier est exécuté. Si l’utilisateur génère une autre exception, le bloc except ne pourra pas la gérer.

Le bloc except contient tout le code qui est exécuté si le bloc de code try génère une exception. Tout le reste est lié à try, car vous ne voulez exécuter ce bloc que si l’utilisateur saisit une valeur correcte, autrement dit un nombre compris entre 1 et 10.

❸ Cliquez sur Exécuter la cellule.

Python vous demande de taper un nombre entre 1 et 10.

❹ Tapez Bonjour et appuyez sur Entrée.

L’application affiche un message d’erreur (voir la Figure 8.1).

[picture]

Figure 8.1 Une saisie du mauvais type génère un message d’erreur au lieu d’une exception.

❺ Répétez les Étapes 3 et 4, mais en tapant cette fois 5.5.

Vous obtenez le même message d’erreur.

❻ Répétez les Étapes 3 et 4, mais en tapant cette fois 22.

L’application affiche le message qui demande de respecter l’intervalle entier (voir la Figure 8.2). Le gestionnaire d’exception ne prend pas en charge ce genre d’erreur. Il faut donc le traiter ailleurs.

[picture]

Figure 8.2 Le gestionnaire d’exception ne traite pas le cas d’un entier qui n’appartient pas à l’intervalle demandé.

❼ Reprenez les Étapes 3 et 4, mais en tapant cette fois 7.

Maintenant, l’application peut vous informer que votre nombre est correct et afficher sa valeur. Même si tout cela semble demander bien du travail pour obtenir ce niveau de contrôle, vous ne pourriez absolument pas être certain que votre application est correcte sans en passer par ce genre d’étapes.

Utiliser la clause except sans exception

Vous pouvez créer dans Python une gestionnaire d’exception générique, et donc sans spécifier une exception particulière. Dans la plupart des cas, il y a quelques bonnes raisons pour fournir une exception spécifique :

• Éviter de masquer une exception que vous n’avez pas prise en compte lors de la conception de l’application.

• S’assurer que d’autres savent précisément quelles exceptions votre application va gérer.

• Gérer les exceptions correctement en utilisant un code adapté à chacune d’elles.

Cependant, vous avez parfois besoin de quelque chose de plus générique, par exemple si vous utilisez une bibliothèque externe ou si vous interagissez avec un service externe. Les étapes qui suivent montrent comment utiliser une clause except sans qu’une exception spécifique lui soit attachée.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne :

try:
   Value = int(input("Tapez un nombre entre 1 et 10 : "))
except:
   print("Erreur générique!")
except ValueError:
   print("Vous devez taper un nombre entre 1 et 10 !")
else:

   if (Value > 0) and (Value <= 10):
     print("Vous avez tapé : ", Value)
   else:
     print("La valeur que vous avez tapée est incorrecte !")

Vous constatez que la seule différence avec l’exemple précédent est la disparition de ValueError à la suite de la clause except. Le résultat, c’est qu’ici cette clause interceptera toute autre exception susceptible de se produire.

❷ Cliquez sur Exécuter la cellule.

Vous voyez le message d’erreur illustré à la Figure 8.3. Python détecte automatiquement que vous avez placé les gestionnaires d’exceptions dans le mauvais ordre (vous en apprendrez davantage sur ce problème dans la section « Gérer des exceptions en allant du plus spécifique au moins spécifique » , plus loin dans le chapitre). Remarquez que les lignes comportant une erreur sont indiquées par des lignes rouges ondulées dans Colab afin que vous puissiez détecter l’erreur facilement.

[picture]

Figure 8.3 Les gestionnaires d’exception sont dans le mauvais ordre.

Tapez le code suivant dans une nouvelle cellule pour inverser l’ordre des deux exceptions afin qu’elles apparaissent de la manière suivante ceci :

try:
  Value = int(input("Tapez un nombre entre 1 et 10 : "))
except ValueError:
  print("Vous devez taper un nombre entre 1 et 10 !")
except:
  print("Erreur générique!")
else:
  if (Value > 0) and (Value <= 10):
  print("Vous avez tapé : ", Value)
else:
  print("La valeur que vous avez tapée est incorrecte !")

❸ Cliquez sur Exécuter la cellule.

Python vous demande de taper un nombre entre 1 et 10.

❹ Tapez Bonjour et appuyez sur Entrée.

L’application affiche un message d’erreur (reportez-vous à la Figure 8.1). Lorsque les exceptions sont dans le bon ordre, le code détecte d’abord les erreurs spécifiques, puis n’utilise des gestionnaires moins spécifiques qu’en cas de besoin.

❺ Cliquez sur Exécuter la cellule.

Python vous demande de taper un nombre entre 1 et 10.

❻ Choisissez Exécution → Interrompre l’exécution (lorsque vous utilisez Colab) ou Kernel → Interrupt (lorsque vous utilisez Jupyter Notebook).

Ceci s’apparente à la combinaison de touches Ctrl + C ou Cmd + C dans d’autres IDE. Apparemment, il ne se passe rien. Si vous travaillez avec Jupyter Notebook, la fenêtre du serveur affiche un message Kernel Interrupted. En outre, vous voyez le message d’erreur générique. Python attend toujours que vous entriez une valeur dans la zone de texte input().

❼ Sélectionnez la zone de texte input() (si nécessaire), tapez 5.5, et appuyez sur Entrée.

Comme auparavant, vous voyez toujours le message d’erreur générique (illustré à la Figure 8.4) parce que Notebook réagit à l’interruption, plutôt qu’à la saisie incorrecte ; la raison en est que l’interruption s’est produite en premier. Python met en file d’attente les erreurs dans l’ordre dans lequel il les reçoit. Par conséquent, vous pouvez constater qu’une application génère parfois ce qui semble être le mauvais message d’erreur.

❽ Effectuez à nouveau les Étapes 3 et 4, mais saisissez cette fois 5.5 au lieu de Bonjour.

L’application génère le même message d’erreur que précédemment (voir à nouveau la Figure 8.1). Dans ce cas, comme aucune interruption ne s’est produite, vous voyez le message d’erreur auquel vous vous attendiez.

[picture]

Figure 8.4 Un gestionnaire d’exception générique va intercepter également l’exception KeyboardInterrupt.

Exceptions et arguments

La plupart des exceptions ne fournissent pas d’arguments (c’est-à-dire une liste de valeurs que vous pouvez tester pour obtenir des informations supplémentaires). Une exception est ou n’est pas, là est la question. Cependant, quelques rares exceptions fournissent des arguments. Vous les verrez plus loin dans ce livre. Ces arguments vous en disent plus sur l’exception et ils fournissent les détails dont vous avez besoin pour corriger le problème.

Pour couvrir tout de même l’éventail des possibilités, cette section vous propose un exemple simple qui génère une exception avec un argument. Vous pouvez sans crainte sauter le reste de cette section si vous le souhaitez, car ces informations sont traitées plus en détail plus loin dans le livre.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne :

import sys
try:
  File = open("myfile.txt")
except IOError as e:
  print("Erreur lors de l’ouverture du fichier ! \r\n" +
     "Numéro de l’erreur : {0}\r\n".format(e.errno) +
     "Texte de l’erreur : {0}".format(e.strerror))
else:
  print("Le fichier a bien été ouvert.")
  File.close()

Cet exemple utilise certaines fonctionnalités avancées. Par exemple, l’instruction import récupère le code d’un autre fichier. Vous en apprendrez davantage à ce sujet dans le Chapitre 5.

La fonction open() ouvre un fichier auquel elle fournit un accès via la variable File. Étant donné que le fichier appelé myfile.txt n’existe pas dans le dossier de l’application, le système d’exploitation ne peut pas l’ouvrir. Il va donc informer Python de ce problème.

Essayer d’ouvrir un fichier inexistant provoque une exception IOError. Celle-ci donne accès à deux arguments :

• errno : le numéro d’erreur entier renvoyé par le système d’exploitation.

• strerror : renvoie une chaîne de caractères (en anglais) décrivant la nature de l’exception.

La clause as place l’information renvoyée par l’exception dans une variable appelée e, ce qui vous permet d’y accéder. Le bloc except contient un appel à print() qui formate les informations fournies par l’exception afin de les rendre plus lisibles.

Si vous décidiez de créer un fichier appelé myfile.txt, c’est la clause else qui serait exécutée. Dans ce cas, vous obtiendriez un message indiquant que le fichier a été ouvert normalement. Le code se termine alors en refermant le fichier, sans effectuer d’autre traitement.

❷ Cliquez sur Exécuter la cellule.

L’application affiche un rapport sur l’ouverture (impossible) du fichier, comme l’illustre la Figure 8.5.

Gérer de multiples exceptions avec une seule clause except

La plupart des applications sont capables de générer plusieurs exceptions pour une même ligne de code. Cela a été démontré dans la section « Utiliser la clause except sans exception » de ce chapitre. La manière de gérer une telle situation dépend de ce que vous cherchez à obtenir, du type des exceptions et du degré de compétence éventuel de vos utilisateurs. Avec des utilisateurs plus ou moins novices, il est souvent préférable de leur dire que l’application a rencontré une erreur non récupérable, et d’enregistrer les détails dans un fichier journal placé dans le dossier de l’application ou bien dans une unité en réseau, ou encore envoyée dans le cloud.

[picture]

Figure 8.5 Tenter d’ouvrir un fichier qui n’existe pas provoque forcément une exception.

Obtenir la liste des arguments d’une exception

La liste des éventuels arguments varie selon l’exception et selon ce que fournit l’appelant. Il n’est pas toujours facile de se faire une idée de ce que vous pouvez obtenir lorsque vous recherchez des informations complémentaires. Une manière de gérer cette question consiste à afficher simplement toute cette liste en utilisant un code tel que celui-ci :

import sys
try:
   File = open(«myfile.txt»)
except IOError as e:
   for Arg in e.args:
     print(Arg)
else:
   print(«Le fichier a bien été ouvert.»)
   File.close()

La propriété args contient toujours une liste des arguments de l’exception au format chaîne de caractères (voir la copie d’écran ci-dessous). Vous pouvez utiliser une simple boucle for pour imprimer chaque argument. Le seul problème avec cette approche est que vous n’obtenez pas le nom de ces arguments. Vous savez donc ce qu’ils contiennent, mais pas comment les appeler.

Une méthode plus complexe consiste à afficher à la fois le nom et la valeur des arguments. Le code suivant affiche à la fois les noms et les valeurs de chacun des arguments :

import sys
try:
   File = open(«myfile.txt»)
except IOError as e:
   for Entry in dir(e):
      if (not Entry.startswith(«_»)):
        try:
           print(Entry, « = «, e.__getattribute__(Entry))
        except AttributeError:
           print(«Attribut «, Entry, « non accessible.»)
else:
   print(«Le fichier a bien été ouvert.»)
   File.close()

Ici, vous commencez par obtenir une liste des attributs associés à l’objet argument de l’erreur en utilisant la fonction dir(). La sortie produite par cette fonction est une liste de chaînes de caractères contenant les noms des attributs affichables (voir la copie d’écran ci-dessous). Seuls les arguments dont le nom ne débute pas par un trait de soulignement (_) contiennent des informations utiles sur l’exception. Cependant, même certaines de ces entrées sont inaccessibles. C’est pourquoi vous devez placer la sortie de print() dans un second bloc try…except (voyez plus loin dans ce chapitre la section « Imbriquer des exceptions » pour plus de détails à ce sujet).

Le nom de l’attribut est simple à traiter, car il est contenu dans Entry. Pour obtenir la valeur correspondante, vous devez faire appel à la fonction __getattribute__ en lui fournissant le nom de l’attribut concerné. Lorsque vous exécutez ce code, vous voyez donc à la fois le nom et la valeur de chacun des attributs de l’argument pour cette exception particulière. Dans ce cas, la sortie produite par l’application se présenterait ainsi :

args  =  (2, ‘No such file or directory’)
Attribut  characters_written  non accessible.
errno  =  2
filename  = myfile.txt
filename2  = None
strerror  = No such file or directory
with_traceback  =  <built-in method with_traceback of FileNot
FoundError object at 0x7f68400bd7c0>

Utiliser une clause except unique pour gérer de multiples exceptions ne marche que s’il est possible de satisfaire aux besoins de tous les types d’exceptions à l’intérieur d’une source commune d’actions. Sinon, cela signifie que chaque exception doit être traitée individuellement. Les étapes suivantes montrent comment traiter plusieurs exceptions à l’aide d’une seule clause except.

❶ Tapez le code suivant dans une nouvelle cellule d’un noteboook, en appuyant sur Entrée après chaque ligne :

try:
  Value = int(input("Tapez un nom entre 1 et 10 : "))
except (ValueError, KeyboardInterrupt):
   print("Vous devez taper un nombre entre 1 et 10 !")
else:
   if (Value > 0) and (Value <= 10):
     print("Vous avez tapé : ", Value)
   else:
     print("La valeur que vous avez tapée est incorrecte !")

Remarquez cependant que la clause except contient maintenant deux exceptions : ValueError et KeyboardInterrupt. De plus, ces exceptions sont placées entre parenthèses et séparées par une virgule.

❷ Cliquez sur Exécuter la cellule.

Python demande de taper un nombre entre 1 et 10.

❸ Tapez Bonjour et appuyez sur Entrée.

L’application affiche un message d’erreur (reportez-vous à la Figure 8.1).

❹ Cliquez sur Exécuter la cellule.

Python demande de taper un nombre entre 1 et 10.

❺ Choisissez Exécution → Interromptre l’exécution pour Colab ou Kernel → Interrupt pour Jupyter Notebook.

Cela s’apparente à l’appui sur les touches Ctrl + C ou Cmd + C dans d’autres IDE.

❻ Répétez les Étapes 2 et 3, mais en tapant cette fois 7 à la place de Bonjour.

Cette fois-ci, l’application signale finalement que vous avez fourni une valeur correcte de 7.

Gérer des exceptions multiples avec plusieurs clauses except

Lorsque vous travaillez avec des exceptions multiples, il est généralement préférable de placer chacune d’entre elles dans sa propre clause except. Cette approche vous permet de fournir un système de gestion personnalisé pour chaque exception, ce qui aide notamment l’utilisateur à savoir plus précisément où se situe le problème. Bien entendu, cette approche demande également plus de travail.

Les étapes suivantes démontrent comment effectuer le traitement des exceptions en utilisant plusieurs clauses except.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne :

try:
  Value = int(input("Tapez un nombre entre 1 et 10 : "))
except ValueError:
  print("Vous devez taper un nombre entre 1 et 10 !")
except KeyboardInterrupt:
  print("Vous avez appuyé sur Ctrl + C!")
else:
  if (Value > 0) and (Value <= 10):
     print("Vous avez tapé : ", Value)
  else:
     print("La valeur que vous avez tapée est incorrecte !")

Remarquez ici l’emploi de plusieurs clauses except. Chacune sert à gérer une exception différente. Vous pouvez combiner les deux techniques, avec certaines clauses except traitant une exception unique, et d’autres prenant en charge plusieurs exceptions. Python vous permet d’utiliser la méthode qui convient le mieux à votre situation.

❷ Cliquez sur Exécuter la cellule.

Python demande de taper un nombre entre 1 et 10.

❸ Tapez Bonjour et appuyez sur Entrée.

L’application affiche un message d’erreur (reportez-vous à la Figure 8.1).

❹ Répétez les Étapes 2 et 3, mais en tapant cette fois 22 à la place de Bonjour.

L’application affiche le message d’erreur attendu qui demande de respecter l’intervalle (reportez-vous à la Figure 8.2).

❺ Répétez à nouveau les Étapes 2 et 3, mais choisissez Exécution → Interrompre l’exécution pour Colab ou Kernel → Interrupt pour Jupyter Notebook.

L’application affiche un message spécifique, disant que vous avez appuyé sur Ctrl + C, qui indique à l’utilisateur l’origine du problème.

❻ Répétez les Étapes 2 et 3, mais en tapant cette fois 7 à la place de Bonjour.

Cette fois-ci, l’application vous informe que vous avez tapé un chiffre qui se trouve bien dans l’intervalle demandé.

Gérer des exceptions en allant du plus spécifique au moins spécifique

Une stratégie de gestion des exceptions consiste à fournir des clauses except spécifiques pour toutes les exceptions connues, et des clauses except génériques pour toutes les exceptions non prévisibles ou inconnues. La documentation en ligne de Python détaille la hiérarchie des exceptions (https://docs.python.org/fr/3/library/exceptions.html#exception-hierarchy). Vous y remarquez par exemple que BaseException est l’exception de plus haut niveau. La plupart des exceptions sont dérivées de Exception. Si vous travaillez sur des opérations mathématiques, vous pouvez utiliser l’exception générique ArithmeticError, ou la plus spécifique ZeroDivisionError.

Python évalue les clauses except dans l’ordre où elles apparaissent dans le fichier du code source. La première rencontrée est examinée en premier, la seconde vient après, et ainsi de suite. Les étapes qui suivent vous aident à examiner un exemple destiné à démontrer l’importance qu’il y a à utiliser un ordre correct pour le traitement des exceptions. Dans ce cas, vous effectuez des tâches qui entraînent des erreurs mathématiques.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne :

try:
   Value1 = int(input("Tapez le premier nombre : "))
   Value2 = int(input("Tapez le second nombre : "))
   Output = Value1 / Value2
except ValueError:
   print("Vous devez taper un nombre entier !")
except KeyboardInterrupt:
   print("Vous avez appuyé sur Ctrl+C !")
except ArithmeticError:
   print("Une erreur de math non définie s’est produite.")
except ZeroDivisionError:
   print("Tentative de division par zéro !")
else:
   print(Output)

Le code commence par vous demander d’entrer deux valeurs : Value1 et Value2. Les deux premiers except gèrent une saisie inattendue. Les deux suivants prennent en charge une exception mathématique, comme une division par zéro. Si tout s’est bien déroulé, le code se contente d’afficher le résultat de l’opération.

❷ Cliquez sur Exécuter la cellule.

Python demande de taper le premier nombre.

❸ Tapez Bonjour et appuyez sur Entrée.

L’application affiche évidemment le message d’erreur ValueError. Cependant, il est toujours utile de vérifier les problèmes potentiels.

❹ Cliquez sur Exécuter la cellule.

Python demande de taper le premier nombre.

❺ Tapez 8 et appuyez sur Entrée.

L’application vous demande de taper le deuxième nombre.

❻ Tapez 0 et appuyez sur Entrée.

Vous voyez cette fois le message d’erreur émis par l’exception ArithmeticError (voir la Figure 8.6). Cependant, ce n’est pas la bonne option. Vous devriez à la place voir le message d’erreur de l’exception ZeroDivisionError, car il est plus spécifique que ArithmeticError.

❼ Inversez l’ordre des deux dernières exceptions pour qu’elles se présentent ainsi :

[picture]

Figure 8.6 L’ordre dans lequel Python traite les exceptions est important.

except ZeroDivisionError:
   print("Tentative de division par zéro !")
except ArithmeticError:
   print("Une erreur de math non définie s’est produite.")

❽ Répétez les Étapes 4 à 6.

Cette fois, vous voyez s’afficher le message de l’exception ZeroDivisionError, car les exceptions apparaissent dans le bon ordre.

❾ Répétez les Étapes 4 et 5, mais en tapant comme second chiffre 2 au lieu de 0.

Maintenant, l’application affiche normalement le résultat de la division (4.0).

Notez que le résultat de la division de 8 par 2 donne un nombre en virgule flottante. C’est la réponse standard, sauf si vous demandez explicitement une valeur entière en utilisant l’opérateur //.

Imbriquer des exceptions

Vous aurez parfois besoin de placer un gestionnaire d’exception à l’intérieur d’un autre. Ce processus est appelé imbrication. Lorsque vous procédez ainsi, Python essaie d’abord de trouver un gestionnaire d’exception dans les niveaux les plus bas, puis il remonte vers les couches extérieures. Vous pouvez empiler autant de niveaux que vous le souhaitez pour rendre votre code aussi sûr qu’il est possible.

L’une des raisons les plus courantes de l’utilisation de cette technique est la construction d’une partie de code dans laquelle vous demandez à l’utilisateur de saisir quelque chose. Vous placez ensuite la réponse obtenue dans une boucle pour vous assurer que vous avez bien obtenu l’information demandée. C’est ce qu’illustrent les étapes qui suivent.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne :

TryAgain = True
while TryAgain:
   try:
     Value = int(input("Tapez un nombre entier : "))
   except ValueError:
     print("Vous devez taper un nombre entier !")
     try:
        DoOver = input("Essayer à nouveau (o/n) ? ")
     except:
        print("OK, à la prochaine fois !")
        TryAgain = False
   else:
      if (str.upper(DoOver) == "N"):
        TryAgain = False
except KeyboardInterrupt:
   print("Vous avez appuyé sur Ctrl+C !")
   print("À la prochaine fois !")
   TryAgain = False
else:
   print(Value)
   TryAgain = False

Ce code commence par créer une boucle de saisie. Cette méthode est en fait assez courante, car vous ne voulez pas que l’application se termine chaque fois qu’il y a une erreur de saisie. Cette boucle est simplifiée, et, normalement, vous devriez créer une fonction séparée pour ce code.

Lorsque la boucle démarre, l’application demande à l’utilisateur de taper un nombre entier. Il peut s’agir de n’importe quelle valeur entière. Si l’utilisateur entre un nombre qui n’est pas un entier, ou encore s’il appuie sur Ctrl + C, Cmd + C ou une autre combinaison de touches d’interruption, le code de gestion des exceptions se déclenche. Sinon, l’application affiche la valeur fournie par l’utilisateur, et la variable TryAgain est mise à la valeur False, ce qui termine la boucle.

Une exception ValueError peut se produire lorsque l’utilisateur fait une erreur. Comme vous ne connaissez pas la raison de celle-ci, vous devez demander à l’utilisateur s’il veut faire une nouvelle tentative. Bien entendu, répéter cette procédure pourrait générer une autre exception. Le bloc de code intérieur try…except gère cette seconde saisie.

Notez l’emploi de la fonction str.upper() à la suite de la question posée à l’utilisateur. Elle permet de répondre par o ou par O sans avoir à distinguer minuscule et majuscule. Chaque fois que vous demandez à l’utilisateur de répondre de cette manière, convertir ce qu’il tape en majuscule est toujours une bonne idée, car cela vous permet de n’effectuer qu’une seule comparaison et réduit le nombre d’erreurs potentielles.

L’exception KeyboardInterrupt affiche deux messages, puis quitte automatiquement la boucle en affectant à la variable TryAgain la valeur False. Cette exception ne se produit que si l’utilisateur appuie sur une certaine combinaison de touches destinée à terminer l’application. Dans ce cas, cela signifie très certainement que l’utilisateur ne veut pas continuer.

❷ Cliquez sur Exécuter la cellule.

Python demande de taper un nombre entier.

❸ Tapez Bonjour et appuyez sur Entrée.

L’application affiche un message d’erreur et vous demande si vous voulez essayer à nouveau.

❹ Tapez O et appuyez sur Entrée.

L’application vous demande à nouveau de saisir un nombre entier (voir la Figure 8.7).

[picture]

Figure 8.7 Utiliser une boucle permet de récupérer l’erreur de saisie.

❺ Tapez 5.5 et appuyez sur Entrée.

L’application affiche à nouveau son message d’erreur et vous demande si vous voulez essayer à nouveau.

❻ Choisissez Exécution → Interrompre l’exécution pour Colab ou Kernel → Interrupt pour Jupyter Notebook pour interrompre l’application.

L’application se termine en affichant « OK, à la prochaine fois ! » . Vous noterez que le message est celui de l’exception interne. L’application n’arrive jamais à l’exception externe parce que le gestionnaire d’exception interne fournit un traitement générique des exceptions.

❼ Cliquez sur Exécuter la cellule.

Python demande de taper un nombre entier.

❽ Choisissez Exécution → Interrompre l’exécution pour Colab ou Kernel → Interrupt pour Jupyter Notebook pour interrompre l’application, saisissez 5.5 et appuyez sur Entrée.

L’application se termine en affichant « Vous avez appuyé sur Ctrl+C ! » . Vous remarquerez que le message est celui de l’exception externe. Aux Étapes 6 et 8, l’utilisateur termine l’application en appuyant sur une touche d’interruption. Toutefois, l’application utilise deux gestionnaires d’exception différents pour résoudre le problème.

Lever des exceptions

Jusqu’ici, les exemples de ce chapitre ont réagi à des exceptions. Quelque chose se produit, et l’application fournit le support d’un gestionnaire d’exception pour traiter cet événement. Cependant, dans certaines circonstances, vous ne savez pas comment gérer tel ou tel événement problématique au cours du processus de développement. Par exemple, vous ne pouvez pas traiter une erreur à un certain niveau, et vous devez donc la transmettre à un autre niveau. Dit autrement, il existe des situations dans lesquelles votre application doit générer une exception. On appelle souvent cela lever une exception (ou parfois propager une exception). Les sections qui suivent décrivent quelques scénarios courants dans lesquels vous avez à lever des exceptions de manière spécifique.

Lever des exceptions lors de conditions exceptionnelles

Cet exemple vous montre comment lever une exception simple, ne réclamant rien de spécial. Les étapes qui suivent créent l’exception, puis la gèrent immédiatement.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne :

try:
   raise ValueError
except ValueError:
   print("Exception ValueError !")

Évidemment, vous n’allez jamais écrire du code tel que celui-ci. Mais ce petit exemple vous montre comment les choses se passent au niveau le plus basique. Dans ce cas, l’appel à raise apparaît à l’intérieur d’un bloc try…except. Il fournit simplement le nom de l’exception qui a été levée (ou propagée). Vous pouvez également fournir des arguments dans la sortie pour fournir des informations complémentaires.

Remarquez que le bloc try…except ne contient pas de clause else, simplement parce qu’il n’y a rien de particulier à faire après l’appel. Même si cette forme est rare, elle est possible. La clause else est optionnelle, même si vous devriez pratiquement toujours en ajouter au moins une.

❷ Cliquez sur Exécuter la cellule.

Python affiche le texte de l’exception attendue, ValueError Exception !

Passer à l’appelant des informations sur une erreur

Python fournit un système de gestion des erreurs exceptionnellement souple en ce que vous pouvez passer des informations à l’appelant (le code qui appelle votre code) quelle que soit l’exception concernée. Bien entendu, cet appelant peut ne pas savoir que cette information est disponible, ce qui pose d’autres questions. Si vous travaillez avec le code de quelqu’un d’autre et que vous ne saviez pas comment les choses se passent, il vous reste toujours la solution consistant à faire appel à la technique décrite plus haut dans l’encadré « Obtenir la liste des arguments d’une exception » .

Vous pouvez vous demander s’il est possible de communiquer de meilleures informations lorsque vous travaillez avec l’exception ValueError, plutôt qu’avec une exception fournie nativement par Python. Les étapes qui suivent vous montrent comment modifier la sortie pour obtenir un tel résultat.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne :

try:
  Ex = ValueError()
  Ex.strerror = "La valeur doit être comprise entre 1 et 10."
  raise Ex
except ValueError as e:
  print("Exception ValueError !", e.strerror)

L’exception ValueError ne fournit normalement pas un attribut appelé strerror, mais vous pouvez l’ajouter simplement en lui affectant une valeur comme dans cet exemple. Lorsque l’exception est levée, la clause except fonctionne comme d’habitude, si ce n’est qu’elle peut accéder à l’attribut via e, puis afficher l’information correspondante.

❷ Cliquez sur Exécuter la cellule.

Python affiche une exception ValueError avec un message supplémentaire, comme cela est illustré à la Figure 8.8.

[picture]

Figure 8.8 Il est possible d’ajouter des informations sur une erreur à toute exception.

Créer et utiliser des exceptions personnalisées

Python fournit une grande quantité d’exceptions standard que vous devriez utiliser chaque fois que c’est possible. Ces exceptions sont incroyablement souples, et vous pouvez même les modifier en fonction de vos besoins (dans des limites raisonnables et justifiées, bien entendu). Par exemple, la section précédente vous montre comment modifier une exception ValueError pour afficher des informations complémentaires. Cependant, il peut arriver qu’aucune exception standard ne réponde à votre problème. Par exemple, le nom de l’exception ne dit rien à l’utilisateur qui puisse le renseigner sur ce qui se passe. Ou bien vous travaillez sur une base de données spécifique, ou encore avec un certain service. Dans de tels cas, vous pouvez donc avoir besoin de créer une exception personnalisée.

L’exemple qui suit peut paraître un peu compliqué, car vous n’avez pas encore découvert la notion de classe. Nous y reviendrons en détail dans le Chapitre 5. Vous pouvez bien entendu passer cette section afin d’y revenir tranquillement plus tard.

L’exemple de cette section illustre une méthode rapide pour créer vos propres exceptions. Pour cela, vous devez créer une classe utilisant comme point de départ une exception existante. Pour rendre les choses un peu plus faciles, cet exemple crée une exception basée sur les fonctionnalités offertes par l’exception ValueError. L’avantage de cette approche, par rapport à celle proposée plus haut dans la section « Passer à l’appelant des informations sur une erreur » , est qu’elle permet de dire à n’importe quelle personne qui vous suit ce qu’est l’ajout à l’exception ValueError. Elle rend également l’utilisation de l’exception modifiée plus simple.

❶ Tapez le code suivant dans le carnet de notes – en appuyant sur la touche Entrée après chaque ligne :

class CustomValueError(ValueError):
   def __init__(self, arg):
     self.strerror = arg
     self.args = {arg}
try:
   raise CustomValueError("La valeur doit être comprise entre 1
et 10.")
except CustomValueError as e:
   print("Exception CustomValueError !", e.strerror)

Pour l’essentiel, vous retrouvez ici la fonctionnalité développée dans le code de la section « Passer à l’appelant des informations sur une erreur » . Cependant, elle place la même erreur à la fois dans strerror et dans args, de manière à ce que le développeur ait accès aux deux (comme cela devrait normalement se produire).

Le code commence par créer une classe appelée CustomValueError. Celle-ci prend comme point de départ la valeur de l’exception ValueError. La fonction __init__() fournit le moyen de créer une nouvelle instance de cette classe. Vous pourriez à ce stade vous figurer une classe comme une sorte de plan d’architecte, et l’instance comme le bâtiment construit à partir de ce plan.

L’attribut strerror prend la valeur qui lui est directement affectée, tandis que args la reçoit sous forme d’un tableau. Le membre args contient normalement un tableau de toutes les valeurs d’exception sous la forme de chaînes. Cette procédure est donc standard, même si dans le cas présent args ne contient qu’une seule valeur.

Le code servant à utiliser l’exception est ensuite considérablement plus simple que la modification directe de la valeur de ValueError. Tout ce que vous avez à faire, c’est d’appeler raise avec le nom de l’exception et les arguments que vous voulez lui passer. Tout cela tient en une ligne.

❷ Cliquez sur Exécuter la cellule.

L’application affiche un message d’erreur similaire à celui illustré à la Figure 8.8, sauf que cette fois, elle signale qu’il s’agit d’un message d’erreur personnalisé en indiquant, « CustomValueError Exception ! La valeur doit être comprise entre 1 et 10 » .

Utiliser la clause finally

Normalement, vous voulez gérer une exception qui se produit sans provoquer un plantage de l’application. Cependant, il peut arriver que vous ne puissiez rien faire pour réparer les dommages. À ce stade, votre objectif est, en quelque sorte, que l’application atterrisse en douceur, en d’autres termes de refermer les fichiers qui ont pu être ouverts et d’effectuer d’autres tâches de cette nature. Ceci permet d’éviter d’endommager des données ou de laisser le système dans un état instable. Ce qui est le minimum qu’on puisse demander à une application…

La clause finally fait partie de cette stratégie. Vous l’utilisez pour réaliser les tâches essentielles de dernière minute. Normalement, cette clause est assez courte et utilise uniquement des appels qui sont supposés pouvoir réussir sans poser davantage de problèmes. Il est essentiel de refermer les fichiers, de déconnecter l’utilisateur, d’effectuer quelques autres tâches ménagères, puis de laisser l’application se terminer avant qu’il n’arrive quelque chose d’absolument terrible (comme un crash violent du système). Cette nécessité étant posée, et bien mémorisée dans votre esprit, les étapes qui suivent vous proposent un exemple simple d’utilisation de la clause finally :

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne :

import sys
try:
  raise ValueError
  print("Exception levée.")
except ValueError:
   print("Exception ValueError !")
   sys.exit()
finally:
   print("Prenez soin des détails de dernière minute.")

print("Ce code ne sera jamais exécuté.")

Dans cet exemple, le code lève une exception ValueError. La clause except s’exécute normalement dans ce genre de circonstances. L’appel à sys.exit() signifie que l’application se termine une fois l’exception gérée. Dans ce cas, la sortie de l’application s’effectue directement, ce qui fait que la fonction print() finale n’est jamais exécutée.

Le code associé à la clause finally est toujours exécuté. Le fait que l’exception se produise ou non n’a aucune importance dans ce cas. Le code que vous placez dans ce bloc doit donc avoir un caractère tout à fait général, puisque vous voulez qu’il soit exécuté systématiquement. Par exemple, si vous travaillez avec un fichier, vous placerez ici le code qui le referme pour vous assurer que les données qu’il contient seront en bon état sur le disque dur.

❷ Cliquez sur Exécuter la cellule.

L’application affiche le message de la clause except ainsi que celui de la clause finally (voir la Figure 8.9). L’appel à sys.exit() empêche tout autre code de s’exécuter.

[picture]

Figure 8.9 Utilisez la clause finally pour effectuer des actions spécifiques avant que l’application ne se termine.

Notez que ce n’est pas une sortie normale, si bien que le notebook Jupyter affiche des informations supplémentaires sous forme de trace-back. Colab n’affiche pas ces informations, mais vous pouvez les afficher en utilisant %tb. Il suffit de taper %tb dans une cellule vide et de cliquer sur Exécuter la cellule pour voir les informations. Lorsque vous utilisez d’autres IDE, l’application peut simplement se terminer sans afficher d’informations supplémentaires.

❸ Transformez l’appel raise ValueError en commentaire en le faisant précéder de deux signes dièse, comme ceci :

 ## raise ValueError

Supprimer l’exception permet de mieux montrer le fonctionnement de la clause finally. L’utilisation de deux caractères dièse fera ressortir le code commenté et le rendra plus facile à rechercher.

❹ Cliquez sur Exécuter la cellule.

L’application affiche une série de messages, y compris celui de la clause finally, comme l’illustre la Figure 8.10. Ceci prouve bien que la clause finally est toujours exécutée, ce qui explique pourquoi il faut l’utiliser avec un luxe de précautions.

[picture]

Figure 8.10 Il est essentiel de se rappeler que la clause finally est toujours exécutée.

Partie 3

Effectuer des tâches courantes

Dans cette partie…

Utiliser des packages au lieu de coder.

Ajouter le traitement du langage aux applications.

Créer des listes de choses à faire.

Définir des collections d’objets.

Travailler avec des classes.

DANS CE CHAPITRE

Organiser votre code.

•

Ajouter à votre application du code provenant de sources extérieures.

•

Localiser et afficher les bibliothèques de code sur votre disque dur.

•

Accéder à la documentation des bibliothèques.

Chapitre 9

Interagir avec des packages

Les exemples de ce livre sont modestes, et les fonctionnalités qu’ils proposent sont par conséquent limitées. Même des applications simples du monde réel contiennent facilement des milliers de lignes de code. En fait, les applications qui contiennent des millions de lignes de code sont relativement courantes. Imaginez maintenant que vous travaillez sur un fichier d’une taille suffisante pour comporter des millions de lignes de code. Vous n’arriverez très certainement jamais à rien, ou pas à grand-chose. Vous avez donc besoin d’organiser votre propre code en un ensemble de parties qui seront plus faciles à gérer, comme les exemples de ce livre. La solution offerte par Python consiste à placer tout ce code dans des groupes appelés packages. Les regroupements couramment utilisés qui contiennent du code source pour des besoins génériques et plusieurs packages sont appelés bibliothèques.

Les packages sont stockés dans des fichiers séparés. Pour pouvoir utiliser un package, vous devez demander à Python de le charger dans l’application courante. Ce processus est appelé importation. Vous importez donc un package ou une bibliothèque pour utiliser le code qu’il ou elle contient. Quelques exemples de ce livre ont déjà fait appel à cette technique en appelant l’instruction import. Dans ce chapitre, vous allez l’étudier plus en détail afin de savoir comment l’utiliser.

Le code des bibliothèques est généralement bien documenté. Certains développeurs pensent qu’ils n’ont pas besoin d’y jeter un coup d’œil, ce qui n’est pas totalement faux. Vous n’avez en effet pas besoin d’ouvrir ces fichiers pour vous en servir. Cependant, la lecture de ce code peut être intéressante pour comprendre son fonctionnement. De plus, cela peut vous aider à progresser et à découvrir des techniques auxquelles vous n’auriez pas songé autrement. Ce genre de lecture est donc certes facultatif, mais cela peut grandement vous aider.

Vous n’avez pas à saisir manuellement le code source de ce chapitre. En fait, l’utilisation de la source téléchargeable est beaucoup plus facile. Vous trouverez le code source de ce chapitre dans les fichiers Chapitre 9 Interagir avec les packages.ipynb et Packages.ipynb. Consultez l’introduction pour savoir comment trouver ces fichiers source.

Créer des regroupements de code

Les regroupements de code sous forme de packages et de bibliothèques font partie des compétences élémentaires, même avec les programmes les plus simples en Python. Certaines bibliothèques sont automatiquement importées, car vous les utilisez pour chaque application. D’autres bibliothèques nécessitent que vous les importiez individuellement. Dans le cadre de la configuration initiale, Python a créé un pointeur vers les bibliothèques à usage général qu’il utilise. C’est pourquoi il vous suffit d’ajouter une instruction import avec le nom de la bibliothèque pour que Python puisse la trouver. Toutefois, il est utile de savoir comment localiser les fichiers sur le disque au cas où vous devriez les mettre à jour ou si vous souhaitez ajouter vos propres packages et bibliothèques à la liste des fichiers que Python peut employer.

Quelle est la différence entre les modules et les packages ?

Dans certains livres, vous constaterez que le terme module est employé à la place de package. Les gens utilisent souvent les deux termes de manière interchangeable, car il n’y a pas vraiment de différence entre eux. La plupart des livres définissent un module comme un fichier unique contenant du code avec une extension .py. Un package est un groupe de modules souvent organisés de manière hiérarchique (bien que l’organisation hiérarchique ne soit pas une exigence du langage Python). Même s’il existe de légères différences entre les deux termes, vous pouvez les considérer comme des méthodes permettant de placer du code dans un conteneur facile à utiliser. Ce chapitre utilise donc le terme « package », car c’est celui que vous voyez le plus souvent dans la documentation officielle. Si vous souhaitez obtenir des informations détaillées sur les différences entre modules et packages, vous pouvez consulter le site https://learnpython.com/blog/python-modules-packages-libraries-frameworks/.

La seule chose que vous devez savoir faire, c’est obtenir et utiliser la documentation de la bibliothèque Python. Ce chapitre vous montre comment réaliser cela dans le cadre du processus de création d’une application. Dans les sections qui suivent, vous en apprendrez davantage sur les regroupements de codes utilisés par Python, y compris sur la manière d’interagir avec eux à un niveau élémentaire.

L’exemple de la section « Créer votre premier package » qui suit ne fonctionne bien qu’avec Jupyter Notebook. Les utilisateurs de Google Colab peuvent toujours tenter l’expérience, mais le fait de travailler en ligne rend cet exemple particulier assez difficile à réaliser (c’est le seul du livre). Le problème est que vous devez télécharger le fichier en local, le renommer parce que Colab confond les noms, puis le charger dans le bon répertoire en ligne, qui peut être extrêmement difficile à trouver. Par conséquent, les étapes se concentreront sur l’utilisation de Jupyter Notebook dans ce cas particulier.

Créer votre premier package

Il est important de regrouper des morceaux de code de manière à rendre celui-ci plus facile à utiliser, à modifier et à comprendre. Plus une application prend de l’ampleur, et plus gérer tout ce code dans un seul fichier devient difficile. À un certain stade, cela devient même impossible tellement le fichier est devenu volumineux.

Le mot code est employé ici dans un sens très large. Le regroupement de code peut concerner :

• des classes ;

• des fonctions ;

• des variables ;

• du code exécutable.

La collection des classes, des fonctions, des variables et du code exécutable contenus dans un package est aussi connue sous le nom d’attributs. Un package possède des attributs auxquels vous pouvez accéder par leur nom. Les sections qui suivent précisent ce mode de fonctionnement.

Le code exécutable peut en fait être écrit dans un autre langage que Python. Par exemple, il est assez courant de trouver des modules qui sont écrits en C++. La raison principale est que cela peut rendre les applications Python plus rapides, moins gourmandes en ressources et mieux adaptées à des plateformes variées. Cependant, cela rend en même temps les applications moins portables, à moins de disposer d’une version de ce code exécutable pour chaque plateforme à laquelle votre application est destinée. De plus, ce « double langage » peut être plus difficile à gérer, puisque vous devez collaborer avec des développeurs capables de parler chacun des langages de programmation utilisés dans l’application.

La manière la plus courante de créer un package consiste à définir un fichier distinct (ou plusieurs) qui contient le code que vous voulez séparer du reste de l’application. Par exemple, vous pourriez placer dans un package une routine d’impression dont une application se sert à de nombreuses reprises. La routine d’impression n’est pas conçue pour fonctionner seule, mais elle fait partie de l’application dans son ensemble. Il est donc utile de la séparer du reste du code, et, de plus, intéressant de le faire puisque vous pourriez en avoir besoin dans d’autres applications. La possibilité de réutiliser du code est un des atouts majeurs des packages.

Pour rendre les choses plus faciles à comprendre, les exemples de ce chapitre font appel à un même package. Celui-ci ne fait rien de sensationnel. Il sert juste de base pour montrer les principes d’utilisation des packages. Ouvrez un projet Python 3 Notebook, nommez-le Chapitre 9 Packages, et saisissez le code présenté dans le Listing 9.1. Une fois cette tâche terminée, téléchargez le code sous la forme d’un nouveau fichier Python nommé Chapitre 9 Packagesl.py en choisissant File → Download as → Python (.py) dans Notebook.

Listing 9.1

Package de démonstration très simple.

   def SayHello(Name):
      print("Bonjour ", Name)
      return
   def SayGoodbye(Name):
      print("Au revoir ", Name)
      return

Cet exemple utilise un nom de fichier Notebook qui contient des espaces, or les noms des packages ne peuvent pas avoir d’espaces ni de caractères spéciaux, sinon ils ne fonctionnent pas correctement. Par conséquent, vous devez renommer Chapitre 9 Packagesl.py en Chapitre_11_Packages.py si vous voulez qu’il fonctionne avec le reste de l’exemple de ce chapitre.

Vous devrez peut-être copier le fichier résultant dans votre dossier Python PLN existant, selon l’endroit où votre navigateur télécharge normalement les fichiers. Si tout a été fait correctement, le tableau de bord de votre Notebook devrait contenir une copie du fichier, comme cela est illustré à la Figure 9.1 L’emploi de la fonction d’importation de Notebook facilite considérablement les choses. Vous noterez que le nom du fichier .py est Chapitre_11_Packages.py.

[picture]

Figure 9.1 Assurez-vous de placer une copie du package dans votre dossier Python PLN.

Ce code contient deux fonctions très simples, l’une pour dire bonjour (SayHello()) et l’autre pour dire au revoir (SayGoodbye()). Dans les deux cas, vous fournissez juste un nom à afficher. Une fois son message affiché, chaque fonction renvoie le contrôle à l’appelant. Évidemment, les fonctions que vous allez créer seront bien plus complexes, mais celles-ci suffisent au propos de ce chapitre.

Changer le dossier courant de Python

Le dossier que Python utilise pour accéder au code influe sur les modules que vous pouvez importer. Les bibliothèques de Python lui-même sont toujours incluses dans la liste des emplacements auxquels il peut accéder, mais Python ne sait rien du dossier dont vous vous servez pour enregistrer votre code, à moins que vous ne lui indiquiez où il doit regarder. La méthode la plus simple consiste à modifier le dossier courant de Python pour qu’il pointe vers votre propre code. Pour cela, procédez ainsi :

❶ Ouvrez un nouveau notebook.

Vous pouvez également utiliser le fichier source téléchargeable, Chapite 11 Interagir avec les packages.ipynb.

❷ Tapez import os et appuyez sur Entrée.

Cette action importe la bibliothèque os de Python. Vous devez réaliser cette importation afin de modifier l’emplacement de travail utilisé pour ce chapitre.

❸ Tapez print(os.getcwd()) et cliquez sur Exécuter la cellule.

Vous voyez le répertoire de travail actuel (CWD) que Python utilise pour obtenir le code local.

❹ Dans une nouvelle cellule, saisissez for entry in os.listdir() : print(entry) et cliquez sur Exécuter la cellule.

Vous voyez une liste des entrées du répertoire. Cette liste vous permet de déterminer si le fichier dont vous avez besoin se trouve dans le répertoire de travail actuel. Si ce n’est pas le cas, vous devez changer de répertoire pour atteindre un emplacement qui contient le fichier requis.

Pour changer de répertoire vers un nouvel emplacement, on utilise la méthode os.chdir() et on inclut le nouvel emplacement sous forme de chaîne de caractères, par exemple os.chdir(‘C : \MyDir’). Cependant, vous constatez normalement avec Notebook que le répertoire de travail actuel contient les fichiers de votre projet en cours.

Le code de cet encadré fonctionne aussi bien avec Google Colab qu’avec Jupyter Notebook, mais les résultats de Google Colab sont moins utiles. Les fichiers exécutables Python de Colab sont normalement situés dans le répertoire /usr/local/bin/python, que vous pouvez déterminer à l’aide de la commande ! which python, où l’opérateur ! (bang) (https://anaconda.zendesk.com/hc/en-us/articles/360023858254-Execu-ting-Terminal-Commands-in-Jupyter-Notebooks) permet d’accéder au shell du système, et la commande which (https://linuxize.com/post/linux-which-command/) fournit l’emplacement d’un exécutable particulier, en l’occurrence python.

Comprendre les types de packages

La documentation de Python est immense. En fait, vous n’en utiliserez probablement jamais qu’une petite fraction, même pour les applications les plus exigeantes. Ce n’est pas que Python lui-même soit si énorme, car le langage est en fait assez concis par rapport à de nombreux autres langages. L’immensité vient du système Python de packages et de bibliothèques qui font tout, du travail scientifique pointu à l’intelligence artificielle, en passant par l’exploration spatiale, la modélisation biologique, tout ce que vous pouvez imaginer et beaucoup de choses auxquelles vous ne pensez pas. Cependant, tous ces packages ne sont pas disponibles en permanence. Vous devez donc avoir une idée du type de packages pris en charge par Python et de l’endroit où vous pouvez les trouver (par ordre de préférence) :

• Intégré : les packages intégrés répondent aux besoins les plus courants. Lorsque vous travaillez avec Jupyter Notebook en ayant installé Anaconda (un package de développement contenant plusieurs outils, dont conda, que vous pouvez trouver à https://www.anaconda.com/), vous les trouvez dans le dossier \Users\<nom_uti-lisateur>\Adaconda3\Lib sur votre système, et pour les utiliser, il suffit de les importer dans votre application. Localiser les packages dans Google Colab est un peu plus difficile. Vous devrez peut-être effectuer une petite recherche. Toutefois, si vous utilisez Python 3.7, vous pouvez employer cette commande (sur une seule ligne) dans une cellule Colab afin d’obtenir une liste des packages intégrés (voir l’encadré « Changer le dossier courant de Python » pour plus de détails sur l’utilisation des commandes os) :

for entry in os.listdir("/usr/local/lib/python3.7/dist-packages") : print(entry)

• Personnalisé : comme nous en faisons la démonstration dans ce chapitre, vous pouvez créer vos propres packages et les utiliser selon vos besoins. Ils apparaissent sur votre disque dur, normalement dans le même répertoire que le code de votre projet, et vous les importez simplement dans votre application.

• Conda : vous pouvez trouver une multitude de packages spécialement conçus pour Anaconda (Jupyter Notebook). La plupart de ces packages sont disponibles à l’adresse https://anaconda.org/mutirri/repo. Avant de pouvoir vous servir de ces packages, vous devez les installer en utilisant l’utilitaire conda sur la ligne de commande d’Anaconda (voir la section « Installer des packages conda » de ce chapitre). Une fois le package installé, on l’utilise comme n’importe quel package intégré. Si vous utilisez Colab, vous n’aurez pas accès à l’utilitaire conda (voir la section « Et pourquoi conda est-il absent de Colab ? » de ce chapitre).

• Non-conda : ce n’est pas parce qu’un package n’est pas spécifiquement conçu pour être utilisé avec Anaconda que vous ne pouvez pas l’employer. Vous trouverez une grande quantité de packages tiers qui fournissent des fonctionnalités importantes. Pour installer ces packages, on se sert de l’utilitaire pip (voir les sections « Installer des packages à l’aide de pip » et « Installer des packages à l’aide de la commande magique %pip », plus loin dans ce chapitre). Après avoir installé le package, vous devrez peut-être effectuer une configuration supplémentaire, en suivant les instructions du créateur du package. En général, lorsque le package est configuré, on l’utilise comme n’importe quel package intégré.

Cache des packages d’Anaconda

Cette section ne s’applique pas à Google Colab, et vous pouvez donc la sauter si vous travaillez en ligne ou si le cache des packages ne vous passionne pas. Anaconda fournit un cache de packages qui réside en dehors de la bibliothèque Python. Ce cache permet d’interagir facilement avec les packages spécifiques à Anaconda en utilisant l’utilitaire de ligne de commande conda. Pour voir comment utiliser ce cache de packages :

❶ Ouvrez une invite de commande Anaconda ou une fenêtre de terminal.

On accède à cette fonction par l’entrée Anaconda Prompt dans le dossier Anaconda3 du système.

❷ Saisissez conda list et appuyez sur Entrée.

Vous voyez une liste des packages qui sont installés.

Notez que la sortie affiche le nom du package tel que l’on y accède depuis Anaconda, la version du package et la version Python associée. Toutes ces informations sont utiles pour gérer les packages. La liste suivante fournit les commandes conda essentielles pour gérer vos packages :

• conda clean : supprime les packages que vous n’utilisez pas.

• conda config : configure les paramètres du cache des packages.

• conda create : définit un nouvel environnement conda qui contient une liste spécifique de packages, ce qui facilite la gestion des packages et peut améliorer la vitesse des applications.

• conda help : affiche une liste complète des commandes de conda.

• conda info : affiche les informations de configuration de conda, qui incluent des détails sur l’endroit où conda stocke les packages et où il recherche les nouveaux packages.

• conda install : installe un ou plusieurs packages dans l’environnement conda par défaut ou l’environnement spécifié.

• conda list : produit une liste de packages conda avec différents niveaux de détails. Vous pouvez spécifier les packages à lister et les environnements à rechercher.

• conda remove : supprime un ou plusieurs packages du cache des packages.

• conda search : recherche des packages spécifiques en utilisant les critères de recherche que vous fournissez.

• conda update : met à jour une partie ou la totalité des packages dans le cache des packages.

Ces commandes peuvent faire beaucoup plus que vous ne le pensez. Il est bien entendu impossible de mémoriser toutes ces informations, et vous pouvez donc vous aider du paramètre de ligne de commande --help pour obtenir tous les détails sur l’utilisation d’une commande particulière. Par exemple, pour en savoir plus sur conda list, saisissez conda list --help et appuyez sur Entrée.

Importer des packages

Pour pouvoir utiliser un package, vous devez l’importer. Python place le code des packages avec le reste de l’application en mémoire, comme si vous aviez créé un très gros fichier. Rien n’est modifié dans le fichier tel qu’il est enregistré sur le disque. Toutes les sources restent bien distinctes. Seule change la manière dont Python « voit » le code.

Il y a deux façons d’importer des packages. Chaque technique est employée dans des circonstances spécifiques :

• import : vous utilisez l’instruction import lorsque vous voulez importer un package entier. C’est la méthode la plus courante, car elle permet de gagner du temps et ne nécessite qu’une seule ligne de code. Cependant, cette approche consomme davantage de ressources mémoire que la technique suivante.

• from...import : vous utilisez cette instruction lorsque vous voulez importer sélectivement des attributs spécifiques dans un package. Ceci permet de gagner des ressources, mais au prix d’une plus grande complexité. De plus, si vous essayez d’utiliser un attribut qui n’a pas été importé, Python provoquera une erreur. Certes, le package contient bien l’attribut voulu, mais Python ne peut pas le voir, car vous avez oublié de l’importer.

Maintenant que vous avez une meilleure idée de l’importation des packages, il est temps d’y regarder de plus près. Les sections qui suivent vont vous aider à vous familiariser avec les deux techniques d’importation de Python.

Utiliser l’instruction import

L’instruction import est la méthode la plus courante pour importer un package dans Python. Cette approche est rapide, et elle s’assure que la totalité du contenu du package est disponible dans Python. Les étapes suivantes vous permettent de commencer à utiliser l’instruction import :

❶ Ouvrez un nouveau notebook, si vous n’en avez pas déjà créé un pour le code dans l’encadré « Changer le dossier courant de Python » .

Vous pouvez également utiliser le fichier téléchargeable Chapitre 9 Interagir avec les packages.ipynb.

❷ Changez les répertoires, si nécessaire, jusqu’au répertoire du code source téléchargeable.

Généralement, Notebook vous place dans le bon répertoire pour utiliser les fichiers de code source, et vous n’aurez donc pas besoin d’effectuer cette étape. Reportez-vous à l’encadré « Changer le dossier courant de Python » .

❸ Dans une nouvelle cellule, tapez import Chapitre_11_Packages et appuyez sur Entrée.

Python importe le contenu du fichier Chapitre_11_Packages.py, celui que vous avez créé plus haut dans ce chapitre (voir « Créer votre premier package » ). La totalité de la bibliothèque est maintenant prête à être utilisée.

Il est important de savoir que Python crée aussi un cache où il stocke ce package. Ce cache est enregistré dans un sous-dossier appelé __pycache__. Ouvrez le dossier de votre application, et vous pourrez remarquer sa présence. En revanche, vous devrez le supprimer si vous voulez éditer votre package. Sinon, Python continuera à utiliser la version qu’il a mise en cache, et non pas celle que vous avez mise à jour.

Le nom du fichier en cache comprend la version de Python pour laquelle il est destiné, et il s’agit donc de Chapitre_11_Packages.cpython-39.pyc dans notre exemple. Le 39 dans le nom de fichier signifie que ce fichier est spécifique à Python 3.9. Un fichier .pyc représente un fichier Python compilé, qui est utilisé pour améliorer la vitesse des applications.

❹ Tapez dir(Chapitre_11_Packages) et cliquez sur Run this cell.

Vous allez voir un listing correspondant au contenu du package, dont notamment les fonctions SayHello() et SayGoodbye() qui sont illustrées à la Figure 9.2. Nous reviendrons sur les autres entrées affichées ici dans la section « Voir le contenu d’un package » , plus loin dans ce chapitre.

[picture]

Figure 9.2 Un listing du répertoire montre que Python a bien importé les deux fonctions du package.

❺ Dans une nouvelle cellule, tapez Chapitre_11_Packages.SayHello("Jean-Pierre") et appuyez sur Entrée.

Vous devez faire précéder le nom de l’attribut, ici SayHello(), de celui du package, Chapitre_11_Packages. Les deux éléments sont séparés par un point. Tout appel à un package importé doit suivre ce schéma.

❻ Tapez maintenant Chapitre_11_Packages.SayGoodbye("Marie") et cliquez sur Run cell.

Les fonctions SayHello() et SayGoodbye() produisent le texte attendu.

Utiliser l’instruction from…import

L’instruction from...import offre l’avantage de n’importer que les attributs d’un package dont vous avez besoin. Elle consomme donc moins de mémoire et de ressources système que l’instruction import. De plus, elle rend l’utilisation du package un peu plus facile, car certaines commandes, telles que dir(), montrent moins d’informations, ou seulement celles dont vous avez vraiment besoin. Vous obtenez donc uniquement ce que vous voulez et rien d’autre. Les étapes suivantes vous montrent comment employer l’instruction from...import. Toutefois, avant de pouvoir importer Chapitre_11_Packages de manière sélective, vous devez le supprimer de l’environnement, ce qui constitue la première partie du processus suivant.

❶ Tapez le code suivant dans une nouvelle cellule de notebook :

import sys
del sys.modules["Chapitre_11_Packages"]
del Chapitre_11_Packages
dir(Chapitre_11_Packages)

❷ Clicquez sur Run cell.

Vous voyez le message d’erreur illustré à la Figure 9.3. Il est impossible de lister le contenu du package Chapitre_11_Packages, car il n’est plus chargé.

❸ Dans une nouvelle cellule tapez from Chapitre_11_Packages import SayHello et appuyez sur Entrée.

Python importe la fonction SayHello() du package Chapitre_11_ Packages, et uniquement celle-ci.

[picture]

Figure 9.3 La suppression d’un package se fait en deux étapes.

Pour importer les deux fonctions dont vous avez besoin, vous devez créer une liste d’attributs à importer ; les noms peuvent être séparés par des virgules, comme dans l’exemple suivant : from Chapitre_11_ Packages import SayHello, SayGoodbye.

❹ Tapez dir(Chapitre_11_ Packages) et cliquez sur Run cell.

Python affiche un message d’erreur, comme celui qui est illustré précédemment à la Figure 9.3. Python importe uniquement les attributs que vous demandez spécifiquement. Cela signifie que le package Chapitre_11_Packages n’est pas en mémoire, car il n’y a en mémoire que les attributs que vous avez demandés.

❺ Dans une nouvelle cellule, tapez dir(SayHello) et cliquez sur Run cell.

[picture]

Figure 9.4 Utilisez la fonction dir() pour obtenir des informations sur les attributs spécifiques que vous importez.

Cette fois, vous voyez une liste d’attributs qui sont associés à la fonction SayHello(), comme l’illustre la Figure 9.4. À ce stade, il n’est pas important de comprendre comment ces attributs fonctionnent, mais vous en utiliserez certains plus tard dans ce livre.

❻ Dans une nouvelle cellule tapez SayHello("Claire") et cliquez sur Run cell.

La fonction SayHello() produit le résultat escompté.

Lorsque vous importez des attributs en utilisant l’instruction from...import, il n’est pas nécessaire de faire précéder leur nom par un nom de package. Ceci en facilite donc l’accès.

L’emploi de l’instruction from...import peut également provoquer des problèmes. Si deux attributs se trouvant dans deux packages différents possèdent le même nom, vous ne pouvez importer que l’un d’entre eux. L’instruction import évite ce genre de collision, ce qui est important si vous devez importer un grand nombre d’attributs. En résumé, vous devez faire preuve de prudence lorsque vous utilisez l’instruction from...import.

❼ Dans une nouvelle cellule, saisissez SayGoodbye("Antoine") et cliquez sur Run cell.

Vous n’avez importé que la fonction SayHello() si bien que Python ne sait rien de la fonction SayGoodbye(), et il affiche par conséquent un message d’erreur. La nature même de l’instruction from...import peut être une source de problèmes si vous n’avez pas pris toutes vos précautions et que vous supposiez qu’un certain attribut est présent, alors qu’il n’y est pas.

Utilisation de l’instruction import...as

Le fait de devoir faire précéder chaque nom d’attribut du nom Cha-pitre_11_Packages va provoquer au bout d’un moment une crampe dans les doigts, ce que personne ne souhaite. Un nom de package aussi long peut également provoquer des nœuds au cerveau lorsque vous essayez de vous en souvenir. Heureusement, Python se préoccupe de votre santé et propose de soulager vos doigts et votre cerveau grâce à l’instruction import...as. La partie as de l’instruction fournit au package un nom plus court, et donc plus facile à retenir. Les étapes suivantes montrent comment utiliser cette approche.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook :

import Chapitre_11_Packages as BPP
dir(BPP)

❷ Cliquez sur Run cell.

Vous voyez la même liste du contenu du package que celle illustrée à la Figure 9.2, mais vous avez dû taper moins de code pour l’obtenir.

❸ Tapez le code suivant dans une nouvelle cellule de notebook :

BPP.SayHello("Albert")
BPP.SayGoodbye("Diane")

❹ Cliquez sur Run cell.

Les deux fonctions produisent le texte attendu. Cependant, vous avez tapé beaucoup moins de code pour obtenir les résultats.

Trouver des packages

Pour pouvoir utiliser le code d’un package, Python doit être capable de le localiser et de le charger en mémoire, et vous devez donc savoir où se trouve le package. Lorsque vous travaillez avec Jupyter Notebook, les packages résident sur votre disque dur. Si vous ne les trouvez pas sur votre disque dur, vous pouvez télécharger ce dont vous avez besoin. Colab stocke en ligne ses fichiers dans le dossier /usr/local/lib/python3.9/dist-packages (3.9 indiquant la version de Python que vous utilisez). Tout ce qui se trouve sur votre disque local est inaccessible.

Utiliser des variables d’environnement avec Python

Les variables d’environnement indiquent à Python comment il peut se configurer et où chercher des ressources. L’emploi de variables d’environnement est utile lorsque vous devez configurer Python de la même manière de façon régulière. Vous trouverez une liste des variables d’environnement à https://www.geeksforgeeks.org/environment-variables-in-python/ et la façon d’interagir avec elles à https://www.askpython.com/python/environment-variables-in-python. Parmi les variables d’environnement, PYTHONPATH est celle que vous utilisez le plus souvent ; elle fournit une liste de répertoires séparés par des points-virgules ( ; ) dans lesquels vous pouvez rechercher des modules. Cette valeur est stockée dans la variable sys. path de Python.

Les sections suivantes expliquent plus en détail comment trouver les packages déjà disponibles pour Python (la section « Télécharger des packages à partir d’autres sources » , plus loin dans le chapitre, indique comment obtenir les packages auxquels vous ne pouvez pas encore accéder).

Localiser les packages sur le disque

Certaines des informations de cette section ne fonctionnent qu’avec Jupyter Notebook ; elles peuvent ne pas fonctionner avec un IDE basé sur un navigateur, comme Google Colab. Les informations d’emplacement sont stockées sous forme de chemins dans Python. Chaque fois que vous demandez à Python d’importer un package, il examine tous les fichiers de sa liste de chemins pour le trouver. Les informations sur les chemins proviennent de trois sources :

• Variables d’environnement : les variables d’environnement, telles que PYTHONPATH, indiquent à Python où trouver les packages sur le disque (voir l’encadré « Utiliser des variables d’environnement avec Python » pour plus de détails).

• Dossier courant : plus haut dans ce chapitre, vous avez appris que vous pouviez modifier le dossier courant de Python de manière à ce qu’il puisse localiser les packages utilisés par votre application.

• Dossiers par défaut : même si vous n’avez pas défini de variables d’environnement, et que le dossier courant ne contienne pas de package utilisable, Python est tout de même capable de retrouver ses propres bibliothèques dans la liste des dossiers par défaut qui est incluse dans sa propre configuration.

Il est utile de connaître les informations de chemin actuelles, car l’absence de chemin peut faire échouer votre application. Pour obtenir des informations sur le chemin, tapez for p in sys.path : print(p) dans une nouvelle cellule et cliquez sur Run cell. Une liste des informations sur le chemin s’affiche (voir la Figure 9.5). Votre liste peut être différente de celle illustrée à la Figure 9.5, en fonction de votre plateforme, de la version de Python et des fonctionnalités Python que vous avez installées.

L’attribut sys.path est fiable, mais cela ne signifie pas qu’il sait tout sur les chemins d’accès connus de Python. Si vous constatez qu’une certaine information manque alors que vous pensez qu’elle devrait être présente, vous pouvez suivre ces étapes :

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook :

[picture]

Figure 9.5 L’attribut sys.path contient une liste des chemins individuels de votre système.

import os
os.environ['PYTHONPATH'].split(os.pathsep)

Lorsque vous avez défini une variable d’environnement PYTHONPATH, vous voyez une liste d’un ou plusieurs chemins, tels que ['C : \PLN'] ou ['/env/python']. Toutefois, si la variable d’environnement n’est pas définie, un message d’erreur s’affiche à la place.

Vous devez fournir à split() une valeur à rechercher pour diviser une liste d’éléments. La constante os.pathsep (une variable ayant une seule valeur définie qui n’est pas modifiable) définit le séparateur de chemin pour la plateforme actuelle afin que vous puissiez utiliser le même code sur toute plateforme prenant en charge Python.

❷ Cliquez sur Run cell.

Vous voyez la liste des chemins d’accès pour votre plateforme et votre installation.

Vous pouvez également ajouter et supprimer des éléments de sys.path. Par exemple, si vous souhaitez ajouter le répertoire de travail actuel à la liste des packages, vous devez saisir sys.path.append(os.getcwd()) dans la cellule du notebook et cliquer sur Run cell. Lorsque vous listez à nouveau le contenu de sys.path, vous constatez que la nouvelle entrée est ajoutée à la fin de la liste. De la même manière, lorsque vous souhaitez supprimer une entrée, vous devez taper sys.path.remove(os. getcwd()) dans la cellule du notebook et cliquer sur Run cell. L’ajout n’est valable que pendant la session en cours.

Si vous modifiez une variable d’environnement alors que le serveur Jupyter Notebook est en cours d’exécution, vous ne verrez aucun changement dans le notebook. Pour voir les changements, vous devez redémarrer complètement Jupyter Notebook.

Trouver des packages en ligne

Lorsque vous travaillez avec un IDE basé sur un navigateur, vous voyez souvent les résultats de l’utilisation d’un environnement virtuel, c’est-à-dire de la plateforme que l’hôte a choisi d’utiliser, ou vous trouvez simplement les informations dont vous avez besoin à un endroit autre que celui auquel vous vous attendiez. Par conséquent, le code de la section « Localiser les packages sur le disque » peut fonctionner, mais ne pas fournir ce dont vous avez besoin. Par exemple, pour vous assurer que vous pouvez réellement trouver le chemin d’accès à Python, vous devez utiliser la commande ! echo $PYTHONPATH pour Linux et macOS, ou ! echo %PYTHONPATH% pour Windows (voir l’article à https://www3.ntu.edu.sg/home/ehchua/programming/howto/Environment_Variables.html pour plus de détails sur le fonctionnement des variables d’environnement sur les différents systèmes d’exploitation). L’emploi d’une commande erronée pour un hôte particulier n’est pas grave, mais vous ne verrez pas le chemin d’accès à Python (en supposant que l’hôte définisse effectivement la variable d’environnement PYTHONPATH).

Dans de nombreuses situations, vous devez vous appuyer davantage sur les commandes magiques (voir le Chapitre 5) lorsque vous travaillez avec des packages et que vous tentez d’effectuer d’autres tâches en utilisant un IDE basé sur un navigateur. Par exemple, lorsque vous travaillez avec Colab, chaque commande avec l’opérateur ! crée un sous-shell entièrement nouveau, ce qui signifie que les modifications apportées par chaque instruction peuvent être perdues. Par conséquent, si vous voulez, par exemple, changer de répertoire pour charger un package, vous devez utiliser la commande magique %cd plutôt que la commande ! cd, même si a priori les deux devraient fonctionner. Le résultat de la commande ! cd disparaît dès que la commande est terminée, mais la commande magique %cd est permanente au cours de la session. Notez que les fonctions du package os (telles qu’utilisées dans la section précédente) sont souvent efficaces lorsque vous travaillez avec des IDE basés sur un navigateur. Par exemple, vous pouvez utiliser os.chdir(), plutôt que de compter sur la commande magiquee %cd.

Télécharger des packages à partir d’autres sources

Votre copie de Python et le composant associé Jupyter Notebook d’Anaconda ou de Google Colab sont fournis avec un large assortiment de packages qui répondent à de nombreux besoins courants. En fait, à des fins d’expérimentation, les packages déjà installés dans le cadre de votre IDE suffisent la plupart du temps à vos besoins. Bien sûr, il y aura toujours quelqu’un pour penser à une nouvelle façon de faire les choses, ce qui nécessitera un nouveau code et des packages pour le stocker. En outre, certaines techniques de codage sont si ésotériques que l’inclusion de leurs packages dans une installation par défaut consommera inutilement de l’espace alors que la plupart des gens ne les utiliseront sans doute jamais. Par conséquent, vous devrez peut-être installer de temps en temps des packages provenant d’autres sources.

Les deux méthodes les plus courantes pour obtenir de nouveaux packages consistent à employer les utilitaires conda (que vous ne trouvez que dans Jupyter Notebook) ou pip (qui est l’acronyme récursif de Pip Installs Packages). Cependant, vous pouvez trouver des packages qui utilisent d’autres méthodes d’installation avec plus ou moins de succès. On utilise conda et pip à des fins différentes :

• conda : fournit une gestion des packages à usage général pour un large éventail de langages ayant des besoins particuliers dans l’environnement conda.

• pip : fournit des services spécifiques à Python dans n’importe quel environnement. Lorsque vous avez besoin d’un package spécifique à Python, tournez-vous d’abord vers pip. Par exemple, pip vous donne accès au Python Package Index (PyPI) qui se trouve à https://pypi.org/.

Vous pouvez en apprendre plus sur les différences entre conda et pip à https://jakevdp.github.io/blog/2016/08/25/conda-myths-and-misconceptions/ et https://docs.conda.io/projects/conda/en/latest/commands.html#conda-vs-pip-vs-virtualenv-commands. Les sections suivantes traitent de ces deux méthodes.

Ouvrir l’invite Anaconda

Avant de pouvoir faire quelque chose en matière de gestion des packages dans Jupyter Notebook, vous devez ouvrir l’invite Anaconda. L’invite Anaconda est comme n’importe quelle autre invite de commande ou fenêtre de terminal, mais elle fournit des caractéristiques spéciales pour faciliter le travail avec les divers utilitaires de ligne de commande fournis avec Anaconda. Pour ouvrir l’invite, localisez son icône dans le dossier Anaconda3 de votre machine. Par exemple, si vous utilisez un système Windows, vous pouvez ouvrir l’invite Anaconda en choisissant Démarrer → Tous les programmes → Anaconda3 → Anaconda Prompt. L’invite Anaconda Prompt peut mettre un certain temps pour apparaître à l’écran en raison de ses exigences de configuration.

Travailler avec les packages conda

Lorsque vous travaillez avec Jupyter Notebook, vous pouvez effectuer un large éventail de tâches à l’aide de conda, mais certaines sont plus courantes que d’autres. Les sections suivantes décrivent comment effectuer cinq tâches essentielles à l’aide de conda. Vous pouvez obtenir des informations supplémentaires sur cet utilitaire à https://docs.conda.io/projects/conda/en/latest/commands.html. En tapant conda --help dans l’invite Anaconda ou la fenêtre du terminal et en appuyant sur Entrée, vous obtiendrez également un aperçu des informations d’aide.

Afficher les packages conda

Vous pouvez consulter les packages conda de deux façons. La première consiste à créer une liste des packages disponibles, tandis que la seconde permet de rechercher un package spécifique. La liste vous aide à découvrir si un package est déjà installé. La recherche vous aide à découvrir les détails du package installé.

Vous pouvez effectuer des recherches et afficher des listes pour localiser tout ce qui est installé sur un système particulier. Dans ce cas, on utilise les commandes de manière autonome :

conda list
conda search

La sortie de ces commandes est longue et peut défiler jusqu’à la fin de la mémoire tampon de l’écran (ce qui rend impossible le défilement en arrière et l’affichage de tous les résultats). Vous pouvez utiliser conda list | more pour afficher la sortie page par page sur certaines plateformes.

Notez que la sortie indique le nom du package, sa version et la version associée de Python. Vous pouvez utiliser cette sortie pour déterminer si un package est installé sur votre système. Cependant, vous avez parfois besoin de plus d’informations, ce qui nécessite une recherche. Par exemple, si vous voulez savoir ce que vous avez installé à partir du package scikit-learn pour la plateforme Windows 64 bits, vous devez saisir conda search --platform win-64 scikit-learn et appuyer sur Entrée.

Il existe des paramètres pour augmenter considérablement la quantité d’informations que vous recevez. Par exemple, lorsque vous utilisez le paramètre --json, vous obtenez des détails tels qu’une liste complète des dépendances pour le package, si le package est complètement installé, et une URL contenant l’emplacement des packages en ligne. Vous pouvez en apprendre plus sur les recherches conda à https://docs.conda.io/projects/conda/en/latest/commands/search.html.

Utiliser conda info

Même si la commande conda info est normalement associée aux informations d’environnement, vous pouvez également l’employer pour travailler avec des packages. Pour découvrir les spécificités d’un package particulier, il vous suffit d’ajouter le nom du package, par exemple conda info numpy. Malheureusement, l’utilisation de cette commande entraîne souvent une surcharge d’informations,et vous devez donc la raccourcir un peu. Pour ce faire, vous pouvez ajouter un numéro de version après le nom du package, séparé par un signe égal (=), par exemple conda info numpy=1.18.5 pour la version 1.18.5 du package NumPy.

Dans la plupart des cas, vous ne recevez pas d’informations supplémentaires en utilisant le paramètre --verbose avec les packages. Cependant, l’emploi du paramètre --json peut donner quelques informations supplémentaires, et ce commutateur met en forme les informations de telle sorte que vous pouvez les manipuler facilement à l’aide d’un script. Grâce à conda info, vous pouvez découvrir des informations profondément enfouies sur vos packages. Vous en apprendrez plus sur conda info à https://docs.conda.io/projects/conda/en/latest/commands/info.html.

Installer des packages conda

Les listes des packages conda figurent à https://docs.anaconda.com/anaconda/packages/pkg-docs/. Ces listes sont regroupées par numéro de version de Python et par plateforme. Pour déterminer si un package est disponible pour votre version de Python sur une plateforme particulière, il suffit de cliquer sur le lien associé. Vous pouvez utiliser conda pour installer un package à l’aide de la commande conda install documentée à https://docs.conda.io/projects/conda/en/latest/commands/install.html. Par exemple, pour installer SciPy, vous devez saisir conda install scipy et appuyer sur Entrée dans l’invite d’Anaconda.

Mettre à jour des packages conda

Les packages que vous utilisez pour développer des applications peuvent devenir obsolètes avec le temps. Les développeurs qui les maintiennent peuvent ajouter de nouvelles fonctionnalités ou appliquer des corrections de bogues. Le problème avec les mises à jour est qu’elles peuvent entraîner un fonctionnement incorrect de votre application, ou parfois empêcher tout simplement leur exécution. Cependant, c’est généralement une bonne idée de maintenir les packages à jour, ne serait-ce que pour appliquer des corrections de bogues liées à la sécurité. Bien sûr, vous devez savoir que le package nécessite une mise à jour et afin de trouver des packages périmés, vous utiliserez la commande conda search --outdated --names-only, suivie du nom du package que vous voulez vérifier.

Une fois que vous savez que vous devez mettre à jour un package, vous pouvez utiliser la commande conda update. Par exemple, pour mettre à jour le package NumPy, vous devez saisir conda update numpy et appuyer sur Entrée. Peu de packages sont autonomes, si bien que conda présentera une liste d’éléments que vous devez mettre à jour en même temps que NumPy. Tapez y et appuyez sur Entrée pour continuer. La Figure 9.6 présente une séquence typique d’événements du processus de mise à jour.

Vous avez la possibilité de mettre à jour tous les packages en une seule fois. Tapez simplement conda update --all et appuyez sur Entrée pour commencer. Cependant, vous pouvez constater que les dépendances entre les packages ne facilitent pas la mise à jour qui pourrait mieux se passer si vous effectuiez les mises à jour individuellement. De plus, la mise à jour peut prendre beaucoup de temps, si bien qu’il est prudent d’avoir une bonne réserve de café et un exemplaire de Guerre et Paix à portée de main. Vous en apprendrez plus sur les mises à jour de conda à https://docs.conda.io/projects/conda/en/latest/commands/update.html.

Supprimer des packages conda

À un moment donné, vous pouvez décider que vous n’avez plus besoin d’un package conda. Le seul problème est que vous ne savez pas si d’autres packages dépendent du package en question. Comme les dépendances de packages peuvent devenir assez complexes, et que vous voulez être sûr que vos applications continueront à fonctionner, vous devez vérifier quels autres packages dépendent de ce package particulier. Malheureusement, la commande conda info (décrite à https://docs.conda.io/projects/conda/en/latest/commands/info.html) ne vous renseigne que sur les exigences du package, c’est-à-dire sur ce dont il dépend. Dans ces conditions, la meilleure pratique consiste à ne pas désinstaller les packages après les avoir installés.

[picture]

Figure 9.6 Beaucoup d’informations sont affichées au cours du processus de mise à jour.

Cependant, en supposant que vous deviez vraiment supprimer un package, vous pouvez utiliser la commande conda remove décrite à https://docs.conda.io/projects/conda/en/latest/commands/remove.html. Cette commande supprime le package que vous spécifiez, ainsi que tous les packages qui dépendent de ce package. Dans ce cas, la meilleure pratique consiste à utiliser d’abord le paramètre --dry-run pour s’assurer que vous voulez vraiment supprimer le package. Par exemple, si vous voulez supprimer NumPy, saisissez conda remove --dry-run numpy et appuyez sur Entrée. La commande ne sera pas réellement exécutée et conda va simplement montrer ce qui se passerait si vous exécutiez réellement la commande.

Et pourquoi conda est-il absent de Colab ?

Colab n’est pas livré avec conda, car il ne fait pas partie de la suite d’outils Anaconda. Malheureusement, un certain nombre d’outils recommandent l’emploi de conda, notamment ceux associés à la data science et aux statistiques. Par conséquent, il se peut que vous souhaitiez réellement installer conda dans votre environnement Colab. Il s’agit là d’une entreprise complexe qui dépasse le cadre de cet ouvrage. Toutefois, vous trouverez des informations sur l’installation de conda dans Colab à https://towardsdatascience.com/conda-google-colab-75f7c867a522, et il existe même une vidéo YouTube montrant le processus à https://www.youtube.com/watch?v=Rm2ak31u1us. La vidéo indique en fait comment accéder à un script qui peut réduire la complexité de la tâche.

Installer des packages à l’aide de pip

Curieusement, travailler avec pip ressemble beaucoup à ce que l’on peut faire avec conda. Ces deux utilitaires effectuant essentiellement les mêmes tâches, si vous savez comment utiliser l’un, vous savez par conséquent utiliser l’autre. La documentation disponible à https://pip.pypa.io/en/stable/cli/ indique que pip prend en charge pratiquement les mêmes commandes (avec quelques différences de syntaxe). Par exemple, si vous voulez trouver une liste de packages périmés, il faut saisir pip list --outdated et appuyer sur Entrée. Voici une liste des commandes courantes prises en charge par pip :

• check : vérifie que les packages installés ont des dépendances compatibles.

• download : télécharge les packages spécifiés pour une installation ultérieure.

• freeze : affiche les packages installés dans un format comparable au fichier requirements.txt.

• help : affiche un écran d’aide donnant un aperçu des commandes.

• install : installe les packages spécifiés.

• list : liste les packages installés.

• search : recherche en ligne les packages sur PyPI.

• show : affiche des informations sur les packages installés.

• uninstall : désinstalle les packages spécifiés.

Lorsque vous travaillez avec Jupyter Notebook ou Google Colab, vous pouvez utiliser l’opérateur ! pour exploiter pip. Par conséquent, en saisissant !pip list --outdated dans une cellule et en cliquant ensuite sur Exécuter la cellule, vous obtenez une liste des packages périmés sur votre système. L’avantage de cette approche est que vous pouvez créer un notebook qui contient des informations sur votre configuration et auquel vous pouvez vous référer au besoin lorsque vous travaillez sur des projets.

Installer des packages à l’aide de la commande magique %pip

Il existe une version magique de la commande pip qui est accessible depuis Colab et Jupyter Notebook. Elle fonctionne exactement comme la version opérateur ! , sauf que l’on utilise %pip à la place. On emploie cette approche lorsque l’on doit travailler avec pip dans le noyau actuel, afin que le résultat d’une commande pip ne s’évapore pas simplement lorsque la commande se termine. Cette technique permet d’exécuter un certain nombre de commandes pip en séquence et de savoir qu’elles fonctionneront comme prévu.

Vous ne devriez pas être surpris d’apprendre qu’une commande magique %conda existe pour les utilisateurs de Jupyter Notebook. Comme pour %pip, %conda fonctionne de la même manière que la version en ligne de commande.

Voir le contenu d’un package

Python fournit plusieurs méthodes pour voir le contenu d’un package. La plus courante est la fonction dir(), qui renvoie les attributs fournis par le package.

Si vous revenez à la Figure 9.2, la liste contient bien d’autres entrées que nos fonctions SayHello() et SayGoodbye(). Ces attributs sont automatiquement générés par Python et ils servent à effectuer certaines tâches, ou à contenir certaines informations :

• __builtins__ : contient une liste de tous les attributs intégrés qui sont accessibles dans le package. Ce sont ces attributs qui sont automatiquement ajoutés par Python.

• __cached__ : indique le nom et l’emplacement du fichier mis en cache lorsque vous importez le package. Ce chemin d’accès est défini de façon relative par rapport au dossier courant de Python.

• __doc__ : affiche une information d’aide sur le package, du moins si celle-ci a été rédigée. Par exemple, si vous tapez print(os.__ doc__) et que vous appuyiez sur Entrée, Python va afficher l’aide associée à la bibliothèque os (l’emploi de la fonction print() garantit que la sortie est bien présentée).

• __file__ : indique le nom et l’emplacement du package. Le chemin d’accès est défini relativement au dossier courant de Python.

• __initializing__ : détermine si le package est en cours d’initialisation. Normalement, cet attribut renvoie la valeur False, puisque le package devrait déjà avoir terminé son initialisation à ce moment. Cet attribut peut cependant être utile si vous avez besoin d’attendre qu’un package ait terminé son chargement avant d’importer un autre package dépendant du premier.

• __loader__ : affiche des informations sur le chargement du package. Vous ne devriez pas avoir à utiliser cet attribut.

• __name__ : indique simplement le nom du package.

• __package__ : sert uniquement au système d’importation pour faciliter le chargement et la gestion des packages. Vous n’avez pas à vous en soucier.

En fait, vous pouvez même plonger encore plus profondément dans la liste des attributs. Tapez dir(Chapitre_11_Packages.SayHello) et appuyez sur Entrée. Vous voyez les attributs associés à la fonction SayHello().

Certaines de ces entrées, comme __name__, apparaissent aussi dans le listing du package. Cependant, certaines autres entrées peuvent attirer votre curiosité. Par exemple, vous voudriez savoir à quoi correspond __ sizeof__. Pour cela, tapez help("__sizeof__") et appuyez sur Entrée. Vous obtenez une information d’aide très sommaire, mais utile, telle que « Taille de l’objet en mémoire, en octets » .

Python ne va pas vous manger si vous essayez l’attribut. Même si le notebook rencontre des problèmes, vous pouvez toujours redémarrer le noyau (ou simplement redémarrer l’environnement dans son ensemble). Une autre manière de rechercher des informations sur un modèle consiste donc à tester simplement les attributs. Si vous tapez par exemple Chapitre_11_Packages.SayHello.__sizeof__() et que vous appuyiez sur Entrée, vous allez voir s’afficher la taille de la fonction SayHello() en octets (120 octets en l’occurrence).

Afficher la documentation des packages de Python

Vous pouvez utiliser la fonction doc() chaque fois que vous en avez besoin pour obtenir de l’aide. Cependant, il existe un meilleur moyen d’étudier les packages et les bibliothèques fournis avec Python : il s’agit de la documentation qui apparaît parfois sous le nom Package Docs dans le dossier de Python. On y fait aussi référence sous le nom de PyDoc. Mais quelle que soit l’appellation, cette documentation est une aide précieuse pour les développeurs. Les sections suivantes décrivent comment travailler avec cette fonctionnalité.

Utiliser !pydoc pour accéder à PyDoc

Vous pouvez accéder à une version en ligne de commande de PyDoc à partir de l’invite Anaconda lorsque vous travaillez avec Jupyter Notebook, mais la meilleure façon et la plus simple d’utiliser PyDoc est de le faire à l’intérieur d’une cellule de notebook en employant l’opérateur ! . Cette approche fonctionne aussi bien dans Colab que dans Jupyter Notebook, vous pouvez donc utiliser les mêmes techniques même si vous passez d’un IDE à l’autre. Si vous tapez ! pydoc et cliquez sur Run cell, vous obtenez la sortie illustrée à la Figure 9.7, qui décrit comment utiliser PyDoc à partir de votre IDE.

Taper un terme à rechercher

Pour rechercher un mot-clé, une fonction ou un autre attribut Python particulier à l’aide de PyDoc, il suffit d’ajouter un terme de recherche après la commande ! pydoc. Par exemple, si vous voulez en savoir plus sur la fonction print(), tapez ! pydoc print et cliquez sur Run cell. Vous obtenez un résultat similaire à celui illustré à la Figure 9.8.

Lorsque vous travaillez avec Jupyter Notebook, vous pouvez effectuer des recherches par mot-clé à l’aide du commutateur -k. Une recherche par mot-clé vous aide à localiser des fonctionnalités qui pourraient être utiles, mais dont vous n’avez pas nécessairement connaissance lorsque vous lancez la recherche. Les recherches par mot-clé ne fonctionnent pas dans Colab (vous obtenez une erreur). Notez que les recherches par mot-clé peuvent prendre beaucoup de temps à s’exécuter et produire de nombreux résultats. Par exemple, tapez ! pydoc -k stream et cliquez sur Run cell pour voir une longue liste de packages, de fonctions et d’autres fonctionnalités qui exploitent les streams (la taille de cette liste vous étonnera.) La Figure 9.9 illustre un échantillon de ce que vous pourriez voir en exécutant cette commande.

[picture]

Figure 9.7 PyDoc permet d’obtenir facilement des instructions sur son utilisation directement dans une cellule.

[picture]

Figure 9.8 PyDoc vous dira tout sur la fonction print() si vous le demandez.

[picture]

Figure 9.9 Les recherches par mot-clé vous aident à localiser des éléments auxquels vous ne sauriez peut-être pas faire référence autrement.

DANS CE CHAPITRE

Prendre en compte les différences entre les chaînes.

•

Utiliser des caractères spéciaux dans les chaînes.

•

Travailler avec des caractères uniques.

•

Effectuer des tâches spécifiques sur les chaînes.

•

Trouver ce dont vous avez besoin dans les chaînes.

•

Modifier l’apparence de sortie des chaînes.

Chapitre 10

Travailler avec les chaînes de caractères

Votre ordinateur ne comprend rien aux chaînes de caractères. C’est juste un fait. Les ordinateurs connaissent les nombres, pas les chaînes. Si vous voyez une suite de caractères à l’écran, lui ne voit que des valeurs numériques. Cependant, les êtres humains, eux, voient des textes et vos applications doivent être capables de convertir les uns dans les autres. Heureusement, Python facilite beaucoup ce genre de travail. Il transcrit vos chaînes de caractères en valeurs compréhensibles par l’ordinateur, et réciproquement.

Pour que vos chaînes de caractères soient utiles, vous devez pouvoir les manipuler. Bien entendu, cela signifie que vous pouvez les décomposer ou encore rechercher une certaine information. Ce chapitre décrit donc les méthodes dont vous disposez pour construire des chaînes de caractères, les décomposer, ou encore en utiliser les parties dont vous avez besoin. Tout cela constitue une partie importante de vos applications, car vos utilisateurs dépendent énormément de ce genre de travail (même si leur ordinateur à eux n’a aucune idée particulière de ce qui se passe).

Une fois que votre chaîne est correctement mise en forme, vous devez la présenter d’une manière plaisante. Là encore, ce n’est pas le problème de l’ordinateur, mais le vôtre. Savoir comment formater la présentation des chaînes de caractères est important, car vos utilisateurs ont besoin de voir des informations selon une présentation qu’ils sont capables de lire. Lorsque vous aurez terminé la lecture de ce chapitre, vous devriez pouvoir créer, manipuler et formater des chaînes de caractères de manière à ce que vos utilisateurs puissent lire les bonnes informations au bon moment.

Vous n’avez pas à saisir manuellement le code source de ce chapitre. En fait, l’utilisation de la source téléchargeable est beaucoup plus facile. Vous trouverez le code source de ce chapitre dans le fichier Chapitre 10 Travailler avec des chaînes.ipynb. Consultez l’introduction pour savoir comment trouver ces fichiers source.

Comprendre que les chaînes sont différentes

La plupart des aspirants développeurs (et même des moins aspirants) ont parfois du mal à comprendre que les ordinateurs ne connaissent que deux choses : les 0 et les 1. Même les nombres les plus grands ne sont faits que de cela, de 0 et de 1. Les comparaisons n’utilisent rien d’autre. Pas plus que les modifications dans les données. En résumé, un ordinateur ne sait rien de ce qu’est une chaîne de caractères. C’est pourquoi ce sujet est un peu plus compliqué que la plupart des autres…

L’informatique ne sait rien des chaînes. Celles-ci sont faites de séquences de caractères individuels qui sont en réalité des valeurs numériques du point de vue de l’ordinateur. Le fait de travailler avec Python n’y change rien. C’est pourquoi les sections qui suivent sont aussi importantes. Elles vous aideront à mieux comprendre en quoi les chaînes sont si particulières, et donc à moins vous prendre la tête par la suite…

Définir une chaîne de caractères en utilisant des nombres

Pour créer un caractère, vous devez d’abord définir sa relation numérique. Plus important encore, tout le monde devrait être conscient que lorsqu’un certain nombre apparaît dans une application et qu’il est considéré comme étant un caractère, alors ce nombre subit une certaine conversion. L’une des méthodes les plus couramment admises est appelée ASCII (American Standard Code for Information Interchange). De ce fait, Python utilise ce code ASCII pour transformer la valeur 65 en la lettre A. Vous pouvez trouver une table d’équivalence à l’adresse https://www.asciitable.com/.

Chaque caractère doit être associé à un code numérique différent. La lettre A a pour code 5. En revanche, la minuscule a a comme code 97. Pour l’ordinateur, ces deux lettres sont totalement différentes, même si la plupart des gens ne les différencient pas.

Les valeurs utilisées dans ce chapitre sont présentées sous une forme décimale. Pour autant, ce sont autant de 0 et de 1 du point de vue de l’ordinateur. Par exemple, la lettre majuscule A vaut en fait 01000001, et la minuscule a 01100001. Lorsque vous voyez sur l’écran un A ou un a, l’ordinateur, de son côté, ne voit que des valeurs binaires.

Bien entendu, l’idéal serait de n’avoir besoin que d’un seul jeu de caractères. De ce point de vue, il est évident que le système ASCII est très limité (même si Python n’a pas de problèmes avec les caractères accentués). C’est pourquoi il a été très largement étendu avec l’introduction du système Unicode. Mais c’est là un sujet qui dépasse largement nos besoins, et donc le cadre de ce livre.

Utiliser des caractères pour créer des chaînes

Python ne vous force pas à sauter au milieu de cerceaux pour créer des chaînes de caractères. En fait, le mot chaîne donne une bonne idée de ce qui se passe. Imaginez par exemple que vous avez un tas de perles et un fil. Vous enfilez une première perle, puis une autre, et ainsi de suite jusqu’à ce que le collier soit terminé. Vous finissez en mettant un fermoir, et c’est fait. Donc, un collier est un ensemble de perles enfilées à la suite les unes des autres (vous pouvez remplacer les perles par autre chose, c’est juste pour l’image).

Le même concept s’applique aux chaînes de caractères. Si vous voyez affichée une phrase sur votre écran, vous savez qu’elle est constituée de caractères individuels qui sont alignés les uns à la suite des autres par le langage de programmation que vous utilisez. Ce langage crée une structure qui contient tous les caractères individuels, les uns mis à la suite des autres pour former une phrase. C’est le langage, et non l’ordinateur, qui sait qu’une certaine quantité de nombres (chaque nombre représentant un certain caractère) définit une chaîne formant une phrase.

Il est réellement important de savoir comment Python travaille avec les caractères. En effet, la plupart des fonctionnalités qu’il fournit dans ce domaine gèrent des caractères individuels. De surcroît, là où vous voyez une phrase, Python, de son côté, ne voit qu’une succession de nombres.

Contrairement à ce qui se passe avec la plupart des langages de programmation, Python vous permet de placer les chaînes de caractères entre des guillemets ou des apostrophes (que l’on désigne aussi sous le nom de guillemet simple, qui est la traduction de simple quote). Par exemple, "Bonjour la Terre ! " est une chaîne. Et 'Bonjour la Terre ! ' est également une chaîne. Python prend en charge également des guillemets et des apostrophes triplés qui vous permettent de créer des chaînes occupant plusieurs lignes. Les étapes qui suivent explorent certaines de ces fonctionnalités.

❶ Ouvrez un nouveau notebook.

Vous pouvez également utiliser le fichier source téléchargeable, Chapitre 10 Travailler avec des chaînes.ipynb.

❷ Tapez le code suivant dans le notebook, en appuyant sur Entrée à la fin de chaque ligne :

print('Bonjour la Terre(Apostrophes simples)!')
print("Bonjour la Terre (Guillemets)!")
print("""Ceci est une ligne multiple
utilisant trois guillemets,
mais vous pourriez également utiliser trois apostrophes
simples.""")

Chacune des fonctions print() correspond à une méthode de travail différente : apostrophes, guillemets, ou encore triples guillemets. Toutes renvoient une chaîne de caractères parfaitement licite. Lorsque vous utilisez un guillemet triple (simple ou double), le texte peut apparaître sur plusieurs lignes.

❸ Cliquez sur Exécuter la cellule.

Python affiche le texte attendu. Remarquez que le dernier exemple répartit la phrase sur trois lignes, exactement comme dans le fichier source du code. Ce type de formatage permet de s’assurer que chaque ligne se termine exactement à l’emplacement souhaité (voir la Figure 10.1).

[picture]

Figure 10.1 Une chaîne est un ensemble de caractères attachés les uns aux autres.

Créer des chaînes comportant des caractères spéciaux

Certaines chaînes contiennent des caractères spéciaux. Ils diffèrent des caractères alphanumériques et des signes de ponctuation que vous avez l’habitude d’utiliser. En fait, ils peuvent être répartis en plusieurs catégories :

• Contrôle : une application a besoin de pouvoir déterminer si un caractère particulier n’est pas destiné à être affiché, mais plutôt à contrôler l’affichage. Tous les déplacements, par exemple, sont basés sur la position du point d’insertion, le trait clignotant que vous voyez sur l’écran quand vous tapez du texte. Par exemple, une tabulation ne se signale par rien de visible. Pourtant, elle fait bien partie du texte. De même, vous appuyez sur la touche Entrée lorsque vous voulez passer à la ligne suivante. Là encore, ceci se traduit par l’ajout d’un code numérique à votre texte.

Pour ajouter du piment à votre vie et rendre les choses complètement confuses, certaines plateformes utilisent simplement un retour chariot pour passer à la ligne suivante, et d’autres plateformes emploient simplement le saut de ligne pour passer à la ligne suivante. Si vous rencontrez des problèmes dans certaines situations avec trop de lignes ou un résultat inattendu, essayez d’utiliser juste un retour chariot ou un saut de ligne à la place de la combinaison des deux. L’ordinateur sait ce qu’il fait, ce qui n’est pas le cas des humains.

• Dessin : certains caractères ASCII permettent de réaliser des formes graphiques simples. Vous pouvez voir des exemples de caractères permettant de dessiner des boîtes à http://jrgraphix.net/r/Unicode/2500-257F. Certaines personnes créent également de l’art en utilisant les caractères ASCII (http://www.asciiworld.com/).

• Typographie : nombre de caractères typographiques peuvent être affichés par un traitement de texte ou un éditeur évolué, comme le marqueur de fin de paragraphe, ou pied-de-mouche (¶).

• Autre : selon le jeu de caractères que vous utilisez, les possibilités sont pratiquement sans fin. Vous avez besoin d’un smiley ? Eh bien, il y a un caractère de visage souriant pour cela.Vous pouvez trouver un caractère pour pratiquement tout ce dont vous pouvez avoir besoin. Tout le problème est d’indiquer à Python comment présenter ces caractères spéciaux.

Lorsque l’on travaille avec des chaînes, même simples, il est courant d’avoir à faire à des caractères de contrôle. Python fournit pour cela des séquences dites d’échappement qui vous permettent de définir directement de tels caractères spéciaux.

Une séquence d’échappement transforme une lettre, comme un a, en lui donnant une nouvelle signification (comme un bip sonore). La combinaison de la barre oblique inverse (\) et d’une lettre (disons un a) sert à définir une telle séquence, ou code, d’échappement, dont le Tableau 10.1 donne un aperçu.

Tableau 10.1

Séquences d’échappement en Python.

Séquence d’échappement | Signification
\newline | Ignoré
\\ | Barre oblique inverse (\)
\' | Apostrophe (')
\" | Guillemets (")
\a | Bip ASCII (BEL)
\b | Retour arrière ASCII (BS)
\f | Saut de page ASCII (FF)
\n | Saut de ligne Linefeed (LF)
\r | Retour chariot ASCII (CR)
\t | Tabulation horizontale ASCII (TAB)
\uhhhh | Caractère Unicode (un système de code étendu des caractères), hhhh désignant une valeur hexadécimale.
\v | Tabulation verticale ASCII (VT)
\ooo | Caractère ASCII, ooo désignant une valeur octale.
\xhh | Caractère ASCII, hh désignant une valeur hexadécimale.

La meilleure manière de voir comment tout cela fonctionne, c’est bien sûr d’essayer. Les étapes suivantes vous aident à créer un exemple qui teste diverses séquences d’échappement afin que vous puissiez les voir en action.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée à la fin de chaque ligne :

print("Une partie de ce texte\r\nse trouve sur la ligne sui-
vante.")
print("Ceci est un A avec un accent grave : \xC0.")
print("Ceci est un caractère dessiné : \u2562.")
print("Et voici un retour chariot: \266.")
print("Ceci est le signe de la division : \xF7.")

Ce code utilise plusieurs techniques dans le même but : produire un caractère spécial. Bien entendu, vous pouvez utiliser directement des caractères de contrôle (voyez la première ligne). Mais de nombreux autres sont accessibles en spécifiant une valeur hexadécimale sur deux chiffres (lignes 2 et 5), voire sur quatre chiffres dans le système étendu dit Unicode (ligne 3). Il est également possible de faire appel à des valeurs octales (ligne 4).

❷ Cliquez sur Exécuter la cellule.

Python affiche le texte ainsi que les caractères spéciaux attendus (voir la Figure 10.2).

[picture]

Figure 10.2 Utiliser des caractères spéciaux permet d’afficher des informations particulières, ou encore de formater la sortie.

Python utilise un jeu de caractères standardisé, ce qui signifie que le résultat devrait être identique quelle que soit la plateforme utilisée. Ce n’est cependant pas une certitude, et vous devriez donc tester votre application sur différents systèmes si l’emploi de caractères spéciaux y joue un rôle important. De plus, des configurations personnalisées d’ordinateur peuvent avoir une influence sur ce qui est affiché. Lorsque vous travaillez sur des applications qui seront utilisées dans plusieurs langues, le risque de rencontrer des problèmes entre les jeux de caractères est beaucoup plus élevé. Veillez toujours à tester complètement l’utilisation des caractères spéciaux.

Sélectionner des caractères individuels

Vous savez maintenant que les chaînes sont formées de caractères individuels, comme des perles enfilées dans un collier. Python vous permet d’accéder directement à chaque perle, autrement dit à chaque caractère. C’est une fonctionnalité importante, puisqu’elle vous permet en particulier de créer de nouvelles chaînes à partir de l’original. De plus, vous pouvez combiner plusieurs chaînes pour en former une nouvelle. Le secret de cette technique, c’est l’emploi de crochets droits. Il vous suffit d’indiquer le rang d’un caractère à l’intérieur de ces crochets pour le récupérer, comme ceci :

MyString = "Bonjour le monde"
print(MyString[0])

Dans cet exemple, la sortie serait le caractère B. Python débute en effet la numérotation des caractères d’une chaîne en partant de zéro pour le premier. Si vous tapez print(MyString[1]), la réponse sera donc o, et ainsi de suite.

Vous pouvez également récupérer une section de votre chaîne. Pour cela, vous spécifiez la position de début et celle de fin en les séparant par un deux-points. Dans l’exemple ci-dessus, l’instruction print(MyString[11 : 16]) renverrait le mot monde. Le douzième caractère de la chaîne, qui a le rang 11 du point de vue de Python, est bien la lettre m.

Les étapes suivantes démontrent certaines tâches de base que vous pouvez effectuer en utilisant la technique de sélection de caractères de Python.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée à la fin de chaque ligne :

String1 = "Bonjour la Terre"
String2 = "Ici Python"
print(String1[0])
print(String1[0:5])
print(String1[:5])
print(String1[6:])
String3 = String1[:6] + String2[:6]
print(String3)
print(String2[:7]*5)

Cet exemple commence par créer deux chaînes. Il pratique ensuite différentes manipulations sur la première. Remarquez que vous pouvez omettre le rang de début ou le rang de fin si vous voulez travailler avec le reste de la chaîne.

L’étape suivante combine deux sous-chaînes, en l’occurrence en plaçant leurs six premiers caractères dans une troisième chaîne.

Le signe d’addition sert à combiner deux chaînes de caractères en les plaçant bout à bout. C’est ce que l’on appelle la concaténation. Il s’agit d’une des opérations les plus utiles pour travailler avec les chaînes.

L’étape finale réalise une opération appelée répétition. Celle-ci permet d’obtenir un certain nombre de copies (cinq en l’occurrence) d’une chaîne ou d’une sous-chaîne.

❷ Cliquez sur Exécuter la cellule.

Python affiche une série de combinaisons de chaînes et de sous-chaînes (voir la Figure 10.3).

[picture]

Figure 10.3 Vous pouvez sélectionner diverses parties d’une chaîne de caractères.

Trancher et couper les chaînes de caractères

Accéder à des plages de caractères offre un certain degré de souplesse, mais cela ne vous permet pas en soi de manipuler le contenu des chaînes de caractères, ni de découvrir quoi que ce soit sur elles. Vous pourriez par exemple avoir besoin de convertir les lettres en majuscules, ou bien encore de savoir si une phrase contient toutes les lettres de l’alphabet.

Python offre de nombreuses fonctions pour manipuler les chaînes de caractères. Voici les plus courantes :

• capitalize() : met la première lettre d’une chaîne en majuscule.

• center(largeur, remplissage=" ") : centre une chaîne de manière à ce qu’elle remplisse l’espace spécifié par largeur. Si vous fournissez un caractère pour remplissage, la fonction l’utilisera. Sinon, elle se sert par défaut du caractère d’espacement pour obtenir la largeur voulue.

• expandtabs(tailletab=8) : modifie les tabulations d’une chaîne en remplaçant l’espacement par défaut (8) par la valeur spécifiée dans tailletab.

• isalnum() : retourne True si la chaîne a au moins un caractère et que tous les caractères sont alphanumériques (lettres ou chiffres).

• isalpha() : retourne True si la chaîne a au moins un caractère et que tous les caractères sont alphabétiques (lettres uniquement).

• isdecimal() : retourne True lorsqu’une chaîne de caractères ne contient que des caractères décimaux (base 10). Par exemple, '100' renvoie True, car la chaîne peut représenter une valeur décimale de 100. La chaîne '0xF' renvoie False, car elle représente un nombre en base 16 et non en base 10. De la même manière, '10.55' renvoie False, car il s’agit d’une valeur à virgule flottante et non d’un nombre décimal.

• isdigit() : retourne True si une chaîne contient uniquement des chiffres (pas de lettres).

• islower() : retourne True si une chaîne contient au moins un caractère alphabétique, et si toutes les lettres sont en minuscules.

• isnumeric() : retourne True si une chaîne contient uniquement des caractères numériques.

• isspace() : retourne True si une chaîne contient uniquement des caractères blancs (espaces, tabulations, retours chariot, saut de ligne ou de page et tabulations verticales, sauf des retours arrière).

• istitle() : retourne True si la première lettre de chaque mot est une majuscule. Attention : une lettre unique est comptée comme un mot.

• isupper() : retourne True si une chaîne contient au moins un caractère alphabétique, et si toutes les lettres sont en majuscules.

• join(seq) : crée une chaîne dans laquelle les caractères de la chaîne de base sont séparés par le contenu de seq d’une manière répétitive. Par exemple, si MaChaine contient "Hello", alors print(MaChaine.join(" ! * ! ")) retournera ! Hello*Hello ! .

La fonction join() est également utilisée pour regrouper des listes de caractères. Par exemple, print('-'.join(['a', 'b', 'c'])) affichera la sortie a-b-c. Elle peut également être utilisée avec des tuples, où print(', '.join(('pommes', 'cerises','et poires'))) affiche pommes, cerises, et poires et des ensembles, tels que print(', '.join({'pommes', 'cerises', 'et poires'})).

• len(chaine) : renvoie la longueur de la chaîne.

• ljust(largeur, remplissage=" ") : justifie à gauche une chaîne de manière à ce qu’elle tienne dans l’espace défini par largeur. Si vous ne fournissez pas un caractère de remplissage, la fonction utilisera des espaces pour créer une chaîne de la largeur désirée.

• lower() : convertit toutes les lettres d’une chaîne en minuscules.

• lstrip() : supprime tous les espaces blancs au début d’une chaîne.

• max(chaine) : retourne le caractère de la chaîne dont la valeur numérique est la plus élevée. Par exemple, a un rang numérique supérieur à A.

• min(chaine) : retourne le caractère de la chaîne dont la valeur numérique est la moins élevée. Par exemple, A a un rang numérique inférieur à a.

• rjust(largeur, remplissage=" ") : justifie à droite une chaîne de manière à ce qu’elle tienne dans l’espace défini par largeur. Si vous ne fournissez pas un caractère de remplissage, la fonction utilisera des espaces pour créer une chaîne de la largeur désirée.

• rstrip() : supprime tous les espaces blancs à la fin d’une chaîne.

• split(sep, num) : découpe une chaîne en sous-chaîne en utilisant le délimiteur spécifié par sep, ou par une espace si cet argument n’est pas spécifié. Par exemple, si votre chaîne d’origine contient Un Jour Sans Fin, le résultat par défaut sera ['Un', 'Jour', 'Sans', 'Fin']. La valeur num permet de spécifier le nombre de sous-chaînes à renvoyer (toutes par défaut).

• splitlines(num) : découpe une chaîne contenant des changements de ligne (\n) en autant de chaînes individuelles. Vous pouvez spécifier avec num le nombre de chaînes à retourner.

• strip() : supprime toutes les espaces en début et en fin de chaîne.

• swapcase() : inverse la casse de chaque caractère alphabétique de la chaîne.

• title() : retourne une chaîne dans laquelle la première lettre de chaque mot est en majuscule, le reste étant en minuscules.

• upper() : convertit toutes les lettres d’une chaîne en minuscules.

• zfill(largeur) : renvoie une chaîne complétée à gauche par des zéros de manière à ce que la chaîne résultat ait la largeur indiquée. Cette fonction est conçue pour formater des chaînes contenant des valeurs numériques. Le signe original est conservé s’il est présent.

Jouer un peu avec ces fonctions peut vous aider à mieux les comprendre. Les étapes suivantes créent un exemple qui démontre certaines des tâches que vous pouvez effectuer en utilisant ces fonctions.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée à la fin de chaque ligne :

MyString = "  Bonjour la Terre  "
print(MyString.upper())
print(MyString.strip())
print(MyString.center(21, "*"))
print(MyString.strip().center(21, "*"))
print(MyString.isdigit())
print(MyString.istitle())
print(max(MyString))
print(MyString.split())
print(MyString.split()[0])

Ce code commence par créer une chaîne appelée MyString. Remarquez qu’elle contient des espaces au début et à la fin. La tâche initiale consiste à convertir tous les caractères en majuscules.

Supprimer les espaces en trop est une tâche courante dans les applications. C’est le rôle de la fonction strip(). La fonction center() vous permet à l’inverse de compléter une chaîne de manière à ce qu’elle remplisse une certaine largeur. Si vous combinez les deux, le résultat est différent de ce que fournit la fonction center() seule.

Vous pouvez combiner plusieurs fonctions pour obtenir le résultat voulu. Python exécute chaque fonction l’une après l’autre, en allant de la gauche vers la droite. L’ordre dans lequel ces fonctions apparaissent affecte donc la sortie. Il faut par conséquent bien veiller à ne pas se tromper de sens. Si le résultat que vous obtenez est différent de ce que vous vouliez obtenir, essayez en premier lieu de changer l’ordre des fonctions.

Certaines fonctions travaillent sur une chaîne passée en argument, plutôt que sur une instance de la chaîne. C’est par exemple le cas de la fonction max(). Si vous tapez MyString.max(), Python renverra une erreur. La liste développée au début de cette section montre quelles fonctions nécessitent ce type d’entrée de chaîne en incluant une entrée str dans la déclaration de la fonction.

Lorsque vous travaillez avec des fonctions qui renvoient une liste, vous pouvez accéder à chaque membre individuel de celle-ci en spécifiant un indice. Par exemple, la fonction split() partage une chaîne en sous-chaînes. La dernière instruction de l’exemple permet de récupérer la première d’entre elles (celle qui a le rang 0). Nous reviendrons sur les listes dans le Chapitre 5.

❷ Cliquez sur Exécuter la cellule.

Python affiche une série de chaînes modifiées (voir la Figure 10.4).

Localiser une valeur dans une chaîne

Vous avez parfois besoin de localiser une valeur dans une chaîne. Par exemple, vous voudriez savoir si une certaine chaîne contient le mot Bonjour. L’un des objets essentiels de la création et de la manipulation des données est justement la possibilité d’y effectuer ensuite des recherches pour retrouver des informations spécifiques. Et les chaînes de caractères ne sont pas différentes : elles sont essentiellement utiles lorsque vous pouvez y retrouver ce dont vous avez besoin rapidement et sans problèmes. Python fournit de nombreuses fonctions pour effectuer ce genre de travail. Voici celles qui sont le plus couramment utilisées :

• count(chaine1, début, fin) : compte le nombre d’occurrences de chaine1 dans chaine2. Vous pouvez spécifier en plus un indice de début et de fin (par défaut le début et la fin de chaine2).

[picture]

Figure 10.4 Python offre de nombreuses fonctions pour manipuler les chaînes de caractères.

• endswith(suffixe, début, fin) : renvoie True si une chaîne se termine par les caractères spécifiés dans suffixe. Vous pouvez spécifier en plus un indice de début et de fin (par défaut, la longueur de la chaîne).

• find(chaine1, début, fin) : détermine si chaine1 se trouve dans chaine2 et renvoie sa position. Vous pouvez spécifier en plus un indice de début et de fin (par défaut, la longueur de chaine2). Cette fonction renvoie une valeur de -1 lorsque la valeur recherchée n’est pas présente.

• index(chaine1, début, fin) : comme find(), mais lève une exception si chaine1 n’est pas trouvée.

• replace(ancien, nouveau [, max]) : remplace toutes les occurrences de la séquence spécifiée dans ancien par celle spécifiée dans nouveau. Vous pouvez spécifier en plus un nombre maximal de remplacements.

• rfind(chaine1, début, fin) : comme find(), mais en partant de la fin de la chaîne et non du début de celle-ci.

• rindex(chaine, début, fin) : comme index(), mais en partant de la fin de la chaîne et non du début de celle-ci.

• startswith(préfixe, début, fin) : renvoie True si une chaîne débute par les caractères spécifiés dans préfixe. Vous pouvez spécifier en plus un indice de début et de fin.

Retrouver les données dont vous avez besoin est une tâche essentielle en programmation, et ce quelle que soit l’application que vous créez. Les étapes qui suivent vous proposent un exemple de ce genre de recherche.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée à la fin de chaque ligne :

SearchMe = "La pomme est rouge et la luzerne est verte !"
print(SearchMe.find("est"))
print(SearchMe.rfind("est"))
print(SearchMe.count("est"))
print(SearchMe.startswith("La"))
print(SearchMe.endswith("La"))
print(SearchMe.replace("pomme", "voiture")
     .replace("luzerne", "camionnette"))

L’exemple commence par créer la chaîne SearchMe, dans laquelle se trouvent deux instances du mot est. Ceci permet de montrer en quoi les recherches diffèrent selon leur point de départ. Avec find(), le code recherche le mot est en partant du début de la chaîne. Avec rfind(), elle part de la fin de la chaîne.

Bien entendu, vous ne savez généralement pas combien de fois une certaine séquence de caractères apparaît dans une chaîne. La fonction count() vous permet de déterminer cette valeur.

Selon la nature des données avec lesquelles vous travaillez, il est possible que celles-ci possèdent un formatage particulier. Vous pouvez alors tirer profit de cette disposition, afin par exemple de déterminer si une chaîne ou une sous-chaîne débute ou se termine par une séquence particulière. Vous pourriez ainsi vous servir de cette technique pour localiser un numéro de composant.

L’instruction finale remplace les mots pomme et luzerne respectivement par voiture et camionnette. Remarquez que ce code est placé sur deux lignes. Cette façon de procéder permet de le rendre plus lisible.

❸ Cliquez sur Exécuter la cellule.

Python affiche une série de réponses (voir la Figure 10.5). Vous pouvez constater que les recherches retournent des résultats différents selon qu’elles sont effectuées en partant du début de la chaîne ou de la fin de celle-ci. Choisir la fonction qui convient est essentiel pour vous assurer que vous obtiendrez les bons résultats.

[picture]

Figure 10.5 Il est essentiel de savoir utiliser correctement les fonctions de recherche de chaînes de caractères.

Utilisation de l’interpolation des chaînes de caractères

Avec Python, vous pouvez formater des chaînes de caractères de plusieurs façons. L’objectif principal du formatage est de présenter la chaîne de caractères sous une forme qui soit à la fois agréable pour l’utilisateur et facile à comprendre. Dans ce cas, le formatage n’implique pas l’ajout de polices de caractères ni d’effets spéciaux, mais fait simplement référence à la présentation des données. Par exemple, l’utilisateur peut vouloir qu’un nombre s’affiche avec deux décimales après la virgule. Les sections suivantes abordent diverses techniques de formatage des chaînes de caractères, un processus connu sous le nom d’interpolation de chaînes de caractères.

Utiliser l’opérateur % (modulo)

Dans certains cas, tout ce dont vous avez vraiment besoin, c’est d’un simple remplacement dans une chaîne de caractères. L’approche de l’opérateur % (modulo) est le formatage le plus simple que propose Python, mais il est également moins utilisé dans les nouvelles applications, car il est extrêmement limité et il existe des méthodes plus sophistiquées (comme vous allez le découvrir dans les sections qui suivent). Malgré tout, si vous êtes pressé et que vous aviez besoin de quelque chose de simple et de facile à mettre en œuvre, l’opérateur % peut être la solution.

Lorsque vous utilisez l’opérateur %, vous devez le placer à chaque endroit où vous souhaitez voir apparaître une information, et spécifier le type de cette information, comme cela est indiqué dans le tableau suivant :

Opérateur | Description | Opérateur | Description | Opérateur | Description
%c | Caractère | %d | Entier décimal | %e | Exposant du flottant (minuscule)
%E | Exposant en virgule flottante (majuscule) | %f | Virgule flottante (minuscule) | %F | Virgule flottante (majuscule)
%g | Format général (minuscules) | %G | Format général (majuscules) | %i | Entier
%o | Nombre entier en octal | %s | Chaîne de caractères | %u | Entier non signé
%x | Nombre entier en hexadécimal |  |  |  |

L’opérateur % permet également l’ajout de certaines informations de formatage, comme cela est décrit dans la section suivante (fonction format()), mais ces informations de formatage sont généralement limitées au nombre de décimales. Les étapes suivantes montrent comment utiliser l’opérateur % de différentes manières :

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne :

print("%c est la minuscule de %c" %('a', 65))
print("%s sont %s" %("Les violets", "des bleus"))
print("%d est égal à %#x en hexadécimal" %(60, 60))

Dans chaque ligne, vous voyez une fonction print() qui utilise l’une des méthodes de remplacements de l’opérateur %, comme %c sur la première ligne. Notez que lorsque vous utilisez %c, vous pouvez employer soit un caractère, 'a', soit un code ASCII, 65. Les valeurs que vous souhaitez remplacer pour chacune des entrées de l’opérateur % apparaissent dans un tuple (une série de valeurs encadrées par des parenthèses et délimitées par des virgules) avec l’opérateur % à l’extérieur. Les valeurs sont toujours remplacées de manière séquentielle, et vous devez fournir suffisamment de valeurs pour chacune des entrées de l’opérateur % dans la chaîne.

L’opérateur % traite les caractères différemment dans les chaînes de caractères. Lorsque vous travaillez avec des chaînes de caractères, il est préférable d’employer l’opérateur %s.

Le troisième appel à la fonction print() concerne la valeur entière 60. Vous noterez que vous devez fournir 60 deux fois, une fois pour chaque entrée de l’opérateur %. Dans ce cas, les valeurs sont émises la première fois sous la forme d’un nombre entier décimal et la deuxième fois sous la forme d’un nombre entier hexadécimal. Le caractère # dans l’opérateur %#x indique à Python de placer un 0x devant la sortie. Le caractère # est l’un des nombreux indicateurs de format :

• # : ajoute 0x devant la sortie hexadécimale.

• + : ajoute un signe plus devant les valeurs positives.

• - : justifie à gauche la sortie, plutôt que d’utiliser la justification à droite par défaut.

• "" : insère des espaces avant une sortie numérique.

• 0 : insère des zéros avant la sortie numérique.

❷ Cliquez sur Exécuter la cellule.

Python affiche des données sous différentes formes :

a est la minuscule de A
Les violets sont des bleus
60 est égal à 0x3c en hexadécimal

L’étape suivante fournit quelques exemples de formatage numérique.

❸ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne :

Valeur1 = 100
Valeur2 = float('inf')
print('% e est plus petit que %E.' %(Valeur1, Valeur2))
print("%.3F est plus petit que %.2f." %(Valeur1,
Valeur2))
print("%05d" %Valeur1)

Cette partie de l’exemple porte sur une valeur entière de 100 et une valeur en virgule flottante d’infini. Le code formate les deux valeurs de différentes manières. Remarquez l’utilisation des opérateurs % en majuscules et en minuscules. En outre, il y a une espace entre le symbole % et le e dans la troisième ligne, ce qui ajoute une espace devant la valeur de sortie (ajouter plus d’espaces n’ajoutera pas plus d’espaces à la sortie). La quatrième ligne a un .3 devant le F qui contrôle le nombre de chiffres après le point décimal. La dernière ligne montre comment ajouter des zéros devant le nombre.

❹ Cliquez sur Exécuter la cellule.

Python affiche les données suivantes sous différentes formes :

 1.000000e+02 est plus petit que INF.
100.000 est plus petit que inf.
00100

La dernière partie de l’exemple montre une utilisation spéciale du formatage de l’opérateur % lorsque vous travaillez avec un dictionnaire.

❺ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne :

Employes = {
    'Monique': {
       'Fonction': "Designer",
       'Nom': "Monique",
       'Departement': "Ingénierie"
    },
    'Samuel': {
       'Fonction': "Programmeur",
       'Nom': "Samuel",
       'Departement': "DSI"
    }
}

for Item in Employes:
    print("%(Nom)s occupe la fonction de %(Fonction)s dans le
service " \
"%(Departement)s." %Employes[Item])

Cet exemple utilise un dictionnaire imbriqué. Ne vous inquiétez pas trop pour l’instant des dictionnaires, car ils seront abordés au Chapitre 10. Pour le moment, considérez ce code comme une sorte de base de données qui contient deux employés, Monique et Samuel. Il y a des entrées pour chaque employé : Fonction, Nom, et Departement.

Il y a ensuite une boucle for qui récupère le nom d’un employé (et non pas les informations sur l’employé) dans Employes et utilise ce nom pour accéder à un enregistrement d’employé particulier grâce à %Employes[Item]. Vous disposez maintenant d’un seul enregistrement d’employé et vous pouvez accéder aux informations par le nom en employant l’opérateur % et en intégrant les informations que vous souhaitez dans un tuple, tel que %(Nom). Comme toujours, vous devez indiquer à Python le type de format de données à utiliser, de telle sorte que l’opérateur complet est %(Nom)s.

Notez l’utilisation du caractère de fin de ligne, \, dans cette partie de l’exemple. Lorsque votre code devient trop long, vous pouvez utiliser un caractère de fin de ligne pour continuer le code sur la ligne suivante.

❻ Cliquez sur Exécuter la cellule.

Python produit des données sous différentes formes et la Figure 10.6 présente les sorties de l’ensemble de cet exemple.

Travailler avec la fonction format()

La fonction format() offre plus de fonctionnalités et de souplesse que l’utilisation de l’opérateur % présenté dans la section précédente. Vous créez des spécifications de formatage à l’intérieur de la chaîne, puis vous utilisez format() pour ajouter des données à celle-ci. Une spécification peut être très simple. Exemple : deux crochets ({}) forment un conteneur pour y saisir des données. Ce conteneur peut servir à créer des effets spéciaux. Ainsi, {0} contiendrait le premier élément de donnée d’une chaîne. Lorsque les éléments de donnée sont numérotés, vous pouvez même les répéter pour qu’ils apparaissent plusieurs fois dans la chaîne.

La spécification de formatage est précédée d’un deux-points. Si vous voulez définir un formatage simple, les crochets ne contiennent que ce deux-points et l’option voulue. Par exemple, { : f} affichera un nombre en numérotation fixe. Si vous voulez numéroter les entrées, la spécification {0 : f} fera la même chose, mais pour le premier élément.

[picture]

Figure 10.6 L’opérateur % convient bien pour les besoins simples d’affichage.

Les spécifications de formatage suivent ce format, les mots en italique étant bien entendu à remplacer par les valeurs voulues :

[[remplissage] alignement][signe][#][0][largeur][,][.précision]
[type]

Les spécifications que vous trouverez à l’adresse https://docs.python.org/fr/3.9/library/string.html fournissent des détails plus approfondis, mais voici un aperçu de ce que signifient ces différentes entrées :

• Remplissage : définit le caractère utilisé lorsque le remplissage ne suffit pas à remplir l’espace assigné.

• Alignement : spécifie la manière dont les données sont alignées à l’intérieur de l’espace d’affichage. Vous pouvez utiliser ces différents modes :

• < : alignement à gauche

• > : alignement à droite

• ^ : centré

• = : justifié

• Signe : détermine l’utilisation du signe lors de la sortie :

• + : les nombres positifs ont un signe plus, et les nombres négatifs un signe moins.

• − : les nombres négatifs ont un signe moins.

• <espace> : les nombres positifs sont précédés d’une espace et les nombres négatifs ont un signe moins.

• # : spécifie que la sortie devrait utiliser le format d’affichage alternatif pour les nombres. Par exemple, les valeurs hexadécimales prendront le préfixe 0x.

• 0 : spécifie que la sortie doit tenir compte du signe, et que la valeur doit être complétée par des zéros pour fournir une sortie cohérente.

• Largeur : détermine la largeur totale du champ de donnée (même si celle-ci n’occupe pas tout cet espace).

• , : spécifie que la valeur numérique doit avoir une virgule comme séparateur des milliers.

• .précision : détermine le nombre de chiffres après le point décimal.

• Type : spécifie le type de la sortie, même si l’entrée ne correspond pas. Les types sont répartis en trois groupes :

• Chaîne : utilisez un s ou rien pour spécifier une chaîne.

• Entier : les types entiers sont les suivants : b (binaire), c (caractère), d (décimal), o (octal), x (hexadécimal avec des lettres en minuscules), X (hexadécimal avec des lettres en majuscules) et n (valeur décimale sensible à la localisation géographique utilisant le caractère approprié comme séparateur des milliers).

• Virgule flottante : les types en virgule flottante sont les suivants : e (exposant utilisant une lettre e minuscule), E (exposant utilisant une lettre E majuscule), f (valeur décimale fixée en minuscules), F (valeur décimale fixée en majuscules), g (format général en minuscules), G (format général en majuscules), n (valeur sensible à la localisation géographique utilisant le caractère approprié comme séparateur décimal et des milliers), et % (pourcentage).

Les éléments qui spécifient le formatage doivent apparaître dans l’ordre indiqué, sinon Python ne pourra pas les traiter correctement. Dans ce cas, il affichera un message d’erreur au lieu du formatage spécifié. Les étapes qui suivent illustrent cette technique. Vous pouvez également les retrouver dans le fichier téléchargeable Formatted.py.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée à la fin de chaque ligne :

Formatted = "{:d}"
print(Formatted.format(7000))
Formatted = "{:,d}"
print(Formatted.format(7000))
Formatted = "{:^15,d}"
print(Formatted.format(7000))
Formatted = "{:*^15,d}"
print(Formatted.format(7000))
Formatted = "{:*^15.2f}"
print(Formatted.format(7000))
Formatted = "{:*>15X}"
print(Formatted.format(7000))
Formatted = "{:*<#15x}"
print(Formatted.format(7000))
Formatted = "Une {1} {0} et un {2} {0}."
print(Formatted.format("rouge", "voiture", "camion"))

Cet exemple débute par un champ formaté en tant que valeur décimale. Il ajoute ensuite une virgule décimale. L’étape qui suit consiste à rendre le champ plus large qu’il n’est besoin pour contenir la donnée, et à centrer celle-ci à l’intérieur de ce champ. On ajoute ensuite un astérisque à la sortie de manière à remplir ce champ.

Bien entendu, l’exemple propose d’autres types de données. Les étapes suivantes affichent la même donnée dans un format préfixé, ainsi qu’en mode hexadécimal avec représentation en majuscules ou en minuscules. Dans le premier cas, la sortie est alignée à droite, et à gauche dans le second. Enfin, l’exemple montre comment tirer profit de champs numérotés à votre avantage. Dans ce cas, des valeurs préfixées sont ensuite remplacées par des chaînes personnalisées, avec ici deux variantes.

❷ Cliquez sur Exécuter la cellule.

Python affiche les données sous différentes formes (voir la Figure 10.7).

[picture]

Figure 10.7 Le formatage vous permet de présenter vos données comme vous le voulez.

Simplifier les choses en utilisant f-string

La plupart des développeurs utilisent aujourd’hui un f-string (littéralement chaîne f) parce qu’il englobe toutes les fonctionnalités du formatage de chaînes de caractères Python antérieur et y ajoute quelques nouvelles astuces. Un f-string est tout simplement une chaîne précédée de la lettre minuscule f. À bien des égards, il ressemble à une version moins compliquée de la méthode format() que nous avons étudiée dans la section précédente. En gardant cette similitude à l’esprit, il est temps de passer à un exemple.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne :

Valeur1 = 22
Valeur2 = 33
print(f"Valeur1 a une valeur de {Valeur1}.")
print(f"{Valeur1} + {Valeur2} = {Valeur1 + Valeur2}.")
print(f"Cette valeur : {str(Valeur1).center(8)} est centrée.")
print(f"Valeur1 a une valeur hexadécimale de {Valeur1:x}.")

Cet exemple commence avec deux valeurs : Valeur1 et Valeur2. Le premier appel à print() montre comment on commence la chaîne de caractères par la lettre minuscule f, puis on ajoute le contenu entre guillemets simples ou doubles. Vous noterez la manière dont l’utilisation d’un nom de variable entre accolades ({}) facilite la lecture de l’instruction.

Contrairement à d’autres formes de formatage de chaînes, une chaîne f-string peut contenir des expressions, telles que {Valeur1 + Valeur2}. En fait, vous pouvez faire des appels de fonction et effectuer une conversion de données dans une chaîne f-string, par exemple {str(Valeur1).centre(8)}. Vous avez toujours la possibilité d’utiliser toutes les fonctionnalités de formatage offertes par la méthode format(), de telle sorte que les chaînes f-string peuvent avoir des instructions de formatage telles que {Valeur1 : x}, qui produit une sortie en hexadécimal.

Remarquez que si vous avez un caractère de continuation de ligne mais que la ligne suivante ne contient pas de variable ou d’expression, vous n’avez pas besoin de la faire précéder d’un f. Cependant, si la ligne suivante contient un contenu remplaçable, vous devez inclure le f, sinon Python ne saura pas comment interpréter le contenu.

❷ Cliquez sur Exécuter la cellule.

Python affiche des données sous différentes formes, comme cela est illustré à la Figure 10.8.

Créer et employer des modèles de chaînes

Les modèles de chaînes sont destinés aux cas où vous répétiez la même information à différents endroits de votre application, mais avec des données différentes. Le modèle est simplement une chaîne, mais il utilise un caractère $ devant les valeurs remplaçables à la place des autres méthodes présentées dans les sections précédentes. La meilleure façon de comprendre le fonctionnement d’un modèle de chaîne est de le voir en action :

[picture]

Figure 10.8 L’emploi d’un f-string est plus simple que les autres méthodes de formatage, tout en étant assez flexible.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne :

from string import Template
X = 1
Y = 2
Sommes = Template("$Valeur1 + $Valeur2 = $Valeur3 »)
print(Sommes.substitute(Valeur1 = X, Valeur2 = Y,
                       Valeur3 = X + Y))

Pour utiliser un modèle de chaîne, vous devez importer Template à partir du package string à l’aide de l’instruction import (le Chapitre 5 vous explique comment fonctionnent les packages et comment utiliser les instructions import).

Le code commence par créer une variable, Sommes, comme modèle contenant trois valeurs de remplacement : $Valeur1, $Valeur2 et $Valeur3. Vous pouvez remplacer ces valeurs par n’importe quoi et dans n’importe quel ordre.

❷ Cliquez sur Exécuter la cellule.

Python produit la chaîne attendue mise en forme grâce à un modèle, comme cela est illustré à la Figure 10.9.

[picture]

Figure 10.9 Les modèles de chaînes permettent de créer facilement du contenu réutilisable.

Les valeurs remplaçables peuvent littéralement représenter n’importe quoi. Par exemple, vous pourriez tout aussi bien remplacer l’appel print() précédent par print(Sommes.substitute(Valeur1 = 'A', Valeur2 = "{:x}".format(90), Valeur3 = True)). La sortie serait A + 5a = True, qui bien entendu ne veut rien dire, mais personne n’a jamais prétendu que la sortie devait avoir un sens.

DANS CE CHAPITRE

Comprendre l’importance des listes.

•

Générer des listes.

•

Parcourir des listes.

•

Travailler avec les éléments des listes.

•

Modifier le contenu d’une liste.

•

Localiser des éléments spécifiques dans des listes.

•

Mettre les listes dans l’ordre.

•

Savoir utiliser l’objet Counter.

Chapitre 11

Gérer des listes

Des tas de gens perdent facilement de vue que la plupart des techniques de programmation sont basées sur le monde réel. Une des raisons tient au fait que les programmeurs utilisent souvent des termes qui ne parlent pas au commun des mortels. Par exemple, vous dites que vous rangez quelque chose dans une boîte. Les développeurs, de leur côté, diront qu’ils affectent une valeur à une variable. Les listes sont différentes. Après tout, tout le monde fait des listes pour tout un tas de choses : des listes de courses, des listes de bagages pour les vacances, des listes de tâches à réaliser, et ainsi de suite. Ce chapitre traite donc de quelque chose que vous utilisez déjà dans la vie de tous les jours. La seule différence, c’est que vous devez envisager ce sujet du point de vue de Python.

Si certaines personnes trouvent qu’il est difficile de travailler avec les listes, c’est vraisemblablement parce qu’elles n’ont pas l’habitude de réfléchir aux listes qu’elles créent. Lorsque vous rédigez une liste, vous écrivez simplement une suite d’éléments dans un ordre qui a un sens pour vous. Parfois, vous reprenez votre liste pour en changer l’ordre (mais pourquoi ont-ils encore déplacé le rayon dans le magasin ?). Ou bien vous parcourez la liste du bout du doigt pour vérifier si vous n’avez rien oublié. En fait, tout ce que vous pouvez faire avec vos propres listes est également réalisable avec Python. Le point essentiel, c’est que vous devez bien réfléchir à ce que vous faites si vous voulez que Python comprenne votre objectif.

Les listes sont incroyablement importantes dans Python. Ce chapitre vous présente les concepts utilisés pour créer, gérer, rechercher et afficher des listes (parmi d’autres tâches). Lorsque vous l’aurez terminé, vous serez à même d’utiliser des listes pour rendre vos applications Python plus robustes, plus rapides et plus souples. En fait, vous pourriez bien vous demander comment vous avez fait sans elles par le passé. La chose importante à se rappeler, c’est que les listes font, depuis pratiquement toujours, partie de votre quotidien. Il n’y a aucune différence ici, si ce n’est que vous devez maintenant réfléchir plus à fond aux actions à mettre en œuvre pour que vos listes soient totalement efficaces.

Vous n’avez pas à saisir manuellement le code source de ce chapitre. En fait, l’utilisation de la source téléchargeable est beaucoup plus facile. Vous trouverez le code source de ce chapitre dans le fichier Chapitre 11 Gérer des listes.ipynb. Consultez l’introduction pour savoir comment trouver ces fichiers source.

Organiser les informations dans une application

Les gens créent des listes pour organiser des informations, et en faciliter l’accès ainsi que les modifications. Vous utilisez des listes dans Python pour les mêmes raisons. Dans de nombreuses situations, vous avez besoin d’une forme d’organisation de vos données pour pouvoir gérer celles-ci. Par exemple, vous pourriez vouloir créer un « quelque chose » qui contiendrait les jours de la semaine ou les mois de l’année. Les noms de ces éléments apparaîtraient dans une liste, comme si vous les aviez écrits sur une feuille de papier dans le monde réel. Les sections qui suivent approfondissent ces notions.

Définir une organisation à l’aide de listes

Dans Python, une liste se définit comme étant un type de séquence. Les séquences fournissent un certain procédé permettant à de multiples données de coexister en tant qu’entités distinctes dans un même espace de stockage. Prenons l’exemple d’un grand immeuble : en bas, vous trouvez plusieurs blocs de boîtes aux lettres. Chacun contient une rangée de boîtes, et chaque boîte contient du courrier. Python prend en charge d’autres types de séquences au nom parfois mystérieux (nous y reviendrons aussi au Chapitre 2) :

• Tuples

• Dictionnaires

• Piles

• Files

• Deques

De toutes les séquences, les listes sont les plus faciles à comprendre, et les plus proches du monde réel. Se familiariser avec des listes vous aide à devenir plus à même de travailler avec d’autres types de séquences fournissant davantage de puissance et de souplesse. L’essentiel à retenir, c’est que les données sont enregistrées dans une liste comme si vous les écriviez sur une feuille de papier, un élément après l’autre. La liste a un début, un milieu et une fin. De plus, même si ce n’est pas forcément ce que vous feriez dans la vie courante, Python numérote automatiquement chacun des éléments de la liste.

Comprendre comment les ordinateurs voient les listes

L’ordinateur ne voit pas les listes de la même façon que vous. Il n’a ni bloc-notes ni crayon. Il a juste de la mémoire. L’ordinateur enregistre donc chaque élément d’une liste dans un emplacement mémoire distinct. Cette mémoire est continue. Lorsque vous ajoutez de nouveaux éléments, ils viennent donc prendre place dans le prochain emplacement en mémoire.

D’une certaine manière, une liste se comporte dans l’ordinateur un peu comme une série de boîtes alignées : le premier élément est rangé dans la première boîte, le second dans la deuxième, et ainsi de suite. L’avantage, c’est bien sûr que la mémoire de l’ordinateur peut contenir un très grand nombre de ces boîtes, beaucoup plus que votre armoire à chaussures…

Python étant vraisemblablement mieux organisé que vous, les emplacements utilisés pour stocker les éléments d’une liste sont numérotés. Cette numérotation commence à 0 (sans doute pour des raisons de facilité). Il s’agit du même système de numérotation que celui utilisé avec les chaînes de caractères, comme nous l’avons expliqué dans la section « Sélectionner des caractères individuels » du Chapitre 2. Prenons l’exemple d’une liste de noms de mois. Elle comportera douze éléments numérotés de 0 à 11. Il est évidemment essentiel de bien intégrer cette manière de procéder.

Selon le type d’information que vous enregistrez dans les différentes « boîtes » , celles-ci n’ont pas besoin d’avoir toutes la même taille. Python vous permet par exemple de placer une chaîne de caractères dans une boîte, un entier dans une autre, et une valeur en virgule flottante dans une troisième. L’ordinateur ne sait pas, et n’a pas besoin de savoir, ce qui se trouve dans chacune des boîtes. Tout ce qu’il voit, ce sont des cases qui pourraient contenir n’importe quoi. C’est Python qui effectue tout le travail nécessaire au traitement des données en fonction de leur type, et qui, si vous demandez l’élément numéro 5, vous renvoie l’élément numéro 5 et rien d’autre.

En règle générale, il est préférable de créer des listes formant des ensembles de données plus faciles à gérer. En vous limitant par exemple à une liste composée uniquement de nombres entiers, vous pouvez facilement faire des suppositions sur ce que vous pouvez y trouver, sans perdre votre temps à tenter de retrouver la bonne valeur. Pour autant, il y a bien entendu des cas où des données de nature différente doivent être associées. Cependant, souvenez-vous que cela impose de déterminer le type des données lorsque vous recherchez des informations afin de pouvoir traiter correctement celles-ci. Prendre une chaîne pour un entier pourrait provoquer des problèmes dans votre application (ce qui n’est pas le cas dans d’autres langages de programmation, qui imposent généralement de ne créer que des listes formées d’éléments de même type).

Créer des listes

Avant de pouvoir faire quoi que ce soit avec une liste, vous devez évidemment commencer par créer celle-ci. Comme cela a été mentionné plus haut, les listes de Python peuvent mélanger différents types de données. Mais c’est toujours une bonne pratique de n’utiliser qu’un seul type chaque fois que c’est possible. Les étapes qui suivent vous montrent comment créer une liste dans Python.

❶ Ouvrez un nouveau notebook.

Vous pouvez également utiliser le fichier source téléchargeable Chapitre 11 Gérer des listss.ipynb.

❷ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne.

List1 = ["Un", 1, "Deux", True]
print(List1)

Python va créer une liste appelée List1. Elle contient deux chaînes ("Un" et "Deux"), une valeur entière (1) et une valeur booléenne (True).

Souvenez-vous que Python affiche chaque type de données dans une couleur différente, ce qui fournit un bon indice et vous aide à réduire les risques d’erreurs lorsque vous créez une liste.

❸ Cliquez sur Exécuter la cellule.

Vous voyez s’afficher la liste en entier (voir la Figure 11.1). Notez que les chaînes apparaissent entre apostrophes, quelle que soit la manière dont vous les avez saisies.

[picture]

Figure 11.1 Python affiche le contenu de la liste List1.

❹ Dans une nouvelle cellule du notebook, tapez dir(List1) et cliquez sur Exécuter la cellule.

Python affiche une série d’actions réalisables avec les listes. Vous pouvez constater au passage que la sortie est aussi une liste… Ainsi, vous utilisez une liste pour déterminer ce que vous pouvez faire avec une autre liste.

Lorsque vous travaillez avec un objet complexe, souvenez-vous que la commande dir() vous montre toujours les tâches que vous pouvez réaliser avec cet objet. Les principales actions dont vous disposez sont celles qui ne sont pas précédées (et suivies) de traits de soulignement. Ici, ce sont les suivantes :

• append (ajoute)

• clear (efface)

• copy (copie)

• count (compte)

• extend (étend)

• index (index)

• insert (insère)

• pop (élimine)

• remove (supprime)

• reverse (inverse)

• sort (trie)

Accéder aux listes

Une fois que vous avez créé une liste, vous voulez accéder aux éléments qu’elle contient. La section précédente vous a montré comment print() et dir() vous permettaient d’interagir avec List1, mais il y a bien entendu des tas d’autres possibilités.

❶ Dans une nouvelle cellule du notebook, tapez List1[1] et cliquez sur Exécuter la cellule.

Vous voyez la valeur 1 en sortie. L’utilisation d’un nombre dans une paire de crochets s’appelle un index. Python utilise toujours des index qui commencent à partir de zéro, si bien qu’en demandant l’élément dont l’index est 1 cela signifie que vous obtenez le deuxième élément de la liste.

❷ Dans une nouvelle cellule du notebook, tapez List1[1 : 3] et cliquez sur Exécuter la cellule.

Vous voyez une plage de valeurs qui comprend deux éléments. Lorsque vous saisissez une plage, la fin de la plage est toujours supérieure d’une unité au nombre d’éléments retournés. Dans cet exemple, cela signifie que vous obtenez les éléments 1 et 2, et non pas les éléments 1 à 3 comme vous pourriez vous y attendre.

❸ Dans une nouvelle cellule du notebook, tapez List1[1 : ] et cliquez sur Exécuter la cellule.

Vous voyez tous les éléments, en commençant par l’élément 1 jusqu’à la fin de la liste. Une plage peut avoir un numéro de fin vide, ce qui signifie simplement qu’il faut afficher le reste de la liste.

❹ Dans une nouvelle cellule du notebook, tapez List1[ : 3] et cliquez sur Exécuter la cellule.

Python affiche les éléments de 0 à 2. Le fait de laisser le début d’une plage en blanc signifie que vous voulez commencer par l’élément 0.

Même si cela paraît un peu étrange, Python vous permet d’utiliser des indices négatifs. Dans ce cas, la liste est parcourue de la fin vers le début, et non l’inverse. Si vous tapez par exemple ici List1[-2], vous obtiendrez le second élément en partant de la fin, soit Deux. De même, List1[-3] renverrait 1. Dans ce cas, l’élément le plus à droite aura pour indice -1.

Parcourir les listes

Pour automatiser le traitement des listes, vous avez besoin de les parcourir. La méthode la plus simple consiste à utiliser une boucle for, comme l’illustrent les étapes qui suivent.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée à la fin de chaque ligne.

List1 = [0, 1, 2, 3, 4, 5]
for Item in List1:
   print(Item)

Cet exemple commence par créer une liste de valeurs numériques. Il utilise ensuite une boucle for pour obtenir chaque élément et l’afficher.

❷ Cliquez sur Exécuter la cellule.

Python affiche les valeurs individuelles de la liste, ligne par ligne (voir la Figure 11.2).

[picture]

Figure 11.2 Une boucle permet d’obtenir facilement chaque élément d’une liste avant d’appliquer les traitements appropriés.

Modifier des listes

Vous pouvez modifier à votre guise le contenu d’une liste. Cela signifie changer une entrée particulière, en ajouter une nouvelle, ou encore supprimer une entrée existante. Python vous offre pour cela toute une série de fonctions :

• append() : ajoute une nouvelle entrée à la fin de la liste.

• clear() : supprime toutes les entrées de la liste.

• copy() : crée une copie de la liste et la place dans une nouvelle liste.

• extend() : ajoute des éléments d’une liste existante à la liste courante.

• insert() : ajoute une nouvelle entrée à la position spécifiée dans la liste.

• pop() : supprime une entrée à la fin de la liste.

• remove() : supprime une entrée à la position spécifiée dans la liste.

Les étapes qui suivent vous montrent comment modifier une liste. Vous retrouverez plus tard les mêmes fonctions appliquées dans des situations plus concrètes. Pour l’instant, il s’agit simplement de se familiariser avec la gestion des listes.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée après chaque ligne.

List2 = []
len(List2)

Python crée une liste nommée List2. Remarquez que les crochets sont vides. List2 ne contient aucune entrée. Il est possible de spécifier des listes vides qui seront remplies par la suite. En fait, c’est précisément ainsi que de nombreuses listes débutent, car vous ne savez pas toujours quelles informations elles vont contenir jusqu’à ce que l’utilisateur commence à interagir avec elles.

❷ Cliquez sur Exécuter la cellule.

La fonction len() renvoie la valeur 0. Ce qui est normal, puisque la liste est pour l’instant vide. Dans ce cas, vous savez que vous ne pouvez pas effectuer certaines actions, comme supprimer un élément, puisque justement il n’y en a pas encore.

❸ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée après chaque ligne.

List2.append(1)
len(List2)

❹ Cliquez sur Exécuter la cellule.

La fonction len() renvoie maintenant une longueur égale à 1.

❺ Dans une nouvelle cellule du notebook, tapez List2[0] et cliquez sur Exécuter la cellule.

Vous voyez la valeur de 1 stockée dans l’élément 0 de List2.

❻ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée après chaque ligne.

List2.insert(0, 2)
List2

La fonction insert() nécessite deux arguments. Le premier est l’indice de l’insertion, en l’occurrence l’élément de rang 0. Le second est l’objet que vous voulez insérer à cet emplacement, donc 2 dans ce cas.

❼ Cliquez sur Exécuter la cellule.

Python a ajouté le nouvel élément à List2. Cependant, l’utilisation de la fonction insert() permet d’ajouter le nouvel élément avant le premier élément, de telle sorte que le résultat est maintenant [2, 1].

❽ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée après chaque ligne.

List3 = List2.copy()
List2.extend(List3)
List2

La nouvelle liste, List3, est une copie exacte de List2. Cette technique est souvent employée pour créer une version temporaire d’une liste de manière à ce qu’un programme puisse y faire des modifications sans toucher à la liste originale, ce qui peut éviter de nombreux problèmes. Lorsque l’utilisateur a terminé, l’application peut alors décider si la liste temporaire doit simplement être détruite, ou si elle doit être recopiée dans la liste d’origine, ce qui permet de valider les modifications apportées.

Python recopie le contenu de List3 à la fin de List2. L’extension est couramment utilisée pour consolider deux listes.

❾ Cliquez sur Exécuter la cellule.

Vous constatez que la copie et l’extension ont bien fonctionné. List2 contient maintenant les valeurs [2, 1, 2, 1].

❿ Tapez List2.pop() dans une nouvelle cellule du notebook et cliquez sur Exécuter la cellule.

Python affiche comme résultat 1. Ce 1 était le dernier élément de la liste, et pop() supprime toujours les valeurs de la fin.

⓫ Tapez List2.remove(1) dans une nouvelle cellule du notebook et cliquez sur Exécuter la cellule.

Cette fois, Python supprime l’élément de rang 1. Contrairement à la fonction pop(), remove() n’affiche pas la valeur de l’élément supprimé.

⓬ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée après chaque ligne.

List2.clear()
len(List2)

L’utilisation de clear() signifie que la liste ne doit plus contenir d’éléments maintenant. La fonction len() confirme que List2 est définitivement vide.

⓭ Cliquez sur Exécuter la cellule.

Vous constatez que la sortie est bien égale à 0. La liste List2 est définitivement vide. À ce stade, vous venez d’essayer toutes les méthodes de modification des listes fournies par Python (voir la Figure 11.3). Travaillez encore un peu avec List2 en utilisant ces différentes fonctions jusqu’à ce que vous vous sentiez à l’aise pour apporter des modifications à la liste.

[picture]

Figure 11.3 Python offre un large éventail d’actions que vous pouvez accomplir avec des listes.

Utiliser des opérateurs avec les listes

Les listes peuvent aussi se servir d’opérateurs pour effectuer certaines tâches. Par exemple, si vous voulez créer une liste contenant quatre occurrences du mot Bonjour, vous pouvez écrire MaListe = [«Bonjour»] * 4 afin de la remplir. L’opérateur de multiplication (*) spécifie combien de fois l’élément indiqué doit être répété. Il est essentiel de bien comprendre que chaque nouvel élément est totalement distinct des autres. Par conséquent, le contenu de MaListe serait ici [‘Bonjour’, ‘Bonjour’, ‘Bonjour’, ‘Bonjour’].

Vous pouvez aussi remplir une liste en faisant appel à la concaténation. Par exemple, l’instruction MaListe = [«Bonjour»] + [«le monde»] + [« ! »] * 4 créerait six éléments dans MaListe. Le premier serait ‘Bonjour’, suivi de ‘le monde’, puis de quatre points d’exclamation.

L’opérateur in fonctionne également avec les listes. Dans ce chapitre, nous allons utiliser une méthode directe et simple (qui est l’approche recommandée). Cependant, vous pouvez parfaitement aussi vouloir aller droit au but en écrivant par exemple «Bonjour» in MaListe. En supposant que MaListe est celle créée au paragraphe précédent, la sortie de cette instruction serait True (vrai).

Faire des recherches dans les listes

Il n’est pas très facile de modifier une liste si vous ne savez pas ce qu’elle contient. La possibilité d’effectuer des recherches dans les listes est donc une tâche essentielle. Les étapes qui suivent démontrent comment vous pouvez rechercher une valeur spécifique dans une liste.

❶ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée à la fin de chaque ligne :

Colors = ["Rouge", "Orange", "Jaune", "Vert", "Bleu"]
ColorSelect = ""
while str.upper(ColorSelect) != "QUITTER":
   ColorSelect = input("Sélectionnez le nom d'une couleur: ")
   if (Colors.count(ColorSelect) >= 1):
   print("La couleur existe dans la liste !")
elif (str.upper(ColorSelect) != "QUITTER"):
   print("La liste ne contient pas cette couleur.")

L’exemple commence par créer une liste nommée Colors. Celle-ci contient des noms de couleurs. Il crée également une variable appelée ColorSelect destinée à contenir le nom de la couleur que l’utilisateur veut retrouver. L’application entre ensuite dans une boucle où elle demande de saisir un nom de couleur, qui est placé dans ColorSelect. Tant que cette variable ne contient pas le mot QUITTER (ou toute variante de ce mot, l’application le convertissant en majuscules), la boucle répète sa question.

Lorsque l’utilisateur entre un nom de couleur, l’application demande à la liste de compter le nombre d’occurrences de ce mot. Si cette valeur est au moins égale à 1, cela signifie que la liste contient bien ce mot, et un message est affiché en conséquence. Dans le cas contraire, l’application affiche un message d’erreur.

Cet exemple utilise une clause elif pour tester le cas où la version en majuscules de ColorSelect contient le mot QUITTER. Cette technique permet de s’assurer qu’un message particulier ne sera pas émis si l’utilisateur veut terminer l’application (mais on pourrait bien entendu lui faire dire aussi : Au revoir ! ). Cette pratique est particulièrement utile pour éviter toute confusion, voire même une perte de données dans le cas où l’application effectuerait une tâche non désirée.

❷ Cliquez sur Exécuter la cellule.

Python vous demande d’entrer le nom d’une couleur.

❸ Tapez Bleu et appuyez sur Entrée.

Un message vous indique cette couleur appartient bien à la liste.

❹ Tapez Violet et appuyez sur Entrée.

Cette fois, un message vous informe que cette couleur n’appartient pas à la liste.

❺ Tapez Quitter et appuyez sur Entrée.

L’application se termine en douceur. Vous noterez que l’application n’affiche ni un message de réussite ni un message d’échec. La Figure 11.4 illustre les résultats des différentes saisies.

[picture]

Figure 11.4 La combinaison d’une boucle avec du code conditionnel permet de proposer un menu avec des sélections multiples.

Trier des listes

L’ordinateur peut localiser des informations dans une liste indépendamment de leur position dans celle-ci. Mais, en fait, ces recherches sont bien plus faciles dans le cas de listes importantes si celles-ci sont classées d’une certaine manière. Pour autant, la raison principale justifiant le fait de trier une liste est tout simplement de simplifier la lecture des informations par l’utilisateur. Les êtres humains s’y retrouvent plus facilement lorsque les choses sont bien classées.

L’exemple qui suit part d’une liste dont les éléments sont placés dans un ordre quelconque. La liste est ensuite triée, et son contenu est affiché.

❶ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée à la fin de chaque ligne :

Colors = ["Rouge", "Orange", "Jaune", "Vert", "Bleu"]
for Item in Colors:
   print(Item, end=" ")
print()
Colors.sort()
for Item in Colors:
   print(Item, end=" ")
print()

Cet exemple commence par créer une liste de couleurs dans un ordre quelconque. Cette liste est affichée pour montrer cet apparent désordre. Notez le end final sur la ligne de la fonction print(). Il permet de s’assurer que tous les éléments sont affichés sur une seule et même ligne (ce qui facilite leur comparaison).

Trier une liste est très simple. Une fois la fonction sort() appelée, la liste peut être affichée dans l’ordre alphabétique.

❷ Cliquez sur Exécuter la cellule.

Python affiche les deux états de la liste : non triée, puis triée (voir la Figure 11.5).

[picture]

Figure 11.5 Trier une liste nécessite simplement l’appel de la fonction sort().

Le tri peut aussi s’effectuer dans l’ordre inverse. Pour cela, vous utilisez la fonction reverse(). Celle-ci doit figurer sur une ligne différente. Dans ce cas, l’exemple précédent ressemblerait à ceci :

Colors = ["Rouge", "Orange", "Jaune", "Vert", "Bleu"]
for Item in Colors:
  print(Item, end=" ")
print()
Colors.sort()
Colors.reverse()
for Item in Colors:
  print(Item, end=" ")
print()

Afficher des listes

Python fournit de très nombreuses façons d’afficher des informations. Jusqu’à présent, ce chapitre n’a montré que quelques-unes des méthodes les plus basiques d’affichage des listes. Dans le monde réel, l’affichage des listes peut vite devenir complexe, et c’est pourquoi vous devez connaître quelques techniques supplémentaires. L’emploi de ces techniques est en fait beaucoup plus facile si vous les testez au fur et à mesure.

❶ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée après chaque ligne :

Colors = ["Rouge", "Orange", "Jaune", "Vert", "Bleu"]
print(*Colors, sep='\n')

Cet exemple commence par employer la même liste de couleurs que dans la section précédente. Dans cette section, vous utilisez une boucle for pour afficher les éléments individuels. Cet exemple adopte une autre approche. Il utilise l’opérateur splat (*), également appelé opérateur d’expansion positionnelle, afin de développer la liste et d’envoyer chaque élément à la méthode print(), un seul élément à la fois. L’argument sep indique comment séparer chacune des sorties, en exploitant un caractère de nouvelle ligne dans cet exemple.

❷ Cliquez sur Exécuter la cellule.

Python affiche la liste, élément par élément.

❸ Tapez le code suivant dans une nouvelle cellule du notebook et cliquez sur Exécuter la cellule.

for Item in Colors: print(Item.rjust(8), sep='\n')

Le code ne doit pas nécessairement apparaître sur plusieurs lignes, et dans cet exemple, le code a été regroupé sur une seule ligne. Ici, on emploie la méthode rjust(), qui justifie à droite la chaîne de caractères. De nombreuses méthodes de ce type sont décrites à https://docs.python.org/fr/3.9/library/string.html.

❹ Tapez le code suivant dans une nouvelle cellule du notebook et cliquez sur Exécuter la cellule.

print('\n'.join(Colors))

Python fournit plusieurs façons d’exécuter une tâche. Dans cet exemple, le code utilise la méthode join() pour ajouter le caractère de nouvelle ligne à chaque membre de Colors. Le résultat est identique à celui de l’Étape 3, même si l’approche est différente. L’idée est d’utiliser la méthode qui convient le mieux à un besoin particulier.

❺ Tapez le code suivant dans une nouvelle cellule du notebook et cliquez sur Exécuter la cellule.

print('Premier : {0}\nDeuxième : {1}'.format(*Colors))

Dans ce cas, la sortie est formatée d’une manière spécifique avec un texte d’accompagnement, et le résultat n’inclut pas tous les membres de Colors. Les entrées {0} et {1} représentent des caractères de remplacement pour les valeurs fournies par *Colors. La Figure 11.6 illustre le résultat de toutes les techniques démontrées dans ces étapes. Vous pourrez en apprendre davantage sur cette approche (le sujet est immense) à https://docs.python.org/fr/3/tutorial/inputoutput.html.

Cette section n’aborde que quelques-unes des techniques courantes utilisées pour formater la sortie en Python. Il en existe bien d’autres. Vous verrez la plupart de ces approches illustrées dans les chapitres qui suivent. Le Chapitre 2 propose également un certain nombre de façons de travailler avec des chaînes de caractères. Ces techniques sont également utiles pour travailler avec des listes. L’objectif essentiel est d’employer une technique lisible, qui fonctionne bien avec toutes les entrées prévues et qui ne vous met pas dans une impasse lorsque vous créez ultérieurement des sorties supplémentaires.

Travailler avec l’objet Counter

Il arrive que vous ayez une source de données, et que vous vouliez simplement savoir combien de fois une certaine chose se produit (comme l’apparence d’un élément donné dans la liste). Si celle-ci est courte, vous pouvez simplement compter les éléments. En revanche, si la liste est réellement longue, il est pratiquement impossible d’obtenir un résultat parfaitement fiable. Supposons ainsi que vous ayez décidé d’écrire un roman appelé Guerre et paix. Vous voulez alors connaître la fréquence de certains mots dans ce roman-fleuve. Cette tâche serait évidemment impossible sans l’aide d’un ordinateur (certaines personnes insisteraient pour compter ces mots un par un, en utilisant des fiches cartonnées pour enregistrer le résultat, mais ce n’est sans doute pas votre cas).

L’objet Counter vous permet de compter rapidement des éléments. Il est de plus incroyablement facile à utiliser. Vous le retrouverez dans d’autres circonstances dans ce livre, mais ici il va uniquement nous servir avec des listes. L’exemple développé dans ce qui suit crée une liste contenant des éléments répétitifs, puis il compte le nombre d’occurrences de ces éléments.

[picture]

Figure 11.6 Python fournit de nombreuses façons différentes d’afficher des listes.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée à la fin de chaque ligne :

from collections import Counter

MyList = [1, 2, 3, 4, 1, 2, 3, 1, 2, 1, 5]
ListCount = Counter(MyList)
print(ListCount)
for ThisItem in ListCount.items():
   print("L’élément : ", ThisItem[0],
        " apparaît : ", ThisItem[1], " fois")
print("La valeur 1 apparaît {0} fois."
     .format(ListCount.get(1)))

Pour utiliser l’objet Counter, il est nécessaire de l’importer depuis la bibliothèque collections. Bien entendu, si vous devez travailler dans votre application avec d’autres types de collections, vous pouvez importer la totalité de ce package en tapant l’instruction import collections.

Cet exemple commence par créer une liste appelée MyList. Celle-ci contient des valeurs numériques répétées. Vous pouvez facilement voir que certains de ces éléments apparaissent plusieurs fois. Cette liste est placée dans un objet de type Counter appelé ListCount. Vous pouvez créer des objets Counter de différentes manières, mais celle-ci est la mieux adaptée dans le cas des listes.

L’objet Counter et la liste ne sont en aucun cas connectés. Si le contenu de la liste change, vous devrez recréer l’objet Counter, car il n’est pas actualisé dans ce cas. Une alternative consiste à invoquer d’abord la méthode clear(), suivie d’un appel à update() pour remplir l’objet Counter avec les nouvelles données.

L’application affiche la valeur de ListCount de diverses manières. La première sortie correspond à ce que vaut le compteur Counter sans aucune manipulation. La seconde sortie affiche les différents éléments de MyList ainsi que le nombre de fois où ils apparaissent dans la liste. Pour obtenir ce résultat, vous utilisez la fonction items(). Enfin, l’exemple montre comment obtenir le décompte pour un élément individuel en faisant appel à la fonction get().

❷ Cliquez sur Exécuter la cellule.

Python affiche les résultats renvoyés en utilisant l’objet Counter (voir la Figure 11.7).

Vous noterez que les informations sont enregistrées dans l’objet Counter sous la forme d’une paire formée d’une clé et d’une valeur. Le Chapitre 2 reviendra sur ce sujet plus en détail. Tout ce que vous avez besoin de savoir pour l’instant, c’est que l’élément trouvé dans MyList devient dans ListCount une clé qui identifie le nom unique de l’élément. La valeur contient le nombre de fois où cet élément apparaît dans MyList.

[picture]

Figure 11.7 L’objet Counter est utile pour obtenir des statistiques intéressantes avec de longues listes.

DANS CE CHAPITRE

Définir une collection.

•

Utiliser des tuples.

•

Utiliser des dictionnaires.

•

Développer des piles en utilisant des listes.

•

Utiliser le module queue. Utiliser des deques.

Chapitre 12

Collecter toutes sortes de données

Les gens collectent toutes sortes de choses. Il peut s’agir de piles de disques, de cartes postales, de bouchons de champagne, de timbres, bref tout ce que vous voulez. Les collections que vous rencontrez lorsque vous écrivez des applications ne sont pas différentes de ce qui se passe dans le monde réel. Une collection est tout simplement un groupe d’éléments semblables, rangés dans un même lieu, et généralement organisés sous une forme facilement compréhensible.

Ce chapitre traite des collections au sens large. L’idée centrale qui préside à toute collection consiste à créer un environnement dans lequel la collection est correctement gérée, et vous permettant de localiser avec précision ce que vous voulez à n’importe quel moment. Par exemple, des rayonnages sont parfaits pour y classer des livres, des DVD, et toutes sortes d’objets plats. En revanche, des timbres seront bien mieux dans un classeur adapté. Et on pourrait multiplier les exemples. Même si le mode de rangement change, cela ne retire rien au fait qu’il s’agit à chaque fois de collections. Ceci vaut également pour les collections traitées par des ordinateurs. Certes, il y a une différence entre une pile et une file, mais cela ne change rien à l’essentiel : vous avez besoin de méthodes qui vous permettent de bien gérer vos données et d’y accéder facilement lorsque vous en avez besoin.

Vous n’avez pas à saisir manuellement le code source de ce chapitre. En fait, l’utilisation de la source téléchargeable est beaucoup plus facile. Vous trouverez le code source de ce chapitre dans le fichier Chapitre 12 Collecter toutes sortes de données.ipynb. Consultez l’introduction pour savoir comment trouver ces fichiers source.

Comprendre les collections

Le Chapitre 5 vous a initié aux séquences. Les collections sont tout simplement un autre type de séquence, mais un peu plus complexe que les chaînes et les listes. Quelle que soit la séquence que vous utilisez, elle prend en charge deux fonctions : index() et count(). La fonction index() renvoie la position d’un élément spécifié dans une séquence. Vous pouvez par exemple obtenir ainsi l’emplacement d’un caractère dans une chaîne ou d’un objet dans une liste. La fonction count() retourne le nombre d’occurrences d’un élément spécifique dans une liste. La nature de cet élément dépend évidemment de celle de la liste.

Vous pouvez utiliser des collections pour créer avec Python des structures de type base de données. Chaque type de collection a un objet différent, et vous utilisez ces divers types de manière spécifique. L’idée importante à retenir ici est que les collections sont une autre forme de séquence. Et, à ce titre, elles supportent également les fonctions index() et count().

Python est conçu pour être extensible. Cependant, il possède un jeu de base de collections qui peuvent servir à créer la plupart des applications. Ce chapitre décrit les collections les plus courantes (certaines sont implémentées en tant que convention de programmation, comme les tuples et les dictionnaires, certaines sont implémentées à l’aide d’une autre classe, comme la pile, et d’autres sont implémentées directement à l’aide de classes, comme les objets queue et deque) :

• Tuple : un tuple est une collection servant à créer des séquences semblables à des listes, mais en plus complexes. L’un des avantages est que vous pouvez imbriquer le contenu d’un tuple. Cette fonctionnalité vous permet par exemple de créer des structures servant à gérer des informations sur le personnel d’une entreprise, ou encore des couples de coordonnées x-y.

• Dictionnaire : comme avec un vrai dictionnaire, vous pouvez créer des paires clé/valeur (sur le modèle mot/définition). Un dictionnaire permet des recherches incroyablement rapides, et rend le classement des données beaucoup plus faciles. Python offre un certain nombre de classes pour implémenter la collection de stockage de données de dictionnaire. Consultez https://docs.python.org/fr/3/library/collections.html pour une liste typique de classes de collections.

• Queue : ce terme désigne une file, c’est-à-dire une collection de type FIFO (First In, First Out, ou premier entré, premier sorti). C’est le principe de la file d’attente devant un guichet.

Python propose une variante appelée LifoQueue, soit une collection de type LIFO (Last In, First Out, ou dernier entré, premier sorti). C’est le principe de la pile, où l’on prend d’abord l’élément du dessus, celui qui a été posé en dernier.

• Deque : sous ce terme barbare se cache une queue à double fin, c’est-à-dire une file dans laquelle il est possible d’ajouter ou de supprimer des éléments au début ou à la fin, mais pas au milieu. Vous pouvez utiliser un deque en tant que collection de type queue, ou comme pile, ou tout autre type de collection dans laquelle vous ajouter ou supprimez des éléments de manière ordonnée (à la différence des listes, des tuples et des dictionnaires qui autorisent un accès aléatoire).

Travailler avec les tuples

Un tuple est une collection servant à créer des listes complexes, dans lesquelles un élément peut être imbriqué dans un autre. Ceci vous permet donc de créer des hiérarchies. Une hiérarchie peut être aussi simple que l’arborescence de votre disque dur, mais il peut aussi s’agir par exemple de l’organigramme complet de votre entreprise. L’idée est que vous pouvez créer des structures de données complexes avec un tuple.

Les tuples sont « immutables » , ce qui signifie que vous ne pouvez pas les changer. Vous pouvez créer un nouveau tuple portant le même nom, et le modifier d’une certaine manière, mais vous ne pouvez pas le faire avec un tuple existant. En revanche, les listes, elles, sont mutables, et donc modifiables. Cette nature des tuples peut paraître un désavantage, mais cette immutabilité a toutes sortes de vertus, notamment en termes de sécurité et de rapidité. De plus, ce type d’objet est plus facile à gérer avec des systèmes multiprocesseurs.

La plus grande différence entre un tuple et une liste est qu’un tuple est immutable ; c’est la raison pour laquelle les tuples sont plus souvent utilisés pour l’accès aux données, que pour les opérations sur les données. L’immutabilité rend également les tuples plus rapides à utiliser, consomme moins de mémoire et engendre moins d’appels de méthodes associés. Les étapes suivantes montrent comment vous pouvez interagir avec un tuple en Python.

❶ Ouvrez un nouveau notebook.

Vous pouvez également utiliser le fichier source téléchargeable, Chapitre 12 Collecter toutes sortes de données.ipynb.

❷ Tapez le code suivant dans le notebook, en appuyant sur la touche Entrée après chaque ligne :

MyTuple = ("Rouge", "Bleu", "Vert")
MyTuple

Python crée un tuple contenant trois chaînes.

❸ Cliquez sur Exécuter la cellule.

Python affiche le contenu de MyTuple (voir la Figure 12.1). Remarquez que les chaînes sont délimitées par des apostrophes, même si vous avez utilisé des guillemets pour les définir. En outre, notez qu’un tuple utilise des parenthèses plutôt que des crochets, comme les listes.

[picture]

Figure 12.1 Les tuples utilisent des parenthèses, et non pas des crochets droits.

❹ Tapez print(dir(MyTuple)) et cliquez sur Exécuter la celulle.

Python affiche la liste des fonctions que vous pouvez utiliser avec les tuples :

['__add__', '__class__', '__contains__',
'__delattr__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__',
'__getitem__', '__getnewargs__', '__gt__',
'__hash__', '__init__', '__init_subclass__',
'__iter__', '__le__', '__len__', '__lt__', '__mul__',
'__ne__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__rmul__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', 'count', 'index']

Remarquez que la liste des fonctions semble nettement plus courte que la liste des fonctions fournies avec les listes au Chapitre 5. Les fonctions count() et index() sont présentes..

Les apparences sont parfois trompeuses. Par exemple, vous pouvez ajouter de nouveaux éléments avec la fonction __add__(). Lorsque vous travaillez avec des objets Python, regardez toutes les entrées avant de prendre la bonne décision.

❺ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée après chaque ligne :

MyTuple = MyTuple.__add__(("Violet",))
MyTuple

Ce code ajoute un tuple à MyTuple, et place le résultat dans une nouvelle copie de MyTuple. L’ancienne version de MyTuple est détruite après cet appel.

La fonction __add__() accepte uniquement des tuples en entrée. Ceci signifie que l’ajout doit être placé entre parenthèses. De plus, lorsque vous créez un tuple ne comprenant qu’une seule entrée, celle-ci doit être suivie d’une virgule. C’est une règle propre à Python. À défaut, vous obtiendrez un message d’erreur du genre :

TypeError: can only concatenate tuple (not “str”) to tuple

❻ Cliquez sur Exécuter la cellule.

L’ajout à MyTuple apparaît en fin de liste. Notez qu’il se situe au même niveau que les autres éléments.

❼ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée après chaque ligne :

MyTuple = MyTuple.__add__(("Jaune",
                          ("Orange", "Noir")))
MyTuple[4]

Cette étape ajoute trois nouvelles couleurs. Cependant, Orange et Noir sont insérés en tant que tuple à l’intérieur du tuple principal, ce qui crée une hiérarchie. Ces deux entrées sont traitées comme une seule entité à l’intérieur du tuple principal.

Il est possible de remplacer la fonction __add__() par l’opérateur de concaténation. Si vous vouliez par exemple ajouter la couleur Magenta en début de liste, vous pourriez taper MyTuple = ('Magenta',) + MyTuple.

❽ Cliquez sur Exécuter la cellule.

Python affiche le membre de MyTuple ayant pour rang 4, c’est-à-dire Jaune. Les tuples utilisent des indices pour accéder à leurs membres individuels, exactement comme les listes. Vous pouvez également définir une plage. Tout ce qu’il est possible de faire avec des indices dans une liste l’est également avec un tuple.

❾ Tapez MyTuple[5] et cliquez sur Exécuter la cellule. Vous voyez maintenant un tuple qui contient les chaînes Orange et Noir. Bien entendu, votre but n’est pas d’utiliser ces éléments sous la forme d’un tuple.

❿ Tapez type(MyTuple[5]) == tuple et cliquez sur Exécuter la cellule.

Les tuples contiennent régulièrement des hiérarchies. Vous pouvez détecter quand un index a retourné un autre tuple, plutôt qu’une valeur, en testant son type. La sortie est True dans ce cas.

⓫ Tapez MyTuple[5][0] et cliquez sur Exécuter la cellule.

La sortie va maintenant afficher la couleur Orange, autrement dit le premier élément du tuple ajouté lors de l’Étape 7. La Figure 12.2 illustre la progression de cet exemple. Les indices apparaissent toujours dans l’ordre de leur niveau dans la hiérarchie.

[picture]

Figure 12.2 Les indices permettent d’accéder aux membres individuels d’un tuple.

En utilisant une combinaison d’indices et la fonction __add__() (ou l’opérateur de concaténation), vous pouvez créer des applications évolutives basées sur des tuples. Par exemple, vous pouvez supprimer un élément d’un tuple en le rendant égal à une plage de valeurs. Vous pourriez supprimer de cette manière le tuple qui contient les chaînes Orange et Noir en tapant MyTuple = MyTuple[0:5].

Travailler avec les dictionnaires

Un dictionnaire Python fonctionne selon le même principe que votre Larousse ou votre Robert préféré. Vous créez une clé qui est associée à une valeur (pour un dictionnaire du monde réel, vous remplacez juste clé par mot, et valeur par définition). Comme les listes, les dictionnaires sont mutables, ce qui signifie que vous pouvez les modifier selon vos besoins. Le principal intérêt des dictionnaires est d’accélérer les recherches. Une clé est normalement courte et toujours unique, ce qui fait que l’ordinateur passe beaucoup moins de temps à rechercher les données dont vous avez besoin.

Les sections qui suivent montrent comment créer et utiliser des dictionnaires de différentes manières. Grâce à eux, il est possible de pallier les déficiences du langage Python. Ainsi, la plupart des langages de programmation possèdent une instruction switch qui sert essentiellement à proposer un menu dans lequel l’utilisateur fait son choix. Or, cette instruction manque à Python, ce qui fait que vous devez faire appel à des instructions if...elif pour effectuer ce genre de tâche, mais vous pouvez également utiliser les dictionnaires (certes, cela fonctionne, mais cette méthode manque de clarté).

Créer et utiliser un dictionnaire

Le principe ressemble beaucoup à celui des listes, si ce n’est que vous devez maintenant définir des paires clé/valeur. Il faut pour cela respecter certaines règles :

• La clé doit être unique. Si vous entrez une clé qui existe déjà, elle vient simplement remplacer la définition antérieure.

• La clé doit être immutable. Cela signifie que la clé peut être une chaîne, un nombre ou encore un tuple. Mais elle ne peut pas être une liste.

Il n’y a aucune restriction sur les valeurs associées aux clés. Il peut s’agir d’un objet Python quelconque, ce qui vous permet par exemple de vous servir d’un dictionnaire pour accéder aux informations sur les membres du personnel de votre entreprise, ou toute autre donnée complexe. Les étapes qui suivent vous aideront à mieux comprendre le fonctionnement de ces dictionnaires.

❶ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée après chaque ligne :

Colors = {"Sam": "Bleu", "Annie": "Rouge",
         "Sarah": "Jaune"}
Colors

Python vient de créer un dictionnaire comprenant trois entrées : des prénoms de personnes, et la couleur favorite de chacune. Remarquez la manière dont les entrées du dictionnaire sont fabriquées : la clé vient en premier, suivie d’un deux-points, puis de la valeur, et on utilise des accolades à la place de crochets ou de parenthèses. Les entrées sont séparées l’une de l’autre par une virgule.

❷ Cliquez sur Exécuter la cellule.

Python affiche les paires clé/valeur, comme l’illustre la Figure 12.3. La contrepartie, c’est que la création d’un dictionnaire est plus lente que celle d’une liste, puisque l’ordinateur doit effectuer ce travail de classement.

[picture]

Figure 12.3 Un dictionnaire place ses entrées dans un certain ordre.

❸ Tapez Colors['Annie'] et appuyez sur Entrée.

Vous voyez la couleur associée à Annie, le Rouge. Utiliser une chaîne comme clé, plutôt qu’un index numérique, facilite la lecture du code ainsi que sa maintenance. En procédant ainsi, les dictionnaires vous permettront de gagner énormément de temps sur le long terme (c’est d’ailleurs pourquoi ils sont si populaires). Cependant, vous devez aussi prendre en compte le coût du temps nécessité par leur création, ainsi que la mobilisation supplémentaire de ressources que cela implique.

❹ Tapez Colors.keys() et cliquez sur Exécuter la cellule. Le dictionnaire présente une liste des clés qu’il contient : dict_ keys(['Sam', 'Annie', 'Sarah']). Vous pouvez vous servir de ces clés pour automatiser l’accès au dictionnaire.

❺ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée après chaque ligne, et cliquez sur Exécuter la cellule.

for Item in Colors.keys():
    print("{0} aime la couleur {1}."
         .format(Item, Colors[Item]))

Cet exemple affiche tous les noms des utilisateurs ainsi que leur couleur préférée :

Sam aime la couleur Bleu.
Annie aime la couleur Rouge.
Sarah aime la couleur Jaune.

Utiliser des dictionnaires peut nettement faciliter l’affichage d’informations. De plus, un emploi judicieux des clés permet d’utiliser celles-ci dans les sorties.

❻ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée après chaque ligne, puis cliquez sur Exécuter la cellule.

Colors["Sarah"] = "Violet"
Colors.update({"Harry": "Orange"})
for name, color in Colors.items():
    print("{0} aime la couleur {1}."
         .format(name, color))

Le contenu du dictionnaire est mis à jour de telle sorte que Sarah aime maintenant le violet à la place du jaune. Le code ajoute également une nouvelle entrée au dictionnaire.

La boucle utilisée dans ce cas diffère également de celle utilisée à l’Étape 5. Cette version obtient chacun des éléments un par un et place la clé dans le nom et la valeur dans la couleur. Le résultat sera toujours le même à partir de la méthode items(). Vous avez besoin de deux variables, une pour la clé et une autre pour la valeur, présentées dans l’ordre indiqué. On emploiera cette deuxième forme dans certains cas, car elle peut être plus facile à lire. Il ne semble pas y avoir une grande différence de vitesse entre les deux versions. Voici la sortie mise à jour :

Sam aime la couleur Bleu.
Annie aime la couleur Rouge.
Sarah aime la couleur Violet.
Harry aime la couleur Orange.

Notez que Harry est ajouté au dictionnaire. De plus, l’entrée de Sarah est modifiée avec la couleur Violet.

❼ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée après chaque ligne, puis cliquez sur Exécuter la cellule.

del Colors["Sam"]
for name, color in Colors.
items():
   print("{0} aime la
couleur {1}."
         .format(name,
color))

Python supprime l’entrée de Sam du dictionnaire. Voici la nouvelle sortie de l’exemple :

Annie aime la couleur
Rouge.
Sarah aime la couleur Vio-
let.
Harry aime la couleur
Orange.

❽ Tapez len(Colors) et cliquez sur Exécuter la cellule.

La valeur de sortie, 3, confirme que le dictionnaire ne contient plus que trois entrées, au lieu de quatre.

❾ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée après chaque ligne, puis cliquez sur Exécuter la cellule.

Colors.clear()
len(Colors)

Python signale que Colors a 0 entrée, et le dictionnaire est donc vide à présent. La Figure 12.4 présente les résultats des commandes précédentes afin que vous puissiez voir la progression de l’utilisation du dictionnaire.

[picture]

Figure 12.4 Les dictionnaires fonctionnent à peu près comme les listes, mais avec une petite fonctionnalité supplémentaire.

Travailler avec des dictionnaires imbriqués

Contrairement au dictionnaire qui se trouve sur votre écran (ou éventuellement sur votre étagère), une entrée de dictionnaire Python peut contenir un autre dictionnaire. C’est un peu comme quand on regarde un de ces films de Dr. Strange où l’on rentre dans le Multivers (https://www.imdb.com/title/tt9419884/). L’imbrication de dictionnaires vous donne toutes sortes de nouveaux pouvoirs Python qui étonneront vos amis, votre famille et vos collègues ! La création d’un dictionnaire imbriqué s’apparente à la création d’un dictionnaire ordinaire dans la mesure où les clés doivent toujours être uniques, mais les valeurs représentent d’autres dictionnaires. Voici comment créer un dictionnaire imbriqué :

❶ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée après chaque ligne :

MonEntreprise = {"Propriétaire": "Moi",
            "Comptabilité": {151: "Martha",
                             152: "Jean"},
            "Informatique": {251: "Annie",
                             252: "Albert"},
            "Logistique": {351: "Gilles",
                           352: "Sylvie"}
           }
MonEntreprise

Python crée un dictionnaire imbriqué à deux niveaux contenant quatre entrées au premier niveau : Propriétaire, Comptabilité, Informatique, et Logistique. Le deuxième niveau contient un numéro d’employé, suivi d’un nom.

❷ Cliquez sur Exécuter la cellule.

Vous voyez les paires de clés et de valeurs (Figure 12.5).

Les dictionnaires imbriqués nécessitent un peu plus de traitement si vous voulez afficher entièrement leur contenu. Cela signifie qu’il faut utiliser la récursivité ou une autre méthode pour parcourir le dictionnaire. L’étape suivante présente une technique permettant d’effectuer cette tâche.

❸ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée après chaque ligne :

[picture]

Figure 12.5 Les dictionnaires imbriqués suivent les mêmes règles que les dictionnaires ordinaires, mais ces règles sont juste un peu plus complexes.

def DisplayDict(Dict):
   for key, value in Dict.items():
       if type(value) == dict:
           print(f"Le service {key} est composé de :")
           DisplayDict(value)
       else:
           print(f"{value} est {key}")

DisplayDict(MonEntreprise)

Le code commence par appeler DisplayDict() avec MonEntreprise. Cet exemple utilise une technique récursive dans laquelle la valeur (value) extraite du dictionnaire (Dict) est d’abord vérifiée pour déterminer s’il s’agit d’un autre dictionnaire (type dict). Si c’est le cas, la clé (key), qui est en fait le nom d’un service est affichée. Vous noterez que cet exemple utilise un f-string, qui est expliqué dans la section « Simplifier les choses en utilisant f-string » du Chapitre 12). Le code appelle ensuite récursivement DisplayDict() avec value.

Une fois que le code a déterminé que value ne contient pas de dictionnaire, il procède à l’affichage de la valeur avec la clé, de la même manière que dans la section précédente du chapitre, sauf que cette version utilise un f-string au lieu de la fonction .format(). Comparez les deux versions et vous remarquerez que l’approche f-string, outre le fait qu’elle permet d’économiser de la frappe, est plus facile à lire. La Figure 12.6 illustre les résultats du travail avec des dictionnaires imbriqués.

[picture]

Figure 12.6 La récursivité facilite le travail avec les dictionnaires imbriqués.

Remplacer l’instruction switch par un dictionnaire

La plupart des langages de programmation disposent d’une instruction switch qui permet d’effectuer des sélections dans des menus. L’utilisateur dispose de plusieurs options et il doit en choisir une. Le programme effectue ensuite l’action correspondante. Voici à quoi pourrait ressembler une telle instruction dans un autre langage (bien entendu, cela ne signifie rien pour Python) :

switch(n)
{
    case 0:
       print("Vous avez choisi bleu.");
       break;
    case 1:
       print("Vous avez choisi jaune.");
       break;
    case 2:
       print("Vous avez choisi vert.");
       break;
}

L’application présente normalement une interface de type menu, récupère une valeur correspondant à la sélection effectuée par l’utilisateur, puis effectue la ou les tâches définies à partir de l’instruction switch. Cette méthode est plus claire et plus simple que l’emploi d’une série d’instructions if...elif pour obtenir le même résultat.

Mais si Python ne propose pas de type d’instruction, vous pouvez tout de même la simuler en utilisant un dictionnaire. C’est ce que vous montrent les étapes suivantes.

❶ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée à la fin de chaque ligne :

def PrintBleu():
  print("Vous avez choisi bleu !\r\n")
def PrintRouge():
  print("Vous avez choisi rouge !\r\n")
def PrintOrange():
  print("Vous avez choisi orange !\r\n")
def PrintJaune():
  print("Vous avez choisi jaune !\r\n")

Avant que le code puisse faire quoi que ce soit, vous devez spécifier les tâches à accomplir. Chacune de ces fonctions définit donc une tâche associée au choix d’une couleur. Un seul de ces éléments peut être appelé à un moment donné.

❷ Tapez le code suivant dans la même cellule du notebook, en appuyant sur Entrée à la fin de chaque ligne :

ColorSelect = {
  0: PrintBleu,
  1: PrintRouge,
  2: PrintOrange,
  3: PrintJaune
}

Ce code définit le dictionnaire. Chaque clé correspond à une partie d’une instruction de type switch. Les valeurs spécifient ce qu’il faut faire en fonction de la valeur de la clé. Les fonctions que vous avez créées plus haut sont les actions associées à la clause switch virtuelle, celle qui se trouvent normalement entre l’instruction case et la clause break.

❸ Tapez le code suivant dans la même cellule du notebook, en appuyant sur Entrée à la fin de chaque ligne :

Selection = 0
while (Selection != 4):
  print("0. Bleu")
   print("1. Rouge")
   print("2. Orange")
   print("3. Jaune")
   print("4. Quitter")
   Selection = int(input("Sélectionnez une option de couleur :
"))
   if (Selection >= 0) and (Selection < 4):
     ColorSelect[Selection]()

Vous voyez ici la partie de l’exemple qui affiche l’interface utilisateur. Le code commence par créer une variable de saisie, Selection. Il rentre ensuite dans une boucle jusqu’à ce que l’utilisateur saisisse la valeur 4, qui termine l’opération.

Lors de chaque boucle, l’application affiche une liste d’options, puis attend que l’utilisateur tape un chiffre. Quand c’est fait, elle vérifie la saisie. Une valeur entre 0 et 3 affiche une des fonctions définies auparavant dans le dictionnaire, afin de remplacer l’instruction switch.

❹ Cliquez sur Exécuter la cellule.

Python affiche un menu.

❺ Tapez 0 et appuyez sur Entrée.

L’application vous dit que vous avez sélectionné la couleur bleue, puis elle affiche de nouveau le menu.

❻ Tapez 4 et appuyez sur Entrée.

L’application se termine. La Figure 12.7 illustre les résultats de l’utilisation de cette technique.

Créer des piles en utilisant des listes

Une pile est une structure de programmation utile, car vous pouvez l’utiliser pour sauvegarder l’environnement d’exécution d’une application (comme l’état des variables et d’autres attributs), ou encore pour déterminer l’ordre d’une exécution. Malheureusement, Python n’offre pas ce type de structure dans ses collections. Cependant, il dispose des listes, et vous pouvez utiliser celles-ci pour créer des piles parfaitement acceptables. Les étapes qui suivent vous montrent comment réaliser ce travail.

❶ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée à la fin de chaque ligne :

[picture]

Figure 12.7 Après avoir affiché votre sélection, l’application affiche à nouveau le menu.

MyStack = []
StackSize = 3
def DisplayStack():
  print("État courant de la pile :")
  for Item in MyStack:
     print(Item)
def Push(Value):
  if len(MyStack) < StackSize:
      MyStack.append(Value)
      print(f"{Value} ajouté")
   else:
      print("La pile est pleine !")
def Pop():
   if len(MyStack) > 0:
      Removed = MyStack.pop()
      print(f"{Removed} supprimé de la pile")
   else:
      print("La pile est vide.")

Push(1)
Push(2)
Push(3)
DisplayStack()
input("Appuyez sur une touche quand vous êtes prêt...")
Push(4)
DisplayStack()
input("Appuyez sur une touche quand vous êtes prêt...")
Pop()
DisplayStack()
input("Appuyez sur une touche quand vous êtes prêt...")
Pop()
Pop()
Pop()
DisplayStack()

L’application crée une liste, ainsi qu’une variable servant à déterminer la taille maximale de la pile (une pile possède normalement une taille pourvue d’une certaine limite). Celle-ci est réellement petite, mais suffisante pour cet exemple.

Les piles fonctionnent en poussant une valeur sur leur sommet, puis en dépilant les valeurs ainsi stockées. Les fonctions Push() et Pop() effectuent ces deux tâches. Le code ajoute une fonction DisplayStack() pour visualiser le contenu de la pile.

Le code restant permet de vérifier les fonctionnalités de la pile en y poussant des valeurs, puis en les dépilant. Quatre sections du code servent à effectuer ce test.

❷ Cliquez sur Exécuter la cellule.

Python remplit la pile, puis elle affiche son état :

1 ajouté
2 ajouté
3 ajouté
État courant de la pile :
1
2
3

Dans ce cas, 3 se trouve au sommet de la pile, car il s’agit de la dernière valeur ajoutée. La fonction input() fournit un bon moyen d’arrêter momentanément l’exécution de l’application afin de pouvoir voir l’état actuel de la pile. L’argument de la fonction input(), "Appuyez sur une touche quand vous êtes prêt...", indique à input() ce qu’il faut fournir comme invite.

❸ Appuyez sur Entrée.

L’application tente de placer une nouvelle valeur sur le sommet de la pile. Mais comme celle-ci est déjà pleine, l’opération échoue avec un message « La pille est pleine ! » suivi du contenu de la pile, qui est le même que précédemment.

❹ Appuyez sur Entrée.

Cette fois, l’application retire une valeur au sommet de la pile. Rappelez-vous que 3 est le sommet de la pile, c’est donc la valeur qui manque dans la sortie :

3 supprimé de la pile
État courant de la pile :
1
2

❺ Appuyez sur Entrée.

L’application essaie de dépiler plus de valeurs que n’en contient la pile, ce qui provoque une erreur, puis elle affiche le contenu de la pile qui est désormais vide :

2 supprimé de la pile
1 supprimé de la pile
La pile est vide.
État courant de la pile :

Toute implémentation de pile doit être capable de détecter aussi bien les débordements (trop d’entrées) que l’inverse (trop peu d’entrées).

Travailler avec les files

Une file fonctionne différemment d’une pile. C’est exactement le principe de la file d’attente à un guichet. Vous arrivez et vous vous placez à la fin de celle-ci. Au fur et à mesure que les autres personnes sont servies, vous avancez dans la file, jusqu’au moment où c’est à votre tour. Cette structure est souvent utilisée pour gérer les listes de tâches, ou encore pour gérer le flux d’un programme (comme dans le monde réel, en fait). Les étapes qui suivent vous montrent comment créer une file.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée à la fin de chaque ligne :

import queue
MyQueue = queue.Queue(3)
print(MyQueue.empty())
input("Appuyez sur une touche quand vous êtes prêt...")
MyQueue.put(1)
MyQueue.put(2)
print(MyQueue.full())
input("Appuyez sur une touche quand vous êtes prêt...")
MyQueue.put(3)
print(MyQueue.full())
input("Appuyez sur une touche quand vous êtes prêt...")
print(MyQueue.get())
print(MyQueue.empty())
print(MyQueue.full())
input("Appuyez sur une touche quand vous êtes prêt...")
print(MyQueue.get())
print(MyQueue.get())

Pour créer une file, vous devez importer le module queue. Celui-ci contient en fait divers types de files, mais cet exemple utilise uniquement le classique FIFO (First In, First Out, premier entré, premier sorti).

Lorsqu’une file est vide, la fonction empty() renvoie True. De même, lorsque la file est pleine, la fonction full() renvoie également True. Il n’est pas possible de parcourir une file à l’aide d’une boucle for comme dans d’autres types de collections. C’est pourquoi il vous faut faire appel à empty() et à full() pour connaître son état.

Les deux fonctions utilisées pour travailler avec les données d’une file sont put(), qui ajoute une nouvelle information, et get(), qui supprime une donnée. L’une des difficultés, avec les files, est que, si vous tentez d’y placer plus de données qu’elles ne peuvent en contenir, elles attendent simplement que de la place se libère pour les y stocker.

❷ Cliquez sur Exécuter la cellule.

Python teste l’état de la file. Dans ce cas, la sortie produite devrait être True, ce qui indique que la file est vide.

❸ Appuyez sur Entrée. L’application ajoute deux nouvelles valeurs à la file. Ce faisant, la file n’est plus vide, comme le montre l’appel à empty(), mais un test de full() montre qu’elle n’est pas pleine non plus.

❹ Appuyez sur Entrée.

L’application ajoute encore une entrée à la file. Celle-ci est donc maintenant pleine, puisqu’elle est conçue pour contenir trois éléments. La fonction full() retourne donc la valeur True.

❺ Appuyez sur Entrée.

Pour libérer de la place dans la file, la fonction get() retire l’une des entrées, puis renvoie celle-ci. Du fait que la première valeur affectée à la file était 1, la fonction print() affiche cette valeur. De plus, les deux fonctions empty() et full() donnent maintenant comme réponse False.

❻ Appuyez sur Entrée.

L’application supprime les deux entrées restantes. Le code affiche donc les valeurs 2 et 3.

Travailler avec des deques

Un deque est simplement une file dans laquelle vous pouvez ajouter ou supprimer des éléments à chaque extrémité. Dans de nombreux langages, une file d’attente ou une pile commence par être un deque. Le code spécialisé sert à limiter la fonctionnalité deque à ce qui est nécessaire pour effectuer une tâche particulière.

Pour fixer les idées, vous pouvez imaginer un deque comme une sorte de ligne horizontale. Certaines fonctions individuelles agissent sur le début et sur la fin de cette ligne pour voir l’agrandir ou au contraire la raccourcir, mais rien ne change le milieu.

Les étapes qui suivent vous aident à créer un exemple qui montre l’usage d’un deque.

❶ Tapez le code suivant dans une nouvelle cellule d’un notebook, en appuyant sur Entrée à la fin de chaque ligne :

import collections
MyDeque = collections.deque("abcdef", 10)
print("État initial :")
for Item in MyDeque:
  print(Item, end=" ")
print("\r\n\r\nAjout et extension à droite")
MyDeque.append("h")
MyDeque.extend("ij")
for Item in MyDeque:
   print(Item, end=" ")
print("\r\nMyDeque contient {0} éléments."
     .format(len(MyDeque)))
print("\r\nPop à droite")
print("Pop de {0}".format(MyDeque.pop()))
for Item in MyDeque:
   print(Item, end=" ")
print("\r\n\r\nAjout et extension à gauche")
MyDeque.appendleft("a")
MyDeque.extendleft("bc")
for Item in MyDeque:
   print(Item, end=" ")
print("\r\nMyDeque contient {0} éléments."
     .format(len(MyDeque)))
print("\r\nPop à gauche")
print("Pop de {0}".format(MyDeque.popleft()))
for Item in MyDeque:
   print(Item, end=" ")
print("\r\n\r\nSuppression")
MyDeque.remove("a")
for Item in MyDeque:
   print(Item, end=" ")

L’implémentation des deques se trouve dans le module collections, qui est donc importé au début du code. Lorsque vous créez un deque, vous pouvez spécifier comme ici (mais facultativement) une liste d’éléments itérables, donc qui peuvent être parcourus, ainsi qu’une taille maximale.

Un deque fait la différence entre ajouter un élément, et ajouter un groupe d’éléments. Dans le premier cas, vous utilisez les fonctions append() et appendleft(). Dans le second cas, vous faites appel aux fonctions extend() et extendleft(). Pour supprimer un élément à la fois, vous disposez des fonctions pop() et popleft(). Ces fonctions renvoient l’élément supprimé, ce qui permet de l’afficher. La fonction remove() est particulière, car elle part toujours de l’extrémité gauche et supprime la première instance de la donnée spécifiée.

Contrairement à certaines autres collections, un objet deque peut être parcouru à l’aide d’une boucle for chaque fois que c’est nécessaire.

❷ Cliquez sur Exécuter la cellule.

Python affiche les informations suivantes :

État initial :
a b c d e f
Ajout et extension à droite
a b c d e f h i j
MyDeque contient 9 éléments.

Pop à droite
Pop de j
a b c d e f h i

Ajout et extension à gauche
c b a a b c d e f h
MyDeque contient 10 éléments.

Pop à gauche
Pop de c
b a a b c d e f h

Suppression
b a b c d e f h

Il est important de suivre de près la sortie. Notez comment la taille du deque change à chaque fois. Une fois que l’application a supprimé le j, le deque contient encore huit éléments. Lorsque l’application étend le deque en lui ajoutant trois éléments, la longueur affichée n’est pourtant que de dix éléments. Dans ce cas, la longueur maximum a été dépassée, et le supplément disparaît simplement.

DANS CE CHAPITRE

Définir les caractéristiques d’une classe.

•

Spécifier les composants d’une classe.

•

Créer et utiliser votre propre classe.

•

Travailler avec les sous-classes.

Chapitre 13

Créer et utiliser des classes

Les classes rendent le code Python plus lisible, plus compréhensible et plus simple à utiliser. Vous vous servez de classes pour créer des conteneurs afin d’y placer votre code, de manière à ce que celui-ci soit réparti de façon claire et nette. Vos utilisateurs voient vos classes comme des boîtes noires : il y a des données qui y entrent, et d’autres données qui en sortent.

Arrivé à un certain point de votre progression, vous aurez besoin de construire des classes pour éviter que vos applications ne ressemblent à ce genre de code spaghetti que l’on trouvait dans d’anciens programmes. Il était alors difficile de savoir où les choses commençaient et où elles se terminaient. Maintenir un tel code était pratiquement impossible. Et certaines organisations finissaient même par s’en débarrasser.

En plus de vous aider à comprendre les classes comme une méthode permettant de gérer votre code sous la forme de packages, ce qui évite le code spaghetti, ce chapitre vous aide à créer et à utiliser vos propres classes. Vous aurez ainsi un aperçu de la manière dont les classes Python fonctionnent et rendent vos applications plus pratiques à utiliser. Il s’agit toutefois d’une introduction en douceur aux classes, et ce chapitre a pour but de rendre le développement des classes simple et gérable.

Vous n’avez pas à saisir manuellement le code source de ce chapitre. En fait, l’utilisation de la source téléchargeable est beaucoup plus facile. Vous trouverez le code source de ce chapitre dans les fichiers Chapitre 13 Créer et utiliser des classes.ipynb et Chapitre 15 MyClass.ipynb. Consultez l’introduction pour savoir comment trouver ces fichiers source.

Comprendre les composants d’une classe

Une classe (le plan utilisé pour créer un objet) a une construction spécifique. Chaque partie d’une classe effectue une tâche particulière qui lui donne ses caractéristiques. Bien entendu, la classe commence avec un conteneur qui va l’englober en entier. C’est ce que nous allons préciser dans la prochaine section. Les sections qui suivent décrivent les autres parties d’une classe afin de mieux comprendre comment elles contribuent à celle-ci dans sa globalité.

Créer la définition d’une classe

Une classe n’a pas besoin d’être particulièrement complexe. En fait, vous pouvez créer simplement un conteneur, y placer un élément, et appeler classe le résultat. Bien sûr, celle-ci ne fera pas grand-chose, mais il sera tout de même possible de l’instancier (c’est-à-dire demander à Python de fabriquer un objet à partir de ce moule). Vous pourrez alors travailler avec cette instance comme si l’objet avait toujours été là. Les étapes qui suivent vous aident à comprendre les notions de base sous-jacentes en créant une classe aussi simple que possible.

❶ Ouvrez un nouveau notebook.

Vous pouvez également utiliser le fichier source téléchargeable, Chapitre 13 Créer et utiliser des classes.ipynb.

❷ Tapez le code suivant dans le notebook, en appuyant sur Entrée à la fin de chaque ligne :

class MyClass:
  MyVar = 0

MyInstance = MyClass()

MyInstance.MyVar

La première ligne définit le conteneur de la classe. Celui-ci est constitué du mot class suivi du nom de cette classe, MyClass. Toutes les classes doivent débuter précisément de cette manière.

La seconde ligne est la suite de la classe. Tous les éléments qui composent la classe sont appelés la suite de la classe. Ici, vous voyez une variable de la classe appelée MyVar dont la valeur est égale à 0. Chaque instance de cette classe possédera le même nom de variable, avec la même valeur initiale.

La quatrième ligne crée une instance de MyClass nommée MyInstance. Bien entendu, vous voudrez vérifier que vous avez réellement créé une telle instance.

La sixième ligne affiche la valeur de MyVar trouvée dans l’instance de MyInstance.

❸ Cliquez sur Exécuter la cellule.

L’affichage de la valeur 0 montre que MyInstance a bien une variable de classe nommée MyVar.

❹ Dans une nouvelle cellule, tapez MyInstance.__class__ et cliquez sur Exécuter la cellule.

Python affiche la classe utilisée pour créer cette instance, comme cela est illustré à la Figure 13.1. La sortie indique que cette classe fait partie du package __main__, ce qui signifie que vous l’avez saisie directement dans le code de l’application et non pas en tant que partie d’un autre package.

[picture]

Figure 13.1 Le nom de la classe est également correct, et vous savez donc que cette instance est créée en utilisant MyClass.

Les classes et leurs attributs intégrés

Lorsque vous créez une classe, vous pourriez penser que tout ce que vous obtenez, c’est cette classe et rien d’autre. Cependant, Python y ajoute des fonctionnalités intégrées. Par exemple, dans la section précédente, vous avez tapé __class__ et appuyé sur Entrée. L’attribut __ class__ est directement intégré. Vous n’avez pas besoin de le créer. Du fait que Python l’ajoute automatiquement, vous n’avez pas à vous en soucier. Une telle fonctionnalité est suffisamment utile pour que toute classe la possède, d’où le travail de Python en arrière-plan. Les étapes qui suivent vont vous montrer comme travailler avec les attributs intégrés des classes. Elles supposent que vous avez suivi les étapes de la section précédente, « Créer la définition d’une classe » .

❶ Dans une nouvelle cellule, saisissez print(dir(MyInstance)) et cliquez sur Exécuter la cellule.

Une liste d’attributs va s’afficher (voir ci-dessous). Ces attributs fournissent des fonctionnalités spécifiques pour votre classe. Ces mêmes attributs vont se retrouver dans toutes les classes que vous aurez à créer, et donc vous pourrez toujours compter sur ces fonctionnalités dans vos applications.

'MyVar'           '__class__'         '__delattr__'
'__dict__'        '__dir__'           '__doc__'
'__eq__'          '__format__'        '__ge__'
'__getattribute__' '__gt__'            '__hash__'
'__init__'        '__init_subclass__' '__le__'
'__lt__'          '__module__'        '__ne__'
'__new__'         '__reduce__'        '__reduce_ex__'
'__repr__'        '__setattr__'       '__sizeof__'
'__str__'         '__subclasshook__'  '__weakref__'

❷ Dans une nouvelle cellule, tapez help('__class__') et cliquez sur Exécuter la cellule.

Python affiche des informations sur l’attribut __class__ (voir la Figure 13.2). Vous pouvez utiliser la même technique pour en apprendre plus sur tous les attributs que Python ajoute à votre classe.

Travailler avec les méthodes

Les méthodes sont simplement un autre terme pour les fonctions qui résident dans les classes. Vous créez des méthodes, et vous travaillez avec elles, exactement comme vous le faites avec des fonctions, si ce n’est que le mot méthode est employé exclusivement dans le cas des classes. Vous pouvez créer deux types de méthodes : celles qui sont associées à la classe elle-même, et celles qui sont associées avec une instance de cette classe. Il est important de faire la différence entre les deux. Les sections qui suivent précisent ces notions.

[picture]

Figure 13.2 Python fournit une aide pour chacun des attributs qu’il ajoute à votre classe.

Créer des méthodes de classe

Une méthode de classe peut être exécutée directement dans la classe sans qu’une instance de celle-ci ne soit créée. C’est souvent nécessaire pour disposer de fonctionnalités applicables à toutes les instances, comme celles que vous avez déjà utilisées avec la classe str pour modifier des chaînes de caractères. Ainsi, l’exemple d’exception multiple de la section « Imbriquer des exceptions » du Chapitre 2 fait appel à la fonction str.upper(). Les étapes qui suivent vous montrent comment créer et utiliser une méthode de classe.

❶ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée à la fin de chaque ligne :

class MyClass:
    def SayHello():
       print("Bonjour le monde !")

MyClass.SayHello()

Cet exemple contient un unique attribut, SayHello(). Cette méthode ne reçoit aucun argument et ne renvoie aucune valeur. Elle se contente d’afficher un message. Pour autant, elle suffit à notre propos.

❷ Cliquez sur Exécuter la cellule.

Vous obtenez bien l’affichage prévu (voir la Figure 13.3). Remarquez que vous n’avez pas eu besoin de créer une instance de la classe. La méthode est immédiatement disponible.

[picture]

Figure 13.3 Cette méthode de classe affiche un simple message.

Une méthode de classe fonctionne uniquement avec les données de celle-ci (les données associées à la classe elle-même, plutôt qu’à une instance particulière de cette classe). Elle ne sait rien des informations qui peuvent être associées avec une instance de la classe. Vous pouvez lui passer des données en argument, et la méthode retournera les valeurs qu’elle aura traitées, mais elle ne peut pas accéder à ce qui relève des instances. Vous devez donc être particulièrement vigilant lorsque vous créez de telles méthodes, puisqu’elles doivent être autosuffisantes.

Créer des méthodes d’instance

Une méthode d’instance est attachée aux instances individuelles. Vous les utilisez pour manipuler les données que la classe doit gérer. Une méthode d’instance ne peut évidemment être utilisée qu’une fois une instance créée.

Toutes les méthodes d’instance acceptent au moins un argument appelé self. Cet argument pointe sur l’instance particulière dont l’application se sert pour manipuler des données. Sans cet argument self, la méthode ne saurait pas quelles données traiter. Cependant, self est fourni par Python lui-même et vous ne pouvez pas le changer dans le cadre de l’appel de la méthode.

Les étapes qui suivent vous montrent comment créer et utiliser des méthodes d’instance dans Python.

❶ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée à la fin de chaque ligne :

class MyClass:
    def SayHello(self):
       print("Bonjour le monde !")

MyInstance = MyClass()
MyInstance.SayHello()

Cet exemple contient un unique attribut, SayHello(). Cette méthode ne reçoit aucun argument et ne renvoie aucune valeur. Elle se contente d’afficher un message. Vous noterez que cette version d’instance de SayHello() inclut l’argument self, ce qui la différencie d’une méthode de classe.

❷ Cliquez sur Exécuter la cellule.

Vous voyez le même message que celui illustré à la Figure 13.3.

Travailler avec les constructeurs

Un constructeur est un type spécial de méthode que Python appelle lorsqu’il instancie un objet en utilisant les définitions trouvées dans votre classe. Python s’appuie sur ce constructeur pour effectuer différentes tâches, comme l’initialisation (l’affectation de valeurs) des variables d’instance dont l’objet a besoin lors de sa création. Les constructeurs peuvent aussi vérifier qu’il y a suffisamment de ressources pour l’objet, et effectuer toute autre procédure d’initialisation à laquelle vous pouvez penser.

Le nom du constructeur est toujours le même, __init__. Il peut si nécessaire accepter des arguments. Si vous fabriquez une classe sans constructeur, Python le fait automatiquement à votre place. Dans ce cas, ce constructeur par défaut ne fait rien. Toute classe doit posséder un constructeur, même s’il s’agit simplement du constructeur par défaut de Python.

Les étapes qui suivent illustrent cette notion de constructeur.

❶ Tapez le code suivant dans une nouvelle cellule de notebook, en appuyant sur Entrée à la fin de chaque ligne :

class MyClass:
   Greeting = ""
   def __init__(self, Name= "le monde "):
       self.Greeting = Name + " !"
   def SayHello(self):
       print(" Bonjour {0} ".format(self.Greeting))

MyInstance = MyClass()
MyInstance.SayHello()

Vous trouvez ici un premier exemple de surcharge de méthode de style Python. Même s’il n’y a qu’une seule déclaration de la méthode __init__(), il en existe en fait deux versions. La première ne demande aucune entrée particulière, puisqu’elle se contente de la valeur par défaut de la variable Name, "le monde". La seconde nécessite un nom en entrée. Elle définit la valeur de Greeting avec cette entrée, et y ajoute un point d’exclamation. La méthode SayHello() est, pour le reste, la même que dans les exemples précédents.

Python ne supporte pas les vraies surcharges de fonctions. De nombreux aficionados de la Programmation Orientée Objet (POO) considèrent que les valeurs par défaut sont différentes de la surcharge. Cependant, l’utilisation de valeurs par défaut permet d’obtenir le même résultat.

❷ Cliquez sur Exécuter la cellule.

Vous voyez le message illustré à la Figure 13.4. Vous noterez que ce message fournit le message d’accueil générique par défaut.

❸ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée après chaque ligne :

MyInstance2 = MyClass("Annie")
MyInstance2.SayHello()
MyInstance.Greeting = "Alain"
MyInstance.SayHello()

Dans la première ligne, Python crée une instance de MyClass nommée MyInstance2. L’instance MyInstance est complètement différente de l’instance MyInstance2. Dans la deuxième ligne, Python affiche le message pour MyInstance2, et non pas le message pour MyInstance.

La troisième ligne modifie le message d’accueil de MaInstance sans modifier le message d’accueil de MaInstance2. La quatrième ligne affiche le nouveau message de MyInstance.

❹ Cliquez sur Exécuter la cellule.

Vous voyez le message illustré à la Figure 13.4. Vous noterez que ce code fournit un message d’accueil spécifique pour chacune des instances. En outre, chaque instance est distincte et vous avez pu modifier le message de la première instance sans affecter la seconde.

[picture]

Figure 13.4 Fournir un nom au constructeur permet d’obtenir une sortie personnalisée.

Travailler avec les variables

Vous savez que les variables sont des conteneurs chargés de stocker des données. Lorsque vous travaillez avec des classes, vous devez prendre en compte la manière dont vos informations sont enregistrées et gérées. Une classe peut contenir à la fois des variables de classe et des variables d’instance. Les premières sont définies comme appartenant à la classe elle-même, tandis que les secondes font partie des méthodes. Les sections qui suivent vous montrent comment utiliser ces deux types de variables.

Créer des variables de classe

Les variables de classe fournissent un accès global aux données manipulées par la classe. Les étapes suivantes montrent le fonctionnement des variables de classe.

❶ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée à la fin de chaque ligne :

class MyClass():
   Greeting = ""
   def SayHello(self):
       print("Bonjour {0}".format(self.Greeting))

MyClass.Greeting = "Elsa"
MyClass.Greeting

C’est une variante de la section précédente, mais cette version ne contient pas de constructeur. Normalement, vous devriez définir un constructeur pour vous assurer que la variable de classe est correctement initialisée. Cependant, cette série d’étapes vous montre comment les variables de classe peuvent mal tourner.

L’instruction MyClass.Greeting = "Elsa" affecte à la variable Greeting une autre valeur que celle utilisée pour créer la classe. La grande question est maintenant de savoir comment les choses vont se passer.

❷ Cliquez sur Exécuter la cellule.

Vous constatez que la valeur de Greeting a bien été modifiée (voir la Figure 13.5).

[picture]

Figure 13.5 Vous pouvez modifier la valeur de la variable de classe.

❸ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée après chaque ligne :

MyInstance = MyClass()
MyInstance.SayHello()

Python crée une instance de MyClass appelée MyInstance.

❹ Cliquez sur Exécuter la cellule.

Vous voyez s’afficher le message illustré sur la Figure 13.6. Le changement que vous avez apporté à la variable Greeting s’est répercuté sur l’instance de la classe. Bien sûr, ce n’est pas réellement un souci dans cet exemple, mais imaginez ce qui pourrait se produire dans une application réelle si quelqu’un voulait provoquer des problèmes.

[picture]

Figure 13.6 Les modifications apportées à Greeting se répercutent sur toutes les instances de la classe.

Ce petit exemple illustre les dangers des variables de classe. Les deux leçons à en retenir sont les suivantes :

• Évitez chaque fois que possible les variables de classe pour empêcher une utilisation abusive.

• Initialisez toujours les variables de classe avec une valeur connue et sans danger dans le code du constructeur.

❺ Vous pouvez refermer la fenêtre de Python.

Créer des variables d’instance

Les variables d’instance sont définies uniquement à l’intérieur d’une méthode. Les arguments passés en entrée dans ces méthodes sont considérés comme faisant partie des variables d’instance, car elles n’existent qu’en même temps que la méthode correspondante. Utiliser des variables d’instance est généralement plus sûr que des variables de classe, puisqu’il est plus facile de les contrôler et de s’assurer que l’appelant a fourni les bonnes entrées. Les étapes suivantes montrent un exemple d’utilisation de variables d’instance.

❶ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée à la fin de chaque ligne :

class MyClass:
   def DoAdd(self, Value1=0, Value2=0):
       Sum = Value1 + Value2
       print("La somme de {0} plus {1} est {2}."
           .format(Value1, Value2, Sum))

MyInstance = MyClass()
MyInstance.DoAdd(1, 4)

Vous avez ici trois variables d’instance. Les arguments en entrée, Value1 et Value2, ont pour valeur par défaut 0. De cette manière, la méthode DoAdd ne peut pas échouer sous le prétexte que l’utilisateur aurait oublié de fournir une valeur. Bien entendu, celui-ci pourrait par mégarde saisir autre chose qu’un nombre, et vous devriez donc effectuer les contrôles nécessaires dans votre code. La troisième variable d’instance, Sum, est la somme de Value1 et de Value2. Le code se contente d’effectuer l’opération et d’afficher le résultat.

Après avoir défini la classe, le code crée une instance de MyClass nommée MyInstance. La dernière ligne exécute DoAdd() avec les valeurs d’entrée 1 et 4.

❷ Cliquez sur Exécuter la cellule.

Vous voyez le message illustré à la Figure 13.7, soit la somme de 1 et de 4.

Utiliser des méthodes avec des listes d’arguments variables

Il peut arriver que vous deviez créer des méthodes avec un nombre variable d’arguments. Gérer ce type de situation est quelque chose que Python sait bien faire. Vous pouvez créer deux sortes d’arguments en nombre variable :

• *args : fournit une liste d’arguments non nommés.

• **kwargs : fournit une liste d’arguments nommés.

Les noms réels des arguments n’a pas d’importance, mais les développeurs Python utilisent la syntaxe *args et **kwargs pour que d’autres

[picture]

Figure 13.7 L’instance affiche simplement la somme de deux nombres.

programmeurs sachent qu’il s’agit d’une liste variable d’arguments. Remarquez que la première variante est précédée d’un unique astérisque (*), tandis qu’il y en a deux (**) dans la seconde. Les étapes qui suivent montrent comment utiliser ces deux approches dans une application.

❶ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée à la fin de chaque ligne :

class MyClass:
   def PrintList1(*args):
     for Count, Item in enumerate(args):
        print("{0}. {1}".format(Count, Item))

   def PrintList2(**kwargs):
     for Name, Value in kwargs.items():
        print("{0} aime le {1}".format(Name, Value))

MyClass.PrintList1("Rouge", "Bleu", "Vert")
MyClass.PrintList2(Georges="Rouge", Annie="Bleu",
                  Sarah="Vert")

Pour cette démonstration, vous voyez les arguments implémentés dans une méthode de classe. Il ne serait pas plus compliqué de réaliser la même chose dans une méthode d’instance.

Observez soigneusement PrintList1(). Vous y retrouvez la technique de parcours dans une liste à l’aide d’une boucle for. Dans ce cas, la fonction enumerate() renvoie à la fois un compteur (la position dans la boucle) et la chaîne qui a été passée en argument à la fonction.

La fonction PrintList2() accepte en entrée un dictionnaire. Comme dans le cas de PrintList1(), cette liste peut être d’une longueur quelconque. Cependant, vous devez traiter les éléments items() trouvés dans le dictionnaire pour récupérer les valeurs individuelles.

❷ Cliquez sur Exécuter la cellule.

Une fenêtre Python en mode Shell va s’ouvrir. L’application affiche la sortie illustrée à la Figure 13.8. Les listes individuelles peuvent être d’une longueur quelconque. Vous pourriez à ce stade jouer un peu avec cette technique. Essayez par exemple de mixer des noms et des chaînes dans la première liste pour voir ce qui se passe. Essayez aussi d’ajouter des valeurs booléennes. L’essentiel, c’est que cela vous permet de créer des méthodes extrêmement flexibles si tout ce que vous voulez est une liste de valeurs en entrée.

[picture]

Figure 13.8 Le code peut traiter n’importe quel nombre d’entrées dans la liste.

Surcharger les opérateurs

Dans certaines situations, vous voudriez être capable de faire quelque chose de particulier sur le résultat de l’utilisation d’un opérateur standard tel que l’addition (+). En fait, Python ne fournit parfois pas de comportement par défaut, car il n’a justement pas de comportement par défaut à implémenter. Quelle qu’en soit la raison, la surcharge des opérateurs rend possible l’affectation de nouvelles fonctionnalités à des opérateurs existants, de manière à ce qu’ils fassent ce que vous voulez, et pas ce que Python a prévu. Les étapes suivantes montrent comment surcharger un opérateur et l’utiliser dans le cadre d’une application.

❶ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée à la fin de chaque ligne :

class MyClass:
   def __init__(self, *args):
     self.Input = args

   def __add__(self, Other):
     Output = MyClass()
     Output.Input = self.Input + Other.Input
     return Output

   def __str__(self):
     Output = ""
     for Item in self.Input:
        Output += Item
        Output += " "
     return Output

Value1 = MyClass("Rouge", "Vert", "Bleu")
Value2 = MyClass("Jaune", "Pourpre", "Cyan")
Value3 = Value1 + Value2

print("{0} + {1} = {2}"
     .format(Value1, Value2, Value3))

Cet exemple illustre plusieurs techniques différentes. Le constructeur __init__() montre une méthode pour créer une variable d’instance attachée à l’objet self. Vous pouvez utiliser cette approche pour créer autant de variables qu’il est nécessaire.

Lorsque vous créez vos propres classes, aucun opérateur + n’est défini dans la plupart des cas, du moins tant que vous ne l’avez pas fait. La seule exception se produit lorsque vous héritez d’une classe existante dans laquelle cet opérateur a été défini (pour plus de détails, voyez plus loin dans ce chapitre la section « Étendre des classes pour en créer de nouvelles » ). Pour pouvoir additionner des entrées MyClass ensemble, vous devez donc définir la méthode __add__(), qui équivaut à l’opérateur +.

Le code utilisé pour la méthode __add__() peut paraître un peu étrange, mais il faut le regarder ligne par ligne. Il commence par fabriquer un nouvel objet, Output, à partir de MyClass. Rien n’est ajouté à Output à ce stade. Il s’agit donc d’un objet vierge. Les deux objets que vous voulez ajouter, self.Input et Other.Input, sont en fait des tuples (revoyez à ce sujet la section « Travailler avec les tuples » du Chapitre 2). Le code place la somme de ces deux objets dans Output.Input. La méthode __add_() retourne ensuite le résultat à l’appelant.

Bien entendu, vous voudriez savoir pourquoi vous ne pouvez tout simplement pas additionner les deux entrées comme vous le feriez pour des nombres. La réponse est que vous finissez en sortie par un tuple, et non par un objet MyClass. Le type de la sortie pourrait être changé, ce qui modifierait également l’utilisation ultérieure de l’objet résultant.

Pour afficher correctement MyClass, vous avez aussi besoin de définir une méthode __str__(). Dans ce cas, la sortie est une chaîne délimitée par une espace (autrement dit, chacun des éléments de la chaîne est séparé d’un autre par une espace). Cette chaîne contient chacune des valeurs trouvées dans self.Input. Bien entendu, la classe que vous créez peut renvoyer n’importe quelle chaîne permettant de représenter pleinement l’objet.

La procédure principale crée deux objets de test, Value1 et Value2, puis elle les additionne et place le résultat dans Value3 afin d’afficher la réponse.

❷ Cliquez sur Exécuter la cellule.

La Figure 13.9 illustre l’affichage obtenu. Certes, cela fait beaucoup de code pour une simple sortie, mais ce résultat démontre que vous pouvez créer des classes qui contiennent tout ce dont elles ont besoin, et qui sont totalement fonctionnelles.

Créer et utiliser une classe externe

Parfois, vous souhaitez réutiliser les classes que vous créez, ce qui signifie les rendre externes à votre application principale. Vous obtenez alors votre propre bibliothèque spéciale, qui est semblable aux autres bibliothèques Python présentées dans ce livre. Dans ce cas, vous créez une classe que vous placez dans un package externe et à laquelle vous accédez finalement dans une application.

[picture]

Figure 13.9 Le résultat de la somme de deux objets MyClass est un troisième objet de même type.

En raison des limitations du fonctionnement de Colab, l’exemple des sections suivantes ne fonctionnera bien qu’avec Jupyter Notebook, car il est difficile d’exporter correctement les fichiers .py dans Colab. C’est pour cette raison que ces sections se concentrent sur l’utilisation de Jupyter Notebook.

Développer la classe externe

Avant de pouvoir faire quoi que ce soit d’autre, vous devez créer la classe externe, qui peut à la fin se transformer en une bibliothèque personnalisée contenant plusieurs classes. Les sections suivantes décrivent comment créer et enregistrer cette classe.

Définir la classe MyClass

Le Listing 13.1 montre le code dont vous avez besoin pour créer la classe. Vous pouvez également trouver ce code dans le fichier Chapitre 5 MyClass.ipynb qui se trouve dans la source téléchargeable, comme cela est décrit dans l’introduction.

Dans ce cas, la classe commence par créer un objet avec deux variables d’instance : Name et Age. Pour le cas où l’utilisateur ne fournirait pas ces informations, ces variables sont initialisées avec les valeurs Samuel et 32.

Listing 13.1 Créer une classe externe.

 class MyClass:
   def __init__(self, Name="Samuel", Age=32):
      self.Name = Name
      self.Age = Age

   def GetName(self):
      return self.Name

   def SetName(self, Name):
      self.Name = Name

   def GetAge(self):
      return self.Age

   def SetAge(self, Age):
      self.Age = Age

   def __str__(self):
      return "{0} est âgé(e) de {1} ans.".format(self.Name,
                                       self.Age)

Cet exemple fournit une nouvelle fonctionnalité des classes, ce que la plupart des développeurs appellent un accesseur. Pour l’essentiel, celui-ci fournit un accès à la valeur sous-jacente. On peut également trouver des variations autour de ce nom. Il y a d’abord les accesseurs proprement dits, ou encore getters. Ainsi, GetName et GetAge sont des accesseurs/getters. Ils offrent un accès en lecture seule à la valeur sous-jacente. Les méthodes SetName et SetAge sont des mutateurs, ou encore setters. Elles fournissent un accès en écriture seule à la valeur sous-jacente. Utiliser une combinaison de méthodes comme celle-ci vous permet de vérifier que les entrées possèdent le type voulu, ou encore que l’appelant a l’autorisation de voir l’information.

Comme pour toute autre classe que vous créez, vous avez besoin de définir la méthode __str__() si vous voulez que l’utilisateur puisse afficher l’objet. Ici, la classe fournit une sortie formatée qui liste les deux variables d’instance.

Sauvegarder une classe sur disque

Vous pourriez conserver votre classe dans le même fichier que votre code de test, mais cela ne refléterait pas très bien le monde réel. Pour utiliser cette classe de façon réaliste dans le reste de ce chapitre, vous devez accomplir les étapes suivantes :

❶ Dans Jupyter Notebook, créez un nouveau notebook appelé Chapitre 5 MyClass.ipynb et ajoutez-y le code présenté dans le Listing 13.1.

Vous pouvez également utiliser le fichier source téléchargeable, Chapitre 5 MyClass.ipynb.

❷ Cliquez sur Run cell.

Python exécutera le code sans erreur si vous avez saisi le code correctement.

❸ Choisissez File → Save and Checkpoint.

Le notebook enregistre le fichier.

❹ Choisissez File → Download as → Python (.py).

Jupyter Notebook produit le code sous la forme d’un fichier Python.

N’oubliez pas que vous devez nommer le fichier Chapitre_15_MyClass. py, car Python ne peut pas importer des bibliothèques externes dont le nom contient des espaces.

Utiliser MyClass dans une application

La plupart du temps, vous utilisez des classes externes lorsque vous travaillez avec Python. Il est assez rare de laisser les classes dans le fichier de l’application elle-même, car celle-ci pourrait rapidement devenir très volumineuse et pratiquement ingérable. De plus, réutiliser le même code dans une autre application serait difficile. Les étapes qui suivent vous aident à utiliser la classe MyClass créée dans la section précédente.

❶ Tapez le code suivant dans une nouvelle cellule du notebook Chapitre 5 Créer et utiliser des classes.ipynb, en appuyant sur Entrée à la fin de chaque ligne :

import Chapitre_15_MyClass

SamuelRecord = Chapitre_15_MyClass.MyClass()
AnnieRecord = Chapitre_15_MyClass.MyClass("Annie", 44)

print(SamuelRecord.GetAge())
SamuelRecord.SetAge(33)

print(AnnieRecord.GetName())
AnnieRecord.SetName("Sophie")
print(SamuelRecord)
print(AnnieRecord)

Cet exemple de code commence par importer le package Cha-pitre_15_MyClass. Le nom de ce package est celui utilisé pour l’enregistrer sur le disque, et non pas le nom de la classe. Un seul package peut contenir plusieurs classes. Pensez donc toujours au package comme étant le fichier réel utilisé pour contenir une ou plusieurs classes que vous devez utiliser avec votre application.

Une fois le package importé, l’application crée deux objets MyClass. Notez que vous spécifiez d’abord le nom du package, suivi du nom de la classe. Le premier objet, SamuelRecord, utilise les valeurs par défaut. Le second, AnnieRecord, définit des valeurs personnalisées.

L’anniversaire de Samuel étant vraisemblablement passé, l’application lui ajoute une année de plus. D’autre part, il y a eu une confusion de prénom, et la titulaire du poste est Sophie et non pas Annie. Le code corrige donc cette erreur. Enfin, la dernière étape affiche les informations actualisées.

❷ Cliquez sur Run cell.

L’application affiche une série de messages au fur et à mesure qu’elle exploite MyClass, comme cela est illustré à la Figure 13.10. À ce stade, vous connaissez tous les éléments essentiels de la création des classes.

Étendre des classes pour en créer de nouvelles

Comme vous pouvez l’imaginer, créer une classe pleinement fonctionnelle, susceptible d’être utilisée dans des applications du monde réel, demande beaucoup de temps et de travail, car les classes sont conçues pour effectuer de nombreuses tâches. Fort heureusement, Python prend en charge une fonctionnalité appelée héritage. Cet héritage vous permet de récupérer les fonctionnalités voulues d’une classe parent lorsque vous créez une classe enfant. Éliminer les fonctions dont vous n’avez pas besoin, et en ajouter d’autres vous permet de créer des classes relativement rapidement et avec moins d’efforts. De plus, du fait que le code parent a déjà été testé, vous avez moins de soucis à vous faire quant au bon fonctionnement de votre nouvelle classe. Les sections qui suivent vous montrent comment profiter de ce processus d’héritage.

[picture]

Figure 13.10 La sortie montre que la classe est pleinement fonctionnelle.

Construire la classe enfant

Les classes parents sont normalement des super-ensembles de quelque chose. Dans notre exemple, nous allons plus modestement fabriquer une classe parent appelée Animal, puis nous en servir pour nous intéresser à une classe enfant dédiée à certains gallinacés, Chicken. Bien entendu, vous pourriez facilement ajouter d’autres classes enfants pour d’autres types d’animaux, comme une classe Gorille. Cependant, nous nous contenterons ici d’une classe parent, et d’une classe enfant. Vous les retrouvez dans le Listing 13.2. Saisissez ce code dans une nouvelle cellule du notebook.

Listing 13.2 Construire une classe parent et une classe enfant.

 class Animal:
   def __init__(self, Name="", Age=0, Type=""):
      self.Name = Name
      self.Age = Age
      self.Type = Type

   def GetName(self):
      return self.Name

   def SetName(self, Name):
      self.Name = Name
  def GetAge(self):
     return self.Age

  def SetAge(self, Age):
     self.Age = Age

  def GetType(self):
     return self.Type

  def SetType(self, Type):
     self.Type = Type

  def __str__(self):
     return "{0} est un {1} âgé de {2} ans.".format(self.Name,
                                           self.Type,
                                           self.Age)
class Chicken(Animal):
  def __init__(self, Name="", Age=0):
     self.Name = Name
     self.Age = Age
     self.Type = "poulet"

  def SetType(self, Type):
     print("Désolé, {0} sera toujours un {1}."
           .format(self.Name, self.Type))

  def MakeSound(self):
     print("{0} dit Cot, Cot, Cot, Codette !".format(self.Name))

La classe Animal gère trois caractéristiques : le nom (Name), l’âge (Age) et le type (Type). Une véritable application aurait vraisemblablement besoin de davantage de paramètres, mais ceux-ci nous suffiront pour cet exemple. Le code contient également les accesseurs nécessaires pour ces trois caractéristiques. La méthode __str__() complète le tableau en affichant un message simple qui décrit ce qu’est l’animal.

La classe Chicken hérite de la définition de la classe Animal. Remarquez l’emploi de Animal entre parenthèses après le nom de la classe Chicken. Ceci signale à Python que Chicken est un enfant de Animal, et donc qu’il hérite des caractéristiques de celui-ci.

Notez aussi que le constructeur Chicken n’accepte comme entrées que Name et Age. L’utilisateur n’a donc pas besoin de fournir une valeur Type, puisque vous savez déjà que l’animal en question est un poulet. Le nouveau constructeur remplace celui de Animal. Les trois attributs sont toujours en place, mais Type est compris directement dans le constructeur Chicken.

Quelqu’un pourrait être tenté de faire preuve d’originalité en définissant le poulet comme étant un gorille. De ce point de vue, la classe Chicken prend le pas sur le mutateur SetType(). En tentant de changer le type Chicken, l’utilisateur ne fait que recevoir un message de moquerie. Normalement, ce genre de problème devrait être traité par une exception, mais la solution proposée ici fonctionne mieux, car elle rend le code plus clair.

Finalement, le code de Chicken ajoute une fonctionnalité supplémentaire, MakeSound(). Si quelqu’un veut entendre le cri du poulet, il n’aura pas besoin d’écouteurs, mais il verra du moins le message correspondant s’afficher à l’écran.

Tester l’héritage de classe dans une application

Tester la classe Chicken signifie tester également la classe Animal, puisque la première est une extension de la seconde. Certaines fonctionnalités sont différentes, mais tout n’a pas besoin d’être utilisé en même temps. La classe Animal est simplement un parent d’un type spécifique d’animaux, en l’occurrence des poulets. Les étapes qui suivent illustrent le fonctionnement de la classe Chicken de manière à montrer comment fonctionne l’héritage.

❶ Tapez le code suivant dans une nouvelle cellule du notebook, en appuyant sur Entrée à la fin de chaque ligne :

MyChicken = Chicken("Arthur", 2)
print(MyChicken)

MyChicken.SetAge(MyChicken.GetAge() + 1)
print(MyChicken)

MyChicken.SetType("Gorille")
print(MyChicken)

MyChicken.MakeSound()

L’exemple crée un objet « poulet » , MyChicken, appelé Arthur, de deux ans d’âge. Il traite ensuite cet objet de différentes manières. Par exemple, Arthur prend un peu d’âge, et donc le code actualise celui-ci. Remarquez comment le code combine l’usage d’un mutateur/setter, SetAge(), avec un accesseur/getter, GetAge(), pour effectuer ce calcul. Après chaque modification, le code affiche les objets résultants.

❷ Cliquez sur Exécuter la cellule.

Vous voyez chacune des étapes utilisées pour exploiter MyChicken (Figure 13.11).

[picture]

Figure 13.11 Arthur prononce quelques mots après son anniversaire.