Les principes SOLID sont des principes de Programmation Orientée Objet (POO) destinés à l’écriture d’un code propre. Selon le livre « Clean Code » de Robert C. Martin, pour conserver un bon rythme de développement dans un projet de développement logiciel, il est important pour une équipe d’écrire du code propre. En implémentant du code sale, des résultats acceptables peuvent être produits rapidement, mais à long terme le projet finit par frapper un mur.
Il est connu qu'un logiciel meurt lorsqu'il n'est plus possible de le comprendre ou de lui ajouter de nouvelles fonctionnalités dans un délai raisonnable. Garder les choses propres est crucial pour mener à bien n'importe quel projet logiciel. Cette réflexion s'applique en particulier aux projets d'apprentissage automatique (Machine Learning) lesquels impliquent souvent de prendre du code provenant de projets de recherche, tels que des projets académiques, et de le refaire ou l’adapter à un environnement de production.
Le code de recherche est reconnu pour être sale. Pourquoi ? Parce qu'il est souvent écrit avec un seul objectif en tête : produire des preuves de concept (PoC) le plus rapidement possible. Dans ce contexte, il n'y a pas vraiment d’incitatifs à garder son code propre. Le code sale s’avère cependant un obstacle considérable pour la création d’un produit logiciel maintenable et évolutif.
Vous avez dit « SOLID » ?
Voici les principes SOLID, en plus du principe Tell Dont Ask (TDA), lui aussi très pertinent.
• S: principe de responsabilité unique (Single Responsibility Principle - SRP).
Une classe doit avoir une seule responsabilité.
• O: principe ouvert-fermé (Open-Closed Principle - OCP).
Une classe doit être fermée aux modifications et ouverte aux extensions.
• L: principe de substitution de Liskov (Liskov Substitution Principle - LSP).
Tout sous-type doit être interchangeable avec le type de base.
• I: principe de ségrégation des interfaces (Interface Segregation Principle - ISP).
Séparer les interfaces entre les différents objets plutôt que d'en implémenter seulement une énorme.
• D: Principe d'inversion des dépendances (Dependency Inversion Principle - DIP).
Dépendre d'abstractions plutôt que d'implémentations détaillées et concrètes.
• Aussi: Indiquer plutôt que d’interroger (Tell Don't Ask - TDA).
Ne pas gérer un objet de l'extérieur, lui dire plutôt quoi faire.
Approfondissons ces concepts dans les sections suivantes.
S: principe de responsabilité unique (Single Responsibility Principle - SRP)
Ce principe stipule que chaque classe a une et une seule responsabilité. Par exemple, si l’on est tenté de nommer une classe CeciEtCela, c'est un signe que la classe devrait probablement être divisée en deux.
Séparer les classes de sorte qu’elles n’aient qu’une responsabilité chacune facilitera le travail lorsque viendra le temps de modifier le code. Non seulement il sera plus facile d’y apporter des changements, mais les différentes pièces seront plus facilement réutilisables et plus malléables. C'est un moyen simple et sûr de coder un grand nombre de prototypes successivement. Cela rend également le protocole de test plus fluide en permettant de tester une chose à la fois. Ainsi, lorsqu'une erreur se produit, il est plus facile d’identifier le problème avec précision.
De plus, lorsque plusieurs développeurs travaillent de concert sur un même projet, le fait de respecter le SRP réduit les chances que deux développeurs se marchent sur les pieds (modifient en même temps un même objet). Imaginons que la classe CeciEtCela ait pour responsabilité d’accomplir deux tâches différentes (la tâche Ceci et la tâche Cela) et que ces tâches doivent être retravaillées. Si deux développeurs prennent en charge une tâche chacun, ils se retrouveront à modifier le même objet en même temps, ce qui engendrera probablement un conflit dans le code lorsque viendra le temps de fusionner (merge) les deux solutions dans le projet. Par contre si CeciEtCela est scindée en deux classes suivant le SRP, les deux développeurs peuvent travailler sur deux objets différents évitant ainsi le conflit. La gestion de conflit dans le code consomme en général beaucoup de temps et est très sensible aux erreurs. Ce phénomène doit être évité le plus possible.
Appliquons le SRP à un contexte de machine learning. Supposons que l’on ait une classe nommée MaNormalisationEtMonModele. Cette approche est très probablement trop rigide, trop spécifique et non malléable. Voici ce qui pourrait (et devrait) être fait. Dans un premier temps, on créerait une classe appelée MonModele. Si l’on pense que normaliser des données fait partie du travail de MonModele, on y implémenterait une méthode nommée ‘’normaliser’’. Sinon, une autre classe nommée MaNormalisation (laquelle pourrait hériter de la même superclasse que MonModele) serait implémentée. Cette classe implémenterait la méthode de normalisation. On pourrait alors implémenter une autre classe nommée Predicteur pour transformer des données avec un modèle dans le but de faire des prédictions. Predicteur représenterait alors ce que l'on appelle un Pipeline. Il pourrait encapsuler des objets de type MaNormalisation, MonModele, etc. De nouveaux modèles pourraient ensuite être développés et utilisés dans le pipeline en remplaçant simplement MonModele par une autre classe représentant un autre modèle. De cette manière, il est facile de réutiliser certains objets dans différents contextes sans avoir à dupliquer du code ou à modifier un modèle existant (voir aussi le principe OCP).
L’utilisation d’un framework comme Neuraxle (lequel est open source) facilite grandement la construction de pipelines de machine learning.
Ce principe s'applique également au chargement des données. Il est souhaitable de confier la responsabilité du chargement des données à une classe à part en dehors de notre algorithme d'apprentissage automatique. On voudrait passer des données déjà chargées et pré-mâchées à un pipeline contenant des instances de modèles, de sorte que la source de données et la manière de les charger (il peut y en avoir plusieurs) n'aient pas d'incidence sur le pipeline. Ceci est également étroitement lié au principe d'inversion des dépendances (DIP).
Un autre exemple du respect du SRP dans un pipeline d’apprentissage automatique est de laisser chaque étape du pipeline définir sa propre façon de s'initialiser, se sauvegarder et se supprimer. Cela est particulièrement important dans un contexte d’apprentissage profond (deep learning) par exemple. Dans un tel cas, certaines étapes du pipeline peuvent utiliser des GPU, comme d'autres peuvent utiliser des CPU (une telle allocation de ressources peut être gérée par les pipelines de Neuraxle). Il est donc important que chaque étape soit en mesure se gérer elle-même. Gérer les spécificités de chaque étape d’un pipeline à l'extérieur du pipeline briserait non seulement le SRP mais aussi le TDA.
Supposons maintenant que l’on tente de trouver les meilleurs hyperparamètres pour les différents modèles d'un pipeline (ce processus s'appelle Automated Machine Learning - AutoML). Dans un tel cas, il est crucial que chaque objet à l'intérieur d'un pipeline puisse gérer son propre chargement, enregistrement, initialisation et suppression afin d'allouer et désallouer efficacement les ressources. Ce serait un cauchemar de gérer ce processus à l’extérieur du modèle, ou pire, à l’extérieur du pipeline. Un défaut majeur de scikit-learn est son incapacité à gérer ce processus de cycle de vie des objets. Il est également important de respecter le SRP et l'OCP (voir section sur l’OCP) lors de la sérialisation et de l'enregistrement des pipelines d’apprentissage automatique contenant du code ne pouvant pas être enregistré nativement par l'interpréteur Python. Voici comment Neuraxle résout le problème de l'utilisation des ressources GPU, en utilisant les abstractions appropriées et en respectant le SRP et l’OCP.
O: Principe ouvert-fermé (Open-Closed Principle - OCP)
Ce principe stipule qu’une classe doit être fermée aux modifications et ouverte aux extensions. Cela signifie que si l’on doit ajouter une nouvelle fonctionnalité à un projet, on ne devrait pas avoir à modifier des classes existantes déjà fonctionnelles pour changer leur logique interne ou leur façon de fonctionner.
L'exemple le plus évident du non-respect de ce principe est le fait d'inclure de longues chaînes d'instructions if/else (switch cases) dans un seul bloc de code. Ce phénomène est communément appelé « polymorphisme du pauvre » (poor man’s polymorphism). Le polymorphisme est la capacité d'un objet à hériter d’abstractions et à prendre différentes formes afin d’offrir différentes fonctionnalités. Plutôt que de coder un long bloc if/else en un point particulier d'un pipeline, il est conseillé d'implémenter une classe abstraite ouverte aux modifications (au polymorphisme, dans ce cas). Tous les différents cas supposément traités par un bloc if/else peuvent ainsi être traités par autant de sous-classes différentes que nécessaire, toutes héritant de la classe abstraite précédemment construite. Une telle sous-classe peut alors s’approprier comme bon lui semble les méthodes de son parent à condition de respecter la signature et le contrat de ces méthodes (voir la section sur le principe LSP pour plus de détails sur la programmation par contrat). Ainsi, nul besoin de modifier la super-classe abstraite ni l'une des autres sous-classes lorsque viendra le temps d’implémenter de nouvelles fonctionnalités. Il suffira simplement d’implémenter une nouvelle sous-classe héritant de la même super-classe précédemment construite et de l'injecter au bon endroit (voir la section sur le DIP pour plus d'informations sur l'injection).
Illustrons ce principe avec l’exemple du chargement de données comme une étape d'un pipeline d'apprentissage automatique. Il n’est certainement pas judicieux d’implémenter une simple fonction appelée « load_data », laquelle aurait un énorme bloc if/else pour couvrir tous les cas possibles comme la récupération de données à partir d'un sous-dossier local, un dossier externe, une table SQL, un fichier CSV, un bucket S3, etc. Il est préférable d’implémenter une classe abstraite appelée DataLoader (ou DataRepository) par exemple. Il serait alors possible d’implémenter autant de sous-classes concrètes que nécessaire pour représenter les différentes façons de charger les données. Ainsi, lorsque viendra le temps d'ajouter une étape supplémentaire de chargement des données à notre pipeline, il sera possible d’implémenter et d’utiliser la bonne sous-classe (héritée de DataLoader) convenant au contexte. Le pipeline ne passerait donc pas par une chaîne inutile d'instructions if/else pour déterminer la façon de charger les données.
Voici un autre exemple pour pousser ce concept un peu plus loin. En respectant l’OCP, il est possible de mettre en œuvre une stratégie efficace pour choisir les bons hyperparamètres dans un pipeline d'apprentissage automatique. Imaginons qu'à une certaine étape d'un pipeline, il faille choisir entre la méthode de prétraitement A et la méthode de prétraitement B. La stratégie la plus tentante (sans égard pour l’OCP) serait sans doute d’implémenter un pipeline complet utilisant la méthode A et d’en implémenter un autre utilisant la méthode B. On passerait ensuite d’un pipeline à l’autre directement dans le fichier principal (main file) à l’aide d’une boucle for (for loop). Cette façon de faire est convenable à première vue et c’est d’ailleurs l’approche que la plupart des programmeurs et/ou chercheurs en science des données préconiseront. Le problème est qu’au fur et à mesure que de nouvelles méthodes et de nouveaux modèles doivent être implémentés et testés, le fichier principal doit être modifié pour inclure un polymorphisme de plus en plus gros. Cette approche engendrera donc très probablement un polymorphisme du pauvre directement dans le fichier principal. Le fichier principal est d’ailleurs le pire endroit où implémenter ce genre de polymorphisme car il est à l’extérieur du projet source et son code n’est pas encapsulé dans des objets. Dans un projet de machine learning, les fichiers principaux créés ici et là (à l’extérieur du code source) au fil de l’évolution du projet ne sont que très rarement maintenus et mis à jour. Tout ce qui y est implémenté est donc souvent perdu d’un contexte à l’autre. Une meilleure stratégie dans ce cas serait de créer une classe (qui serait une étape de pipeline), dans le code source bien sûr, dont le travail serait de sélectionner un modèle parmi une liste de modèles, puis de s’optimiser elle-même en choisissant le meilleur modèle selon une certaine métrique dans une boucle AutoML. Cette classe serait fermée aux modifications et ouverte aux extensions et tout pipeline implémenté dans un fichier principal pourrait y faire appel. Cette approche respecte aussi le principe de ségrégation des interfaces (ISP).
La librairie Neuraxle dispose d'outils pratiques pour mettre en œuvre ce type de stratégie. Le code pour définir le pipeline à optimiser se lirait comme suit :
CODE: https://gist.github.com/jeromebedard12/30d17fb53fe2f1106d44fd430dbdc17e.js
Suivant l’OCP, chaque étape d'un pipeline d’apprentissage automatique devrait être responsable non seulement de la définition de ses propres hyperparamètres, mais aussi de son espace d’hyperparamètres. S’il s’agit d’un wrapper, il doit également être responsable de gérer l'espace des objets qu'il enveloppe. Par exemple, dans le code ci-dessus, l'étape ChooseOneStepOf ne modifie pas seulement le flux de données, elle choisit également l’objet à utiliser (et ainsi les sous-espaces d'hyperparamètres à utiliser). Ce processus serait irréalisable si l’OCP n’était pas respecté et que les hyperparamètres étaient ‘’codés dur’’ (hardcoded) dans l’objet, auquel cas il faudrait continuellement procéder à des modifications manuelles pour explorer différents hyperparamètres.
L: Principe de substitution de Liskov (Liskov Substitution Principle - LSP)
Les sous-types d’un objet doivent être interchangeables avec leur type de base et vice-versa.
Supposons que l’on dispose par exemple de différents chargeurs de données. Selon le LSP, ils doivent tous obéir à la même interface. Ils doivent tous respecter le comportement prévu par cette interface, et ce, sans surprise. De telles interfaces sont également appelées des « contrats » dans le monde OOP. La « programmation par contrat » est en fait un principe fortement lié au LSP. En ce qui concerne l'héritage, cela signifie qu’un objet doit respecter les règles dont il hérite par polymorphisme et qu’il ne doit pas trahir les spécifications de l'objet de base.
L'exemple le plus simple d’un bris de ce principe est celui du canard en caoutchouc. Supposons qu’il faille implémenter une classe nommée CanardEnCaoutchouc (un faux canard) et que cette classe hérite de la classe Animal (un animal vivant). Selon le LSP, une question se pose : un CanardEnCaoutchouc est-il un substitut approprié à un Animal ? Autrement dit, une instance de CanardEnCaoutchouc peut-elle se comporter comme une instance d’Animal sans enfreindre les règles de fonctionnement du royaume animal défini par Animal ? La réponse est non. Par exemple, un appel de fonction tel que « CanardEnCaoutchouc.manger(nourriture) » engendre très certainement une erreur. Soit le canard en caoutchouc ne peut manger et fait semblant d'être un animal, soit il s'agit d'un imposteur devant être renommé car il est vivant et s’approprie illégitimement le concept de canard en caoutchouc. Faisant semblant d'être un animal, le CanardEnCaoutchouc a besoin de s’attribuer des mécanismes supplémentaires pour gérer l'imitation, ce qui brise son « contrat ». Briser le LSP de cette façon produit du code sale et est plus sensible aux erreurs.
Dans le cas d'un pipeline d'apprentissage automatique, ce principe implique qu'une étape particulière d’un pipeline doit suivre les mêmes règles de base que n’importe quelle autre étape. Un pipeline peut également contenir des sous-pipelines (pipelines imbriqués). Ces pipelines imbriqués devraient également suivre les règles d'une étape. En fait, un pipeline devrait être vu comme une étape. Par exemple, étant donné un format de données précis, il devrait être possible de remplacer une méthode de prétraitement par une autre sans briser le flux de données ou le comportement attendu du pipeline.
Remontons à l'exemple de code de la section OCP. Selon le LSP l'étape "YourPreprocessingA" pourrait être remplacée sans problème par l'étape "YourPreprocessingB". De la même manière, « TrainOnlyWrapper(DataShuffler()) » pourrait également être remplacé par « DataShuffler() » sans briser le flux des données dans le pipeline.
I: Principe de ségrégation des interfaces (Interface Segregation Principle - ISP)
Ne pas implémenter une énorme interface unique, mais plutôt distribuer plusieurs interfaces entre les différents objets.
Reprenons l'exemple du canard en caoutchouc explicité dans la section sur le LSP. Un canard en caoutchouc n'est pas un animal et ne devrait pas hériter de la classe Animal. Mais la classe CanardEnCaoutchouc a quand même ‘’envie’’ d’en hériter. Elle voudrait réutiliser certains comportements définis dans une classe Canard par exemple (héritée d'Animal). Une solution ici est de faire un « refactoring » de la classe Canard afin qu'elle hérite non seulement d'Animal, mais également d'une nouvelle classe de base FlotteurSurEau. De cette façon, Canard et CanardEnCaoutchouc peuvent tous deux hériter de FlotteurSurEau. Il est ainsi possible de réutiliser une partie du comportement de Canard dans CanardEnCaoutchouc sans briser le LSP (sans avoir à déclarer que CanardEnCaoutchouc est un animal et/ou un canard). La séparation des interfaces permet donc une réutilisation convenable du code entre différentes classes.
Dans le cas d’un pipeline d'apprentissage automatique, différentes interfaces doivent être implémentées pour des tâches différentes. Toutes les étapes d'un pipeline ainsi que le pipeline lui-même peuvent hériter de la même abstraction (une classe BaseStep par exemple) qui rend les étapes substituables en respectant le LSP. Cependant, certains objets, comme TrainOnlyWrapper ou même Pipeline, enveloppent (imbriquent récursivement) d'autres objets constituant une étape du pipeline. Suivant l'ISP, ces objets pourraient hériter d'une nouvelle classe (différente de BaseStep). De cette façon, BaseStep n’est pas alourdie inutilement en étant forcée de posséder une logique « d'étapes imbriquées récursivement », d’autant plus que ce n’est pas sa responsabilité (voir aussi SRP plus haut). Avec de telles méthodes récursives, il serait possible, par exemple, d'appeler "pipeline.get_hyperparams()" de l'extérieur du Pipeline en laissant le Pipeline creuser dans toutes ses étapes imbriquées pour extraire l'arborescence complète des hyperparamètres.
La séparation des interfaces facilite la réutilisation du code en combinant différents comportements. Le respect du principe ISP rend une base de code plus petite, plus simple, plus modulaire et sans duplication de blocs de code. Il est de mise d’extraire une classe abstraite commune (interface) à partir d'objets similaires afin de permettre à ces objets d'être interchangeables dans certaines situations - comme Canard et CanardEnCaoutchouc peuvent être interchangés lorsqu’il s'agit de flotter sur l'eau. De la même manière, un Pipeline ou tout autre Wrapper (comme TrainOnlyWrapper) ont la possibilité de creuser dans leurs étapes imbriquées récursivement pour définir ou récupérer des informations (comme des hyperparamètres) ou sérialiser leurs étapes. Ils seraient donc tous interchangeables en tant qu'objets ayant la possibilité d'imbriquer d'autres objets.
D: Principe d'inversion des dépendances (Dependency Inversion Principle - DIP)
Dépendre d'abstractions plutôt que d'implémentations détaillées et concrètes.
Ce principe est particulièrement important. Le non-respect de ce principe peut facilement tuer un projet d'apprentissage automatique, ou du moins rendre très difficile le déploiement de nouveaux modèles en production.
Supposons par exemple que lors de la construction d'un prototype, les données soient chargées au même niveau d'abstraction que l’enregistrement du modèle. Cela signifierait que dans une seule méthode (fonction), les données sont chargées, traitées, transmises à un modèle, évaluées par certaines mesures de performances et le modèle est enregistré dans un sous-dossier particulier (comme dans un Jupiter Notebook sale n’ayant recours à aucune classe – ce qui se voit lors de prototypages et expérimentations de recherche).
Supposons maintenant que l’on souhaite déployer un tel prototype en production. Il faudra par conséquent se connecter à une nouvelle source de données, être capable d’enregistrer le modèle et les mesures de performances à un emplacement particulier (différent de celui des expérimentations) et pouvoir recharger le tout. Le tout en gardant à l'esprit que les sources de données et les emplacements pour les modèles et mesures de performances sont susceptibles d'être modifiés et repensés dans l'avenir. Si l’inversion des dépendances n’a pas été effectuée dès le départ, un problème survient. S’il n’est pas géré correctement, du polymorphisme de pauvre apparaîtra probablement partout dans le code (voir section OCP) et énormément de temps sera perdu à garder le code à jour et fonctionnel au cours du processus de déploiement. La seule solution viable à long terme consiste à créer des classes abstraites pour le chargement de données, les étapes de pipeline, les pipelines eux-mêmes et ainsi de suite. Par exemple, une classe de chargement de données spécifique au prototype initial peut être implémentée. Ensuite, lorsque vient le temps de se connecter aux données en production, une autre classe de chargement de données peut être implémentée pour être substituée à la précédente dans le pipeline de manière transparente. Ces deux classes pourraient hériter d'une abstraction appelée DataLoader.
De plus, une classe de chargement de données ne devrait pas être instanciée au même niveau d'abstraction que celui où le modèle est implémenté (c'est-à-dire à l'intérieur du modèle lui-même). La bonne façon d'implémenter une inversion des dépendances selon le DIP est de passer une instance de DataLoader à un modèle. On doit faire passer des objets concrets à un niveau supérieur plutôt que de créer une instance de classe à l'intérieur d'une autre classe. Voici un exemple de code où les données sont préparées à l’avance comme un itérable, puis envoyées dans un pipeline.
De plus, si une mise en cache est nécessaire au cours de l’exécution d’un pipeline, des étapes spécifiques de points de contrôle (checkpoints) devraient s'en charger indépendamment des étapes régulières (ce qui respecte aussi le SRP). Le chemin d'accès à l'emplacement sur disque pour la mise en cache devrait être géré par le pipeline lui-même via un objet contextuel. Cet objet devrait posséder une fonction (get_path par exemple) fournissant un accès au chemin. Ce chemin peut ensuite être transmis aux étapes internes (imbriquées) d'un pipeline en tant que répertoire racine pour y enregistrer ou charger le contenu de la cache.
Indiquez plutôt que d’interroger (Tell Don’t Ask - TDA)
Ce dernier principe est également très important à comprendre. Le TDA sert à éviter de se retrouver avec des abstractions qui « fuient » (leaky abstractions). Une abstraction fuit lorsque nous devons en extraire du contenu pour la manipuler. Avoir trop de « getters » dans un objet est un bon indice du non-respect du TDA, bien que dans certains contextes il soit normal d'avoir beaucoup de getters. Un objet de données (Data Transfer Object - DTO) est un bon exemple d’un objet ayant peu de logique interne, ayant beaucoup de getters et de setters et servant simplement à stocker des attributs en bloc.
Voici un exemple de non-respect du TDA:
1. Obtenir (get) des attributs d'un objet.
2. Combiner ces attributs pour les mettre à jour ou faire quelque chose de nouveau.
3. Retourner (set) le résultat dans l'objet.
Voici la bonne façon de procéder pour respecter le TDA:
1. Indiquer à l’objet quoi faire (à l’aide d’une méthode précise) et passer au besoin des éléments additionnels en tant que arguments dans la méthode (notez également le DIP ici).
Ainsi, chaque fois qu’une tâche doit être répétée, plutôt que de dupliquer du code pour effectuer la tâche en dehors de l'objet, la méthode déjà implémentée est simplement appelée par une instance de l'objet. La duplication de code est l'une des choses les plus difficiles à gérer et elle brise également l’OCP.
Appliquons maintenant le TDA à un pipeline d’apprentissage automatique. Chacune des étapes doit se gérer elle-même de manière indépendante sans devoir passer par du micro-management. Il n’est pas souhaitable de constamment avoir à creuser dans un pipeline pour le modifier. Une étape (step) doit pouvoir effectuer la bonne tâche au bon moment. Par exemple, supposons qu’un pipeline doive basculer en mode test. En une simple ligne de commande, on doit pouvoir appeler une méthode qui se charge de cette tâche. Cette tâche consisterait par exemple à désactiver les étapes encapsulées dans TrainOnlyWrapper et activer les étapes dans TestOnlyWrapper. Chaque étape du pipeline doit être responsable de se mettre à jour suivant sa propre logique. Il n’est pas commode d’avoir à creuser dans chaque objet du pipeline pour y changer un booléen (is_train par exemple). Briser l'encapsulation du pipeline engendrerait non seulement du code sale, mais rendrait aussi le code plus vulnérable aux erreurs. Par exemple, l'ajout d'une deuxième puis d'une troisième modification déclenchée lors du basculement en mode test nécessiterait de creuser trois fois dans tout le pipeline si le TDA n’était pas respecté. Plus le micro-management est lourd, plus le risque de voir apparaître des erreurs est élevé et plus la programmation est longue, fastidieuse et répétitive. Mieux vaut donner au pipeline et aux étapes du pipeline la logique interne nécessaire pour passer en mode test de manière transparente.
Cet article de Martin Fowler donne une compréhension approfondie du TDA.
Conclusion
Les principes SOLID (en plus du TDA) de la POO ont été détaillés et des exemples d’applications aux pipelines d’apprentissage automatique ont été exposés. Cet article démontre l’importance de coder un pipeline d'apprentissage automatique de manière propre et structurée. Le monde du développement logiciel est en constante évolution, tout comme les concepts présentés ici. Les méthodes partagées dans cet article sont pour le moment, au meilleur de nos connaissances, la bonne manière de coder des pipelines d'apprentissage automatique en Python. Par exemple, chez Umaneo, nous avons surmonté les défis sous-jacents au respect des principes SOLID en utilisant la librairie open source Neuraxle.