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.
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.
| Nivå | Spørsmålet | Eksempel | Start med |
|---|---|---|---|
| 1. Én verdi | Er tallet innenfor forventet spenn? | Effekten på en maskin | Gjennomsnitt og standardavvik |
| 2. Én verdi over tid | Er dette normalt for tidspunktet og trenden? | Temperatur gjennom døgnet | Glidende gjennomsnitt, STL-dekomponering |
| 3. Flere verdier | Er kombinasjonen normal? | Effekt gitt belastning og temperatur | Isolation Forest, Mahalanobis-avstand |
| 4. Flere verdier over tid | Er maskinen i ferd med å endre seg? | Mange sensorer på samme maskin | Autoencoder, 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.
É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.
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.
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.
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.
Ellers har modellen pugget treningsdataene i stedet for å lære mønsteret.
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ø.
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.
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.
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.
É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.
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.
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.
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.
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.
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)")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.
# 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))}")[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.