Créer des outils dans des projets d'apprentissage automatique, un aperçu

Je me posais des questions sur l'apprentissage automatique / la structure / le flux de travail d'un projet de science des données et je lisais différentes opinions sur le sujet. Et lorsque les gens commencent à parler de workflow, ils souhaitent que leurs workflows soient reproductibles. Il existe de nombreux articles qui suggèrent d'utiliser make pour garder le flux de travail reproductible. Bien que make soit très stable et largement utilisé, j'aime personnellement les solutions multiplateformes. C'est 2019 après tout, pas 1977. On peut dire que se faire est multiplateforme, mais en réalité vous aurez des problèmes et passerez du temps à réparer votre outil plutôt qu'à faire le travail réel. J'ai donc décidé de jeter un coup d'œil et de vérifier quels autres outils sont disponibles. Oui, j'ai décidé de passer du temps sur les outils.

image

Ce message est plus une invitation à un dialogue qu'à un tutoriel. Votre solution est peut-être parfaite. Si c'est le cas, il sera intéressant d'en entendre parler.

Dans ce post, j'utiliserai un petit projet Python et ferai les mêmes tâches d'automatisation avec différents systèmes:


Il y aura un tableau de comparaison à la fin de l'article.

La plupart des outils que j'examinerai sont appelés logiciels d'automatisation de construction ou systèmes de construction . Il y en a des myriades dans toutes les saveurs, tailles et complexités différentes. L'idée est la même: le développeur définit des règles pour produire des résultats de manière automatisée et cohérente. Par exemple, un résultat peut être une image avec un graphique. Pour créer cette image, il faudrait télécharger les données, nettoyer les données et faire quelques manipulations de données (exemple classique, vraiment). Vous pouvez commencer avec quelques scripts shell qui feront le travail. Une fois que vous reviendrez au projet un an plus tard, il sera difficile de se souvenir de toutes les étapes et de leur ordre que vous devez prendre pour créer cette image. La solution évidente est de documenter toutes les étapes. Bonne nouvelle! Les systèmes de construction vous permettent de documenter les étapes sous forme de programme informatique. Certains systèmes de construction sont comme vos scripts shell, mais avec des cloches et des sifflets supplémentaires.

Le fondement de ce message est une série de messages de Mateusz Bednarski sur le flux de travail automatisé pour un projet d'apprentissage automatique. Mateusz explique son point de vue et propose des recettes d'utilisation de make . Je vous encourage à aller vérifier ses messages en premier. J'utiliserai principalement son code, mais avec différents systèmes de build.

Si vous souhaitez en savoir plus sur make , voici quelques références pour quelques articles. Brooke Kennedy donne un aperçu de haut niveau en 5 étapes faciles pour rendre votre projet de science des données reproductible. Zachary Jones donne plus de détails sur la syntaxe et les capacités ainsi que les liens vers d'autres publications. David Stevens écrit un article très hype sur les raisons pour lesquelles vous devez absolument commencer à utiliser make immédiatement. Il fournit de bons exemples comparant l'ancienne et la nouvelle méthode . Samuel Lampa , d'autre part, explique pourquoi utiliser make est une mauvaise idée.

Ma sélection de systèmes de construction n'est ni complète ni impartiale. Si vous voulez faire votre liste, Wikipedia pourrait être un bon point de départ. Comme indiqué ci-dessus, je couvrirai CMake , PyBuilder , pynt , Paver , doit et Luigi . La plupart des outils de cette liste sont basés sur Python et cela a du sens puisque le projet est en Python. Ce message ne couvrira pas comment installer les outils. Je suppose que vous maîtrisez assez bien Python.

Je suis surtout intéressé à tester cette fonctionnalité:

  1. Spécification de deux cibles avec dépendances. Je veux voir comment le faire et à quel point c'est facile.
  2. Vérifier si des constructions incrémentielles sont possibles. Cela signifie que le système de génération ne reconstruira pas ce qui n'a pas été modifié depuis la dernière exécution, c'est-à-dire que vous n'avez pas besoin de retélécharger vos données brutes. Une autre chose que je vais rechercher est les builds incrémentiels lorsque les dépendances changent. Imaginez que nous ayons un graphique des dépendances A -> B -> C La cible C sera-t-elle reconstruite si B change? Si un?
  3. Vérifier si la reconstruction sera déclenchée si le code source est modifié, c'est-à-dire que nous modifions le paramètre du graphique généré, la prochaine fois que nous construisons l'image doit être reconstruite.
  4. Découvrez les moyens de nettoyer les artefacts de génération, c'est-à-dire de supprimer les fichiers qui ont été créés pendant la génération et de revenir au code source propre.

Je n'utiliserai pas toutes les cibles de construction du post de Mateusz, seulement trois d'entre elles pour illustrer les principes.

Tout le code est disponible sur GitHub .

CMake


CMake est un générateur de script de génération, qui génère des fichiers d'entrée pour différents systèmes de génération. Et son nom signifie marque multiplateforme. CMake est un outil de génie logiciel. Sa principale préoccupation est de créer des exécutables et des bibliothèques. CMake sait donc comment construire des cibles à partir du code source dans les langues prises en charge. CMake est exécuté en deux étapes: configuration et génération. Lors de la configuration, il est possible de configurer la future version en fonction des besoins. Par exemple, des variables fournies par l'utilisateur sont fournies au cours de cette étape. La génération est normalement simple et produit des fichiers avec lesquels les systèmes de construction peuvent fonctionner. Avec CMake, vous pouvez toujours utiliser make , mais au lieu d'écrire directement makefile, vous écrivez un fichier CMake, qui générera le makefile pour vous.

Un autre concept important est que CMake encourage les builds hors source . Les versions hors source éloignent le code source de tout artefact qu'il produit. Cela a beaucoup de sens pour les exécutables où la base de code source unique peut être compilée sous différentes architectures CPU et systèmes d'exploitation. Cette approche, cependant, peut contredire la façon dont beaucoup de scientifiques de données travaillent. Il me semble que la communauté de la science des données a tendance à avoir un couplage élevé de données, de code et de résultats.

Voyons ce dont nous avons besoin pour atteindre nos objectifs avec CMake. Il existe deux possibilités pour définir des éléments personnalisés dans CMake: des cibles personnalisées et des commandes personnalisées. Malheureusement, nous devrons utiliser les deux, ce qui se traduit par plus de frappe par rapport au makefile vanila. Une cible personnalisée est considérée comme toujours obsolète, c'est-à-dire s'il existe une cible pour le téléchargement de données brutes CMake la téléchargera toujours. Une combinaison de commande personnalisée et de cible personnalisée permet de maintenir les cibles à jour.

Pour notre projet, nous allons créer un fichier nommé CMakeLists.txt et le mettre à la racine du projet. Voyons le contenu:

 cmake_minimum_required(VERSION 3.14.0 FATAL_ERROR) project(Cmake_in_ml VERSION 0.1.0 LANGUAGES NONE) 

Cette partie est basique. La deuxième ligne définit le nom de votre projet, sa version et spécifie que nous n'utiliserons aucune prise en charge de langage intégré (nous appellerons alors des scripts Python).

Notre première cible téléchargera l'ensemble de données IRIS:

 SET(IRIS_URL "https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data" CACHE STRING "URL to the IRIS data") set(IRIS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/data/raw) set(IRIS_FILE ${IRIS_DIR}/iris.csv) ADD_CUSTOM_COMMAND(OUTPUT ${IRIS_FILE} COMMAND ${CMAKE_COMMAND} -E echo "Downloading IRIS." COMMAND python src/data/download.py ${IRIS_URL} ${IRIS_FILE} COMMAND ${CMAKE_COMMAND} -E echo "Done. Checkout ${IRIS_FILE}." WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) ADD_CUSTOM_TARGET(rawdata ALL DEPENDS ${IRIS_FILE}) 

La première ligne définit le paramètre IRIS_URL , qui est exposé à l'utilisateur lors de l'étape de configuration. Si vous utilisez l'interface graphique CMake, vous pouvez définir cette variable via l'interface graphique:



Ensuite, nous définissons des variables avec l'emplacement téléchargé de l'ensemble de données IRIS. Ensuite, nous ajoutons une commande personnalisée, qui produira IRIS_FILE en sortie. En fin de compte, nous définissons une rawdata cible rawdata qui dépend de IRIS_FILE ce qui signifie que pour construire des données rawdata IRIS_FILE doit être construit. L'option ALL de la cible personnalisée indique que les données rawdata seront l'une des cibles par défaut à créer. Notez que j'utilise CMAKE_CURRENT_SOURCE_DIR afin de conserver les données téléchargées dans le dossier source et non dans le dossier de construction. C'est juste pour faire la même chose que Mateusz.

D'accord, voyons comment nous pouvons l'utiliser. Je l'exécute actuellement sur WIndows avec le compilateur MinGW installé. Vous devrez peut-être ajuster le paramètre du générateur à vos besoins (exécutez cmake --help pour voir la liste des générateurs disponibles). Lancez le terminal et accédez au dossier parent du code source, puis:

 mkdir overcome-the-chaos-build cd overcome-the-chaos-build cmake -G "MinGW Makefiles" ../overcome-the-chaos 

résultat
- Configuration terminée
- Génération terminée
- Les fichiers de construction ont été écrits dans: C: / home / workspace / surmonter-le-chaos-build

Avec CMake moderne, nous pouvons construire le projet directement à partir de CMake. Cette commande appellera la commande build all :

 cmake --build . 

résultat
Analyse des dépendances des données brutes cibles
[100%] Données brutes cibles construites

Nous pouvons également consulter la liste des cibles disponibles:

 cmake --build . --target help 

Et nous pouvons supprimer le fichier téléchargé par:

 cmake --build . --target clean 

Voyez que nous n'avions pas besoin de créer la cible propre manuellement.

Passons maintenant à la prochaine cible - les données IRIS prétraitées. Mateusz crée deux fichiers à partir d'une seule fonction: processed.pickle et processed.xlsx . Vous pouvez voir comment il s'en va avec le nettoyage de ce fichier Excel en utilisant rm avec un caractère générique. Je pense que ce n'est pas une très bonne approche. Dans CMake, nous avons deux options pour y faire face. La première option consiste à utiliser la propriété de répertoire ADDITIONAL_MAKE_CLEAN_FILES . Le code sera:

 SET(PROCESSED_FILE ${CMAKE_CURRENT_SOURCE_DIR}/data/processed/processed.pickle) ADD_CUSTOM_COMMAND(OUTPUT ${PROCESSED_FILE} COMMAND python src/data/preprocess.py ${IRIS_FILE} ${PROCESSED_FILE} --excel data/processed/processed.xlsx WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DEPENDS rawdata ${IRIS_FILE} ) ADD_CUSTOM_TARGET(preprocess DEPENDS ${PROCESSED_FILE}) # Additional files to clean set_property(DIRECTORY PROPERTY ADDITIONAL_MAKE_CLEAN_FILES ${CMAKE_CURRENT_SOURCE_DIR}/data/processed/processed.xlsx ) 

La deuxième option consiste à spécifier une liste de fichiers en tant que sortie de commande personnalisée:

 LIST(APPEND PROCESSED_FILE "${CMAKE_CURRENT_SOURCE_DIR}/data/processed/processed.pickle" "${CMAKE_CURRENT_SOURCE_DIR}/data/processed/processed.xlsx" ) ADD_CUSTOM_COMMAND(OUTPUT ${PROCESSED_FILE} COMMAND python src/data/preprocess.py ${IRIS_FILE} data/processed/processed.pickle --excel data/processed/processed.xlsx WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DEPENDS rawdata ${IRIS_FILE} src/data/preprocess.py ) ADD_CUSTOM_TARGET(preprocess DEPENDS ${PROCESSED_FILE}) 

Voyez que dans ce cas, j'ai créé la liste, mais je ne l'ai pas utilisée dans la commande personnalisée. Je ne connais pas de moyen de référencer les arguments de sortie de la commande personnalisée à l'intérieur.

Une autre chose intéressante à noter est l'utilisation de depends dans cette commande personnalisée. Nous définissons la dépendance non seulement à partir d'une cible personnalisée, mais aussi de sa sortie et du script python. Si nous IRIS_FILE pas de dépendance à IRIS_FILE , la modification manuelle d' iris.csv n'entraînera pas la reconstruction de la cible de preprocess . Eh bien, vous ne devez pas modifier les fichiers de votre répertoire de construction manuellement en premier lieu. Je vous fais juste savoir. Plus de détails dans le post de Sam Thursdayfield . La dépendance au script python est nécessaire pour reconstruire la cible si le script python change.

Et enfin le troisième objectif:

 SET(EXPLORATORY_IMG ${CMAKE_CURRENT_SOURCE_DIR}/reports/figures/exploratory.png) ADD_CUSTOM_COMMAND(OUTPUT ${EXPLORATORY_IMG} COMMAND python src/visualization/exploratory.py ${PROCESSED_FILE} ${EXPLORATORY_IMG} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DEPENDS ${PROCESSED_FILE} src/visualization/exploratory.py ) ADD_CUSTOM_TARGET(exploratory DEPENDS ${EXPLORATORY_IMG}) 

Cet objectif est fondamentalement le même que le second.

Pour conclure. CMake semble désordonné et plus difficile que Make. En effet, beaucoup de gens critiquent CMake pour sa syntaxe. D'après mon expérience, la compréhension viendra et il est absolument possible de donner un sens à des fichiers CMake même très compliqués.

Vous devrez toujours vous coller beaucoup car vous devrez passer des variables correctes. Je ne vois pas un moyen facile de référencer la sortie d'une commande personnalisée dans une autre. Il semble qu'il soit possible de le faire via des cibles personnalisées.

Pybuilder


La partie PyBuilder est très courte. J'ai utilisé Python 3.7 dans mon projet et PyBuilder version actuelle 0.11.17 ne le prend pas en charge. La solution proposée consiste à utiliser la version de développement. Cependant, cette version est limitée à pip v9. Pip est v19.3 au moment de l'écriture. Bummer. Après l'avoir un peu tripoté, ça n'a pas marché du tout pour moi. L'évaluation de PyBuilder a été de courte durée.

pynt


Pynt est basé sur python, ce qui signifie que nous pouvons utiliser directement les fonctions python. Il n'est pas nécessaire de les encapsuler avec un clic et de fournir une interface de ligne de commande. Cependant, pynt est également capable d'exécuter des commandes shell. J'utiliserai des fonctions python.

Les commandes de construction sont données dans un fichier build.py . Les cibles / tâches sont créées avec des décorateurs de fonctions. Les dépendances de tâches sont fournies par le même décorateur.

Comme je voudrais utiliser des fonctions python, je dois les importer dans le script de construction. Pynt n'inclut pas le répertoire courant comme script python, donc écrire smth comme ceci:

 from src.data.download import pydownload_file 

ne fonctionnera pas. Nous devons faire:

 import os import sys sys.path.append(os.path.join(os.path.dirname(__file__), '.')) from src.data.download import pydownload_file 

Mon fichier build.py initial était comme ceci:

 #!/usr/bin/python import os import sys sys.path.append(os.path.join(os.path.dirname(__file__), '.')) from pynt import task from path import Path import glob from src.data.download import pydownload_file from src.data.preprocess import pypreprocess iris_file = 'data/raw/iris.csv' processed_file = 'data/processed/processed.pickle' @task() def rawdata(): '''Download IRIS dataset''' pydownload_file('https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data', iris_file) @task() def clean(): '''Clean all build artifacts''' patterns = ['data/raw/*.csv', 'data/processed/*.pickle', 'data/processed/*.xlsx', 'reports/figures/*.png'] for pat in patterns: for fl in glob.glob(pat): Path(fl).remove() @task(rawdata) def preprocess(): '''Preprocess IRIS dataset''' pypreprocess(iris_file, processed_file, 'data/processed/processed.xlsx') 

Et la cible de preprocess n'a pas fonctionné. Il se plaignait constamment des arguments d'entrée de la fonction pypreprocess . Il semble que Pynt ne gère pas très bien les arguments de fonction optionnels. J'ai dû retirer l'argument pour faire le fichier Excel. Gardez cela à l'esprit si votre projet a des fonctions avec des arguments facultatifs.

Nous pouvons exécuter pynt à partir du dossier du projet et répertorier toutes les cibles disponibles:

 pynt -l 

résultat
 Tasks in build file build.py: clean Clean all build artifacts exploratory Make an image with pairwise distribution preprocess Preprocess IRIS dataset rawdata Download IRIS dataset Powered by pynt 0.8.2 - A Lightweight Python Build Tool. 


Faisons la distribution par paire:

 pynt exploratory 

résultat
 [ build.py - Starting task "rawdata" ] Downloading from https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data to data/raw/iris.csv [ build.py - Completed task "rawdata" ] [ build.py - Starting task "preprocess" ] Preprocessing data [ build.py - Completed task "preprocess" ] [ build.py - Starting task "exploratory" ] Plotting pairwise distribution... [ build.py - Completed task "exploratory" ] 


Si nous pynt exploratory nouveau la même commande (c'est-à-dire pynt exploratory ), il y aura une reconstruction complète. Pynt n'a pas constaté que rien n'avait changé.

Pavé


Paver ressemble presque exactement à Pynt. Il diffère légèrement d'une manière qui définit les dépendances entre les cibles (un autre décorateur @needs ). Paver effectue une reconstruction complète à chaque fois et ne joue pas bien avec les fonctions qui ont des arguments facultatifs. Les instructions de construction se trouvent dans le fichier pavement.py .

doit


Doit semble être une tentative de créer un véritable outil d'automatisation de construction en python. Il peut exécuter du code python et des commandes shell. Cela semble assez prometteur. Ce qui semble manquer (dans le contexte de nos objectifs spécifiques), c'est la capacité de gérer les dépendances entre les cibles. Disons que nous voulons créer un petit pipeline où la sortie de la cible A est utilisée comme entrée de la cible B. Et disons que nous utilisons des fichiers comme sorties, donc la cible A crée un fichier nommé outA .



Afin de créer un tel pipeline, nous devrons spécifier le fichier outA deux fois dans la cible A (à la suite d'une cible, mais aussi retourner son nom dans le cadre de l'exécution de la cible). Ensuite, nous devrons le spécifier comme entrée pour la cible B. Il y a donc 3 endroits au total où nous devons fournir des informations sur le fichier outA . Et même après cela, la modification du fichier outA n'entraînera pas la reconstruction automatique de la cible B. Cela signifie que si nous demandons doit construire la cible B, doit uniquement vérifier si la cible B est à jour sans vérifier aucune des dépendances. Pour surmonter cela, nous devrons spécifier outA 4 fois - également en tant que dépendance de fichier de la cible B. Je vois cela comme un inconvénient. Make et CMake sont capables de gérer ces situations correctement.

Les dépendances dans doit doivent être basées sur des fichiers et exprimées sous forme de chaînes. Cela signifie que les dépendances ./myfile.txt et myfile.txt sont considérées comme différentes. Comme je l'ai écrit ci-dessus, je trouve un peu étrange la façon de transmettre des informations d'une cible à l'autre (lors de l'utilisation de cibles python). La cible a une liste d'artefacts qu'elle va produire, mais une autre cible ne peut pas l'utiliser. Au lieu de cela, la fonction python, qui constitue la cible, doit renvoyer un dictionnaire, accessible dans une autre cible. Voyons cela sur un exemple:

 def task_preprocess(): """Preprocess IRIS dataset""" pickle_file = 'data/processed/processed.pickle' excel_file = 'data/processed/processed.xlsx' return { 'file_dep': ['src/data/preprocess.py'], 'targets': [pickle_file, excel_file], 'actions': [doit_pypreprocess], 'getargs': {'input_file': ('rawdata', 'filename')}, 'clean': True, } 

Ici, le preprocess cible dépend des données rawdata . La dépendance est fournie via la propriété getargs . Il indique que l'argument input_file de la fonction doit_pypreprocess est le filename de filename de sortie des données rawdata cibles. Jetez un œil à l'exemple complet dans le fichier dodo.py.

Il peut être utile de lire les histoires de réussite de l'utilisation de doit. Il a certainement de belles fonctionnalités comme la possibilité de fournir une vérification cible mise à jour personnalisée.

Luigi


Luigi reste à l'écart des autres outils car c'est un système pour construire des pipelines complexes. Il est apparu sur mon radar après qu'un collègue m'a dit qu'il avait essayé Make, qu'il n'a jamais pu l'utiliser sur Windows / Linux et qu'il s'est éloigné de Luigi.

Luigi vise des systèmes prêts pour la production. Il est livré avec un serveur, qui peut être utilisé pour visualiser vos tâches ou pour obtenir un historique des exécutions de tâches. Le serveur est appelé un ordonnanceur central . Un planificateur local est disponible à des fins de débogage.

Luigi est également différent des autres systèmes dans la mesure où les tâches sont créées. Lugi n'agit pas sur certains fichiers prédéfinis (comme dodo.py , pavement.py ou makefile). Il faut plutôt passer un nom de module python. Donc, si nous essayons de l'utiliser de la même manière que d'autres outils (placer un fichier avec des tâches à la racine du projet), cela ne fonctionnera pas. Nous devons soit installer notre projet, soit modifier la variable d'environnement PYTHONPATH en ajoutant le chemin d'accès au projet.

Ce qui est génial avec luigi, c'est la façon de spécifier les dépendances entre les tâches. Chaque tâche est une classe. La output méthode indique à Luigi où les résultats de la tâche finiront. Les résultats peuvent être un seul élément ou une liste. La méthode requires des dépendances de tâches spécifiées (autres tâches; bien qu'il soit possible de créer une dépendance à partir d'elle-même). Et c'est tout. Tout ce qui est spécifié comme output dans la tâche A sera transmis comme entrée à la tâche B si la tâche B s'appuie sur la tâche A.


Luigi ne se soucie pas des modifications de fichiers. Il se soucie de l'existence de fichiers. Il n'est donc pas possible de déclencher des reconstructions lorsque le code source change. Luigi n'a pas de fonctionnalité propre intégrée.

Les tâches Luigi pour ce projet sont disponibles dans le fichier luigitasks.py . Je les lance depuis le terminal:

 luigi --local-scheduler --module luigitasks Exploratory 

Comparaison


Le tableau ci-dessous résume le fonctionnement de différents systèmes par rapport à nos objectifs spécifiques.
Définir la cible avec dépendanceConstructions incrémentiellesConstructions incrémentielles si le code source est modifiéPossibilité de déterminer les artefacts à supprimer lors de clean commande de clean
CMakeouiouiouioui
Pyntouinonnonnon
Pavéouinonnonnon
doitUn peu ouiouiouioui
Luigiouinonnonnon

Source: https://habr.com/ru/post/fr451962/


All Articles