Repérer une anomalie dans une métrique

Être réveillé quand une métrique de production sort de son comportement habituel, et pas le reste du temps.

Pas besoin d’IA

GratuitInstantanéVos données ne sortent pas

Le verdict

N0 est le seul niveau dont la sortie est le raisonnement : la valeur mesurée, ce que la fenêtre appelait habituel, l’écart, et l’écart qui était toléré. C’est la moitié du diagnostic livrée avec l’alerte, sans coût marginal, et le test la vérifie nombre par nombre. Mais ne branchez pas `anomalies` sur un téléphone : aux réglages par défaut, une métrique saine échantillonnée à la minute y met une dizaine de points par jour, le test le mesure, et une marche franche en met une douzaine pour un seul incident. C’est `episodes` qu’un téléphone lit : il faut trois minutes consécutives hors limite pour qu’un épisode compte, et un épisode déjà ouvert ne sonne pas une seconde fois — sur une semaine de bruit, le test compte au plus un épisode. Ce détecteur sert de toute façon mieux un tableau de bord qu’un réveil : le livre SRE de Google, cité plus bas, conseille d’alerter sur les symptômes que voit l’utilisateur plutôt que sur l’inhabituel. N1 juge les métriques ensemble, contre tout son entraînement, là où N0 juge chaque série contre sa propre fenêtre : il voit la minute dont chaque valeur est connue et dont seule la combinaison ne l’est pas — le test la construit et montre que les trois séries, prises une à une, ne sonnent pas — et le paie en rendant un rang entre zéro et un : on apprend que la minute était inhabituelle, pas en quoi.

Seuil robuste sur médiane et écart absolu médian, en fenêtre glissante, avec règle de persistance

Pas besoin d’IA

Règle et algorithme classiqueN0GratuitInstantané

Python

snippets/detect-anomalies-in-metrics/n0.pyTesté tel quel — Cet extrait s’exécute avec ses vraies dépendances, et son test tourne à chaque construction du site.
"""
Signaler les anomalies d'une métrique par un seuil robuste sur une fenêtre
glissante.

Niveau N0. Médiane et écart absolu médian, sans dépendance, et un verdict
qui se lit à trois heures du matin.

La moyenne et l'écart type sont les mauvais outils ici. Un seul incident
entraîne les deux : un pic assez gros élargit donc la bande même qui devait
l'attraper. La médiane et l'écart absolu médian tiennent tant que les
valeurs aberrantes occupent moins de la moitié de la fenêtre : l'habituel
reste parmi les valeurs ordinaires, et l'écart toléré s'élargit à mesure
qu'elles s'accumulent au lieu de bondir avec la première. C'est la propriété
qu'il faut à une alerte.

Chaque verdict porte les nombres dont il est fait. « Anomalie à 03:12 »
oblige la personne réveillée à reconstruire le raisonnement avant d'agir ;
« mesuré 4800, habituel 1200, toléré jusqu'à 1511 » est déjà la moitié du
diagnostic, et ce sont les trois mêmes nombres que le seuil a employés.

Un point hors limite n'est pas une alerte. Sur du bruit pur, un point sur
cent à deux sur cent franchit la limite aux réglages par défaut : sur une
métrique échantillonnée à la minute, cela fait dix à vingt réveils par jour,
par métrique, pour rien. Et une vraie marche franche franchit la limite
autant de minutes que la fenêtre met à l'avaler, soit une douzaine de réveils
pour un seul incident. C'est `episodes` qu'un téléphone doit lire : une série
de points doit tenir `consecutive` minutes pour compter, et un épisode déjà
ouvert ne sonne pas une seconde fois. `anomalies` reste, point par point,
pour un tableau de bord.
"""

from dataclasses import dataclass
from statistics import median

# Scaling that puts the median absolute deviation on the same footing as a
# standard deviation for normally distributed data. Estimated on a short
# window it is noisy: pure Gaussian noise crosses 3.5 of these far more often
# than it crosses 3.5 true standard deviations, and the tests measure how often.
NORMAL_SCALE = 1.4826


@dataclass
class Verdict:
    """One judged point, and the whole reasoning behind the judgement."""

    index: int
    value: float
    usual: float  # median of the window that came before
    deviation: float  # how far the point sits from that median
    limit: float  # how far it was allowed to sit
    is_anomaly: bool


def scan(series: list[float], window: int = 24, threshold: float = 3.5,
         min_spread: float = 0.0) -> list[Verdict]:
    """
    Judge every point against the `window` points that precede it.

    The first `window` points get no verdict at all. A point cannot be
    compared with a history that does not exist yet, and saying nothing is
    more honest than comparing it with a shorter, noisier window.

    `min_spread` is the smallest spread the metric may have, in its own unit.
    A count that is zero most minutes, such as errors, has a median absolute
    deviation of zero, and every single error would ring: pass 1 for it.
    """
    if window < 1:
        raise ValueError("window must hold at least one point")
    verdicts = []
    for index in range(window, len(series)):
        reference = series[index - window : index]
        usual = median(reference)
        spread = max(NORMAL_SCALE * median([abs(value - usual) for value in reference]), min_spread)
        deviation = abs(series[index] - usual)
        limit = threshold * spread
        verdicts.append(Verdict(index, series[index], usual, deviation, limit, deviation > limit))
    return verdicts


def anomalies(series: list[float], window: int = 24, threshold: float = 3.5,
              min_spread: float = 0.0) -> list[Verdict]:
    """
    Every point over the limit, each one still carrying its numbers.

    This is the dashboard view, not the pager view: at the default settings a
    healthy metric puts one point in a hundred here. `episodes` is the pager
    view.
    """
    return [verdict for verdict in scan(series, window, threshold, min_spread) if verdict.is_anomaly]


@dataclass
class Episode:
    """A run of points over the limit, reported once."""

    start: int  # index of the first point of the run
    length: int  # how many points in a row stayed over the limit
    opened_by: Verdict  # the first point, with the numbers that judged it
    worst: Verdict  # the point that sat furthest outside its limit


def episodes(series: list[float], window: int = 24, threshold: float = 3.5,
             min_spread: float = 0.0, consecutive: int = 3) -> list[Episode]:
    """
    What a pager should see: one entry per run of points over the limit.

    Two rules, and they are the ones every on-call rota ends up adding. A run
    has to hold for `consecutive` points before it counts, which is what keeps
    the noise of a healthy metric off the phone. And a run is reported once,
    not once per minute, which is what keeps one step change from paging a
    dozen times.

    `consecutive = 1` covers every point of `anomalies`, runs of them grouped
    into one episode each.
    """
    if consecutive < 1:
        raise ValueError("consecutive must be at least one point")
    found, run = [], []
    for verdict in scan(series, window, threshold, min_spread) + [None]:
        if verdict is not None and verdict.is_anomaly:
            run.append(verdict)
            continue
        if len(run) >= consecutive:
            worst = max(run, key=lambda item: item.deviation - item.limit)
            found.append(Episode(run[0].index, len(run), run[0], worst))
        run = []
    return found

JavaScript

snippets/detect-anomalies-in-metrics/n0.jsTesté tel quel — Cet extrait s’exécute avec ses vraies dépendances, et son test tourne à chaque construction du site.
/**
 * Signaler les anomalies d'une métrique par un seuil robuste sur une fenêtre
 * glissante.
 *
 * Niveau N0. Médiane et écart absolu médian, sans dépendance, et un verdict
 * qui se lit à trois heures du matin.
 *
 * La moyenne et l'écart type sont les mauvais outils ici. Un seul incident
 * entraîne les deux : un pic assez gros élargit donc la bande même qui devait
 * l'attraper. La médiane et l'écart absolu médian tiennent tant que les
 * valeurs aberrantes occupent moins de la moitié de la fenêtre : l'habituel
 * reste parmi les valeurs ordinaires, et l'écart toléré s'élargit à mesure
 * qu'elles s'accumulent au lieu de bondir avec la première. C'est la propriété
 * qu'il faut à une alerte.
 *
 * Chaque verdict porte les nombres dont il est fait. « Anomalie à 03:12 »
 * oblige la personne réveillée à reconstruire le raisonnement avant d'agir ;
 * « mesuré 4800, habituel 1200, toléré jusqu'à 1511 » est déjà la moitié du
 * diagnostic, et ce sont les trois mêmes nombres que le seuil a employés.
 *
 * Un point hors limite n'est pas une alerte. Sur du bruit pur, un point sur
 * cent à deux sur cent franchit la limite aux réglages par défaut : sur une
 * métrique échantillonnée à la minute, cela fait dix à vingt réveils par jour,
 * par métrique, pour rien. Et une vraie marche franche franchit la limite
 * autant de minutes que la fenêtre met à l'avaler, soit une douzaine de réveils
 * pour un seul incident. C'est `episodes` qu'un téléphone doit lire : une série
 * de points doit tenir `consecutive` minutes pour compter, et un épisode déjà
 * ouvert ne sonne pas une seconde fois. `anomalies` reste, point par point,
 * pour un tableau de bord.
 */

// Scaling that puts the median absolute deviation on the same footing as a
// standard deviation for normally distributed data. Estimated on a short
// window it is noisy: pure Gaussian noise crosses 3.5 of these far more often
// than it crosses 3.5 true standard deviations, and the tests measure how often.
const NORMAL_SCALE = 1.4826;

/** Middle value, or the average of the two middle ones on an even count. */
function median(values) {
  const sorted = [...values].sort((a, b) => a - b);
  const middle = Math.floor(sorted.length / 2);
  return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
}

/**
 * Judge every point against the `window` points that precede it.
 *
 * The first `window` points get no verdict at all. A point cannot be compared
 * with a history that does not exist yet, and saying nothing is more honest
 * than comparing it with a shorter, noisier window.
 *
 * `minSpread` is the smallest spread the metric may have, in its own unit. A
 * count that is zero most minutes, such as errors, has a median absolute
 * deviation of zero, and every single error would ring: pass 1 for it.
 *
 * Each verdict is the whole reasoning: the value, what the window called
 * usual, how far the value sat from it, and how far it was allowed to sit.
 */
export function scan(series, { window = 24, threshold = 3.5, minSpread = 0 } = {}) {
  if (!(window >= 1)) throw new RangeError('window must hold at least one point');
  const verdicts = [];
  for (let index = window; index < series.length; index += 1) {
    const reference = series.slice(index - window, index);
    const usual = median(reference);
    const spread = Math.max(NORMAL_SCALE * median(reference.map((value) => Math.abs(value - usual))), minSpread);
    const deviation = Math.abs(series[index] - usual);
    const limit = threshold * spread;
    verdicts.push({
      index,
      value: series[index],
      usual,
      deviation,
      limit,
      isAnomaly: deviation > limit,
    });
  }
  return verdicts;
}

/**
 * Every point over the limit, each one still carrying its numbers.
 *
 * This is the dashboard view, not the pager view: at the default settings a
 * healthy metric puts one point in a hundred here. `episodes` is the pager
 * view.
 */
export function anomalies(series, options = {}) {
  return scan(series, options).filter((verdict) => verdict.isAnomaly);
}

/**
 * What a pager should see: one entry per run of points over the limit.
 *
 * Two rules, and they are the ones every on-call rota ends up adding. A run has
 * to hold for `consecutive` points before it counts, which is what keeps the
 * noise of a healthy metric off the phone. And a run is reported once, not once
 * per minute, which is what keeps one step change from paging a dozen times.
 *
 * `consecutive: 1` covers every point of `anomalies`, runs of them grouped into
 * one episode each.
 *
 * Each episode carries `start`, `length`, the verdict that `openedBy` it and
 * the `worst` point of the run, with the numbers that judged them.
 */
export function episodes(series, options = {}) {
  const { consecutive = 3, ...rest } = options;
  if (consecutive < 1) throw new RangeError('consecutive must be at least one point');
  const found = [];
  let run = [];
  const close = () => {
    if (run.length >= consecutive) {
      const worst = run.reduce((a, b) => (b.deviation - b.limit > a.deviation - a.limit ? b : a));
      found.push({ start: run[0].index, length: run.length, openedBy: run[0], worst });
    }
    run = [];
  };
  for (const verdict of scan(series, rest)) {
    if (verdict.isAnomaly) run.push(verdict);
    else close();
  }
  close();
  return found;
}

Essayer

interactif

Le code ci-dessus tourne dans votre navigateur. Écrivez dans le champ : le résultat se refait sous vos yeux.

Chaque verdict porte les nombres qui l’ont produit : la valeur mesurée, ce que les douze minutes précédentes appelaient normal, l’écart entre les deux, et l’écart qui était toléré.

à vous d’écrire

Recalculé à chaque frappe.

Ou partez d’un exemple

La saisie libre et les autres exemples ont besoin de JavaScript. L’exemple affiché ci-dessous, lui, a été calculé à la construction du site.

Ce que le code en fait

1 passage attrapé

Ce que le code a repéré

1080 1120 1160 1200 1240 1280 1320 1080 1120 1160 1200 1240 1280 1320 1080 1120 1160 1200 1240 1280 1320 1080 1120 1160 1200 1240 1280 1320 1080 1120 1160 1200 1240 4800 1320 1080 1120 1160 1200 1240 1280 1320 1080 1120 1160 1200 1240 1280

Ce qu’il renvoie

MinuteMesuréHabituelÉcartÉcart toléré
33480011803620311

1 anomalie sur 36 minutes jugées, 48 points fournis. Les 12 premières minutes ne sont pas jugées : elles servent de fenêtre.

Ce que ça implique

  • Vos données ne sortent pas
  • Résultat toujours identique
  • Se teste unitairement, cas par cas
  • Aucun fournisseur dans la boucle
  • Empreinte négligeable

Ce que cela ajoute à votre périmètre réglementaire

  • Aucun périmètre spécifique ajouté : les points restent sur votre infrastructure
  • Ne vous dispense pas de regarder ce que contiennent vos séries : une métrique indexée par utilisateur est une donnée personnelle avant d’être une courbe

Point de rupture

Une dérive lente entre dans la fenêtre et devient la nouvelle normale. Le test fait grimper la métrique de quarante points par minute pendant plus d’une heure : chaque pas reste loin en deçà de l’écart toléré et la fenêtre avale les précédents, si bien que l’écart à l’habituel approche la limite sans jamais la franchir. Pas un seul point n’est signalé, et la série finit à plus du triple de son niveau de repos. La même hausse totale livrée d’un coup est signalée dès sa première minute, et pour une douzaine de minutes seulement : elle aussi devient la nouvelle normale. Une montée quotidienne rapide, un lundi matin après une nuit de dimanche, sonne pour la même raison : il n’y a pas de saisonnalité ici, seulement une fenêtre.

Quand passer au niveau suivant

Vos post-mortem se terminent sur le même constat : aucune métrique ne sortait de sa propre fenêtre, seule leur combinaison était inhabituelle.

Les quatre options

  1. RecommandéN0Seuil robuste sur médiane et écart absolu médian, en fenêtre glissante, avec règle de persistanceGratuit · Instantané
  2. N1Forêt d’isolement sur plusieurs métriques conjointesnégligeable · Instantané
  3. N2Niveau sans objet ici
  4. N3Niveau sans objet ici

Les autres options

N1Forêt d’isolement sur plusieurs métriques conjointesnégligeable · Instantané

Forêt d’isolement sur plusieurs métriques conjointes

Modèle classique légerN1négligeableInstantané

Python

snippets/detect-anomalies-in-metrics/n1.pyTesté tel quel — Cet extrait s’exécute avec ses vraies dépendances, et son test tourne à chaque construction du site.
"""
Repérer des anomalies sur plusieurs métriques à la fois avec une forêt
d'isolement.

Niveau N1. Le seuil robuste de N0 surveille une métrique à la fois, chacune
contre sa propre fenêtre récente. Une forêt d'isolement surveille les
métriques ensemble, contre tout ce sur quoi elle a été entraînée : un trafic
de nuit portant des erreurs de plein jour est une minute dont chaque valeur
reste dans la plage que le service a connue, et dont la combinaison, elle,
est inconnue. Un seuil sur chaque série ne sonne pour une telle minute que si l'une de
ses valeurs sort de la fenêtre de sa propre série.

Une forêt d'isolement coupe l'espace au hasard et mesure en combien de coupes
un point se retrouve seul. Un point au milieu de la foule en demande
beaucoup ; un point à l'écart en demande trois ou quatre. Il n'y a rien à
étiqueter, et la molette qui change ce qui sonne est le seuil, pas la
taille de la forêt.

Ce que ce niveau coûte, c'est ce en quoi N0 était bon. La sortie est un rang
entre zéro et un, pas un écart mesuré face à une limite annoncée. On dit à la
personne réveillée que la minute était inhabituelle, pas en quoi.
"""

import math

from sklearn.ensemble import IsolationForest

# Trees are grown on random cuts, so the seed is part of the contract: an
# alert that changes between two runs on the same data is not an alert.
SEED = 0


def train(rows: list[list[float]], trees: int = 100, seed: int = SEED) -> IsolationForest:
    """
    `rows` is one observation per minute, the metrics always in the same order.

    Nothing is labelled and nothing is scaled: the cuts are drawn between the
    smallest and the largest value of each metric, so a metric counted in
    milliseconds and one counted in requests weigh the same.
    """
    model = IsolationForest(n_estimators=trees, random_state=seed).fit(rows)
    # The range each metric was seen in, missing values left out.
    columns = [[value for value in column if not math.isnan(value)] for column in zip(*rows)]
    model.bounds_ = [(min(c), max(c)) if c else (-math.inf, math.inf) for c in columns]
    return model


def scores(model: IsolationForest, rows: list[list[float]]) -> list[float]:
    """
    Between zero and one. Above one half, the point took fewer cuts to isolate
    than the crowd did, which is the whole definition of an anomaly here.

    A value past the range its metric was trained on scores one: no cut was
    ever drawn out there, and the forest would file it with the largest values
    it saw.
    """
    ranks = -model.score_samples(rows)  # one call for all the rows
    return [
        1.0 if any(v < low or v > high for v, (low, high) in zip(row, model.bounds_)) else float(rank)
        for row, rank in zip(rows, ranks)
    ]


def score(model: IsolationForest, row: list[float]) -> float:
    """The score of a single row."""
    return scores(model, [row])[0]


def anomalies(model: IsolationForest, rows: list[list[float]], threshold: float = 0.6) -> list[int]:
    """
    The indices worth looking at, and the threshold is yours to set.

    Move it towards one to be woken up less often and miss more; towards zero
    for the opposite trade. There is no value of it that turns the score into
    an explanation.
    """
    return [index for index, value in enumerate(scores(model, rows)) if value > threshold]

JavaScript

snippets/detect-anomalies-in-metrics/n1.jsTesté tel quel — Cet extrait s’exécute avec ses vraies dépendances, et son test tourne à chaque construction du site.
/**
 * Repérer des anomalies sur plusieurs métriques à la fois avec une forêt
 * d'isolement.
 *
 * Niveau N1. Le seuil robuste de N0 surveille une métrique à la fois, chacune
 * contre sa propre fenêtre récente. Une forêt d'isolement surveille les
 * métriques ensemble, contre tout ce sur quoi elle a été entraînée : un trafic
 * de nuit portant des erreurs de plein jour est une minute dont chaque valeur
 * reste dans la plage que le service a connue, et dont la combinaison, elle,
 * est inconnue. Un seuil sur chaque série ne sonne pour une telle minute que si l'une de
 * ses valeurs sort de la fenêtre de sa propre série.
 *
 * La forêt coupe l'espace au hasard et mesure en combien de coupes un point se
 * retrouve seul. Un point au milieu de la foule en demande beaucoup ; un point
 * à l'écart en demande trois ou quatre. Il n'y a rien à étiqueter, et la
 * molette qui change ce qui sonne est le seuil, pas la taille de la forêt.
 *
 * Écrit en entier plutôt que tiré d'une bibliothèque, parce que c'est tout
 * l'argument de ce niveau : l'outil classique est assez petit pour être lu. Ce
 * qu'il coûte, c'est ce en quoi N0 était bon — la sortie est un rang entre zéro
 * et un, pas un écart mesuré face à une limite annoncée.
 */

/** Deterministic pseudo-random numbers: an alert that changes between two runs is not an alert. */
function generator(seed) {
  let state = seed >>> 0;
  return () => {
    state = (state + 0x6d2b79f5) >>> 0;
    let t = Math.imul(state ^ (state >>> 15), 1 | state);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

/** Average depth of an unsuccessful search in a binary tree of n points. */
function averageDepth(n) {
  if (n <= 1) return 0;
  const EULER = 0.5772156649015329;
  return 2 * (Math.log(n - 1) + EULER) - (2 * (n - 1)) / n;
}

/** One tree: cut on a random metric at a random value until the points are alone. */
function grow(rows, random, depth, maxDepth) {
  if (depth >= maxDepth || rows.length <= 1) return { size: rows.length };
  const metric = Math.floor(random() * rows[0].length);
  const values = rows.map((row) => row[metric]);
  const low = Math.min(...values);
  const high = Math.max(...values);
  if (low === high) return { size: rows.length };
  const cut = low + random() * (high - low);
  return {
    metric,
    cut,
    below: grow(rows.filter((row) => row[metric] < cut), random, depth + 1, maxDepth),
    above: grow(rows.filter((row) => row[metric] >= cut), random, depth + 1, maxDepth),
  };
}

/** How deep this row falls, plus what an unfinished leaf would still have cost. */
function depthOf(node, row, depth) {
  if (node.below === undefined) return depth + averageDepth(node.size);
  return depthOf(row[node.metric] < node.cut ? node.below : node.above, row, depth + 1);
}

/** A subsample without replacement, so every tree sees a different crowd. */
function subsample(rows, size, random) {
  const shuffled = [...rows];
  for (let i = 0; i < size; i += 1) {
    const j = i + Math.floor(random() * (shuffled.length - i));
    [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
  }
  return shuffled.slice(0, size);
}

/**
 * `rows` is one observation per minute, the metrics always in the same order.
 *
 * Nothing is labelled and nothing is scaled: the cuts are drawn between the
 * smallest and the largest value of each metric, so a metric counted in
 * milliseconds and one counted in requests weigh the same.
 */
export function train(rows, { trees = 100, seed = 0 } = {}) {
  // A row with a missing value would put NaN in every cut drawn on its metric.
  const complete = rows.filter((row) => row.every(Number.isFinite));
  if (complete.length === 0) throw new RangeError('no complete row to train on');
  const random = generator(seed);
  const size = Math.min(256, complete.length);
  const maxDepth = Math.ceil(Math.log2(size));
  const forest = [];
  for (let i = 0; i < trees; i += 1) {
    forest.push(grow(subsample(complete, size, random), random, 0, maxDepth));
  }
  const bounds = complete[0].map((_, k) => complete.reduce(
    ([low, high], row) => [Math.min(low, row[k]), Math.max(high, row[k])],
    [Infinity, -Infinity],
  ));
  return { forest, normaliser: averageDepth(size), bounds };
}

/**
 * Between zero and one. Above one half, the point took fewer cuts to isolate
 * than the crowd did, which is the whole definition of an anomaly here.
 *
 * A value past the range its metric was trained on scores one: no cut was
 * ever drawn out there, and the forest would file it with the largest values
 * it saw.
 */
export function score(model, row) {
  if (row.some((value, k) => value < model.bounds[k][0] || value > model.bounds[k][1])) return 1;
  if (model.normaliser === 0) return 0.5; // a single training row: no crowd to compare with
  const total = model.forest.reduce((sum, tree) => sum + depthOf(tree, row, 0), 0);
  return 2 ** (-total / model.forest.length / model.normaliser);
}

/**
 * The indices worth looking at, and the threshold is yours to set.
 *
 * Move it towards one to be woken up less often and miss more; towards zero
 * for the opposite trade. There is no value of it that turns the score into
 * an explanation.
 */
export function anomalies(model, rows, threshold = 0.6) {
  return rows.map((_, index) => index).filter((index) => score(model, rows[index]) > threshold);
}

Ce que ça implique

  • Vos données ne sortent pas
  • Résultat toujours identique
  • Se mesure sur un échantillon, pas cas par cas
  • Dépend d’une bibliothèque
  • Empreinte faible

Ce que cela ajoute à votre périmètre réglementaire

  • Traitement sur votre infrastructure : aucune métrique ne part chez un tiers
  • Le modèle est dérivé de vos données d’exploitation : ses coupes sont tirées entre la plus petite et la plus grande valeur observée de chaque métrique, et en gardent la trace

Point de rupture

L’habituation. Quinze minutes de trafic de nuit portant un nombre d’erreurs de plein jour, glissées dans les données d’entraînement, suffisent pour que la forêt en fasse un troisième régime ordinaire. La minute que le test signalait juste avant ne l’est plus, et rien dans la sortie ne dit que quoi que ce soit a changé. C’est le point de rupture de N0, monté d’un étage : ce qui entre dans l’entraînement devient normal.

Quand passer au niveau suivant

Il n’y a pas de niveau au-dessus dans cette fiche. Ce qui manque ensuite n’est pas un modèle plus gros, c’est quelqu’un pour décider quels régimes ont le droit d’exister.

N2Niveau sans objet ici

Niveau sans objet ici

Petit modèle spécialisé auto-hébergéN2

Un modèle de séries temporelles auto-hébergé est un service de plus à faire tourner, et un service qui décide des alertes est un service qu’il faut surveiller à son tour. À trois heures du matin, la question n’est pas de savoir si le score est meilleur : c’est de savoir pourquoi le téléphone a sonné. N0 répond en trois nombres, celui-là ne répond pas.

N3Niveau sans objet ici

Niveau sans objet ici

API de LLM généralisteN3

Deux appels sur la même fenêtre peuvent rendre deux verdicts, et une alerte qu’on ne peut pas rejouer est une alerte que l’astreinte apprend à ignorer. S’y ajoute un appel par minute et par métrique, pour une décision que la comparaison de deux nombres tranche déjà.

Pour aller plus loin

Métadonnées