Å finne det unormale

Feil er sjeldne. Det er hele problemet med å lære en maskin å finne dem, og samtidig nøkkelen. Du trenger ikke nok feil til å trene på. Du trenger mye data fra da alt var bra, og noen få feil til å sette grensen og teste.

Magnus Gribbestad 8. oktober 2026 8 min
Figur · trinn for trinn (konstruert illustrasjon)
Et avvik i en tidsserie En sensorverdi svinger jevnt. Systemet lærer et normalt område. En enkelt topp ligger utenfor, men er støy. Deretter driver verdiene sakte oppover uten å forlate området. Avviket fra det forventede mønsteret regnes om til en score som stiger jevnt og krysser en grense. Varselet kommer før sensoren svikter, og tiden mellom varsel og svikt er tiden man har til å handle. SENSORVERDI OVER TID NORMALT OMRÅDE Enkelt topp: støy? Sakte drift: innenfor AVVIK FRA FORVENTET (SCORE) topp: lite utslag drift: jevn stigning GRENSE VARSEL TID TIL Å HANDLE SVIKT
  1. Her er én sensor i normal drift. Verdiene svinger i et jevnt mønster.
  2. Systemet lærer det normale av historiske data: et område verdiene holder seg innenfor, og et forventet mønster.
  3. En enkelt topp er lett å se. Den ligger utenfor området, og er ofte bare støy.
  4. Det vanskelige er sakte drift. Hver enkelt verdi ligger fortsatt innenfor området, men avstanden til det forventede vokser.
  5. Avviket regnes om til en score. Toppen gir et lite utslag. Driften gir en jevn stigning.
  6. Når scoren holder seg over grensen i flere målinger på rad, varsler systemet. En enkelt topp klarer ikke det.
  7. Varselet kommer før svikt. Tiden mellom varsel og svikt er det du har til å handle. Serien er konstruert for å vise prinsippet.
Kort sagt

Anomalideteksjon er å lære hva normalt ser ut, og slå alarm når noe ikke ser sånn ut. Modellen trenger mye data fra normal drift, og ikke nok feil til å trene på. Noen få feil trenger du likevel, til å sette grensen og teste.

Feil er sjeldne. Det er hele problemet med å lære en maskin å finne dem, og samtidig nøkkelen. Du trenger ikke nok feil til å trene på. Du trenger mye data fra da alt var bra, og noen få feil til å sette grensen og teste.

Du har ikke nok feil å lære av

Vanlig maskinlæring lærer av eksempler: dette er en feil, dette er ikke en feil. Det forutsetter at du har mange av begge. I industrien har du sjelden det. Feil skjer sjelden, og når de skjer, er de dyre. Å samle tusen eksempler på hver mulig feiltype er urealistisk. Normal drift har du derimot nok av.

Så du snur det på hodet. Lær hva normalt er, og reager på alt som bryter med det. Det har en pris: modellen sier at noe er annerledes, ikke hva som er galt. Og annerledes er ikke alltid galt. En ny arbeidsoppgave, en kald dag eller en sensor som er byttet kan se ut som en feil. Det er der mesteparten av jobben ligger.

Fire nivåer, og hvor det blir vanskelig

NivåSpørsmåletEksempelStart med
1. Én verdiEr tallet innenfor forventet spenn?Effekten på en maskinGjennomsnitt og standardavvik
2. Én verdi over tidEr dette normalt for tidspunktet og trenden?Temperatur gjennom døgnetGlidende gjennomsnitt, STL-dekomponering
3. Flere verdierEr kombinasjonen normal?Effekt gitt belastning og temperaturIsolation Forest, Mahalanobis-avstand
4. Flere verdier over tidEr maskinen i ferd med å endre seg?Mange sensorer på samme maskinAutoencoder, gjerne med LSTM

Begynn på det enkleste nivået som kan svare på spørsmålet ditt. Hopper du rett til nivå 4, får du kompleksitet du kanskje ikke trenger. Det er også derfor figuren øverst bare viser én sensor: der er prinsippet lettest å se.

Det vanskelige er samspillet

Én sensor kan ligge innenfor sitt normale område hele veien og likevel være en del av noe unormalt. Temperaturen er litt høy, vibrasjonen litt over, effekten litt lav. Hver for seg er det ingenting. Sammen er det et mønster du aldri har sett i normal drift. Det er derfor flere variabler samtidig krever en modell, og ikke bare en grense per sensor.

Autoencoderen: pakk sammen og bygg opp igjen

En autoencoder er et nevralt nettverk med én jobb: ta inn data, presse dem gjennom en trang flaskehals og bygge dem opp igjen. For å klare det må nettverket lære hva som er vesentlig i dataene og kaste resten. Trener du det bare på normale data, blir det flinkt til akkurat det. Gir du det noe det aldri har sett, bygger det ofte noe som ligner det normale, og avstanden mellom det som kom inn og det som kom ut blir stor. Den avstanden er alarmen din. Den kalles rekonstruksjonsfeil, og den er en helt vanlig feilmåling. Det virker bare når flaskehalsen er trang nok. Ellers lærer nettverket å gjenskape alt, også feilen.

Figur · autoencoder (illustrasjon, tallene er konstruert)
Autoencoder: normalt mot ukjent En autoencoder pakker data sammen gjennom en trang flaskehals og bygger dem opp igjen. Trent på kvadrater gjenskaper den en kvadrat med lav feil. Får den en trekant, bygger den fortsatt noe som ligner en kvadrat, og feilen blir høy. INN UT Flaskehals Pakker sammen Bygger opp igjen REKONSTRUKSJONSFEIL 8 % 84 % Lav feil: alt normalt Høy feil: varsel
  1. En autoencoder har en bred inngang, en trang flaskehals og en bred utgang. Jobben er å bygge opp igjen det som kom inn.
  2. Den trenes bare på normale data. Her er det normale en kvadrat.
  3. Kvadraten presses gjennom flaskehalsen og bygges opp igjen nesten uendret. Lav feil, ingen varsel.
  4. Så kommer noe den aldri har sett: en trekant.
  5. Den kan bare bygge noe som ligner det normale, så ut kommer ikke trekanten. Høy feil, og det er varselet. Prosentene i figuren er konstruert.

Det som tar tiden: grense og forklaring

Selve nettverket er den enkle delen. Det som skiller en demo fra noe operatørene stoler på, er det som kommer rundt. Ett mulig oppsett, i fem steg.

1

Tren på normal drift

Du trenger ikke merke noe som normalt. Modellen lærer av det som dominerer i datasettet. Data fra idriftsettelsen kan brukes som referanse for hva normalt betyr.

2

Sjekk at feilen er lav på normal drift den ikke har sett

Ellers har modellen pugget treningsdataene i stedet for å lære mønsteret.

3

Sett grensen på et eget sett

Gjerne med noen kjente feil, hvis du har dem. Her avgjør du balansen mellom falske alarmer og oversette feil, og den er forskjellig for hver maskin og hvert miljø.

4

Test på feil, og se etter tidspunktet

Er feilen markant høyere? Og stiger den allerede ved første tegn til degradering, lenge før noen ser noe? Det er den tidlige stigningen som gir verdi.

5

Gjør tallet forståelig

Du kan regne rekonstruksjonsfeilen om til en score fra 0 til 100, der 100 er helt normalt. Driftsfolk kan forholde seg til en score, ikke til en feilmåling.

Ett mulig oppsett for datadelingen: når over 95 prosent av dataene er normal drift, kan du dele i for eksempel 60 prosent trening, 10 validering, 10 til å sette grensen og 20 til test. Legg gjerne noen av feiltypene bare i testsettet, så du kan måle om modellen fanger feil den aldri er vist. Det er den prøven som sier om du har en modell for feil du kjenner, eller for feil du ikke har tenkt på. Delingen er et eksempel på tankegangen, ikke et resultat jeg har målt her.

Hva er galt?

En modell som bare sier «noe er galt» blir ikke brukt. Regn ut hvilke sensorer som bidrar mest til den høye feilen. Hver feiltype har sitt fingeravtrykk: en feil i kjølingen lyser opp på temperaturene, en mekanisk feil på vibrasjon og lagertemperatur. Da blir modellen mer enn en alarm. Den peker på hvor teknikeren skal lete.

Falske alarmer og skiftende normal

Én falsk alarm for mye, og operatørene slutter å lytte. Kalibrer grensen etter hva en falsk alarm koster mot hva en oversett feil koster, og krev at avviket varer en stund før du varsler, slik figuren øverst viser. Husk også at det normale flytter seg. Ny driftsform, ny sensor eller ny sesong kan kreve ny trening.

Prøv selv, uten å bygge noe

Nivå 1 og 2 trenger ikke et nevralt nettverk. Har du en tidsserie som en tabell, kan en AI-tjeneste som kan kjøre kode gjøre første runde for deg.

Prompt · Første runde med avvik
Vedlagt er en CSV med [én måleverdi per time fra en maskin]. Regn ut glidende gjennomsnitt og standardavvik over de siste [24] verdiene. Marker punkter som ligger mer enn tre standardavvik fra gjennomsnittet, og vis dem i en graf. Si hva som kan være støy og hva som kan være et reelt avvik, og hva jeg bør sjekke før jeg stoler på resultatet.
Dette finner topper, ikke sakte drift. For drift trenger du å følge gjennomsnittet over lengre tid.

Pass på hva du laster opp

Driftsdata fra en bedrift er sjelden noe du kan legge inn i en vanlig AI-tjeneste. Bruk en løsning virksomheten har godkjent, eller anonymiser og avgrens dataene først.

Start enkelt, og sjekk at det enkle ikke er nok før du bygger noe dyrt. Når det ikke er nok, er du på nivå 3 eller 4, og da har du fått mye å hente ved å lære det normale. Om valget mellom enkle og store modeller generelt, se klassisk maskinlæring.

For deg som bygger

Den enkleste detektoren som er verdt å kjøre: for hver måling, se hvor mange standardavvik den ligger fra snittet av de 48 foregående. Bare Python 3 og standardbiblioteket. Tidsserien er konstruert: én måling i timen i 14 døgn, et døgnmønster rundt 50, én enkelt topp i time 200 og en sakte drift fra time 280. Lagre som avvik.py.

Python · avvik.py
import math
import random
import statistics

# Konstruert tidsserie: én måling i timen i 14 døgn. Normal verdi svinger
# rundt 50 med et døgnmønster. Ingen av tallene er fra en ekte maskin.
def lag_serie():
    rng = random.Random(7)
    serie = []
    for t in range(14 * 24):
        verdi = 50 + 3 * math.sin(2 * math.pi * t / 24) + rng.gauss(0, 0.8)
        if t == 200:
            verdi += 9          # enkelt topp
        if t >= 280:
            verdi += (t - 280) * 0.06   # sakte drift fra time 280
        serie.append(verdi)
    return serie

def glidende_avvik(serie, vindu=48, grense=3.0):
    """Flagger punkter som ligger mer enn `grense` standardavvik fra
    snittet av de `vindu` foregående målingene."""
    varsler = []
    for t in range(vindu, len(serie)):
        bak = serie[t - vindu:t]
        snitt, sd = statistics.mean(bak), statistics.stdev(bak)
        z = (serie[t] - snitt) / sd
        if abs(z) > grense:
            varsler.append((t, serie[t], z))
    return varsler

def drift_mot_referanse(serie, referanse=7 * 24, vindu=12, grense=3.0):
    """Lærer døgnmønsteret av de første døgnene og ser på hvor langt de
    siste `vindu` timene ligger over det, målt med median så en enkelt
    topp ikke utløser den. Fanger sakte drift."""
    profil = [statistics.mean(serie[h:referanse:24]) for h in range(24)]
    rest = [serie[t] - profil[t % 24] for t in range(referanse)]
    sd = statistics.stdev(rest)
    for t in range(referanse + vindu, len(serie)):
        midt = statistics.median(serie[u] - profil[u % 24] for u in range(t - vindu, t))
        if midt > grense * sd / vindu ** 0.5:
            return t
    return None

if __name__ == "__main__":
    serie = lag_serie()
    varsler = glidende_avvik(serie)
    print(f"{len(serie)} målinger, {len(varsler)} varsler")
    for t, verdi, z in varsler:
        print(f"  time {t:3} (døgn {t // 24 + 1}, kl. {t % 24:02}): {verdi:5.1f}  z = {z:+.1f}")
    forste = drift_mot_referanse(serie)
    print(f"Drift mot fast referanse: første varsel i time {forste}"
          f" (døgn {forste // 24 + 1}, kl. {forste % 24:02}), driften startet i time 280")
    print(f"Siste måling: {serie[-1]:.1f} (normalt rundt 50)")
Utskrift
336 målinger, 1 varsler
  time 200 (døgn 9, kl. 08):  62.6  z = +5.8
Drift mot fast referanse: første varsel i time 297 (døgn 13, kl. 09), driften startet i time 280
Siste måling: 52.9 (normalt rundt 50)

Detektoren med glidende vindu finner toppen (z = +5,8) og ingenting annet. Der feiler den: et glidende vindu følger driften etter seg, så sakte drift ser aldri unormal ut mot de siste 48 timene. Den andre funksjonen sammenligner mot et døgnmønster lært fra en uke der alt var bra, og fanger driften 17 timer etter at den startet. Den forutsetter at referanseuka faktisk var frisk, og at det normale ikke flytter seg etterpå.

Tabellen over har nivå 3, flere verdier som må være normale sammen, og der holder ikke en grense per sensor. Her er det samme prinsippet på to variabler som henger sammen, med to av metodene fra tabellen: Mahalanobis-avstand og Isolation Forest. Den trenger pip install scikit-learn numpy. Dataene er konstruert: effekten følger temperaturen, og tre målinger har hver verdi i vanlig område, men en kombinasjon som ikke passer.

Python · samspill.py
# pip install scikit-learn numpy
import random
import numpy as np
from sklearn.ensemble import IsolationForest

# Konstruert: effekten følger temperaturen. Stiger den ene, stiger den andre.
rng = random.Random(3)
normal = []
for _ in range(1000):
    temp = rng.gauss(70, 5)
    effekt = 100 + 4 * (temp - 70) + rng.gauss(0, 3)
    normal.append([temp, effekt])
X = np.array(normal)

# Tre målinger der hver verdi er vanlig for seg, men kombinasjonen ikke er det.
mistenkelige = [[78, 70], [62, 130], [72, 62]]

snitt, sd = X.mean(axis=0), X.std(axis=0, ddof=1)
invers = np.linalg.inv(np.cov(X.T))
def mahalanobis(x):  # avstand fra snittet, målt langs hvordan variablene henger sammen
    d = np.array(x) - snitt
    return float(np.sqrt(d @ invers @ d))

# contamination er andelen av treningsdataene som skal kalles unormale. Standardverdien
# ("auto") flagget nesten hver femte normale måling her, så sett den selv.
skog = IsolationForest(contamination=0.01, random_state=0).fit(X)

for x in mistenkelige:
    z = float(max(abs(np.array(x) - snitt) / sd))
    skog_sier = "unormalt" if skog.predict([x])[0] == -1 else "normalt"
    print(f"{x}: største z per variabel {z:.1f} | Mahalanobis {mahalanobis(x):.1f} | Isolation Forest: {skog_sier}")

print("Av 1000 normale målinger flagges:")
print(f"  z over 3 på én variabel: {int(sum(max(abs(r - snitt) / sd) > 3 for r in X))}")
print(f"  Mahalanobis over 3:      {int(sum(mahalanobis(r) > 3 for r in X))}")
print(f"  Isolation Forest:        {int(sum(skog.predict(X) == -1))}")
Utskrift
[78, 70]: største z per variabel 1.6 | Mahalanobis 20.4 | Isolation Forest: normalt
[62, 130]: største z per variabel 1.6 | Mahalanobis 20.4 | Isolation Forest: normalt
[72, 62]: største z per variabel 1.9 | Mahalanobis 15.1 | Isolation Forest: normalt
Av 1000 normale målinger flagges:
  z over 3 på én variabel: 3
  Mahalanobis over 3:      13
  Isolation Forest:        10

Grensen per variabel ser ingenting: ingen av de tre ligger lenger enn to standardavvik fra snittet på noen av sensorene. Mahalanobis-avstanden, som tar med hvordan variablene henger sammen, gir 15 til 20 mot en grense på 3, og tar alle tre. Isolation Forest, med samme data og med contamination=0.01, tar ingen av dem. Det betyr ikke at metoden er dårlig, men at den ikke er et svar du kan bruke uten å teste på dine egne data og dine egne feil. Standardinnstillingen flagget i tillegg nesten hver femte normale måling på disse dataene (193 av 1 000, kjørt med samme frø), så grensen må du sette selv. Mahalanobis-avstanden forutsetter at normal drift er jevnt fordelt rundt én klynge. Har maskinen flere driftstilstander, slår den dårligere ut.

Kjørt 8. oktober 2026 med Python 3.13.16 på Windows 11: avvik.py, og utskriften over er fra den kjøringen. samspill.py er kjørt med scikit-learn 1.9.1 og NumPy 2.4.4 på Python 3.13.11, i et eget miljø på maskinen. Ingenting ble installert for anledningen. Tidsserien er konstruert med et fast tilfeldighetsfrø, så du får samme tall. Grensene (3 standardavvik, 48 og 12 timers vindu) er valgt for å vise poenget, ikke tunet mot noen maskin.

Oppdatert oktober 2026. Figurene og prosentene i dem er konstruert for å vise prinsippet og er ikke målt på en bestemt maskin. Isolation Forest er beskrevet av Liu, Ting og Zhou (IEEE ICDM, 2008) og finnes i scikit-learn. Datadelingen i teksten er ett eksempel for å vise tankegangen, ikke et publisert eller målt resultat.