Aller au contenu principal

Module 8 — Apprentissage par transfert avec des modèles préentraînés

Entraîner un réseau de vision depuis zéro demande des millions d'images et des jours de calcul. L'apprentissage par transfert obtient de meilleurs résultats avec quelques milliers d'images et quelques minutes. Ce module explique pourquoi cela fonctionne, puis comment le faire sans tomber dans le piège qui gâche la moitié des tentatives.

Pourquoi les premières couches se réutilisent

Un réseau profond ne construit pas une seule représentation : il en empile plusieurs, de la plus générale à la plus spécifique.

Hiérarchie de représentations d'un réseau de vision en cinq étages. Les pixels bruts alimentent des premières couches qui détectent contours et gradients, puis des couches intermédiaires qui reconnaissent textures et parties d'objets, puis des dernières couches qui identifient des objets entiers, et enfin une tête de classification propre au jeu d'origine.Pixels brutsvaleurs de couleuraucune structurePremières couchescontours, coinsgradients de couleuruniverselCouches intermédiairestextures, motifs répétésparties d'objetsDernières couchesobjets entiersspécifique aux classes vuesTête de classificationprobabilités des 1000 classesà remplacer
Plus on descend, plus la représentation est spécifique au jeu d'origine. Les deux premiers étages détectent des contours utiles à n'importe quelle tâche visuelle : c'est cette généralité qui rend le transfert possible. Seule la tête est à remplacer systématiquement.

Un détecteur de contours reste un détecteur de contours, que l'image représente un chat ou une radiographie pulmonaire. C'est ce qui justifie de conserver les poids d'un réseau entraîné sur ImageNet et de ne remplacer que son extrémité.

Charger un modèle sans sa tête

from tensorflow import keras

base = keras.applications.EfficientNetB0(
weights="imagenet",
include_top=False, # retire la tete des 1000 classes
input_shape=(224, 224, 3),
)
base.trainable = False # gele toute la base

include_top=False retire la couche de classification d'origine, inutile puisque vos classes ne sont pas celles d'ImageNet. trainable = False gèle les poids : ils ne recevront plus de gradient.

On ajoute ensuite sa propre tête, avec l'API fonctionnelle du module 3 :

from tensorflow.keras import layers

entree = keras.Input(shape=(224, 224, 3))
x = keras.applications.efficientnet.preprocess_input(entree)
x = base(x, training=False)
x = layers.GlobalAveragePooling2D()(x)
x = layers.Dropout(0.3)(x)
sortie = layers.Dense(nb_classes, activation="softmax")(x)

modele = keras.Model(entree, sortie)

Le preprocess_input propre à chaque famille de modèles n'est pas optionnel. Chaque architecture attend ses entrées dans une plage précise — certaines dans [0,1][0, 1], d'autres dans [1,1][-1, 1], d'autres centrées sur les moyennes d'ImageNet. Fournir des pixels bruts de 0 à 255 à un réseau qui attend [1,1][-1, 1] donne des résultats médiocres sans aucun message d'erreur.

Le piège de BatchNormalization

Voici l'erreur qui explique la majorité des transferts ratés, et elle est presque invisible.

Une couche BatchNormalization contient deux sortes de grandeurs : des poids appris, et des statistiques courantes — moyenne et variance accumulées pendant l'entraînement d'origine. Or base.trainable = False gèle les poids, mais ne suffit pas à figer les statistiques : si la couche est appelée en mode entraînement, elle continue de les mettre à jour avec vos données.

Sur un petit jeu, ces statistiques dérivent, s'écartent de celles pour lesquelles les poids gelés ont été optimisés, et détruisent la représentation qu'on cherchait justement à préserver. Le symptôme est une exactitude qui stagne ou régresse alors que tout paraît correct.

La parade tient dans le training=False de l'appel base(x, training=False) ci-dessus. Il force le mode inférence pour toute la base, statistiques comprises.

Deux mécanismes distincts, souvent confondus

base.trainable = False décide quels poids reçoivent un gradient. L'argument training=False décide comment les couches se comportent. Les deux sont nécessaires et ne se remplacent pas. Sur une base contenant de la normalisation par lots — c'est le cas de presque toutes les architectures modernes — omettre le second annule l'essentiel du bénéfice du premier.

Deux régimes, dans cet ordre

L'approche efficace se déroule en deux phases, et l'ordre n'est pas négociable.

Phase 1, extraction de caractéristiques. La base est gelée, seule la tête neuve s'entraîne, avec un taux d'apprentissage ordinaire.

modele.compile(
optimizer=keras.optimizers.Adam(1e-3),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)
modele.fit(jeu, validation_data=jeu_val, epochs=10)

Phase 2, réglage fin. On dégèle une partie de la base et on reprend l'entraînement avec un taux beaucoup plus faible.

base.trainable = True
for couche in base.layers[:-30]: # ne degele que les 30 dernieres
couche.trainable = False

modele.compile(
optimizer=keras.optimizers.Adam(1e-5), # cent fois plus faible
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)
modele.fit(jeu, validation_data=jeu_val, epochs=10)

Recompiler après avoir modifié trainable est obligatoire : sans cela, la liste des variables entraînables reste celle établie à la compilation précédente et le dégel n'a aucun effet.

Le taux très faible s'explique simplement. La tête neuve, initialisée au hasard, produit au départ des gradients énormes. Appliqués à une base préentraînée avec un taux ordinaire, ils effacent en quelques lots l'information accumulée sur des millions d'images. C'est aussi la raison pour laquelle la phase 1 doit précéder la phase 2 : elle amène la tête à un état raisonnable avant que la base ne soit exposée.

Combien dégeler

SituationStratégie
peu de données, domaine prochetout geler, entraîner la tête seule
beaucoup de données, domaine prochedégeler le dernier tiers
peu de données, domaine éloignédégeler les couches intermédiaires, garder les premières gelées
beaucoup de données, domaine très éloignéenvisager un entraînement complet

Le cas « peu de données, domaine éloigné » est le plus délicat, et l'imagerie médicale en est l'exemple type. Les premières couches restent utiles, les dernières sont trop spécifiques aux objets d'ImageNet, et il n'y a pas assez de données pour tout réapprendre. Dégeler par le milieu est le compromis, à valider expérimentalement.

En résumé

  • Le transfert fonctionne parce que les premières couches apprennent des motifs universels — contours, textures — indépendants de la tâche d'origine.
  • include_top=False retire la tête d'origine, et le preprocess_input de la famille de modèles est indispensable, chaque architecture attendant ses entrées dans une plage précise.
  • trainable = False gèle les poids mais pas les statistiques de normalisation par lots : sans training=False à l'appel, elles dérivent et détruisent la représentation préservée.
  • Toujours extraction de caractéristiques d'abord, réglage fin ensuite avec un taux cent fois plus faible, et recompiler après chaque changement de trainable.

Module suivant : l'entraînement distribué, quand un seul accélérateur ne suffit plus.