Comment déployer des modèles TensorFlow en production à l'aide de TF Serving

introduction

La production de modèles Machine Learning (ML) est devenue un sujet récurrent et populaire. De nombreuses entreprises et structures proposent différentes solutions visant à résoudre ce problème.

Pour répondre à cette préoccupation, Google a publié TensorFlow (TF) Serving dans l'espoir de résoudre le problème du déploiement de modèles ML en production.

Cet article propose un tutoriel pratique sur la desserte d'un réseau de segmentation sémantique par convolution pré-formé. À la fin de cet article, vous pourrez utiliser TF Serving pour déployer et adresser des demandes à un Deep CNN formé à TF. De plus, je vous présenterai un aperçu des principaux blocs de la gestion de la fonction TF, ainsi que ses API et son fonctionnement.

Vous remarquerez tout de suite que très peu de code est nécessaire pour servir un modèle TF. Si vous souhaitez suivre le didacticiel et exécuter l’exemple sur votre ordinateur, suivez-le tel quel. Toutefois, si vous souhaitez en savoir plus sur le service TensorFlow, vous pouvez vous concentrer sur les deux premières sections.

Cet article souligne certains des travaux que nous réalisons ici au sein du groupe Daitan.

TensorFlow Serving Libraries - Vue d'ensemble

Prenons un peu de temps pour comprendre comment TF Serving gère le cycle de vie complet des modèles ML. Ici, nous allons passer en revue (à un niveau élevé) chacun des blocs de construction principaux de la portion TF. Le but de cette section est de fournir une introduction logicielle aux API de serveur TF. Pour un aperçu détaillé, rendez-vous sur la page de documentation TF Serving.

TensorFlow Serving est composé de quelques abstractions. Ces abstractions implémentent des API pour différentes tâches. Les plus importants sont Servable, Loader, Source et Manager. Voyons comment ils interagissent.

En résumé, le cycle de vie des serveurs commence lorsque TF Serving identifie un modèle sur disque. Le composant source s'en occupe. Il est chargé d'identifier de nouveaux modèles à charger. En pratique, il surveille le système de fichiers pour identifier le moment où une nouvelle version du modèle arrive sur le disque. Lorsqu'il voit une nouvelle version, il crée un chargeur pour cette version spécifique du modèle.

En résumé, le Loader sait presque tout sur le modèle. Il explique également comment le charger et estimer les ressources requises par le modèle, telles que la mémoire vive demandée et la mémoire du processeur graphique. Le chargeur a un pointeur sur le modèle sur disque avec toutes les métadonnées nécessaires pour le charger. Mais il y a un problème ici: le chargeur n'est pas autorisé à charger le modèle pour l'instant.

Après avoir créé le chargeur, la source l'envoie au gestionnaire sous forme de version Aspired.

À la réception de la version Aspired du modèle, le responsable procède au processus de desserte. Ici, il y a deux possibilités. La première est que la première version du modèle est poussée pour le déploiement. Dans cette situation, le responsable s'assurera que les ressources requises sont disponibles. Une fois qu’ils le sont, le gestionnaire donne au chargeur l’autorisation de charger le modèle.

La seconde est que nous poussons une nouvelle version d'un modèle existant. Dans ce cas, le responsable doit consulter le plug-in Version Policy avant d'aller plus loin. La stratégie de version détermine le déroulement du processus de chargement d'une nouvelle version du modèle.

Plus précisément, lors du chargement d'une nouvelle version d'un modèle, vous pouvez choisir entre la préservation (1) de la disponibilité ou (2) des ressources. Dans le premier cas, nous souhaitons nous assurer que notre système est toujours disponible pour les demandes des clients entrants. Nous savons que le gestionnaire permet au chargeur d’instancier le nouveau graphique avec les nouvelles pondérations.

À ce stade, nous avons deux versions de modèle chargées en même temps. Mais le gestionnaire ne décharge l'ancienne version qu'une fois le chargement terminé et vous pouvez basculer d'un modèle à un autre en toute sécurité.

D'autre part, si nous voulons économiser des ressources en ne disposant pas de la mémoire tampon supplémentaire (pour la nouvelle version), nous pouvons choisir de conserver les ressources. Il peut être utile pour les modèles très lourds d’avoir un léger manque de disponibilité en échange d’une économie de mémoire.

À la fin, lorsqu'un client demande un descripteur pour le modèle, le gestionnaire renvoie un descripteur au Servable.

Avec cet aperçu, nous allons plonger dans une application du monde réel. Dans les sections suivantes, nous décrivons comment desservir un réseau de neurones convolutionnels (CNN) à l'aide du service TF.

Exportation d'un modèle à servir

La première étape pour servir un modèle ML intégré dans TensorFlow est de s’assurer qu’il est au bon format. Pour ce faire, TensorFlow fournit la classe SavedModel.

SavedModel est le format de sérialisation universel pour les modèles TensorFlow. Si vous connaissez bien TF, vous avez probablement déjà utilisé TensorFlow Saver pour conserver les variables de votre modèle.

TensorFlow Saver fournit des fonctionnalités pour enregistrer / restaurer les fichiers de point de contrôle du modèle sur / à partir du disque. En fait, SavedModel encapsule TensorFlow Saver et il s’agit du moyen standard d’exporter des modèles TF pour les servir.

L'objet SavedModel a quelques fonctionnalités intéressantes.

Tout d'abord, il vous permet d'enregistrer plusieurs méta-graphes dans un seul objet SavedModel. En d’autres termes, cela nous permet d’avoir différents graphiques pour différentes tâches.

Par exemple, supposons que vous veniez de terminer la formation de votre modèle. Dans la plupart des situations, pour effectuer une inférence, votre graphique n’a pas besoin d’opérations spécifiques à la formation. Ces opérations peuvent inclure les variables de l’optimiseur, les tenseurs de planification de la fréquence d’apprentissage, les opérations de prétraitement supplémentaires, etc.

De plus, vous pouvez vouloir servir une version quantifiée d'un graphique pour un déploiement mobile.

Dans ce contexte, SavedModel vous permet de sauvegarder des graphiques avec différentes configurations. Dans notre exemple, nous aurions trois graphiques différents avec des balises correspondantes telles que «formation», «inférence» et «mobile». De plus, ces trois graphiques partageraient le même ensemble de variables, ce qui met l’accent sur l’efficacité de la mémoire.

Il n'y a pas si longtemps, lorsque nous voulions déployer des modèles TF sur des appareils mobiles, nous devions connaître le nom des tenseurs d'entrée et de sortie pour alimenter et extraire les données du modèle. Cette nécessité a obligé les programmeurs à rechercher le tenseur dont ils avaient besoin parmi tous les tenseurs du graphe. Si les tenseurs n'étaient pas nommés correctement, la tâche pourrait être très fastidieuse.

Pour faciliter les choses, SavedModel offre un support pour SignatureDefs. En résumé, SignatureDefs définit la signature d'un calcul pris en charge par TensorFlow. Il détermine les bons tenseurs d’entrée et de sortie pour un graphe de calcul. En termes simples, avec ces signatures, vous pouvez spécifier les nœuds exacts à utiliser pour l’entrée et la sortie.

Pour utiliser ses API de service intégrées, TF Serving requiert que les modèles incluent un ou plusieurs SignatureDef.

Pour créer de telles signatures, nous devons fournir des définitions pour les entrées, les sorties et le nom de la méthode souhaitée. Les entrées et les sorties représentent un mappage d'une chaîne à des objets TensorInfo (plus d'informations sur ce dernier). Ici, nous définissons les tenseurs par défaut pour l’alimentation et la réception de données depuis et vers un graphique. Le paramètre method_name cible l'une des API de service de haut niveau TF.

Il existe actuellement trois API de service: Classification, Prédire et Régression. Chaque définition de signature correspond à une API RPC spécifique. Classification SegnatureDef est utilisé pour l'API Classify RPC. Predict SegnatureDef est utilisé pour l'API Predict RPC, et ainsi de suite.

Pour la signature de classification, il doit exister un tenseur d’entrées (pour recevoir des données) et au moins un des deux tenseurs de sortie possibles: les classes et / ou les scores. La régression SignatureDef nécessite exactement un tenseur pour l’entrée et un autre pour la sortie. Enfin, la signature Predict permet un nombre dynamique de tenseurs d’entrée et de sortie.

De plus, SavedModel prend en charge le stockage des actifs dans les cas où l'initialisation ops dépend de fichiers externes. En outre, il dispose de mécanismes pour effacer les périphériques avant de créer le modèle enregistré.

Voyons maintenant comment pouvons-nous le faire en pratique.

Mise en place de l'environnement

Avant de commencer, clonez cette implémentation TensorFlow DeepLab-v3 à partir de Github.

DeepLab est le meilleur réseau de segmentation sémantique ConvNet de Google. Fondamentalement, le réseau prend une image en entrée et génère une image de type masque qui sépare certains objets de l'arrière-plan.

Cette version a été formée sur le jeu de données de segmentation Pascal VOC. Ainsi, il peut segmenter et reconnaître jusqu'à 20 classes. Si vous souhaitez en savoir plus sur la segmentation sémantique et DeepLab-v3, consultez Plongée dans les réseaux de segmentation sémantique par convolution profonde et Deeplab_V3.

Tous les fichiers liés au service résident dans: ./deeplab_v3/serving/. Vous y trouverez deux fichiers importants: deeplab_saved_model.py et deeplab_client.ipynb

Avant d’aller plus loin, assurez-vous de télécharger le modèle pré-entraîné Deeplab-v3. Rendez-vous au référentiel GitHub ci-dessus, cliquez sur le lien des points de contrôle et téléchargez le dossier nommé 16645 /.

À la fin, vous devriez avoir un dossier nommé tboard_logs / avec le dossier 16645 / placé à l'intérieur.

Nous devons maintenant créer deux environnements virtuels Python. Un pour Python 3 et un autre pour Python 2. Pour chaque env, assurez-vous d'installer les dépendances nécessaires. Vous pouvez les trouver dans les fichiers portion_requirements.txt et client_requirements.txt.

Nous avons besoin de deux environnements Python car notre modèle, DeepLab-v3, a été développé sous Python 3. Cependant, l'API TensorFlow Serving Python n'est publiée que pour Python 2. Par conséquent, pour exporter le modèle et exécuter le service TF, nous utilisons l'environnement Python 3. . Pour exécuter le code client à l'aide de l'API python TF Serving, nous utilisons le package PIP (disponible uniquement pour Python 2).

Notez que vous pouvez renoncer à l’env Python 2 en utilisant les API de service de bazel. Reportez-vous à l’installation de service TF pour plus de détails.

Une fois cette étape terminée, commençons par ce qui compte vraiment.

Comment faire

Pour utiliser SavedModel, TensorFlow fournit une classe d’utilitaires de haut niveau, facile à utiliser, appelée SavedModelBuilder. La classe SavedModelBuilder fournit des fonctionnalités pour enregistrer plusieurs méta-graphes, variables associées et actifs.

Examinons un exemple courant d’exportation d’un modèle CNN de segmentation approfondie pour la servir.

Comme indiqué ci-dessus, pour exporter le modèle, nous utilisons la classe SavedModelBuilder. Il générera un fichier tampon de protocole SavedModel ainsi que les variables et les actifs du modèle (si nécessaire).

Disséquons le code.

SavedModelBuilder reçoit (en entrée) le répertoire dans lequel enregistrer les données du modèle. Ici, la variable export_path est la concaténation de export_path_base et de model_version. Par conséquent, différentes versions de modèle seront enregistrées dans des répertoires distincts du dossier export_path_base.

Disons que nous avons une version de base de notre modèle en production, mais nous souhaitons en déployer une nouvelle version. Nous avons amélioré la précision de notre modèle et souhaitons proposer cette nouvelle version à nos clients.

Pour exporter une version différente du même graphique, il suffit de définir FLAGS.model_version sur une valeur entière supérieure. Ensuite, un dossier différent (contenant la nouvelle version de notre modèle) sera créé dans le dossier export_path_base.

Maintenant, nous devons spécifier les tenseurs d’entrée et de sortie de notre modèle. Pour ce faire, nous utilisons SignatureDefs. Les signatures définissent le type de modèle que nous voulons exporter. Il fournit un mappage des chaînes (noms de tenseurs logiques) aux objets TensorInfo. L'idée est que, au lieu de référencer les noms de tenseurs réels pour les entrées / sorties, les clients peuvent faire référence aux noms logiques définis par les signatures.

Pour servir une segmentation sémantique CNN, nous allons créer une signature de prédiction. Notez que la fonction build_signature_def () prend le mappage des tenseurs d’entrée et de sortie ainsi que de l’API souhaitée.

SignatureDef nécessite la spécification de: entrées, sorties et nom de la méthode. Notez que nous attendons trois valeurs pour les entrées: une image et deux autres tenseurs spécifiant ses dimensions (hauteur et largeur). Pour les sorties, nous n'avons défini qu'un résultat: le masque de sortie de segmentation.

Notez que les chaînes ‘image’, ‘hauteur’, ‘largeur’ et ‘segmentation_map’ ne sont pas des tenseurs. Ce sont plutôt des noms logiques qui font référence aux tenseurs réels input_tensor, image_height_tensor et image_width_tensor. Ainsi, ils peuvent être n'importe quelle chaîne unique que vous aimez.

De plus, les mappages dans SignatureDefs concernent les objets TensorInfo protobuf, et non les tenseurs réels. Pour créer des objets TensorInfo, nous utilisons la fonction utilitaire suivante: tf.saved_model.utils.build_tensor_info (tensor).

C'est ça. Nous appelons maintenant la fonction add_meta_graph_and_variables () pour générer l'objet tampon de protocole SavedModel. Ensuite, nous exécutons la méthode save () et il restera un instantané de notre modèle sur le disque contenant les variables et les actifs du modèle.

Nous pouvons maintenant exécuter deeplab_saved_model.py pour exporter notre modèle.

Si tout se passe bien, vous verrez le dossier ./serving/versions/1. Notez que le «1» représente la version actuelle du modèle. Dans chaque sous-répertoire de version, vous verrez les fichiers suivants:

  • saved_model.pb ou saved_model.pbtxt. Ceci est le fichier SavedModel sérialisé. Il comprend une ou plusieurs définitions de graphique du modèle, ainsi que les définitions de signature.
  • Variables Ce dossier contient les variables sérialisées des graphiques.

Nous sommes maintenant prêts à lancer notre serveur de modèles. Pour cela, lancez:

$ tensorflow_model_server --port = 9000 - nom_modèle = deeplab --model_base_path = 

Model_base_path fait référence à l'endroit où le modèle exporté a été enregistré. De plus, nous ne spécifions pas le dossier de version dans le chemin. Le contrôle de versioning de modèle est géré par TF Serving.

Génération de requêtes client

Le code client est très simple. Jetez-y un coup d'oeil dans: deeplab_client.ipynb.

Premièrement, nous lisons l'image que nous souhaitons envoyer au serveur et la convertissons au bon format.

Ensuite, nous créons un stub gRPC. Le stub nous permet d’appeler les méthodes du serveur distant. Pour ce faire, nous instancions la classe beta_create_PredictionService_stub du module prediction_service_pb2. À ce stade, le stub contient la logique nécessaire pour appeler des procédures distantes (à partir du serveur) comme si elles étaient locales.

Maintenant, nous devons créer et définir l'objet de requête. Puisque notre serveur implémente l'API TensorFlow Predict, nous devons analyser une demande Predict. Pour émettre une demande Predict, nous instancions d'abord la classe PredictRequest à partir du module predict_pb2. Nous devons également spécifier les paramètres modèle_spec.name et modèle_spec.signature_nom. Le nom param est l’argument ‘model_name’ que nous avons défini lors du lancement du serveur. Et le nom_signature fait référence au nom logique attribué au paramètre signature_def_map () de la routine add_meta_graph ().

Ensuite, nous devons fournir les données d’entrée telles que définies dans la signature du serveur. N'oubliez pas que, sur le serveur, nous avons défini une API Predict pour qu'elle attende une image ainsi que deux scalaires (la hauteur et la largeur de l'image). Pour alimenter les données d'entrée dans l'objet de requête, TensorFlow fournit l'utilitaire tf.make_tensor_proto (). Cette méthode crée un objet TensorProto à partir d'un objet numpy / Python. Nous pouvons l'utiliser pour alimenter l'image et ses dimensions avec l'objet de la requête.

On dirait que nous sommes prêts à appeler le serveur. Pour ce faire, nous appelons la méthode Predict () (à l'aide du stub) et passons l'objet de la requête en tant qu'argument.

Pour les demandes qui renvoient une seule réponse, gRPC prend en charge les appels: synchrones et asynchrones. Ainsi, si vous souhaitez effectuer certains travaux pendant le traitement de la demande, vous pouvez appeler Predict.future () au lieu de Predict ().

Maintenant, nous pouvons aller chercher et profiter des résultats.

J'espère que vous avez aimé cet article. Merci d'avoir lu!

Si vous voulez plus, consultez: