Aller au contenu principal

Module 4 — Entraînement conscient de la quantification

Le module 3 a laissé le fil rouge à un compromis presque acceptable : 14 Mo → 3,7 Mo, exactitude 96,3 % → 95,7 %. Sur trente-huit classes de maladies, ces 0,6 point représentent environ une classe rare mal diagnostiquée par tranche de mille inférences. Pour un usage grand public, c'est tolérable ; pour un usage professionnel où un mauvais diagnostic d'anthracnose sur du manioc coûte une récolte, ce n'est plus le cas.

L'entraînement conscient de la quantification (QAT, pour Quantization Aware Training) rattrape ce type de perte. Il coûte une nouvelle phase d'entraînement, mais préserve les 3,7 Mo et récupère la quasi-totalité de l'exactitude float32.

Le principe : simuler la perte avant qu'elle arrive

Un réseau entraîné en float32 puis converti en int8 a été optimisé pour un régime numérique différent de celui dans lequel il finira par tourner. Les poids trouvés sont optimaux pour des multiplications à virgule flottante, pas pour leurs approximations quantifiées.

Le QAT insère, dans le graphe d'entraînement, des nœuds de fausse quantification qui arrondissent chaque activation et chaque poids comme le fera l'inférence int8. Le calcul reste en float32 — les gradients peuvent toujours circuler — mais la valeur numérique subit exactement la même perte d'information qu'à la conversion. Le réseau apprend alors à compenser cet arrondi pendant l'entraînement, en déplaçant ses poids vers des valeurs plus tolérantes à la quantification.

C'est une simulation, pas un vrai calcul entier : à chaque passage arrière, le gradient est calculé comme si le nœud d'arrondi était l'identité (astuce appelée estimateur passant tout droit, straight-through estimator). Sans cette astuce, la dérivée d'un arrondi serait nulle presque partout et le réseau n'apprendrait plus rien.

Le code : trois lignes ajoutées à un entraînement Keras

Le paquet tensorflow_model_optimization (TFMOT) fournit la fonction qui enveloppe le modèle. À partir du MobileNetV2 déjà affiné :

import tensorflow_model_optimization as tfmot

modele = tf.keras.models.load_model("plantvillage_mobilenetv2.keras")

# Enveloppe chaque couche compatible avec des noeuds de fausse quantification
modele_qat = tfmot.quantization.keras.quantize_model(modele)

modele_qat.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)

modele_qat.fit(donnees_entrainement, epochs=5, validation_data=donnees_validation)

Deux détails importants. Le taux d'apprentissage est réduit d'un ordre de grandeur (1e-5 au lieu du 1e-4 de l'affinage initial), parce qu'on polit un modèle déjà proche de l'optimum. Le nombre d'époques est court — trois à cinq suffit — parce que la convergence sur la fausse quantification est rapide dès lors que l'exactitude float32 est déjà atteinte.

Après l'entraînement, le modèle QAT se convertit en int8 complet comme au module 3, sans jeu représentatif cette fois : les statistiques de quantification ont été apprises pendant l'entraînement.

convertisseur = tf.lite.TFLiteConverter.from_keras_model(modele_qat)
convertisseur.optimizations = [tf.lite.Optimize.DEFAULT]
convertisseur.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
convertisseur.inference_input_type = tf.uint8
convertisseur.inference_output_type = tf.uint8
tflite = convertisseur.convert()

Effet mesuré sur le fil rouge : fichier à 3,7 Mo (identique à la quantification post-training), exactitude à 96,2 % (soit 0,1 point perdu par rapport au float32), latence identique. Le QAT récupère essentiellement l'exactitude perdue en gardant tous les autres bénéfices.

Le coût réel : temps d'entraînement, pas complexité

L'ajout de la fausse quantification alourdit chaque passe d'entraînement de 20 à 40 %. Une époque qui prenait 10 minutes en float32 en prend 12 à 14 en QAT, sur le même matériel. Sur cinq époques, c'est une dizaine de minutes supplémentaires, sans commune mesure avec le coût de l'affinage initial.

Le vrai coût est plutôt organisationnel : le QAT exige que le pipeline d'entraînement soit reproductible, que les données d'entraînement soient disponibles, et qu'une machine avec accélérateur soit encore accessible. Dans certains contextes de conformité, les données d'entraînement ont été purgées après l'entraînement initial ; le QAT devient alors impraticable, et il faut revenir à la quantification post-training du module 3.

Quand faut-il vraiment le sortir ?

Le QAT n'est pas une bonne pratique par défaut. Il se justifie dans quatre cas de figure :

  • La perte à la conversion int8 dépasse un seuil métier — typiquement 1 point d'exactitude ou 3 points de rappel sur une classe critique.
  • Le modèle est petit (moins de 5 Mo en float32). Les petits modèles ont peu de redondance dans leurs poids et pardonnent moins la quantification.
  • La distribution des activations est fortement asymétrique — une couche dont les activations vont de -20 à 200 sature en int8 symétrique. Le QAT apprend des poids qui ramènent ces activations dans une plage saine.
  • La cible matérielle est un accélérateur qui n'accepte que l'int8 (certains DSP, EdgeTPU) et qu'aucune perte n'est acceptable.

Hors ces cas, la quantification post-training du module 3 suffit et évite l'aller-retour d'entraînement.

La normalisation par lots doit être « repliée » avant le QAT

Les couches BatchNormalization sont fusionnées avec la convolution qui les précède pendant l'inférence. Un modèle qui subit le QAT sans ce repli enveloppe chaque BatchNormalization de fausse quantification, ce qui n'a aucun sens (elles disparaissent à la conversion). TFMOT gère ce cas automatiquement pour les architectures standard ; pour une architecture personnalisée, le repli doit être fait à la main avant l'enveloppement, sous peine d'un modèle qui plante à la conversion.

Vérifier que la simulation est fidèle

Un piège classique du QAT est de croire au chiffre affiché par modele_qat.evaluate() sur le jeu de validation. Cette évaluation utilise la simulation float32 avec fausse quantification, pas le vrai int8 de l'interprète. L'écart est habituellement faible, mais il existe.

Comme pour le module 3, la seule mesure qui compte est celle du .tflite final sur l'interprète, avec le jeu de test complet :

interprete = tf.lite.Interpreter(model_path="modeles/plantvillage_qat.tflite")
interprete.allocate_tensors()
entree = interprete.get_input_details()[0]
sortie = interprete.get_output_details()[0]

correct = 0
for image, etiquette in jeu_de_test:
interprete.set_tensor(entree["index"], image.astype("uint8")[None, ...])
interprete.invoke()
predite = interprete.get_tensor(sortie["index"])[0].argmax()
if predite == etiquette:
correct += 1
print("exactitude sur .tflite QAT :", correct / len(jeu_de_test))

Un écart de plus de 0,3 point entre l'evaluate du modèle QAT et cette mesure signale qu'une couche n'a pas été correctement enveloppée, ou qu'un opérateur du graphe échappe à la simulation. Dans ce cas, il faut inspecter le modèle avec modele_qat.summary() et repérer les couches qui n'ont pas été transformées en QuantizeWrapperV2.

Le tableau de compromis après le module 4

VarianteTailleLatence Nokia G21Exactitude
float32 (module 2)14 Mo240 ms96,3 %
Plage dynamique3,6 Mo175 ms96,1 %
int8 post-training3,7 Mo145 ms95,7 %
int8 QAT3,7 Mo145 ms96,2 %
float167,1 Mo240 ms96,3 %

La ligne QAT est la nouvelle référence du fil rouge : elle égale pratiquement le float32 en exactitude, tout en pesant quatre fois moins et en tournant 65 % plus vite. Le prochain module y ajoutera l'élagage pour grignoter encore le fichier.

En résumé

  • Le QAT simule la perte de quantification pendant l'entraînement avec des nœuds de fausse quantification, en gardant les gradients float32 grâce à l'estimateur passant tout droit.
  • L'implémentation tient en trois lignes (quantize_model, recompilation, quelques époques à taux réduit) et coûte 20 à 40 % de temps d'entraînement en plus.
  • Il se déclenche quand la perte post-training dépasse un seuil métier, sur des modèles petits, avec des activations asymétriques, ou pour cibler un accélérateur strictement int8.
  • Toujours réévaluer sur le .tflite final, pas sur le modèle QAT en mémoire : l'écart est faible mais réel.

Module suivant : l'élagage, seconde technique de réduction de taille, qui combine avec la quantification pour descendre sous les 2 Mo.