L’alarme qui criait au loup — et personne n’écoutait plus
Une usine de découpe laser. Quarante capteurs sur la tête de coupe : température buse, pression assistance gaz, courant moteur axe X, puissance laser, dizaines d’autres. Un système de supervision qui génère des alarmes. Problème : les opérateurs ont coupé les sons d’alerte il y a six mois. Trop de fausses alarmes. 97 par jour en moyenne. Quand la vraie anomalie est arrivée — une dérive thermique sur le miroir de focalisation — l’alarme s’est noyée dans les 97 autres. Résultat : une tête de coupe à 45 000 € à remplacer.
Ce n’est pas un problème de capteurs. Les données étaient là. C’est un problème de modèle de détection mal calibré, trop sensible au bruit, et jamais révisé depuis sa mise en service.
La détection d’anomalies IoT industriel est un domaine où deux familles d’approches coexistent : les classifiers supervisés comme XGBoost (qui nécessitent des labels historiques) et les détecteurs non-supervisés comme Isolation Forest (qui fonctionnent sans labels). Ni l’un ni l’autre ne domine l’autre. Le choix dépend de ce que vous avez — et de ce que vous ne savez pas encore.
Le vrai problème : 99,5 % de normal
Avant de parler d’algorithme, parlons de la réalité des données industrielles de supervision.
Sur une ligne de production qui tourne bien, un flux de capteurs typique ressemble à ceci : 8 heures sur 8, tout est normal. Les anomalies — vraies pannes, dérives significatives, événements anormaux — représentent 0,3 % à 2 % des observations au maximum. La plupart du temps, vous êtes à 99,5 % de points “normaux” et 0,5 % d’anomalies.
Si vous entraînez un classifieur naïf sur ces données et que vous lui demandez d’optimiser l’accuracy, il va apprendre à prédire “normal” pour tout. Résultat : 99,5 % d’accuracy, 0 % d’utilité. Le modèle n’a rien appris. Il joue juste les probabilités.
L’accuracy est une métrique toxique en détection d’anomalies industrielles. Ne jamais l’utiliser comme critère de sélection de modèle.
Ce déséquilibre de classes est le problème central. Toute la stratégie technique qui suit — XGBoost avec scale_pos_weight, SMOTE, Isolation Forest, approche hybride — est une réponse à ce problème fondamental.
Architecture du système : du capteur à l’alerte opérateur
Avant de rentrer dans le code, voici comment s’articule un système de détection d’anomalies industriel typique :
flowchart TD
A[Capteurs industriels\ntemp / pression / courant\nvibrations / débit] -->|Protocoles Modbus / OPC-UA\n100ms - 1s| B[Gateway IoT\nRaspberry Pi / Jetson\nou PLC avec port ETH]
B -->|Prétraitement local\nfenêtres glissantes\nfeature engineering| C{Edge ML\nModèle léger\nXGBoost / IF}
C -->|Score anomalie| D{Seuil\nconfidence}
D -->|Score < seuil\nNormal| E[Archivage\nbase de données\ntime-series]
D -->|Score > seuil\nSuspect| F[Envoi cloud\npayload enrichi\n+ contexte machine]
F --> G[Cloud / Serveur\nmodèle secondaire\nanalyse approfondie]
G -->|Anomalie confirmée| H[Alerte opérateur\nwhatsapp / dashboard\nmaintenance GMAO]
G -->|Faux positif| I[Feedback loop\nréentraînement\nactif]
E --> J[(Time-series DB\nInfluxDB / TimescaleDB)]
H --> J
style C fill:#ff6b35,color:#fff
style G fill:#2563eb,color:#fff
style H fill:#16a34a,color:#fff
L’architecture en deux niveaux est délibérée. Le modèle edge (Raspberry Pi, Jetson Nano, ou directement dans la gateway) gère le premier filtre en temps réel, avec une latence inférieure à 50 ms. Il ne doit pas envoyer tout dans le cloud — ça coûte cher en bande passante, en API, et en latence de décision. Le modèle cloud confirme les vrais positifs, élimine les faux positifs résiduels, et alimente la boucle de réentraînement.
XGBoost est particulièrement adapté au niveau edge : un modèle entraîné pèse typiquement 1-5 MB, tourne sans GPU, et prédit en moins de 5 ms sur un Raspberry Pi 4.
XGBoost supervisé : quand vous avez des labels
Le prérequis : des labels historiques fiables
XGBoost en mode classificateur supervisé nécessite des données labellisées. Concrètement : pour chaque observation, vous savez si c’était une anomalie ou non. Ces labels viennent en général de :
- Historiques de maintenance (GMAO) : interventions correctives avec date/heure
- Rapports d’incidents : alarmes confirmées manuellement par les opérateurs
- Rejets qualité : lots non-conformes tracés dans le MES
- Campagnes de labellisation : un expert annotateur passe sur un échantillon
La qualité des labels prime sur leur quantité. Cinquante labels propres valent mieux que cinq cents labels douteux. Un label “anomalie” posé par erreur sur un point normal crée un bruit d’entraînement qui va dégrader toutes vos métriques.
Gérer le déséquilibre : scale_pos_weight et SMOTE
Deux stratégies complémentaires pour compenser le 99,5 % / 0,5 %.
scale_pos_weight est l’approche la plus directe avec XGBoost. Ce paramètre pondère la pénalité des faux négatifs (anomalies manquées) par rapport aux faux positifs. La valeur de référence est nb_négatifs / nb_positifs. Si vous avez 10 000 points normaux pour 50 anomalies, scale_pos_weight = 200. XGBoost va alors optimiser comme si chaque anomalie comptait 200 fois plus qu’un point normal.
SMOTE (Synthetic Minority Oversampling Technique) génère des points synthétiques de la classe minoritaire par interpolation entre voisins existants. C’est une alternative plus coûteuse en calcul, mais utile quand le déséquilibre est extrême (> 1/500) ou quand scale_pos_weight seul ne suffit pas.
La stratégie optimale en pratique : commencer par scale_pos_weight, évaluer sur un jeu de validation, appliquer SMOTE uniquement si le rappel reste < 70 % sur les anomalies.
Métriques : F1, precision-recall, jamais accuracy
Pour évaluer un modèle de détection d’anomalies, trois métriques comptent :
- F1-score = 2 × (precision × recall) / (precision + recall). La synthèse. Pénalise autant les fausses alarmes que les anomalies manquées.
- Recall (sensibilité) = TP / (TP + FN). Combien d’anomalies réelles sont détectées. Dans un contexte de maintenance critique, on préfère un recall élevé (manquer une panne est plus coûteux qu’une fausse alarme).
- Precision = TP / (TP + FP). Sur toutes les alarmes déclenchées, combien sont des vraies anomalies. Un système avec trop de faux positifs crée le syndrome du “loup qui criait”.
- AUC-PR (aire sous la courbe Precision-Recall). Plus robuste que l’AUC-ROC pour les classes déséquilibrées.
La courbe Precision-Recall est votre boussole opérationnelle : elle permet de trouver le seuil qui équilibre le coût des fausses alarmes (charge opérateur) avec le coût des anomalies manquées (impact production).
Isolation Forest : quand vous n’avez pas de labels
Le principe en 90 secondes
Isolation Forest est un algorithme non-supervisé. Il ne sait pas ce qu’est une anomalie — il n’a vu aucun exemple labellisé. Son raisonnement est probabiliste : une anomalie est un point qui s’isole facilement.
L’algorithme construit un ensemble d’arbres de décision aléatoires. Pour chaque arbre, il tire aléatoirement une variable et une valeur de coupure, et divise l’espace. Il répète jusqu’à isoler chaque point. Un point normal, entouré de voisins denses, nécessite beaucoup de coupures pour être isolé. Un point anormal, dans une zone peu dense, est isolé en peu de coupures.
Le score d’anomalie est inversement proportionnel au nombre moyen de coupures pour isoler le point, normalisé sur l’ensemble de l’arbre.
Isolation Forest est votre point de départ quand vous n’avez aucun historique de pannes labellisé. C’est souvent le cas sur une nouvelle ligne, un nouveau site, ou un équipement dont la maintenance a toujours été corrective.
Ce qu’il fait bien, ce qu’il fait moins bien
Isolation Forest détecte bien les anomalies qui sont des outliers dans l’espace des features : valeurs extrêmes, combinaisons inhabituelles de variables, points isolés. Il est robuste, rapide, et ne nécessite pas de normalisation stricte.
Il est moins bon sur les anomalies contextuelles : une température de 80°C peut être normale pendant la phase de chauffe et anormale pendant la production stable. Isolation Forest ne capture pas ce contexte temporel sans feature engineering adapté (ajout de variables de phase, d’index temporel, de fenêtres glissantes).
Il est aussi sensible au contamination parameter — la proportion supposée d’anomalies dans les données. Par défaut à auto (estimé), il peut sur-détecter ou sous-détecter selon la distribution. Ce paramètre est à calibrer empiriquement sur votre flux.
L’approche hybride : Isolation Forest pour pré-labelliser, XGBoost pour classifier
C’est la stratégie qui fonctionne en production quand on démarre sans labels.
Phase 1 — Pré-labellisation non-supervisée
On entraîne un Isolation Forest sur les données historiques. On génère un score d’anomalie pour chaque observation. On étiquette comme “anomalie candidate” les points avec un score supérieur à un percentile élevé (95e ou 99e selon la contamination attendue).
Ces étiquettes ne sont pas parfaites. Elles contiennent des faux positifs (points normaux dans des zones de faible densité), et elles manquent les anomalies dans des zones denses. Mais elles sont suffisantes pour amorcer.
Phase 2 — Validation experte
On extrait les 50-100 anomalies candidates avec les scores les plus élevés. Un expert métier les passe en revue : sur les données capteurs, sur la base de maintenance, sur les rapports opérateurs. Il confirme ou infirme chaque candidat. Ce travail de labellisation ciblée prend 2-4 heures, pas 2 semaines.
Phase 3 — Réentraînement supervisé
On entraîne un XGBoost sur les labels validés. On obtient un modèle supervisé, avec des métriques fiables sur un jeu de validation propre, et une interprétabilité via les feature importances. On peut maintenant expliquer à l’opérateur pourquoi une anomalie a été détectée : “température buse +12°C et courant moteur +8% simultanément.”
Phase 4 — Boucle d’amélioration active
Chaque faux positif confirmé par l’opérateur est un label négatif. Chaque anomalie manquée remontée via la GMAO est un label positif manqué. On alimente la boucle de réentraînement. Le modèle s’améliore itérativement, sans campagne de labellisation massive.
Implémentation Python — les deux approches
import numpy as np
import pandas as pd
from sklearn.ensemble import IsolationForest
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, precision_recall_curve, f1_score
from sklearn.preprocessing import StandardScaler
from imblearn.over_sampling import SMOTE
import xgboost as xgb
# ── Préparation des données capteurs ──────────────────────────────────────────
# features : fenêtres glissantes calculées en amont (mean, std, min, max par variable)
# df_features : DataFrame avec colonnes de features + colonne 'label' (0=normal, 1=anomalie)
df = pd.read_parquet("capteurs_features.parquet")
X = df.drop(columns=["label", "timestamp"])
y = df["label"] # 0 = normal, 1 = anomalie
# Ratio déséquilibre
ratio = (y == 0).sum() / (y == 1).sum()
print(f"Ratio normal/anomalie : {ratio:.0f}:1") # typiquement 150:1 à 500:1
# ═══════════════════════════════════════════════════════════════════════════════
# APPROCHE 1 — Isolation Forest (non-supervisé)
# ═══════════════════════════════════════════════════════════════════════════════
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
iso_forest = IsolationForest(
n_estimators=200,
max_samples="auto",
contamination=0.01, # 1% d'anomalies supposées — à calibrer sur votre flux
random_state=42,
n_jobs=-1
)
iso_forest.fit(X_scaled)
# Scores : négatif = anomalie, positif = normal
# decision_function retourne le score brut (plus négatif = plus anormal)
anomaly_scores = iso_forest.decision_function(X_scaled)
iso_labels = iso_forest.predict(X_scaled) # -1 = anomalie, 1 = normal
# Convertir en 0/1
iso_preds = np.where(iso_labels == -1, 1, 0)
# Évaluation si labels disponibles (sinon, review manuelle des top anomalies)
if y.sum() > 0:
print("Isolation Forest :")
print(classification_report(y, iso_preds, target_names=["Normal", "Anomalie"]))
# Extraire les top anomalies pour review experte (phase hybride)
top_anomalies_idx = np.argsort(anomaly_scores)[:100] # 100 scores les plus bas
df_review = df.iloc[top_anomalies_idx].copy()
df_review["iso_score"] = anomaly_scores[top_anomalies_idx]
df_review.to_csv("anomalies_candidates_review.csv", index=False)
print(f"100 anomalies candidates exportées pour review experte.")
# ═══════════════════════════════════════════════════════════════════════════════
# APPROCHE 2 — XGBoost supervisé (avec labels historiques)
# ═══════════════════════════════════════════════════════════════════════════════
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# Option A : scale_pos_weight (recommandé en premier)
scale_weight = (y_train == 0).sum() / (y_train == 1).sum()
print(f"scale_pos_weight : {scale_weight:.1f}")
xgb_model = xgb.XGBClassifier(
n_estimators=300,
max_depth=6,
learning_rate=0.05,
subsample=0.8,
colsample_bytree=0.8,
scale_pos_weight=scale_weight, # Compensation déséquilibre
eval_metric="aucpr", # AUC Precision-Recall, pas accuracy
use_label_encoder=False,
random_state=42,
n_jobs=-1,
tree_method="hist" # Rapide sur CPU, compatible edge
)
xgb_model.fit(
X_train, y_train,
eval_set=[(X_test, y_test)],
verbose=50
)
# Prédictions avec seuil personnalisé (pas forcément 0.5)
y_proba = xgb_model.predict_proba(X_test)[:, 1]
# Option B : SMOTE si recall reste < 70% avec scale_pos_weight seul
# smote = SMOTE(sampling_strategy=0.1, random_state=42)
# X_train_sm, y_train_sm = smote.fit_resample(X_train, y_train)
# xgb_model.fit(X_train_sm, y_train_sm, ...)
# Calibration du seuil via courbe Precision-Recall
precisions, recalls, thresholds = precision_recall_curve(y_test, y_proba)
# Trouver le seuil qui maximise F1
f1_scores = 2 * (precisions * recalls) / (precisions + recalls + 1e-9)
best_threshold_idx = np.argmax(f1_scores)
best_threshold = thresholds[best_threshold_idx]
best_f1 = f1_scores[best_threshold_idx]
print(f"\nSeuil optimal : {best_threshold:.3f}")
print(f"F1 optimal : {best_f1:.3f}")
print(f"Precision : {precisions[best_threshold_idx]:.3f}")
print(f"Recall : {recalls[best_threshold_idx]:.3f}")
y_pred_optimized = (y_proba >= best_threshold).astype(int)
print("\nXGBoost supervisé (seuil optimisé) :")
print(classification_report(y_test, y_pred_optimized, target_names=["Normal", "Anomalie"]))
# Feature importances — pour l'explication opérateur
importances = pd.Series(
xgb_model.feature_importances_,
index=X.columns
).sort_values(ascending=False)
print("\nTop 10 features :")
print(importances.head(10))
# Export modèle — léger, déployable edge
xgb_model.save_model("anomaly_detector_edge.json")
print(f"Modèle sauvegardé : {xgb_model.get_booster().save_raw().__sizeof__()} bytes env.")
Feature engineering des fenêtres glissantes
Le code ci-dessus suppose que vous avez déjà calculé des features. En pratique, voici le bloc de préparation :
def compute_window_features(df_raw: pd.DataFrame,
sensor_cols: list,
window_sec: int = 60,
freq: str = "1S") -> pd.DataFrame:
"""
Calcule des features statistiques sur fenêtres glissantes pour chaque capteur.
df_raw : colonnes timestamp + valeurs capteurs (une ligne par mesure)
window_sec : taille de la fenêtre en secondes
"""
df = df_raw.set_index("timestamp").resample(freq).mean().ffill()
features = {}
for col in sensor_cols:
w = df[col].rolling(window=window_sec, min_periods=window_sec // 2)
features[f"{col}_mean"] = w.mean()
features[f"{col}_std"] = w.std()
features[f"{col}_min"] = w.min()
features[f"{col}_max"] = w.max()
features[f"{col}_range"] = w.max() - w.min()
features[f"{col}_skew"] = w.skew()
# Dérivée approximée (taux de variation)
features[f"{col}_diff"] = df[col].diff(periods=5)
return pd.DataFrame(features).dropna()
Avec 10 capteurs et une fenêtre de 60 secondes, on génère 70 features. XGBoost gère ça sans problème. Sur Raspberry Pi 4, la prédiction sur un batch de 100 fenêtres prend environ 12 ms.
Déploiement edge : XGBoost sur Raspberry Pi et Jetson Nano
Pourquoi XGBoost est le bon choix pour l’edge industriel
Les modèles deep learning (LSTM, Transformer, Autoencoder) sont séduisants pour les séries temporelles. Ils capturent mieux les dépendances longue distance. Mais sur une gateway industrielle avec contraintes de coût et de connectivité, ils posent des problèmes concrets :
- Dépendances lourdes : PyTorch pèse 800 MB+ sur ARM. XGBoost pèse 12 MB.
- Consommation : inférence LSTM sur Pi4 = 200-400 ms et chauffe le CPU. XGBoost = 5-15 ms, CPU tranquille.
- Offline-first : le gateway doit fonctionner sans connexion au cloud. Un modèle léger peut être mis à jour manuellement ou via OTA ; un modèle deep learning nécessite une infrastructure de déploiement.
- Explicabilité : un technicien de maintenance peut comprendre “pression buse anormale + courant moteur élevé = détection”. Il ne peut pas interpréter un score LSTM.
Le format natif XGBoost .json se charge en 3 lignes Python, consomme 10-50 MB de RAM selon la complexité, et peut être intégré dans un service systemd qui tourne au démarrage.
# Déploiement sur Raspberry Pi 4 (Raspberry Pi OS 64-bit)
pip install xgboost==2.0.3 # ~12 MB, pas de dépendances GPU
# Service de détection en continu
cat /etc/systemd/system/anomaly-detector.service
# [Unit]
# Description=Anomaly detector — capteurs ligne 3
# After=network.target
#
# [Service]
# ExecStart=/usr/bin/python3 /opt/detector/run.py
# Restart=always
# RestartSec=5
#
# [Install]
# WantedBy=multi-user.target
Contraintes spécifiques au Jetson Nano / Orin
Le Jetson est plus puissant (GPU CUDA embarqué), ce qui permet d’exécuter des modèles plus complexes. Mais pour la détection d’anomalies capteurs, la puissance supplémentaire est rarement nécessaire. XGBoost reste le bon choix : prédictible, stable, sans dépendances CUDA. Réserver le GPU Jetson pour des tâches de vision ou de traitement audio si le cas d’usage l’exige.
Calibration opérationnelle : le seuil d’alerte
Un modèle bien entraîné peut quand même générer trop d’alarmes si son seuil de décision est mal calibré pour le contexte opérationnel.
La courbe Precision-Recall est votre outil de négociation avec les équipes production. Elle matérialise le trade-off :
- Recall 90%, Precision 40% : 6 fausses alarmes pour 10 vraies détections. Acceptable pour une machine critique où une anomalie manquée coûte cher (presse d’injection, four de traitement thermique).
- Recall 70%, Precision 80% : 2 fausses alarmes pour 10 vraies détections. Acceptable pour une supervision de routine où les opérateurs sont occupés.
Ce seuil n’est pas une décision technique. C’est une décision métier, à prendre avec le chef de production ou le responsable maintenance. Le rôle du data scientist est de présenter la courbe et les implications de chaque point — pas de décider seul.
Le seuil d’alerte optimisé sur le F1 global n’est pas forcément le bon seuil opérationnel. La décision finale appartient au métier, pas au modèle.
Recalibration périodique
Un modèle entraîné sur des données de janvier peut dériver en juillet. Les raisons sont multiples : changement de matière première, remplacement d’un capteur, modification du process, saisonnalité thermique du bâtiment. La recalibration est obligatoire, idéalement mensuelle sur les 30 derniers jours de production.
En pratique : mettre en place un script cron qui recalcule les métriques sur le mois glissant, envoie une alerte si le F1 chute de plus de 10 points, et déclenche une procédure de review. La boucle de feedback alimente le réentraînement automatique si les labels sont disponibles (faux positifs validés par opérateur + anomalies manquées via GMAO).
Quand utiliser quoi — arbre de décision
| Situation | Approche recommandée |
|---|---|
| Labels historiques propres (>50 anomalies) | XGBoost supervisé + scale_pos_weight |
| Labels très peu nombreux (<20) ou douteux | Isolation Forest + validation experte |
| Nouvelle machine, zéro historique | Isolation Forest pour 6 mois, puis hybride |
| Anomalies multitypes (catégories différentes) | XGBoost multi-class ou un IF par type |
| Contrainte edge temps réel <10ms | XGBoost (éviter IF pour les scores en streaming) |
| Explicabilité demandée par l’opérateur | XGBoost (SHAP values disponibles) |
| Budget de labellisation nul | Isolation Forest seul |
Les pièges à éviter
Entraîner sur les données de production sans holdout temporel. Le split train/test doit respecter l’ordre temporel : entraînement sur T-12M à T-3M, validation sur T-3M à T, test sur T à aujourd’hui. Un split aléatoire crée du data leakage temporel — le modèle “voit le futur” pendant l’entraînement, et ses métriques sont artificiellement bonnes.
Normaliser avec les paramètres du test set. StandardScaler doit être fit sur le train set uniquement, puis appliqué (transform) sur le test et la production. Une normalisation globale leak les statistiques du test dans le train.
Oublier les anomalies en cluster. Isolation Forest suppose que les anomalies sont des points isolés. Si votre process peut rester en état dégradé plusieurs heures (anomalie persistante = cluster de points anormaux), IF va sous-estimer leur score. Une variante : IForest avec un downsampling temporel pour briser les clusters avant entraînement.
Ignorer le concept drift. Un capteur qui dérive lentement va “polluer” le espace normal progressivement. Le modèle va s’adapter à la dérive et ne plus la détecter. Mécanisme indispensable : monitorer les distributions des features en production avec un test de Mann-Whitney ou Kolmogorov-Smirnov sur une fenêtre glissante. Si la distribution dérive significativement → alerte recalibration.
Conclusion : commencer simple, itérer vite
La tentation est de construire un pipeline sophistiqué dès le départ : autoencoder LSTM, attention mechanism, pipeline MLflow avec feature store. Résistez à cette tentation.
Commencez par un Isolation Forest sur des features statistiques de fenêtres glissantes. Déployez-le en production en 2 semaines. Collectez les retours opérateur pendant 2 mois. Utilisez les labels émergents pour entraîner un XGBoost. Calibrez le seuil avec le chef de maintenance. Puis — et seulement si les résultats métier le justifient — complexifiez.
Le système de détection qui améliore le ratio signal/bruit des alarmes de 10:1 à 3:1 a plus de valeur industrielle qu’un transformer state-of-the-art qui reste sur le laptop du data scientist.
Ce n’est pas la sophistication du modèle qui compte. C’est le nombre d’anomalies vraies détectées avant que la machine casse.