| name | model-optimization-edge |
| description | Optimisation de modèles pour le déploiement Edge — pruning structurel et non-structuré, quantification-aware training (QAT), clustering de poids, distillation de connaissances, NAS (Neural Architecture Search) pour small models, TensorFlow Model Optimization Toolkit, compilateurs hardware. |
| version | 1.0.0 |
| author | EVA |
| license | Privée EVA St-Étienne |
| platforms | ["linux","macos","windows"] |
| metadata | {"EVA":{"tags":["model-optimization","pruning","quantization-aware-training","qat","distillation","nas","weight-clustering","tensorflow-model-optimization","edge-ai"],"related_skills":["tinyml-fundamentals","tensorflow-lite-deep-dive","onnx-edge-deployment","nvidia-jetson-deployment"]}} |
Optimisation de Modèles pour Edge
Vue d'ensemble
L'optimisation de modèles pour le déploiement Edge combine pruning (élagage), quantification, distillation et NAS (Neural Architecture Search) pour produire des modèles ultra-légers sans sacrifier la précision. Contrairement à la quantification post-entraînement simple, ces techniques sont intégrées dans le cycle d'entraînement.
Stratégies d'optimisation
┌──────────────────────────────────────────────────────────┐
│ Modèle original (FP32) │
├──────────────────────────────────────────────────────────┤
│ Combinaison des techniques d'optimisation : │
│ │
│ Pruning QAT Distillation NAS │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌────┐ │
│ │ Retirer │ │ Simuler │ │ Student │ │Cher│ │
│ │ poids │ │ INT8 │ │ apprend de │ │cher│ │
│ │ inutiles │ │ pendant │ │ Teacher │ │arch│ │
│ │ │ │ training │ │ (modèle plus │ │ │ │
│ │ │ │ │ │ petit) │ │ │ │
│ └──────────┘ └──────────┘ └──────────────┘ └────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Modèle optimisé (INT8, sparse, petit) │ │
│ │ TFLite / ONNX / TensorRT │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
Gains typiques
| Technique | Réduction taille | Perte précision | Effort |
|---|
| Pruning 50% non-structuré | 50% (sparse) | < 1% | Faible |
| Pruning 80% structuré | 80% | 2-5% | Moyen |
| QAT INT8 | 75% | 1-3% | Moyen |
| Clustering (16 clusters) | 93% | 2-5% | Faible |
| Distillation (4× plus petit) | 75% | 3-8% | Élevé |
| NAS + QAT combiné | 90% | < 5% | Très élevé |
1. Pruning (Élagage)
1.1 Pruning non-structuré (poids individuels)
import tensorflow_model_optimization as tfmot
import tensorflow as tf
pruning_params = {
"pruning_schedule": tfmot.sparsity.keras.PolynomialDecay(
initial_sparsity=0.30,
final_sparsity=0.80,
begin_step=500,
end_step=5000,
frequency=100,
),
"block_size": (1, 1),
"block_pooling_type": "AVG",
}
model = tf.keras.Sequential([...])
model_for_pruning = tfmot.sparsity.keras.prune_low_magnitude(
model, **pruning_params
)
model_for_pruning.compile(
optimizer="adam",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)
callbacks = [
tfmot.sparsity.keras.UpdatePruningStep(),
tfmot.sparsity.keras.PruningSummaries(log_dir="./logs"),
]
model_for_pruning.fit(
x_train, y_train,
batch_size=32,
epochs=50,
validation_data=(x_val, y_val),
callbacks=callbacks,
)
final_model = tfmot.sparsity.keras.strip_pruning(model_for_pruning)
final_model.save()
1.2 Pruning structuré (canaux / filtres)
from tensorflow_model_optimization.python.core.sparsity.keras import pruning_wrapper
class PruningFiltre(tfmot.sparsity.keras.pruning_schedule.PruningSchedule):
"""Pruning par filtre : toute la norme L1 du filtre."""
def __call__(self, step):
pass
def prune_filtres_conv(model, couche_name: str, taux_sparsity: float = 0.5):
"""Prune les filtres d'une couche Conv2D par norme L1."""
couche = model.get_layer(couche_name)
poids = couche.get_weights()[0]
norms = np.sum(np.abs(poids), axis=(0, 1, 2))
n_filtres = len(norms)
n_a_garder = int(n_filtres * (1 - taux_sparsity))
seuil = np.sort(norms)[n_a_garder]
mask = norms >= seuil
mask
1.3 Pruning avec re-apprentissage (fine-tuning)
def pruning_avec_finetuning(model, x_train, y_train, x_val, y_val,
sparsity_cible: float = 0.7) -> tf.keras.Model:
"""Pipeline complet : pruning progressif + fine-tuning."""
pruning_params = {
"pruning_schedule": tfmot.sparsity.keras.ConstantSparsity(
target_sparsity=sparsity_cible,
begin_step=0,
),
}
model_pruned = tfmot.sparsity.keras.prune_low_magnitude(
model, **pruning_params
)
model_pruned.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001),
loss="categorical_crossentropy",
metrics=["accuracy"],
)
callbacks = [
tfmot.sparsity.keras.UpdatePruningStep(),
tfmot.sparsity.keras.PruningSummaries(),
tf.keras.callbacks.EarlyStopping(
monitor="val_accuracy", patience=5, restore_best_weights=True
),
]
history = model_pruned.fit(
x_train, y_train,
batch_size=32,
epochs=30,
validation_data=(x_val, y_val),
callbacks=callbacks,
)
final_model = tfmot.sparsity.keras.strip_pruning(model_pruned)
final_model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=0.00005),
loss="categorical_crossentropy",
metrics=["accuracy"],
)
final_model.fit(
x_train, y_train,
batch_size=32,
epochs=10,
validation_data=(x_val, y_val),
)
return final_model
2. QAT — Quantization-Aware Training
2.1 Pipeline QAT complet
import tensorflow_model_optimization as tfmot
import tensorflow as tf
qat_model = tfmot.quantization.keras.quantize_model(model)
qat_model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001),
loss="categorical_crossentropy",
metrics=["accuracy"],
)
qat_model.fit(
x_train, y_train,
batch_size=32,
epochs=15,
validation_data=(x_val, y_val),
)
converter = tf.lite.TFLiteConverter.from_keras_model(qat_model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
converter.representative_dataset = representative_dataset
tflite_qat = converter.convert()
2.2 QAT sélectif (par couche)
def quantiser_selectif(layer):
"""Applique QAT sauf aux couches spécifiques."""
if isinstance(layer, tf.keras.layers.Dense) and layer.units <= 16:
return layer
if layer.name in ["input_conv", "output_dense"]:
return layer
return tfmot.quantization.keras.quantize_annotate_layer(layer)
model = tf.keras.Sequential([...])
annotated_model = tf.keras.models.clone_model(
model, clone_function=quantiser_selectif
)
qat_model = tfmot.quantization.keras.quantize_apply(annotated_model)
2.3 Configuration QAT fine
from tensorflow_model_optimization.python.core.quantization.keras import quantize_emulada
default_config = {
"quantize_weights": True,
"quantize_activations": True,
"num_bits_weight": 8,
"num_bits_activation": 8,
"per_channel_quantization": True,
}
config_agressif = {
"quantize_weights": True,
"quantize_activations": True,
"num_bits_weight": 4,
"num_bits_activation": 8,
"per_channel_quantization": True,
}
config_precis = {
"quantize_weights": True,
"quantize_activations": True,
"num_bits_weight": 8,
"num_bits_activation": 16,
"per_channel_quantization": True,
}
2.3 QAT + Pruning combiné
pruned_model = pruning_avec_finetuning(model, ...)
qat_pruned_model = tfmot.quantization.keras.quantize_model(pruned_model)
converter = tf.lite.TFLiteConverter.from_keras_model(qat_pruned_model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.representative_dataset = representative_dataset
model_final = converter.convert()
3. Clustering de Poids
3.1 Clustering avec TFMO
import tensorflow_model_optimization as tfmot
cluster_params = {
"number_of_clusters": 16,
"cluster_centroids_init": tfmot.clustering.keras.CentroidInitialization.LINEAR,
}
clustered_model = tfmot.clustering.keras.cluster_weights(
model, **cluster_params
)
clustered_model.compile(
optimizer="adam",
loss="categorical_crossentropy",
metrics=["accuracy"],
)
clustered_model.fit(
x_train, y_train,
epochs=5,
validation_data=(x_val, y_val),
)
final_model = tfmot.clustering.keras.strip_clustering(clustered_model)
final_model.save("model_clustered.h5")
3.2 Clustering hiérarchique
def clusterer_adaptatif(layer):
"""Applique un nombre de clusters adapté à chaque couche."""
if isinstance(layer, tf.keras.layers.Conv2D):
return tfmot.clustering.keras.cluster_weights(
layer, number_of_clusters=32
)
elif isinstance(layer, tf.keras.layers.Dense) and layer.units > 128:
return tfmot.clustering.keras.cluster_weights(
layer, number_of_clusters=8
)
else:
return layer
4. Distillation de Connaissances
4.1 Distillation classique (logits)
import tensorflow as tf
class Distiller(tf.keras.Model):
"""Distillation de connaissances avec température."""
def __init__(self, student, teacher, temperature=4.0, alpha=0.5):
super().__init__()
self.student = student
self.teacher = teacher
self.temperature = temperature
self.alpha = alpha
def compile(self, optimizer, metrics):
super().compile(optimizer=optimizer, metrics=metrics)
self.distillation_loss = tf.keras.losses.KLDivergence()
self.true_loss = tf.keras.losses.CategoricalCrossentropy()
def train_step(self, data):
x, y_true = data
with tf.GradientTape() as tape:
student_logits = self.student(x, training=True)
teacher_logits = self.teacher(x, training=False)
soft_student = tf.nn.softmax(student_logits / .temperature)
soft_teacher = tf.nn.softmax(teacher_logits / .temperature)
d_loss = .distillation_loss(soft_teacher, soft_student)
c_loss = .true_loss(y_true, student_logits)
loss = .alpha * d_loss + ( - .alpha) * c_loss
gradients = tape.gradient(loss, .student.trainable_variables)
.optimizer.apply_gradients(
(gradients, .student.trainable_variables)
)
.compiled_metrics.update_state(y_true,
tf.nn.softmax(student_logits))
{m.name: m.result() m .metrics}
():
x, y_true = data
student_logits = .student(x, training=)
.compiled_metrics.update_state(y_true,
tf.nn.softmax(student_logits))
{m.name: m.result() m .metrics}
4.2 Distillation avec features (Hint Learning)
class HintDistiller(tf.keras.Model):
"""Distillation avec features intermédiaires."""
def __init__(self, student, teacher, hint_layer="conv2", beta=0.3):
super().__init__()
self.student = student
self.teacher = teacher
self.hint_layer = hint_layer
self.beta = beta
self.teacher_hint = tf.keras.Model(
inputs=teacher.input,
outputs=[teacher.get_layer(hint_layer).output, teacher.output],
)
student_hint_out = student.get_layer(hint_layer).output
self.regressor = tf.keras.layers.Dense(
teacher.get_layer(hint_layer).output_shape[-1],
name="hint_regressor",
)
def train_step(self, data):
x, y_true = data
with tf.GradientTape() as tape:
student_output = self.student(x, training=True)
teacher_hints, teacher_output = self.teacher_hint(x, training=False)
student_hint = self.regressor(
.student.get_layer(.hint_layer).output
)
hint_loss = tf.reduce_mean(
tf.square(teacher_hints - student_hint)
)
kd_loss = tf.keras.losses.KLDivergence()(
tf.nn.softmax(teacher_output / ),
tf.nn.softmax(student_output / ),
)
ce_loss = tf.keras.losses.CategoricalCrossentropy()(
y_true, student_output
)
loss = ce_loss + kd_loss + .beta * hint_loss
tape.gradient(loss, .student.trainable_variables)
{: loss}
4.3 Distillation pour TinyML (student extrêmement petit)
def creer_tiny_student(n_classes: int = 10) -> tf.keras.Model:
"""Créer un modèle student ultra-léger pour TinyML."""
model = tf.keras.Sequential([
tf.keras.layers.Input(shape=(32, 32, 1)),
tf.keras.layers.Conv2D(4, 3, padding="same", strides=2),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.ReLU(),
tf.keras.layers.DepthwiseConv2D(3, padding="same"),
tf.keras.layers.Conv2D(8, 1, padding="same"),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.ReLU(),
tf.keras.layers.GlobalAveragePooling2D(),
tf.keras.layers.Dense(n_classes),
])
print(f"Paramètres student : {model.count_params():,}")
return model
teacher = creer_teacher_modele()
distiller = Distiller(
student=creer_tiny_student(),
teacher=teacher,
temperature=8.0,
alpha=0.7,
)
5. NAS — Neural Architecture Search
5.1 NAS pour modèles Edge
from tensorflow.keras.applications import MobileNetV3Small
model = MobileNetV3Small(
input_shape=(224, 224, 3),
alpha=0.75,
include_top=True,
weights="imagenet",
classes=1000,
)
print(f"Paramètres : {model.count_params():,}")
5.2 Recherche d'architecture manuelle (NAS-like)
def rechercher_architecture_optimum(
x_train, y_train, x_val, y_val,
budgets=[(5000, 0.90), (20000, 0.95), (50000, 0.98)]
) -> tf.keras.Model:
"""Recherche d'architecture avec contrainte de taille."""
for max_params, accuracy_target in budgets:
print(f"\nRecherche: max {max_params:,} params, target {accuracy_target:.0%}")
architectures = [
(8, 16, 2),
(16, 32, 3),
(8, 32, 2),
(16, 16, 4),
(16, 32, 2),
(32, 64, 2),
]
best_acc = 0
best_config = None
for conv_f, dw_f, n_l in architectures:
model = creer_modele_personnalise(
conv_filtres=conv_f,
dw_filtres=dw_f,
n_couches=n_l,
)
model.count_params() > max_params:
()
model.(
optimizer=,
loss=,
metrics=[],
)
history = model.fit(
x_train, y_train[:],
epochs=, validation_data=(x_val[:], y_val[:]),
verbose=,
)
val_acc = (history.history[])
val_acc > best_acc:
best_acc = val_acc
best_config = (conv_f, dw_f, n_l)
()
()
best_config
6. Pipeline d'Optimisation Complet
6.1 Workflow production Edge
def pipeline_optimisation_complet(model: tf.keras.Model,
x_train, y_train, x_val, y_val,
sparsity_target: float = 0.6,
n_clusters: int = 16) -> bytes:
"""Pipeline complet : Pruning → Clustering → QAT → TFLite."""
print("Phase 1: Pruning...")
model_pruned = pruning_avec_finetuning(
model, x_train, y_train, x_val, y_val,
sparsity_cible=sparsity_target,
)
print("Phase 2: Clustering...")
clustering_params = {"number_of_clusters": n_clusters}
model_clustered = tfmot.clustering.keras.cluster_weights(
model_pruned, **clustering_params
)
model_clustered.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001),
loss="categorical_crossentropy",
metrics=["accuracy"],
)
model_clustered.fit(
x_train, y_train,
batch_size=32, epochs=5,
validation_data=(x_val, y_val),
verbose=0,
)
model_clustered = tfmot.clustering.keras.strip_clustering(model_clustered)
print("Phase 3: QAT...")
qat_model = tfmot.quantization.keras.quantize_model(model_clustered)
qat_model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=0.00005),
loss="categorical_crossentropy",
metrics=["accuracy"],
)
qat_model.fit(
x_train, y_train,
batch_size=32, epochs=10,
validation_data=(x_val, y_val),
verbose=0,
)
()
converter = tf.lite.TFLiteConverter.from_keras_model(qat_model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
converter.representative_dataset = : [
[x_val[i:i+].astype(np.float32)] i ((, (x_val)))
]
tflite_model = converter.convert()
taille_init = (
np.prod(w.shape) * w model.get_weights()
) / ( * )
taille_final = (tflite_model) / ( * )
_, acc_init = model.evaluate(x_val, y_val, verbose=)
interpreter = tf.lite.Interpreter(model_content=tflite_model)
interpreter.allocate_tensors()
acc_final = evaluer_tflite(interpreter, x_val, y_val)
()
()
()
()
()
()
tflite_model
Pièges Courants
-
Pruning + QAT dans le mauvais ordre : toujours pruning → QAT, jamais l'inverse. QAT d'un modèle déjà pruné est plus stable.
-
Température de distillation trop basse : T < 2 donne des soft labels trop durs (équivalent aux hard labels). T > 8 dilue trop l'information. T=4 est un bon point de départ.
-
Clustering avec trop peu de clusters : 2-4 clusters ne capturent pas la diversité des poids (perte > 10%). 16-32 clusters est un bon compromis.
-
QAT sans fine-tuning suffisant : QAT avec < 5 epochs peut ne pas converger. 10-20 epochs est recommandé, avec un LR 10× plus faible.
-
Student trop petit pour la distillation : si le student a < 1% des paramètres du teacher, la distillation seule ne suffit pas. Ajouter hint learning.
-
NAS coûteux en compute : NAS peut nécessiter 100-1000 entraînements. Utiliser des super-réseaux (Once-for-All) ou des modèles déjà NAS-optimisés.
-
Métriques trompeuses : la réduction de taille ne reflète pas la réduction de latence. Le pruning non-structuré réduit la taille mais pas la latence (sans hardware sparse). Toujours mesurer les deux.
Références