Module 10 — SavedModel et mise en service avec TensorFlow Serving
Un modèle qui n'est pas servi n'a produit aucune valeur. Ce module couvre le dernier segment : sortir le modèle du carnet et le rendre interrogeable, sans reproduire l'erreur qui casse la moitié des mises en production.
Deux formats, deux usages
Keras propose deux formats de sortie, et les confondre coûte du temps.
| Format | Commande | Contient | Usage |
|---|---|---|---|
.keras | modele.save("m.keras") | architecture, poids, état de l'optimiseur | reprendre l'entraînement |
| SavedModel | modele.export("m/") | graphe optimisé et signatures | servir en production |
Le format .keras est une archive destinée à Python : elle permet de recharger le modèle et de continuer un entraînement là où il s'était arrêté. Le SavedModel est un répertoire contenant un graphe sérialisé, sans dépendance à Python : c'est lui que TensorFlow Serving, TensorFlow Lite ou un runtime C++ savent lire.
modele.save("modeles/classifieur.keras") # pour reprendre l'entrainement
modele.export("modeles/servi/1") # pour la production
Le 1 final n'est pas décoratif : c'est le numéro de version, et TensorFlow Serving l'exige.
Le prétraitement doit vivre dans le modèle
Voici le point le plus important du module. Au module 8, les images passaient par preprocess_input avant d'entrer dans le réseau. Si cette étape reste dans votre script Python d'entraînement, elle n'existe pas dans le SavedModel exporté. Le service reçoit alors des pixels bruts et applique un réseau qui attend des valeurs normalisées.
Le résultat est le pire cas de figure : aucune erreur, aucune alerte, et des prédictions dégradées d'une manière difficile à relier à sa cause. C'est l'écart entre l'entraînement et le service, et il se manifeste presque toujours par le prétraitement.
La parade consiste à intégrer le prétraitement comme des couches du modèle :
from tensorflow import keras
from tensorflow.keras import layers
entree = keras.Input(shape=(None, None, 3), dtype="uint8", name="image")
x = layers.Resizing(224, 224)(entree)
x = layers.Rescaling(1.0 / 127.5, offset=-1.0)(x)
x = base(x, training=False)
x = layers.GlobalAveragePooling2D()(x)
sortie = layers.Dense(nb_classes, activation="softmax")(x)
modele_servi = keras.Model(entree, sortie)
modele_servi.export("modeles/servi/1")
Le modèle accepte désormais des images de taille quelconque en entiers non signés, et se charge lui-même du redimensionnement et de la normalisation. Le client n'a plus rien à savoir des conventions internes. Les couches Normalization, TextVectorization et StringLookup suivent la même logique pour les données tabulaires et textuelles : elles s'adaptent aux données avec adapt pendant l'entraînement, puis emportent leurs statistiques dans l'export.
Chaque fois qu'une transformation existe dans le script d'entraînement mais pas dans l'artefact exporté, elle devient une divergence. Le test décisif tient en une phrase : le SavedModel accepte-t-il exactement ce que le client enverra ? Si la réponse suppose une étape en amont, cette étape doit rejoindre le modèle ou être versionnée avec lui.
Inspecter avant de déployer
Un SavedModel s'inspecte sans écrire une ligne de Python, et cette vérification prend dix secondes.
saved_model_cli show --dir modeles/servi/1 --tag_set serve \
--signature_def serving_default
La sortie décrit les entrées et sorties attendues :
inputs['image'] tensor_info:
dtype: DT_UINT8
shape: (-1, -1, -1, 3)
outputs['output_0'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 10)
Les -1 correspondent aux dimensions libres, dont celle du lot. Deux vérifications : le type d'entrée doit être celui que le client produira, et la forme de sortie doit correspondre au nombre de classes. Un shape: (-1, 1) là où vous attendiez dix classes signale une tête mal dimensionnée, et il vaut mieux le découvrir ici qu'après le déploiement.
Servir le modèle
TensorFlow Serving se lance en conteneur et expose deux interfaces.
docker run -p 8501:8501 \
--mount type=bind,source=$(pwd)/modeles/servi,target=/models/classifieur \
-e MODEL_NAME=classifieur \
tensorflow/serving
Le répertoire monté est le parent des versions, pas la version elle-même. Serving y cherche les sous-répertoires numériques et charge le plus élevé.
L'interrogation se fait alors en HTTP :
curl -X POST http://localhost:8501/v1/models/classifieur:predict \
-d '{"instances": [[[[12, 34, 56], [78, 90, 12]]]]}'
L'interface REST sur le port 8501 est pratique et lisible. L'interface gRPC sur le port 8500 est nettement plus rapide, car elle évite l'encodage des tenseurs en JSON — coûteux dès que les entrées sont volumineuses. Pour un service d'images à fort trafic, gRPC n'est pas un raffinement mais une nécessité.
Le versionnement est gratuit, utilisez-le
La structure de répertoires suffit à gérer les versions :
modeles/servi/
1/ saved_model.pb variables/
2/ saved_model.pb variables/
Déposer un répertoire 2 suffit : Serving le détecte, le charge, bascule le trafic dessus et décharge le précédent, sans interruption. Un retour arrière consiste à retirer le répertoire fautif.
Une politique de service permet d'aller plus loin, en maintenant deux versions actives simultanément pour comparer leurs réponses sur du trafic réel avant de basculer. C'est le socle du déploiement progressif, traité au module sur la mise en production du cours 20.
Vérifier après déploiement, pas seulement avant
Un point de contrôle final évite les mauvaises surprises :
import numpy as np, requests
lot = images_test[:8]
attendu = modele_servi.predict(lot)
reponse = requests.post(
"http://localhost:8501/v1/models/classifieur:predict",
json={"instances": lot.tolist()},
).json()
obtenu = np.array(reponse["predictions"])
print("ecart maximal :", np.abs(attendu - obtenu).max())
L'écart doit rester au niveau du bruit numérique, de l'ordre de . Un écart plus important révèle une divergence de prétraitement, une version chargée différente de celle attendue, ou une conversion de type perdue en route. Ce test tient en dix lignes et détecte la quasi-totalité des erreurs de mise en service.
En résumé
- Le format
.kerassert à reprendre un entraînement, le SavedModel à servir en production ; ce dernier ne dépend pas de Python et se range dans un répertoire numéroté. - Le prétraitement doit être une couche du modèle, sinon il disparaît à l'export et crée un écart silencieux entre entraînement et service.
saved_model_cli showvérifie types et formes des signatures avant déploiement, en dix secondes.- Serving surveille le répertoire parent des versions et bascule sans interruption ; un test comparant les prédictions locales et servies détecte l'essentiel des erreurs restantes.
Module suivant : la récapitulation du cours et l'examen final de 40 questions.