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.
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 , d'autres dans , d'autres centrées sur les moyennes d'ImageNet. Fournir des pixels bruts de 0 à 255 à un réseau qui attend 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.
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
| Situation | Stratégie |
|---|---|
| peu de données, domaine proche | tout geler, entraîner la tête seule |
| beaucoup de données, domaine proche | dé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=Falseretire la tête d'origine, et lepreprocess_inputde la famille de modèles est indispensable, chaque architecture attendant ses entrées dans une plage précise.trainable = Falsegèle les poids mais pas les statistiques de normalisation par lots : sanstraining=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.