Amener son projet de machine learning jusqu’en production avec Wheel et Docker

De nos jours, il fait consensus que pour aller en production, le code produit dans un projet de Machine Learning doit tĂŽt ou tard quitter le format Jupyter Notebook, si cher aux data scientists, pour trouver sa place dans un IDE:

En effet, les « bonnes pratiques » de conception logicielle peuvent s’appliquer plus facilement et de façon plus systĂ©matique dans un IDE, et cela peut nous aider Ă  produire du code de qualitĂ© tout en restant confiant en ce que nous comptons livrer en production.

Toutefois, migrer du code de data science depuis le notebook vers l’IDE peut s’avĂ©rer compliquĂ© pour le data scientist de formation, non-initiĂ© aux concepts de software craftsmanship. Le signal peut encore plus se brouiller au moment fatidique d’aller en production, action qui fait appel Ă  un second ensemble de compĂ©tences Ă  dĂ©couvrir: l’ingĂ©nierie opĂ©rationnelle (ou l’ops pour les intimes).

Sans avoir la prĂ©tention de faire de vous des data scientists moutons Ă  cinq pattes, nous vous proposons dans cet article de faire un premier pas dans le monde du MLOps en abordant le sujet du packaging d’applications de Machine Learning. Cela nous amĂšnera, petit Ă  petit, Ă  le dĂ©mystifier en commençant par aborder le format Wheel, puis Docker, et nous couvrirons enfin quelques stratĂ©gies de dĂ©ploiement dont le choix peut impacter la façon de packager.

Si packager du code de Machine Learning en Python est pour vous synonyme de demander à vos utilisateurs de cloner votre repository git sur leur machine, cet article devrait vous intéresser.

Pour illustrer cela, nous utiliserons le dĂ©sormais iconique challenge de classification en Computer Vision: « Muffin versus Chihuahua », qui consiste Ă  produire un programme capable de dĂ©tecter si un muffin đŸȘ ou un chihuahua đŸ¶Â  est prĂ©sent dans une image et les distinguer. Nous rĂ©aliserons le packaging d’une application capable de faire cela, avec son code Python mais aussi un modĂšle de deep learning et des fichiers (des images de muffin et de chihuahua Ă  des fins de dĂ©monstration).

Du code de dĂ©monstration est disponible sur GitHub, dans ce repo-ci pour illustrer une approche de packaging avec modele embarquĂ©, et dans cet autre repo pour illustrer une approche de packaging avec un modele isolĂ© en tant que service. Ces approches seront dĂ©taillĂ©es en fin d’article.

📩 Livrer du code (littĂ©ralement) en production, est-ce bien sĂ©rieux ? đŸ€”

La question peut paraßtre provocatrice, mais le Python étant un langage interprété, contrairement au C qui se compile ou au JavaScript norme ES6 qui se transpile, il est légitime de se la poser.

Nous pouvons en effet vous livrer le code suivant en vous demandant de le copier sur votre poste local dans un fichier greetings.py et vous arriveriez Ă  l’exĂ©cuter sans trop de problĂšmes (si tant est que vous l’exĂ©cutiez dans une version de Python supportant le type hinting) :

def greeting(name: str) -> str:
    return "Hello " + name

greeting("OCTO")

Toutefois, si on met de cÎté cet exemple volontairement simpliste pour se pencher sur du code de data science réaliste, comme celui de la librairie de calcul scientifique Numpy, les choses se compliquent :

  • Il n’y a plus uniquement 1 fonction et son appel Ă  livrer, mais plusieurs fichiers. Comment tous les rĂ©cupĂ©rer ? Comment s’assurer qu’aucun fichier ne va manquer lors de la livraison ?
  • Dans les sources, il y a du code Python, mais aussi du C. Est-ce qu’on peut livrer ces sources telles quelles ? Faut-il les compiler au prĂ©alable ou laisser l’utilisateur faire, sachant que, selon la cible (Windows, Linux ou OS X), les modalitĂ©s de compilation peuvent changer ?
  • Ce n’est pas le cas de Numpy, mais si votre application a besoin d’un modĂšle de Machine Learning ou de donnĂ©es pour fonctionner, comment les prendre en compte lors du packaging ?

Il y a une dizaine d’annĂ©es, ces questions n’avaient pas de rĂ©ponse standard : il fallait construire et partager ses propres recettes et imposer Ă  nos utilisateurs d’exĂ©cuter du code arbitraire sur leur poste, rendant le packaging d’applications Python (de Machine Learning ou non) une douleur. Nous aborderons dans cet article des approches standards pour y remĂ©dier.

Et pour notre classifieur de muffins đŸȘ et de chihuahuas đŸ¶ ?

Nous pouvons packager le code Python, le modÚle et les images dans une unique archive pour démarrer simplement comme suit :

# dans un Makefile, en suivant la convention "self-documented"

.PHONY: package  ## 📩 packaging de l'application au format zip
package:
	zip -r muffin-v-chihuahua.zip ${path_to_code} ${path_to_model} ${path_to_data}

.PHONY: install-app  ## ⚙ Code arbitraire Ă  exĂ©cuter chez vous si vous voulez vous servir de cette application
install-app: MY_OS := 'windows_vista'
	unzip muffin-v-chihuahua.zip -d ${site_packages_destination_path}
	$(MAKE) compilation_en_c OS=${MY_OS}

Il devient alors de notre responsabilitĂ© de produire ce code de packaging et d’installation, mais aussi : de le tester, de le maintenir dans la durĂ©e et de s’assurer qu’il fonctionne comme souhaitĂ© dans tous les environnements que nos utilisateurs pourraient utiliser (Windows, Linux, Mac OS, Raspberry Pi, 
).

Devant la multiplicitĂ© des façons de faire ou l’effort de maintenance Ă  fournir, il n’est pas rare de voir des projets open-source se passer complĂštement de packaging pour demander Ă  leurs utilisateurs de cloner le code via git en ligne de commande s’ils veulent s’en servir.

Toutefois, il est possible de faire diffĂ©remment: le format standard Wheel a fini par Ă©merger, et nous pouvons nous appuyer dessus dĂ©sormais pour Ă©viter d’avoir Ă  produire, maintenir, et faire exĂ©cuter chez autrui du code arbitraire.

🐍 Le format Wheel ☞ pour Ă©viter de rĂ©inventer la roue đŸ„

Le format Wheel est un format de packaging d’applications standard en Python.

📩  Petit point vocabulaire: par packager, nous entendons encapsuler :
  • une version qui peut correspondre Ă  un commit ou une date,
  • le code qui est exĂ©cutĂ© ou importĂ© par l’utilisateur,
  • avec des metadata pour le package manager (ex: pip, apt, yum),
  • le tout dans une archive : zip | tar.gz | bzip2.
👉 Ce tout est communĂ©ment appelĂ© un artefact ou un paquet … sauf en Python, oĂč l’on parle de distribution.

Le format Wheel est un standard car celui-ci a Ă©mergĂ© du processus de crĂ©ation standard de la communautĂ© Python, via une Python Enhancement Proposal (ou PEP): en l’occurrence, la PEP427 initiĂ©e en 2012. Aussi, ce format est le seul format officiellement prĂ©conisĂ© Ă  ce jour par la Python Packaging Authority (PyPA).

Extrait de la documentation de la Python Packaging Authority (PyPA) sur le packaging en Python

Les distributions Wheel se publient généralement dans un dépÎt public: le Python Package Index (PyPI), elles se génÚrent avec la librairie standard setuptools, elles se publient dans PyPI via la librairie twine et se téléchargent avec la librairie pip.

cinematique du packaging, des sources vers la publication

La librairie setuptools s’emploie par convention dans un fichier setup.py, positionnĂ© par convention Ă  la racine du projet :

from setuptools import setup

setup()

Le contenu de ce fichier setup.py peut paraßtre bien maigre. Il est en effet possible de renseigner la configuration de packaging via du code, de façon impérative, en renseignant les nombreux arguments de la fonction setup.

On préférera déporter cette configuration dans un fichier setup.cfg, en suivant un paradigme déclaratif. Ceci est possible depuis décembre 2016 et fonctionne comme suit :

[metadata]
name = muffin-v-chihuahua-with-embedded-model
version = 1.0
author = Mehdi Houacine
author_email = <mon adresse>@octo.com
home-page = https://github.com/Mehdi-H/muffin-v-chihuahua-with-embedded-model
license = <une licence>
description = To detect a muffin or a chihuahua in an image.
platform = any
classifiers =
   Programming Language :: Python :: 3
   Intended Audience :: Developers, DataScientists, MLEngineers
   Operating System :: OS Independent
   Bug Tracker = https://github.com/Mehdi-H/muffin-v-chihuahua-with-embedded-model/issues

[options]
zip_safe = false
packages = find:
install_requires =
   numpy==1.19.4
   Pillow==8.1.0
   streamlit==0.74.1
   watchdog==1.0.2
   keras==2.4.3
   tensorflow==2.4.0
include_package_data = True
python_requires = >=3.8

[options.entry_points]
console_scripts =
   muffin-v-chihuahua-with-embedded-model = muffin_v_chihuahua.__main__:main

On remarquera qu’il devient alors possible de renseigner les dĂ©pendances du projet directement au niveau de setup.py ou de setup.cfg (option install_requires) pour ainsi Ă©viter de maintenir un fichier de dĂ©pendances ad-hoc, gĂ©nĂ©ralement nommĂ© requirements.txt. Les dĂ©pendances python s’installent alors avec la commande « pip install . » Ă  la racine du projet.

On notera que si PyPI est un dĂ©pĂŽt de distributions Wheel publique, il existe un dĂ©pĂŽt « bac Ă  sable » TestPyPI, lui aussi publique, qui permet de s’essayer Ă  la publication de packages Python. Il est aussi possible de crĂ©er des dĂ©pĂŽts privĂ©s en montant son propre index ou en s’appuyant sur des offres managĂ©es comme le service Package Registry de Gitlab, le service PyPI repositories de JFrog ou encore le service Code Artifact chez AWS.

La librairie setuptools, qui permet de gĂ©nĂ©rer des distributions Wheel, permet aussi de gĂ©rer la compilation et le packaging d’extensions C au travers de l’argument ext_modules dans le fichier setup, ou encore de spĂ©cifier des fichiers de donnĂ©es devant ĂȘtre embarquĂ©s dans la distribution via les directives packages, package_data ou data_files comme employĂ©es, pour certaines, dans l’extrait de code ci-avant. 

Si les subtilitĂ©s entre les directives data_files et py_modules peuvent ĂȘtre difficiles Ă  cerner (usage de chemin vers des fichiers avec extension, usage de chemins vers des modules sans extension, certaines directives mutuellement exclusives, sous-modules et rĂ©cursivitĂ©, 
), il est possible d’utiliser en remplacement la directive include_package_data = True ainsi qu’un fichier MANIFEST.in au mĂȘme niveau que le fichier setup.py pour dĂ©clarer simplement les fichiers que l’on souhaite voir ĂȘtre embarquĂ©s dans l’archive, comme suit:

# MANIFEST.in
## 👇 On embarque toutes les donnĂ©es au format images JPG
recursive-include muffin_v_chihuahua/data/muffin *.jpg
recursive-include muffin_v_chihuahua/data/chihuahua *.jpg


## 👇 On embarque le modùle de classification
include muffin_v_chihuahua/inception_v3_weights_tf_dim_ordering_tf_kernels.h5

Et pour notre classifieur de muffins đŸȘ et de chihuahuas đŸ¶ ?

⚠  Pour rappel, cet article sert un but dĂ©monstratif: montrer ce qu’il est possible de faire en termes de packaging avec setuptools et le format Wheel.

Nous allons illustrer cela en embarquant des fichiers dans une distribution Wheel. Cela a pour effet de crĂ©er une dĂ©pendance directe (un couplage fort) entre le code Python et d’autres artĂ©facts qui gravitent autour de celui-ci : des images et un modĂšle de Machine Learning dans notre cas.

Une dépendance aussi forte est rarement souhaitable car, souvent, le code, les données et les modÚles ont des cycles de vie différents: on peut vouloir déployer une nouvelle version du code sans toucher aux images ou au modÚle (et inversement).

Pour Ă©viter ce couplage, nous aborderons en fin d’article une alternative Ă  ce packaging en isolant le code d’une part et le modĂšle de Machine Learning d’autre part, en tant que service sĂ©parĂ©.

Avec le fichier setup.py ci-avant, nous pouvons packager le code Python, le modĂšle et les images au format de distribution Wheel comme suit :

# makefile v2, remplacement du code de packaging arbitraire pour créer une distribution Wheel

.PHONY: package  ## 📩 packaging de l'application au format standard wheel via le fichier setup.py
package:
	pip install wheel && python setup.py bdist_wheel  # 👉 produit un fichier .whl dans le dossier dist/

.PHONY: install-app  ## ⚙ Installation de l'application de façon standard avec l'installateur de paquets python
install-app:
	pip install dist/muffin-v-chihuahua-1.0-py3-none-any.whl

.PHONY: run-app  ## ⚙ Pour lancer l'application, tel que dĂ©fini dans la directive entry_point du fichier setup
run-app: install-app
	muffin-v-chihuahua run-demo

On notera que setuptools permet aussi de gĂ©nĂ©rer une archive au format .zip si le format Wheel ne vous convient pas. Pour cela, on parle dans l’Ă©cosystĂšme Python de gĂ©nĂ©rer une « distribution source » (ou source distribution dans la langue de Jay-Z), qui se produirait comme suit :

.PHONY: package  ## 📩 packaging de l'application au format zip
package:
	python setup.py sdist  # produit une archive muffin-v-chihuahua.zip

Enfin, un dernier apartĂ© : la PEP427, qui dĂ©crit le format Wheel, prĂ©cise: a wheel is a ZIP-format archive with a specially formatted file name and the .whl extension. Cela signifie qu’il est possible d’interagir avec une distribution Wheel programmatiquement de la mĂȘme façon qu’avec un fichier au format .zip. Pour vĂ©rifier que le modĂšle de Machine Learning ou les images y sont bien prĂ©sentes, il est alors possible de lister le contenu d’une distribution Wheel de la façon suivante :

from zipfile import ZipFile
path_to_wheel = './dist/muffin-v-chihuahua-1.0-py3-none-any.whl'
print(ZipFile(path_to_wheel).namelist())

>>> ['muffin_v_chihuahua/__init__.py', 'muffin_v_chihuahua/__main__.py', 
# 👇 Le code source Python est prĂ©sent
'muffin_v_chihuahua/classifier.py', 
'muffin_v_chihuahua/display_predictions_with_an_embedded_model.py', 
# 👇 On retrouve bien le modùle dans la distribution Wheel
'muffin_v_chihuahua/inception_v3_weights_tf_dim_ordering_tf_kernels.h5',
'muffin_v_chihuahua/data/__init__.py', # 👇 On retrouve bien aussi les images au format .jpg 'muffin_v_chihuahua/data/chihuahua/1285578556_f4815c46f3.jpg',
'muffin_v_chihuahua/data/chihuahua/137564013_7dd48b5f1e.jpg',
'muffin_v_chihuahua/data/chihuahua/13803476_d1751cb3ec.jpg',
'muffin_v_chihuahua/data/chihuahua/7463030_7c1a554dc2.jpg',
'muffin_v_chihuahua/data/chihuahua/75541.jpg', # 
 plein d'autres fichiers ... 'muffin_v_chihuahua/data/muffin/520365653_d07fe2128e.jpg',
'muffin_v_chihuahua/data/muffin/7232767_aeca4dc59f.jpg',
'muffin_v_chihuahua/data/muffin/877626288_59961572e4.jpg']

On notera enfin que si pip permet d’installer une distribution depuis un fichier .whl comme ci-avant, il peut aussi en installer une via une URL pointant vers un projet versionnĂ© avec un systĂšme de gestion de version (VCS) supportĂ©.

Il devient alors possible de distribuer son application Python auprĂšs d’utilisateurs qui peuvent l’installer avec pip en pointant vers une URL d’un dĂ©pĂŽt de code, par exemple $> pip install git+<url HTTPS d’un repo github>, plutĂŽt que de leur demander de cloner un repository quelque part sur leur poste. Le module pip se base sur la prĂ©sence d’un fichier setup.py qui doit impĂ©rativement se trouver Ă  la racine du projet pour parvenir Ă  cela.

📩  Pour aller plus loin sur l’histoire du packaging d’applications en Python et l’Ă©cosystĂšme d’outils citĂ©s dans cette partie (pip, index PyPI, 
), la confĂ©rence de Dustin Ingram lors de la PyCon de Cleveland en 2018: Inside the cheesechop – How Python Packaging works aborde ces sujets đŸŽ„

🐳 Le packaging en image Docker, pour aller plus loin

MĂȘme s’il est prĂ©fĂ©rable de suivre des pratiques telles que le DevOps pour favoriser la collaboration entre dĂ©veloppeurs et ops dans une organisation, il peut arriver que le dĂ©ploiement d’une application (de Machine Learning ou non) soit rĂ©alisĂ© par des ops qui ne l’ont pas conçue. 

En dĂ©couvrant le code de l’application, ces personnes dĂ©couvriront aussi qu’il existe des prĂ©-requis, parfois implicites, au bon fonctionnement de celle-ci qu’il faut installer sur une infrastructure cible (sur leur propre poste pour la tester ou bien dans un environnement de production, par exemple):

  • des dĂ©pendances systĂšmes, spĂ©cifiques Ă  l’OS, 
    • comme des drivers GPU ou des paquets Ă  installer avec apt ou yum,
  • une version de Python bien prĂ©cise,
    • ex: Python 2 Ă  partir de la version 2.6 ou python 3 Ă  partir de la version 3.6.5,
  • une liste de dĂ©pendances Python Ă  installer avec pip,
    • pouvant provenir d’un index publique (comme PyPi) ou privĂ©,
  • de la configuration, 
    • sous la forme de fichiers contenant des variables ou pour le logging,
    • ou sous la forme de variables d’environnement,
  • des donnĂ©es et/ou un Ă©ventuel modĂšle de Machine Learning, qui devront ĂȘtre positionnĂ©s Ă  un endroit spĂ©cifique du systĂšme relativement aux sources Python.

Si le format Wheel permet d’automatiser la rĂ©alisation de quelques-unes de ces tĂąches, comme tĂ©lĂ©charger puis embarquer les dĂ©pendances Python de l’application, il ne permet pas aujourd’hui d’embarquer une version de Python ou d’adresser le sujet des dĂ©pendances systĂšmes, par exemple. Il existe bien une directive python_requires qui permet de spĂ©cifier une version de Python dans le fichier setup.py, mais elle ne sert qu’Ă  bloquer l’installation si la version de Python de l’utilisateur n’est pas compatible.

Il y a une dizaine d’annĂ©es, ce soucis se serait rĂ©glĂ© par la rĂ©daction d’une procĂ©dure, pourquoi pas au format Word, pour dĂ©crire toutes les actions manuelles Ă  rĂ©aliser sur notre systĂšme pour livrer une version fonctionnelle de notre application, comme nous le rapporte Arnaud Mazin dans le podcast CafĂ© Craft. Des outils comme Docker existent aujourd’hui pour automatiser la rĂ©alisation de ces prĂ©-requis et limiter les actions manuelles.

Si vous souhaitez vous familiariser avec Docker, voici un article pouvant servir d’introduction Ă  ce vaste sujet qu’est la conteneurisation.

Et pour notre classifieur de muffins đŸȘ et de chihuahua đŸ¶ ?

Afin d’aider Ă  la lecture du dockerfile qui va suivre, voici un aperçu de l’arborescence de fichiers du code que nous allons packager. Il est aussi disponible sur github.

.
├── MANIFEST.in
├── README.md
├── dist  # gĂ©nĂ©rĂ© par la commande python setup.py bdist_wheel
│   └── muffin_v_chihuahua_with_embedded_model-1.0-py3-none-any.whl
├── dockerfile
├── makefile
├── muffin_v_chihuahua
│   ├── __init__.py
│   ├── __main__.py
│   ├── classifier.py  # classif. des images de muffins et de chihuahuas
│   ├── data
│   │   ├── __init__.py
│   │   ├── chihuahua  # contient des images de chihuahuas .jpeg
│   │   └── muffin     # contient des images de muffins .jpeg .jpeg
│   ├── display_predictions_with_an_embedded_model.py
│   └── inception_v3_weights_tf_dim_ordering_tf_kernels.h5
├── setup.cfg
└── setup.py

En python, un dockerfile permettant de dĂ©crire le packaging d’une application de Machine Learning serait le suivant :

FROM python:3.8.0-slim  # 👈 dĂ©finition de la version de Python nĂ©cessaire, tirĂ©e du dockerhub


# 👇 Installation de dĂ©pendances sur le systĂšme
RUN apt-get update \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# 👇 Packaging du modùle de machine learning
COPY inception_v3_weights_tf_dim_ordering_tf_kernels.h5 /app/

# 👇 Packaging des sources python
COPY display_predictions_with_embdded_model.py \
    pretrain_model.py \
    requirements.txt /app/

# 👇 Packaging des donnĂ©es
COPY data/ /app/data

# 👇 Installation de dĂ©pendances Python via pip
WORKDIR /app
RUN pip install --user -U pip && pip install -r requirements.txt
# 👇 Exposition de l'application avec Streamlit via le port 8080
EXPOSE 8080
CMD streamlit run display_predictions.py --server.port 8080

Si ce dockerfile permet de packager l’application de Machine Learning en y copiant et exĂ©cutant les sources mĂȘmes via les instructions COPY et CMD, il est aussi possible de se servir du format wheel pour Ă©viter cette situation prĂ©sentĂ©e en premiĂšre partie d’article :

FROM python:3.8.0-slim
...

# 👇 Packaging des sources python
COPY display_predictions.py classifier.py /app/

# 👇 Installation de dĂ©pendances Python via une wheel copiĂ©e depuis le poste local
WORKDIR /app
COPY /app/dist/ /app/dist/  # 👈 la wheel est prĂ©alablement gĂ©nĂ©rĂ©e en local, dans un dossier dist/
RUN pip install dist/muffin_v_chihuahua-1.0-py3-none-any.whl
...

Au lieu de copier la distribution Wheel depuis le poste local, il peut ĂȘtre intĂ©ressant de gĂ©nĂ©rer celle-ci au sein du dockerfile afin de le rendre plus autoportant et d’automatiser la gĂ©nĂ©ration d’artefacts.

FROM python:3.8.0-slim  # 👈 dĂ©finition de la version de Python nĂ©cessaire
...

# 👇 Packaging des sources python
COPY display_predictions_with_embedded_model.py classifier.py \
    requirements.txt setup.py /app/

# 👇 GĂ©nĂ©ration et installation de dĂ©pendances Python via wheel
WORKDIR /app
RUN pip install --user -U pip && python setup.py bdist_wheel
RUN pip install dist/muffin_v_chihuahua-1.0-py3-none-any.whl
...

Toutefois, ce packaging est bien complexe. Le rĂ©sultat de la construction est une image docker dans laquelle vont se trouver les sources Python en double: celles copiĂ©es dans l’image via l’instruction COPY et celles produites en installant la distribution wheel fraĂźchement produite.

Pour remĂ©dier Ă  cela, docker propose la fonctionnalitĂ© multi-stage build qui permet de produire une image docker Ă  publier dans laquelle on peut venir positionner des artĂ©facts issus d’images intermĂ©diaires (appelĂ©es gĂ©nĂ©ralement dans les exemples de la documentation de Docker: builder images).

Cette fonctionnalitĂ© propose d’apporter plus de lisibilitĂ© au dockerfile, en le dĂ©coupant en Ă©tapes, et de produire une image docker plus petite en n’embarquant que ce qui sera nĂ©cessaire en production.

Cela donnerait dans notre cas un dockerfile que nous pourrions séparer en plusieurs étapes intermédiaires: une étape de génération pour le modÚle de machine learning, une pour les données, et une derniÚre pour la distribution Wheel :

FROM python:3.8.0-slim as model-builder # 👈 Ă©tape de production du modĂšle : il peut ĂȘtre gĂ©nĂ©rĂ© ici ou tĂ©lĂ©charger depuis un dĂ©pĂŽt de modĂšles
... 
# 👇 GĂ©nĂ©ration des data
FROM python:3.8.0-slim as data-builder
... 
# 👇 GĂ©nĂ©ration de la Wheel
FROM python:3.8.0-slim as app-builder
... 
# 👇 GĂ©nĂ©ration de l'image rĂ©sultante Ă  publier
FROM python:3.8.0-slim as app

 

En complĂ©tant les trous, nous obtenons enfin : 

FROM python:3.8.0-slim as model-builder # 👈 Ă©tape de gĂ©nĂ©ration du modĂšle
RUN apt-get update \
    && apt-get install -y wget --no-install-recommends \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*
# 👇 On pourrait entraĂźner un modĂšle ici, mais on va plutĂŽt tĂ©lĂ©charger un modĂšle prĂ©-entraĂźnĂ©.
RUN wget https://github.com/fchollet/deep-learning-models/releases/download/v0.5/inception_v3_weights_tf_dim_ordering_tf_kernels.h5

# 👇 GĂ©nĂ©ration des data
FROM python:3.8.0-slim as data-builder
COPY Makefile /app/
WORKDIR /app/
## 👇 TĂ©lĂ©chargement des donnĂ©es dans /app/data/ (via HTTP)
RUN make chihuahua-dataset && make muffin-dataset

# 👇 GĂ©nĂ©ration de la distribution Wheel
## pour cela il faut: les sources python, le modÚle produit ci-avant, et les données (images de muffins et de chihuahuas)
FROM python:3.8.0-slim as app-builder
RUN apt-get update \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*
COPY muffin_v_chihuahua/ /app/muffin_v_chihuahua/
COPY MANIFEST.in setup.cfg setup.py /app/
## 👇 RĂ©cupĂ©ration du modĂšle, qui est une dĂ©pendance de la distribution Wheel, via l'option --from
COPY --from=model-builder ./inception_v3_weights_tf_dim_ordering_tf_kernels.h5 /app
## 👇 RĂ©cupĂ©ration des donnĂ©es qui sont une dĂ©pendance de la Wheel via --from
COPY --from=data-builder /app/data/ /app/data/
WORKDIR /app
## 👇 GĂ©nĂ©ration de la wheel avec le nĂ©cessaire: sources, modĂšle et data
RUN pip install --user -U pip \
    && python setup.py bdist_wheel

# 👇 GĂ©nĂ©ration de l'image rĂ©sultante Ă  publier
FROM python:3.8.0-slim as app
COPY --from=app-builder /root/.local /root/.local
COPY --from=app-builder /app/dist/ /app/dist/
WORKDIR /app
## 👇 Installation de la distribution Wheel qui embarque les sources, le modùle et la data
RUN pip install dist/muffin_v_chihuahua_with_embedded_model-1.0-py3-none-any.whl
ENV PATH=/root/.local/bin:$PATH
EXPOSE 8080
CMD ["muffin-v-chihuahua-with-embedded-model", "run-demo", "--server.port", "8080"]

Pouir rappel, ce dockerfile est consultable dans ce repo Github.

⚠  Pour rappel, cet article sert un but dĂ©monstratif: montrer ce qu’il est possible de faire en termes de packaging avec setuptools, le format Wheel et Docker.

Pour un usage de production, il conviendra de réaliser ces différentes actions de packaging dans le dockerfile avec un utilisateur non-root

Voici un panorama de quelques outils que vous pouvez utiliser pour analyser vos dockerfiles ou vos images docker pour en améliorer la qualité ou détecter des vulnérabilités de sécurité.

Le packaging peut alors se réaliser de la façon suivante :

.PHONY: package  ## 📩 packaging de l'application au format docker. "." dĂ©signe le dossier courant contenant le dockerfile
package:
	docker build -t muffin-v-chihuihua:v1 .

.PHONY: run-app  ## ⚙ Pour lancer l'application une fois l'image docker buildĂ©e
run-app:	
      docker run muffin-v-chihuihua:v1
📩  Quelques ressources supplĂ©mentaires sur le packaging avec Docker pour Python : 
👉 Exemple de packaging en Python sur le blog officiel de docker
👉 De bonnes pratiques lors de la rĂ©daction d’un dockerfile
👉 Un panorama d’outils pour scanner un dockerfile (lint, scan de sĂ©curitĂ©, 
)

Les stratégies de déploiement impactent la façon de packager

Nous avons vu jusqu’Ă  maintenant une unique façon de packager nos applications de Machine Learning avec Docker, mais il en existe plusieurs.

En effet, l’article Continuous Delivery 4 Machine Learning (CD4ML) dĂ©crit l’approche de conteneurisation que nous avons Ă©tudiĂ©e jusqu’Ă  maintenant comme une approche de packaging intĂ©ressante pour envisager un dĂ©ploiement d’application avec modĂšle embarquĂ© : le modĂšle de Machine Learning est traitĂ© comme une dĂ©pendance de l’application qui consomme ses prĂ©dictions, il est construit et packagĂ© avec celle-ci.

Packaging pour un dĂ©ploiement avec "modĂšle embarquĂ©" (embedded model) avec docker, la mĂȘme image docker produit des prĂ©dictions et les affiche.
Packaging pour un dĂ©ploiement avec « modĂšle embarqué » (embedded model) avec docker, la mĂȘme image docker produit des prĂ©dictions et les affiche.

Nous sommes naturellement arrivés à ce format de packaging dans le déroulé de cet article, mais il est possible de réaliser le packaging différemment.

Pour illustrer cela, voici une façon de packager, avec Docker, au service d’une autre stratĂ©gie de dĂ©ploiement: la stratĂ©gie du modĂšle utilisĂ© en tant que service (model deployed as a separate service)

Ici, deux images docker sont créés puis exécutées:

  • la premiĂšre a pour objectif d’afficher des prĂ©dictions afin que des utilisateurs les consultent,
  • La seconde image peut prendre la forme d’un service pouvant ĂȘtre interrogĂ© par la premiĂšre pour lui fournir des prĂ©dictions.
Packaging pour dĂ©ploiement avec « modĂšle sĂ©parĂ© en tant que service » avec Docker. L’image Docker de gauche affiche uniquement des prĂ©dictions sur un frontend, l’image de droite est un service web HTTP backend rĂ©alisant une infĂ©rence quand il est requĂȘtĂ©.

Ce dernier mode de packaging peut demander un certain effort dans la conception d’une application pour isoler le contenu relatif au Machine Learning, mais cet investissement pour obtenir une application avec un faible couplage ouvre la porte Ă  des stratĂ©gies de dĂ©ploiement intĂ©ressantes : lorsque le modĂšle de Machine Learning doit Ă©voluer, il est possible de mettre Ă  jour uniquement celui-ci, sans impacter le code de l’application (et vice-versa).

Et pour notre classifieur de muffins đŸȘ et de chihuahuas đŸ¶ ?

Cette approche d’isolation du modĂšle en tant que service auquel nous pourrions demander des prĂ©dictions de classification de muffins ou de chihuahuas peut s’illustrer avec les deux dockerfiles qui suivent :

# Fichier app-dockerfile pour packager le code de présentation des prédictions
FROM python:3.8.1-slim as app-builder
RUN apt-get update && apt-get clean && rm -rf /var/lib/apt/lists/*
COPY muffin_v_chihuahua/ /app/muffin_v_chihuahua/
COPY MANIFEST.in setup.cfg setup.py /app/
WORKDIR /app
RUN pip install --user -U pip \
   && python setup.py bdist_wheel

FROM python:3.8.1-slim as app
COPY --from=app-builder /root/.local /root/.local
COPY --from=app-builder /app/dist/ /app/dist/
COPY muffin_v_chihuahua/data/ /app/data
WORKDIR /app
RUN pip install --user -U pip \
   && pip install dist/muffin_v_chihuahua_frontend-1.0-py3-none-any.whl
ENV PATH=/root/.local/bin:$PATH
ENV INFERENCE_HOST localhost
EXPOSE 8090
CMD ["muffin-v-chihuahua-model-as-a-service", "run-demo", "--server.port", "8090"]
# Fichier ml-web-service-dockerfile pour packager un service web capable de fournir des prédictions
FROM python:3.8.1-slim as model-builder
RUN apt-get update \
   && apt-get install --no-install-recommends -y wget \
   && apt-get clean \
   && rm -rf /var/lib/apt/lists/*
RUN wget https://github.com/fchollet/deep-learning-models/releases/download/v0.5/inception_v3_weights_tf_dim_ordering_tf_kernels.h5

FROM python:3.8.1-slim as web-service-builder
RUN apt-get update \
   && apt-get clean \
   && rm -rf /var/lib/apt/lists/*
COPY muffin_v_chihuahua_ml_service/ml_web_service.py setup.py setup.cfg \
    muffin_v_chihuahua_ml_service/classifier.py /app/
WORKDIR /app
RUN pip install --user -U pip && pip install --user --no-cache-dir .

FROM python:3.8.1-slim as ml-web-service
COPY --from=web-service-builder /root/.local /root/.local
COPY --from=web-service-builder /app /app
COPY --from=model-builder ./inception_v3_weights_tf_dim_ordering_tf_kernels.h5 /app
WORKDIR /app
ENV PATH=/root/.local/bin:$PATH
EXPOSE 8000
CMD ["uvicorn", "ml_web_service:app", "--host", "0.0.0.0", "--port", "8000"]

Pour rappel, ces deux dockerfiles sont consultables dans cet autre repo Github.

En exécutant les deux images produites avec ces dockerfiles en local,

  • on accĂšde sur http://localhost:8090 Ă  l’image « application frontend » qui a pour but d’afficher des prĂ©dictions afin de rĂ©pondre Ă  la question: « est-ce une image de muffin ou de chihuahua ? ».
Application (frontend Streamlit) qui affiche des images avec une probabilité sur la présence d'un muffin ou d'un chihuahua dans celles-ci.
Application (frontend Streamlit) qui affiche des images avec une probabilitĂ© sur la prĂ©sence d’un muffin ou d’un chihuahua dans celles-ci.
  • et on accĂšde sur http://localhost:8080 au service web, cĂŽtĂ© backend. 
    • Notamment, un healthcheck est exposĂ© sur la route / afin de renvoyer le code HTTP 200 OK pour s’assurer que le service est actif, 
    • et ce service fournit des prĂ©dictions avec un modĂšle de Deep Learning quand il reçoit une image en HTTP POST sur la route /predict.
Envoi dans Postman d'une image de chihuahua dans le corps de la requĂȘte HTTP POST sur la route /predict au service web de machine learning (backend FastAPI).
Envoi dans Postman d’une image de chihuahua dans le corps de la requĂȘte HTTP POST sur la route /predict au service web de machine learning (backend FastAPI).

Conclusion

Dans cet article, nous avons vu plusieurs façons de packager une application de Machine Learning. Au cours de la derniĂšre dĂ©cennie, des standards ont Ă©mergĂ© dans l’Ă©cosystĂšme Python afin d’Ă©viter de partager et exĂ©cuter nos applications en se servant des sources mĂȘmes, via l’usage de setuptools et du format Wheel. Plus largement, Docker est devenu un incontournable du packaging, quel que soit le langage. Ces formats sont adaptĂ©s pour le Machine Learning: pour packager un modĂšle ou des donnĂ©es par exemple grĂące Ă  leurs philosophies de conception : build once, run anywhere ou encore batteries included. 

Si cet article se voulait dĂ©monstratif des capacitĂ©s de Wheel et Docker pour packager du code de Machine Learning, on veillera toutefois Ă  Ă©viter de mettre tous nos oeufs dans le mĂȘme panier : packager du code, un modĂšle et des donnĂ©es dans un unique artĂ©fact peut nous amener a crĂ©er des adhĂ©rences non-souhaitĂ©es qui peuvent devenir difficiles Ă  rompre avec le temps. Aussi, il est gĂ©nĂ©ralement de bon ton de suivre les grands principes de conception logicielle que sont separation of concerns ou encore les 12-factors app, notamment le troisiĂšme facteur qui encourage la stricte sĂ©paration de la configuration, que nous n’avons pas abordĂ© dans cet article, et du code.

L’usage mĂȘme de ces formats (Docker ou Wheel) peut varier en fonction des usages que nous souhaitons en faire, comme nous avons pu le voir au travers des stratĂ©gies de dĂ©ploiement de modĂšles de Machine Learning dĂ©taillĂ©es dans l’article CD4ML (modĂšle embarquĂ© dans l’application, ou isolĂ© pour ĂȘtre consultĂ© en tant que service). Dans l’absolu, aucune stratĂ©gie n’est vraiment meilleure qu’une autre, mais choisir la plus adaptĂ©e dans son contexte demande de se projeter sur l’usage que nos utilisateurs feront de notre application de Machine Learning ainsi qu’une rĂ©flexion sur la façon d’intĂ©grer cette application de maniĂšre pĂ©renne dans son SI.

Pour rappel, du code permettant de jouer ces différentes approches de packaging est disponible sur Github : dans ce repo-ci (modÚle embarqué) et dans cet autre repo (modÚle isolé en tant que service), et des ressources ont été disposées en fin de chaque partie de cet article si vous souhaitez aller plus loin.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.


Ce formulaire est protégé par Google Recaptcha