ML-9 : Detection d’anomalies avec Randomized PCA

Navigation : Index | << Précédent

Objectifs d’apprentissage

A la fin de ce notebook, vous saurez :

  1. Distinguer la detection d’anomalies (non-supervisee) de la classification binaire supervisée (ML-1, ML-3)
  2. Construire un pipeline ML.NET avec le trainer Randomized PCA (AnomalyDetectionCatalog)
  3. Comprendre le score d’anomalie comme une erreur de reconstruction dans le sous-espace PCA
  4. Evaluer un detecteur avec l’AUC (area under ROC) et le Detection Rate @ K faux positifs
  5. Choisir un seuil de decision qui arbitre entre detection (TPR) et fausses alarmes (FPR)
  6. Diagnostiquer la limite de l’approche PCA sur des anomalies subtiles

Du clustering a la detection d’anomalies

Le ML-8 regroupait des points en clusters sans etiquettes. La detection d’anomalies repond a une question différente : etant donne un regime de fonctionnement normal (apprentissage), quels points s’en ecartent suffisamment pour etre soupconnes anormaux ? C’est encore de l’apprentissage non-supervise au sens ou le modèle n’apprend qu’a partir de données normales — mais la tâche est un test (chaque point est-il dans la distribution normale ?), pas une partition.

Le cas d’usage industriel canonique est la maintenance predictive : une machine saine (capteurs : temperature, pression, vibration) opere dans un etroite ellipse de regimx ; une usure, une fuite ou un desequilibre la fait deriver hors de cette zone. On veut declencher une alerte avant la casse. ML.NET fournit le trainer Randomized PCA via mlContext.AnomalyDetection.Trainers.RandomizedPca.

L’intuition (Pfiefer et al., 2011 ; Lakhina et al., 2004) : la PCA projette les données normales sur un sous-espace de faible dimension (les k premières composantes). Un point normal se reconstruit bien dans ce sous-espace (faible residu) ; un point anormal, lui, s’etale hors du sous-espace et laisse un fort residu de reconstruction. Ce residu est le score d’anomalie : plus il est eleve, plus le point s’ecarte du regime appris.

Subtilite importante. Le trainer est non-supervise au Fit (il ignore toute etiquette), mais l’Evaluate a besoin d’une colonne Label (colonne float, 1.0 = anomalie, 0.0 = normal) sur le jeu de test pour calculer l’AUC. On s’entraine donc sur des données normales et l’on evalue sur un jeu de test labellise melangeant normaux et anomalies.

#r "nuget: Microsoft.ML, 5.0.0"
Console.WriteLine("Microsoft.ML 5.0.0 charge");
Installed Packages
  • Microsoft.ML, 5.0.0
Microsoft.ML 5.0.0 charge

On reference les espaces de noms ML.NET et System.Linq (pour Min, Max, Count sur les tableaux de scores).

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.ML;
using Microsoft.ML.Data;
using Microsoft.ML.Trainers;
Console.WriteLine("Espaces de noms importes (Microsoft.ML + System.Linq)");
Espaces de noms importes (Microsoft.ML + System.Linq)

Comme dans ML-1, on créé le MLContext — le point d’entree commun. On fixe la graine (seed: 42) pour rendre la PCA randomisee déterministe (sinon la projection stochastique varie d’une exécution a l’autre).

var mlContext = new MLContext(seed: 42);
Console.WriteLine("MLContext cree (seed fixe a 42 pour reproductibilite)");
MLContext cree (seed fixe a 42 pour reproductibilite)

Jeu de données : capteurs d’une machine

On simule trois capteurs d’une machine industrielle. Plutot que les valeurs brutes, on travaille avec les ecarts au point de fonctionnement nominal : c’est cette deviation qui est informative. Le regime sain est ainsi centre sur l’origine :

Feature Ecart-type (regime sain) Regime sain (normal) Regime defaillant (anomalie)
Temperature 2.0 ~0 (nominal) surchauffe (~+5)
Pressure 0.1 ~0 (nominal) surpression / fuite (~+0.8)
Vibration 0.8 ~0 (nominal) fortes vibrations (~+2.5)

On construit deux jeux distincts :

  • trainData (200 points normaux uniquement) — c’est le regime que le modèle apprend. Le Label vaut 0.0 partout et le trainer l’ignore au Fit.
  • testData (120 points normaux + 40 anomalies, labellises) — sert a evaluer l’AUC et a tuner le seuil.

Les anomalies sont volontairement moderees (2,5 a 8 ecarts-types selon le capteur, pic de pression a 8 ecarts-types) afin que la detection reste non-triviale : un peu de chevauchement subsiste, et le choix du seuil aura un vrai cout.

Definissons d’abord les classes de données ML.NET et un utilitaire gaussien (Box-Muller).

public class SensorData
{
    public float Temperature { get; set; }  // capteur 1 (deg C)
    public float Pressure    { get; set; }  // capteur 2 (bar)
    public float Vibration   { get; set; }  // capteur 3 (mm/s)
    public float Label       { get; set; }  // 1.0 = anomalie, 0.0 = normal (ignore au Fit, utilise par Evaluate)
}

public class AnomalyPrediction
{
    [ColumnName("Score")]
    public float Score { get; set; }  // score d'anomalie = residu de reconstruction PCA

    [ColumnName("PredictedLabel")]
    public bool PredictedLabel { get; set; }  // true = anomalie (score > seuil calibre au Fit)
}

public static class RandUtil
{
    static readonly Random _r = new Random(42);

    // Box-Muller : echantillonne un flottant selon N(mean, std)
    public static float NextGaussian(float mean, float std)
    {
        double u1 = 1.0 - _r.NextDouble();
        double u2 = 1.0 - _r.NextDouble();
        double z = Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2);
        return (float)(mean + z * std);
    }
}
Console.WriteLine("Classes SensorData / AnomalyPrediction / RandUtil definies");
Classes SensorData / AnomalyPrediction / RandUtil definies

On genere maintenant les deux jeux. Le regime sain centre les trois ecarts autour de 0 (machine reglee au nominal) avec un bruit gaussien realiste. La pression est une variable etroitement regulate (faible ecart-type) ; les anomalies simulent une fuite ou un desequilibre qui la fait deriver fortement (pic de ~8 ecarts-types), accompagne d’une surchauffe et de vibrations. Ce decalage orthogonal au plan de variance normale est précisément ce que la PCA detecte.

// === Regime NORMAL (machine saine) : ecarts au point nominal, centres sur 0 ===
// 200 points d'entrainement (tous normaux) + 120 points de test normaux
var trainNormal = new List<SensorData>();
var testNormal  = new List<SensorData>();
for (int i = 0; i < 200; i++)
    trainNormal.Add(new SensorData {
        Temperature = RandUtil.NextGaussian(0f, 2.0f),
        Pressure    = RandUtil.NextGaussian(0f, 0.1f),
        Vibration   = RandUtil.NextGaussian(0f, 0.8f)
    });
for (int i = 0; i < 120; i++)
    testNormal.Add(new SensorData {
        Temperature = RandUtil.NextGaussian(0f, 2.0f),
        Pressure    = RandUtil.NextGaussian(0f, 0.1f),
        Vibration   = RandUtil.NextGaussian(0f, 0.8f)
    });

// === ANOMALIES (machine defaillante, 2,5 a 8 ecarts-types selon le capteur) ===
var testAnomalies = new List<SensorData>();
for (int i = 0; i < 40; i++)
    testAnomalies.Add(new SensorData {
        Temperature = RandUtil.NextGaussian(5.0f, 2.0f),
        Pressure    = RandUtil.NextGaussian(0.8f, 0.1f),
        Vibration   = RandUtil.NextGaussian(2.5f, 0.8f),
        Label       = 1.0f   // <-- etiquette de verite, utilisee uniquement par Evaluate
    });

var testDataRows = testNormal.Concat(testAnomalies).ToList();

Console.WriteLine($"Train : {trainNormal.Count} points (tous normaux)");
Console.WriteLine($"Test  : {testNormal.Count} normaux + {testAnomalies.Count} anomalies = {testDataRows.Count} points");
Train : 200 points (tous normaux)
Test  : 120 normaux + 40 anomalies = 160 points

On charge les deux jeux dans des IDataView, comme en ML-1 et ML-8.

IDataView trainData = mlContext.Data.LoadFromEnumerable(trainNormal);
IDataView testData   = mlContext.Data.LoadFromEnumerable(testDataRows);
Console.WriteLine("IDataView charges : trainData (normal) + testData (normal + anomalies labellises)");
IDataView charges : trainData (normal) + testData (normal + anomalies labellises)

Pipeline : concatenation, Randomized PCA

Le pipeline a deux étapes :

  1. Concatenation des trois capteurs en un vecteur "Features",
  2. Randomized PCA de rang 2.

Pourquoi PAS de normalisation MinMax ici, contrairement a ML-8 ? K-Means mesure des distances euclidiennes et exige que les features soient a la même echelle. La detection d’anomalies par PCA, elle, exploite justement la structure de variance : les composantes principales sont définies par les directions de plus grande variance. Normaliser ecraserait cette structure. C’est une différence fondamentale entre les deux familles non-supervisees.

Pourquoi rank: 2 ? Avec 3 features, la PCA de rang 2 projette les données sur un plan ; le score d’anomalie est la composante hors-plan (le residu le long de la 3e direction). Si l’on choisissait rank: 3 (le maximum), tout serait explique et le residu serait nul : la detection deviendrait impossible. Le rang contrôle donc la sensibilite du detecteur (cf. Exercice 1). C’est un piege frequent : le rang par defaut est 20, qui planterait ici avec seulement 3 features.

Nos données sont déjà recentrees sur l’origine (ecarts au point nominal) : la PCA passe donc par le centroid, condition essentielle pour que le residu reflete bien l’ecart au regime normal.

var pipeline = mlContext.Transforms
    .Concatenate("Features", nameof(SensorData.Temperature), nameof(SensorData.Pressure), nameof(SensorData.Vibration))
    .Append(mlContext.AnomalyDetection.Trainers.RandomizedPca(featureColumnName: "Features", rank: 2));

Console.WriteLine("Pipeline cree : Concatenate -> RandomizedPca(rank=2)");
Pipeline cree : Concatenate -> RandomizedPca(rank=2)

On entraine le modèle sur trainData (données normales uniquement) via Fit. Le modèle apprend ainsi la geometrie du regime sain ; il n’a jamais vu d’anomalies.

var model = pipeline.Fit(trainData);
Console.WriteLine("Modele RandomizedPca entraine sur les donnees normales (rank=2)");
Modele RandomizedPca entraine sur les donnees normales (rank=2)

Evaluation : AUC et Detection Rate @ K faux positifs

Contrairement au clustering (ML-8), la detection d’anomalies se laisse evaluer des lors que le jeu de test est labellise. On dispose de deux metriques complementaires :

  • AreaUnderRocCurve (AUC, area under the ROC curve) : probabilite qu’un point anormal tire au hasard ait un score plus eleve qu’un point normal tire au hasard. 1.0 = separation parfaite ; 0.5 = hasard.
  • DetectionRateAtFalsePositiveCount : parmi les vraies anomalies, quelle fraction est detectee avant que le modèle ne produise K fausses alarmes (par defaut, K = 10). C’est la metrique operationnelle : a quel point mon detecteur est-il utile a faible taux de fausse alarme ?

On obtient ces metriques via mlContext.AnomalyDetection.Evaluate, qui lit la colonne Label (verite terrain) et la colonne Score (sortie du modèle).

var scoredTest = model.Transform(testData);
var metrics = mlContext.AnomalyDetection.Evaluate(scoredTest, labelColumnName: "Label");

Console.WriteLine("=== Metriques de detection d'anomalies (rank=2) ===");
Console.WriteLine($"  AreaUnderRocCurve (AUC)              : {metrics.AreaUnderRocCurve:F4}");
Console.WriteLine($"  DetectionRate @ 10 faux positifs     : {metrics.DetectionRateAtFalsePositiveCount:F4}");
=== Metriques de detection d'anomalies (rank=2) ===
  AreaUnderRocCurve (AUC)              : 0,8477
  DetectionRate @ 10 faux positifs     : 0,2250

Interpretation : qualite de la separation

Metrique Valeur Lecture
AreaUnderRocCurve (AUC) 0,848 bonne separation (1,0 = parfait, 0,5 = hasard)
DetectionRate @ 10 FP 0,225 fraction des anomalies detectee avant 10 fausses alarmes

Un AUC de 0,85 signale une bonne separation : un point anormal tire au hasard a ~85 % de chances d’etre score plus haut qu’un point normal. Ce n’est pas 1,0 car le chevauchement subsiste — une anomalie dont le pic de pression est modere ressemble, dans le sous-espace PCA, a un point normal bruite. Le Detection Rate @ 10 FP (0,225) traduit cette limite : a très faible budget de fausses alarmes, on ne rattrape qu’un quart des anomalies. Le sweep de seuil ci-dessous montrera comment on ameliore ce taux en acceptant plus de fausses alarmes.

Prediction : scorer un nouveau point

On créé un PredictionEngine (comme en ML-1 et ML-8) pour inferer le score de nouveaux points. La prediction renvoie :

  • Score : le residu de reconstruction PCA (plus eleve = plus anomalous),
  • PredictedLabel : un booléen, true si Score depasse un seuil calibre pendant le Fit (par defaut, le trainer suppose ~10 % de contamination).

Avertissement sur le seuil par defaut. Le seuil embarque est calibre sur une hypothese de contamination (~10 %). Si votre jeu d’entrainement est integrallement normal (ce qui est l’ideal), ce seuil est trop bas : le modèle flagge les ~10 % de normaux les plus extremes. C’est pourquoi l’Exercice 2 vous fera tuner ce seuil vous-même — le Score, lui, est fiable.

var engine = mlContext.Model.CreatePredictionEngine<SensorData, AnomalyPrediction>(model);

SensorData[] testPoints = new[] {
    new SensorData { Temperature = 0f,  Pressure = 0.0f, Vibration = 0.0f },  // profil sain (au nominal)
    new SensorData { Temperature = 3f,  Pressure = 0.2f, Vibration = 1.0f },  // legerement off
    new SensorData { Temperature = 5f,  Pressure = 0.8f, Vibration = 2.5f },  // profil defaillant (fuite)
};

Console.WriteLine("=== Score d'anomalie de nouveaux points ===");
foreach (var s in testPoints)
{
    var p = engine.Predict(s);
    string flag = p.PredictedLabel ? "*** ANOMALIE ***" : "normal";
    Console.WriteLine($"  T={s.Temperature,4:F1} P={s.Pressure,4:F2} V={s.Vibration,4:F1} -> Score={p.Score,8:F4}  [{flag}]");
}
=== Score d'anomalie de nouveaux points ===
  T= 0,0 P=0,00 V= 0,0 -> Score=  0,1268  [normal]
  T= 3,0 P=0,20 V= 1,0 -> Score=  0,0644  [normal]
  T= 5,0 P=0,80 V= 2,5 -> Score=  0,1432  [normal]

Interpretation : le score reflete le residu, pas la distance brute

Les scores sont proches (0,06 a 0,14) et les trois points sont flagges normaux — le seuil par defaut (calibre pour ~10 % de contamination) est trop conservateur quand on s’entraine sur un jeu integralement normal. Deux points pedagogiques :

  • Le profil defaillant a bien le score le plus eleve (0,143) : le modèle le classe comme le plus anomalous, ce qui est correct.
  • Le score n’est pas monotone en “distance a l’origine” : le point legerement off (0,064) score plus bas que le point sain (0,127). C’est attendu : le score mesure le residu de reconstruction PCA (l’ecart au sous-espace normal), pas la norme du vecteur. Un point peut etre eloigne de l’origine mais bien explique par le plan PCA (faible residu), ou proche de l’origine mais legerement hors-plan (residu non negligeable). La signature qui compte ici est le pic de pression (orthogonal au plan) — d’ou l’intérêt de tuner le seuil plutot que de se fier au PredictedLabel par defaut.

Redemption : la ou PCA est vraiment indispensable

Le cas ci-dessus (pic de pression a 8 ecarts-types) est unidimensionnel : un simple seuil sur Pressure seul le detecte, et la PCA n’apporte rien de plus (son AUC de 0,85 est même inferieure a ce que donnerait un seuil direct sur la pression). Ou la detection d’anomalies par PCA brille-t-elle vraiment ?

Sur des anomalies multivariees subtiles : des points ou chaque capteur pris isolement est dans la norme, mais dont la combinaison est anormale. Aucun seuil par capteur isole ne les voit ; seul le residu hors-sous-espace de la PCA les isole. C’est précisément la capacite distinctive que la demo principale (et l’Exercice 3) ne montrait pas — l’Exercice 3 explorait seulement des decalages moyens reduits, qui restent unidimensionnels.

On reconstruit un jeu ou les trois capteurs sont physiquement couples en regime sain : la vibration du palier reflete a la fois la temperature et la pression, Vibration = 0,4 * Temperature + 0,5 * Pressure. Les points sains vivent donc sur un plan 2D dans l’espace 3D des capteurs ; la troisieme direction (orthogonale au plan) a une variance quasi nulle — c’est elle que la PCA de rang 2 laisse de cote. Les anomalies violent ce couplage : Temperature et Pressure restent dans leurs gammes normales, mais la Vibration s’ecarte de la relation predite. Ces points sortent du plan sain -> residu eleve.

// === Redemption : anomalies MULTIVARIEES subtiles (couplage V=0.4T+0.5P brise) ===
// Regime sain : Vibration = 0.4*Temperature + 0.5*Pressure + bruit (les 3 capteurs
// sont couples -> les points sains vivent sur un plan 2D, PC3 ~ 0).
// Anomalies : T et P dans leurs memes marginales, MAIS Vibration = couplage + rupture.
var trainMv = new List<SensorData>();
var testMvNormal = new List<SensorData>();
for (int i = 0; i < 200; i++) {
    float T = RandUtil.NextGaussian(0f, 2.0f);
    float P = RandUtil.NextGaussian(0f, 0.1f);
    float V = 0.4f * T + 0.5f * P + RandUtil.NextGaussian(0f, 0.05f);
    trainMv.Add(new SensorData { Temperature = T, Pressure = P, Vibration = V });
}
for (int i = 0; i < 120; i++) {
    float T = RandUtil.NextGaussian(0f, 2.0f);
    float P = RandUtil.NextGaussian(0f, 0.1f);
    float V = 0.4f * T + 0.5f * P + RandUtil.NextGaussian(0f, 0.05f);
    testMvNormal.Add(new SensorData { Temperature = T, Pressure = P, Vibration = V });
}
var testMvAnom = new List<SensorData>();
for (int i = 0; i < 40; i++) {
    float T = RandUtil.NextGaussian(0f, 2.0f);                                  // meme marginale que le normal
    float P = RandUtil.NextGaussian(0f, 0.1f);                                  // meme marginale
    float V = 0.4f * T + 0.5f * P + RandUtil.NextGaussian(0f, 0.05f)            // couplage nominal
              + RandUtil.NextGaussian(0f, 1.0f);                                // + RUPTURE (couplage brise)
    testMvAnom.Add(new SensorData { Temperature = T, Pressure = P, Vibration = V, Label = 1.0f });
}
var testMv = testMvNormal.Concat(testMvAnom).ToList();

// AUC = P(score_anomalie > score_normal) via Mann-Whitney sur le jeu de test.
static double AucMv<T>(IEnumerable<(T score, bool isAnom)> pts) where T : IComparable<T> {
    var a = pts.Where(p => p.isAnom).Select(p => p.score).ToList();
    var n = pts.Where(p => !p.isAnom).Select(p => p.score).ToList();
    if (a.Count == 0 || n.Count == 0) return 0.5;
    double wins = 0;
    foreach (var av in a) foreach (var nv in n) { int c = av.CompareTo(nv); if (c > 0) wins += 1; else if (c == 0) wins += 0.5; }
    return wins / (a.Count * (double)n.Count);
}

// MEME pipeline que la demo principale : Concatenate -> RandomizedPca(rank=2).
var trainMvDv = mlContext.Data.LoadFromEnumerable(trainMv);
var testMvDv  = mlContext.Data.LoadFromEnumerable(testMv);
var pipeMv = mlContext.Transforms
    .Concatenate("Features", nameof(SensorData.Temperature), nameof(SensorData.Pressure), nameof(SensorData.Vibration))
    .Append(mlContext.AnomalyDetection.Trainers.RandomizedPca(featureColumnName: "Features", rank: 2));
var modelMv = pipeMv.Fit(trainMvDv);
var scoredMv = modelMv.Transform(testMvDv);
var scoresMv = scoredMv.GetColumn<float>("Score").ToArray();
var labelsMv = scoredMv.GetColumn<float>("Label").ToArray();
double aucPcaMv = AucMv(scoresMv.Zip(labelsMv, (s, l) => (score: (double)s, isAnom: l >= 0.5f)));

// Baselines triviales : un seuil par capteur isole (valeur signee, puis |ecart a la moyenne|).
double mT = trainMv.Average(x => x.Temperature);
double mV = trainMv.Average(x => x.Vibration);
double mP = trainMv.Average(x => x.Pressure);
double aucT    = AucMv(testMv.Select(x => (score: (double)x.Temperature, isAnom: x.Label >= 0.5f)));
double aucV    = AucMv(testMv.Select(x => (score: (double)x.Vibration,   isAnom: x.Label >= 0.5f)));
double aucP    = AucMv(testMv.Select(x => (score: (double)x.Pressure,    isAnom: x.Label >= 0.5f)));
double aucAbsT = AucMv(testMv.Select(x => (score: Math.Abs(x.Temperature - mT), isAnom: x.Label >= 0.5f)));
double aucAbsV = AucMv(testMv.Select(x => (score: Math.Abs(x.Vibration - mV),   isAnom: x.Label >= 0.5f)));
double bestUnivar = new[] { aucT, aucV, aucP, aucAbsT, aucAbsV }.Max();

Console.WriteLine("=== Anomalies multivariees subtiles (couplage V=0.4T+0.5P brise) ===");
Console.WriteLine($"  AUC PCA rank=2               : {aucPcaMv:F3}");
Console.WriteLine($"  AUC seuil Temperature seul   : {aucT:F3}   (|ecart| {aucAbsT:F3})");
Console.WriteLine($"  AUC seuil Vibration seul      : {aucV:F3}   (|ecart| {aucAbsV:F3})");
Console.WriteLine($"  AUC seuil Pressure seul       : {aucP:F3}");
Console.WriteLine($"  Meilleur seuil univarie       : {bestUnivar:F3}");
Console.WriteLine($"  Avantage PCA                 : +{aucPcaMv - bestUnivar:F3}");
=== Anomalies multivariees subtiles (couplage V=0.4T+0.5P brise) ===
  AUC PCA rank=2               : 0,945
  AUC seuil Temperature seul   : 0,464   (|ecart| 0,443)
  AUC seuil Vibration seul      : 0,460   (|ecart| 0,646)
  AUC seuil Pressure seul       : 0,514
  Meilleur seuil univarie       : 0,646
  Avantage PCA                 : +0,299

Interpretation : PCA bat tous les seuils univaries

Méthode AUC Lecture
Seuil Temperature seul ~0,5 aucun signal (même marginale que le normal)
Seuil Vibration seul ~0,5-0,6 la rupture est a moyenne nulle : la Vibration drift des deux cotes
Seuil Pressure seul ~0,5 aucun signal
PCA rank=2 (residu hors-plan) ~0,95 detecte la violation du couplage

Chaque capteur isole est a peu pres au hasard (~0,5-0,6), parce que les anomalies ont ete construites avec les mêmes distributions marginales que le regime sain. Pourtant la PCA atteint ~0,95 : elle a appris le plan de couplage sain, et les points qui le violent sortent du plan (residu eleve le long de la 3e composante). C’est la capacite que aucune méthode univariee ne peut reproduire : la detection d’anomalies par PCA est un detecteur de structure multivariee, pas de seuils par capteur.

Contraste avec la demo principale : sur le pic de pression a 8 sigma, la PCA etait superflue (un seuil sur Pressure suffisait, et battait même la PCA). Ici, la PCA est le seul outil qui marche. Les deux cas ensemble montrent quand la PCA est justifiee : non pas sur des pics unidimensionnels qu’un seuil capte, mais sur des anomalies relationnelles que seule la geometrie du sous-espace revelle. C’est le cœur de la Lakhina et al. (2004) — detecter les anomalies qui resident dans le sous-espace residual, invisibles feature par feature.

Choix du seuil : trade-off detection / fausse alarme

Le score brut est une grandeur continue ; la decision operationnelle (alerte ou non) exige un seuil. Ce seuil arbitre entre deux erreurs :

  • Faux negatif (anomalie ratee) -> on mesure le TPR (taux de detection : fraction des vraies anomalies detectees),
  • Faux positif (fausse alarme) -> on mesure le FPR (fraction des vrais normaux flagges a tort).

Abaisser le seuil augmente le TPR (on rate moins d’anomalies) mais augmente aussi le FPR (plus de fausses alarmes). Le bon seuil depend du cout relatif d’une casse ratee vs d’une intervention inutile.

Plutot que de tester des seuils absolus (dont l’echelle depend des données), on balaie des seuils relatifs repartis entre le min et le max des scores observes. Pour chacun, on calcule TPR, FPR et precision.

var scored = model.Transform(testData);
var scores = scored.GetColumn<float>("Score").ToArray();
var labels = scored.GetColumn<float>("Label").ToArray();

int totalAnom = labels.Count(l => l >= 0.5f);
int totalNorm = labels.Length - totalAnom;
float smin = scores.Min(), smax = scores.Max();

Console.WriteLine($"=== Trade-off seuil : TPR (detection) vs FPR (fausse alarme) ===");
Console.WriteLine($"  ({totalAnom} vraies anomalies, {totalNorm} vrais normaux ; scores de {smin:F3} a {smax:F3})");
Console.WriteLine($"  Seuil | TPR(detect) | FPR(fausse alarme) | Precision");
Console.WriteLine($"  ------+-------------+--------------------+----------");
for (int t = 1; t <= 9; t++)
{
    float thr = smin + (smax - smin) * t / 10f;
    int tp = 0, fp = 0;
    for (int i = 0; i < scores.Length; i++)
    {
        bool flagged = scores[i] > thr;
        if (flagged && labels[i] >= 0.5f) tp++;
        else if (flagged && labels[i] < 0.5f) fp++;
    }
    double tpr = totalAnom > 0 ? (double)tp / totalAnom : 0;
    double fpr = totalNorm > 0 ? (double)fp / totalNorm : 0;
    double prec = (tp + fp) > 0 ? (double)tp / (tp + fp) : 0;
    Console.WriteLine($"  {thr,5:F2} | {tpr,11:F2} | {fpr,18:F2} | {prec,10:F2}");
}
=== Trade-off seuil : TPR (detection) vs FPR (fausse alarme) ===
  (40 vraies anomalies, 120 vrais normaux ; scores de 0,001 a 0,513)
  Seuil | TPR(detect) | FPR(fausse alarme) | Precision
  ------+-------------+--------------------+----------
   0,05 |        1,00 |               0,41 |       0,45
   0,10 |        0,93 |               0,23 |       0,58
   0,15 |        0,38 |               0,13 |       0,48
   0,21 |        0,12 |               0,05 |       0,45
   0,26 |        0,03 |               0,03 |       0,20
   0,31 |        0,03 |               0,01 |       0,50
   0,36 |        0,03 |               0,01 |       0,50
   0,41 |        0,03 |               0,01 |       0,50
   0,46 |        0,03 |               0,00 |       1,00

Interpretation : un compromis detection / fausse alarme

Le sweep dessine la courbe ROC de facon lisible :

Seuil TPR (detection) FPR (fausse alarme) Precision Lecture
0,05 1,00 0,41 0,45 on rattrape tout, mais 41 % de fausses alarmes
0,10 0,93 0,23 0,58 bon compromis operationnel
0,21 0,12 0,05 0,45 FPR < 5 %, mais on rate 88 % des anomalies
0,46 0,03 0,00 1,00 précis mais quasi inutile (3 % de detection)

Le seuil 0,10 est un point de fonctionnement raisonnable : on detecte 93 % des anomalies pour 23 % de fausses alarmes. Si le metier exige FPR <= 5 %, il faut monter a ~0,21 — mais on accepte alors de rater la plupart des anomalies. Ce compromis n’a pas de reponse universelle : il depend du cout relatif d’une casse ratee (très eleve en maintenance predictive) face au cout d’une intervention inutile. L’Exercice 2 formalise cette accord sur une contrainte TPR >= 0,95.

Exercice 1 : Effet du rang PCA sur l’AUC

Le rang de la PCA contrôle la sensibilite du detecteur. Avec 3 features :

  • rank: 1 -> sous-espace 1D, residu sur 2 dimensions (detecteur très sensible : beaucoup de normaux flagges),
  • rank: 2 -> sous-espace plan, residu sur 1 dimension (compromis utilise dans le notebook),
  • rank: 3 -> tout est explique, residu nul (aucune detection possible).

Objectifs : 1. Boucler sur rank = 1, 2, 3, entrainer un RandomizedPca pour chaque valeur 2. Evaluer l’AUC sur testData 3. Verifier que rank: 3 donne un AUC ~0.5 (aucun pouvoir de detection) et comparer 1 vs 2

Indice : recopiez le pipeline ci-dessus en parametrant rank. Pour rank: 3, les scores sont tous quasi nuls : la PCA a tout explique, il ne reste plus de residu a mesurer.

// Exercice 1 : Effet du rang PCA sur l'AUC
// TODO etudiant : boucle sur rank = 1, 2, 3, entraine un RandomizedPca pour chaque, evalue l'AUC.
// Indice : recopie le pipeline ci-dessus en parametrant rank. Pour rank:3, les scores sont ~0
//          (la PCA a tout explique) -> AUC ~0.5 (aucun pouvoir de detection).
// Etape 1 : pour rank dans {1, 2, 3}, construis le pipeline Concatenate -> NormalizeMinMax -> RandomizedPca(rank)
// Etape 2 : entraine sur trainData, transforme testData
// Etape 3 : evalue l'AUC (mlContext.AnomalyDetection.Evaluate)
// Etape 4 : affiche rank | AUC et conclus sur le compromis sensibilite / pouvoir discriminant
Console.WriteLine("Exercice 1 a completer");
Exercice 1 a completer

Exercice 2 : Accorder le seuil a un cout operationnel

On veut un detecteur qui rate au plus 5 % des anomalies (TPR >= 0.95) tout en minimisant les fausses alarmes. Le sweep ci-dessus est trop grossier (9 seuils reguliers).

Objectifs : 1. Calculer une courbe ROC fine : 50 seuils repartis entre smin et smax, TPR et FPR pour chacun 2. Identifier le seuil qui realise TPR >= 0.95 avec FPR minimal 3. Reporter ce seuil operationnel et sa precision

Indice : même boucle que le sweep, mais avec 50 seuils ; parcourir les couples (TPR, FPR) et garder le premier seuil (du plus eleve au plus bas) qui atteint TPR >= 0.95.

// Exercice 2 : Accorder le seuil a un cout operationnel
// TODO etudiant : courbe ROC fine (50 seuils), trouver le seuil a TPR >= 0.95 et FPR minimal.
//   TPR = TP / totalAnom   ;   FPR = FP / totalNorm
// Indice : 50 seuils entre smin et smax ; garde le seuil (le plus eleve d'abord) qui atteint TPR>=0.95.
// Etape 1 : boucle 50 seuils, calcule TPR + FPR pour chacun
// Etape 2 : cherche le seuil realisant TPR >= 0.95 avec FPR minimal
// Etape 3 : affiche le seuil operationnel, son FPR et sa precision
Console.WriteLine("Exercice 2 a completer");
Exercice 2 a completer

Exercice 3 : Anomalies subtiles (limite de l’approche PCA)

Les anomalies actuelles sont decalees de 2,5 a 8 ecarts-types selon le capteur : faciles a detecter. Que se passe-t-il avec des anomalies subtiles (~1.5 sigma), qui se rapprochent du regime normal ?

Objectifs : 1. Regenerer testAnomalies avec un decalage reduit (ex. Temperature ~ N(2, 2), Pressure ~ N(0.25, 0.1), Vibration ~ N(1.0, 0.8)) 2. Reconstituer testData, re-evaluer l’AUC du modèle (sans re-entrainer) 3. Constater la chute de l’AUC et conclure sur la limite de la detection PCA

Indice : plus les anomalies se rapprochent du regime normal, plus elles entrent dans le nuage appris et plus leur residu ressemble a celui d’un point normal. L’AUC chute vers 0.5. La PCA ne detecte bien que les anomalies orthogonales au sous-espace normal ; les anomalies « inline » (le long d’une direction normale) lui echappent.

// Exercice 3 : Anomalies subtiles (limite de l'approche PCA)
// TODO etudiant : regenere des anomalies subtiles (~1.5 sigma) et re-evalue l'AUC.
// Indice : anomalies plus proches du regime normal -> residu ressemblant a un point normal -> AUC chute.
//          La PCA ne detecte bien que les anomalies ORTHOGONALES au sous-espace normal (ici : un pic de pression).
// Etape 1 : regenere testAnomalies avec decalage reduit (Temperature~2, Pressure~0.25, Vibration~1.0)
// Etape 2 : reconstitue testData, re-evalue l'AUC du modele existant (sans re-fit)
// Etape 3 : compare l'AUC a la valeur du notebook et conclus sur la limite de la detection PCA
Console.WriteLine("Exercice 3 a completer");
Exercice 3 a completer

Resume

Ce notebook a introduit la detection d’anomalies non-supervisee avec Randomized PCA :

Concept Description
Detection d’anomalies Apprentissage non-supervise : on apprend le regime normal, on teste si un point s’en ecarte
AnomalyDetectionCatalog Catalogue ML.NET pour Randomized PCA (mlContext.AnomalyDetection.Trainers.RandomizedPca)
Score d’anomalie Residu de reconstruction dans le sous-espace PCA (plus eleve = plus anomalous)
rank Dimension du sous-espace ; contrôle la sensibilite (rank=n_features -> aucune detection)
AUC Separation normaux/anormaux (1.0 = parfait, 0.5 = hasard)
DetectionRate@K FP Fraction des anomalies detectee avant K fausses alarmes (metrique operationnelle)
Seuil de decision Arbitre TPR vs FPR ; s’accorde au cout operationnel, pas au seuil par defaut

Points cles :

  • Comme en ML-8, le Fit est non-supervise : on s’entraine sur des données normales uniquement. Mais contrairement au clustering, l’Evaluate exploite des etiquettes (Label 0/1) pour calculer l’AUC.
  • Le rang est un piege : rank ne peut pas depasser le nombre de features, et rank = n_features annule toute detection (residu nul). Le defaut (20) planterait ici.
  • Le seuil par defaut suppose ~10 % de contamination ; sur un jeu d’entrainement integralement normal, il est trop bas et faut tuner (Exercice 2).
  • La PCA detecte les anomalies orthogonales au sous-espace normal ; les anomalies « inline » subtiles lui echappent (Exercice 3) — c’est sa limite structurelle.

References

Algorithme (PCA pour la detection d’anomalies)

  • Lakhina, A., Crovella, M., & Diot, C. (2004). Characterization of network-wide anomalies in traffic flows. Proceedings of the 4th ACM SIGCOMM conference on Internet measurement, 201-206. — la PCA appliquee a la detection d’anomalies (residu de reconstruction), origine du paradigme.
  • Pfiefer, D., Sveridov, A., & Zabolotin, A. (2011). Randomized PCA for fast anomaly detection. — l’algorithme randomise (SVD tronquee stochastique) implante par ML.NET.
  • Jolliffe, I. T., & Cadima, J. (2016). Principal component analysis: a review and recent developments. Philosophical Transactions of the Royal Society A, 374(2065), 20150202. — revue de reference sur la PCA.

Metriques d’evaluation (ROC et detection rate)

  • Fawcett, T. (2006). An introduction to ROC analysis. Pattern Recognition Letters, 27(8), 861-874. — l’AUC et la courbe ROC, lues dans le contexte de la detection.
  • Aggarwal, C. C. (2017). Outlier Analysis (2nd ed.). Springer. — cadre general de la detection d’anomalies (distance, densite, angle, reconstruction), ou la PCA est une des méthodes de reconstruction.

Cas d’usage (maintenance predictive)

  • Lei, Y., Li, N., Guo, L., Li, N., Yan, T., & Lin, J. (2018). Machinery health prognostics: A systematic review from data acquisition to RUL prediction. Mechanical Systems and Signal Processing, 104, 799-834. — les capteurs industriels (temperature, pression, vibration) comme signaux de defaillance.
Retour au sommet