# Maskinlæring

## Hvad det er

Maskinlæring betyder at bygge software, der lærer en opgave af eksempler i
stedet for at følge regler, som en programmør har formuleret på forhånd. Man
viser den mange tilfælde, hvor svaret allerede er kendt. Den finder en
sammenhæng mellem det, der gik ind, og det svar, der kom ud, og bruger derefter
sammenhængen til at forudsige svaret i hvert nyt tilfælde. Betegnelsen er ældre
end næsten al den software, der bærer den i dag: A. L. Samuel brugte den i
titlen på en undersøgelse af brætspillet dam, som IBM Journal of Research and
Development udgav i juli 1959.

Begreberne er nemmest at holde fast i med scikit-learn, et open source-bibliotek
til Python, og det er dets dokumentation, denne artikel bygger på. Eksemplerne
ligger i en tabel. Hver række er en observation (scikit-learn siger sample), for
eksempel en faktura, en kunde eller en måling fra en sensor. Hver kolonne er en
feature, altså et tal, der beskriver observationen. Det svar, softwaren skal
lære at give, er målvariablen. Det, der lærer, kalder scikit-learn en estimator,
og alle estimatorer har samme to greb: `fit` lærer af de rækker, man viser den,
og `predict` giver svar på rækker, den aldrig har set.

Langt det meste arbejde falder i to grupper. Ved superviseret læring har hver
træningsrække sin målvariabel med. Er målvariablen en kategori, for eksempel
betalt til tiden eller betalt for sent, kaldes opgaven klassifikation. Er den en
størrelse, for eksempel antal dage til betaling, hedder det regression. Ved
ikke-superviseret læring findes der ingen målvariabel, og softwaren leder selv
efter struktur i data, typisk ved at samle kunder, der opfører sig ens, i
grupper. I scikit-learns dokumentation har hver af de to grupper sit eget
kapitel, og bagefter kommer et kapitel om modelvalg og evaluering. Det er dér,
de fleste praktiske vanskeligheder gemmer sig.

Maskinlæring er derimod ikke et samlebegreb for al software, der virker klog.
Når en regel markerer hver faktura over et bestemt beløb, har et menneske
skrevet den, og den gør hverken mere eller mindre, end der står. En model, der
markerer fakturaer, har selv udledt sin regel af gamle fakturaer. Den kan derfor
tage fejl på måder, som ingen har skrevet ned, og den skifter adfærd, så snart
de data, den lærer af, skifter.

## Hvorfor det betyder noget

Visse opgaver er langt lettere at vise end at forklare. Ingen kan skrive en
udtømmende regel for, hvilke fakturaer der ender med at blive betalt for sent,
men de fleste virksomheder ligger inde med flere års fakturaer, hvor det
faktiske udfald er registreret. Maskinlæring forvandler den historik til en
regel, der kan bruges, og kan gøre det for mange lignende spørgsmål på én gang.
Derfor er teknikken gået fra forskningslaboratoriet til helt almindelig
forretningssoftware.

Bagsiden er, at data nu gør det arbejde, som koden før gjorde. Er der fejl,
dubletter eller huller i historikken, lærer modellen dem også, og der er intet i
koden, der røber det. D. Sculley og ni kolleger fra Google beskrev følgerne i
*Hidden Technical Debt in Machine Learning Systems*, en artikel i proceedings
fra NIPS-konferencen i 2015, og deres emne er teknisk gæld. Artiklens første
figur tegner selve læringskoden som en lille sort boks midt i et meget større
system, og ifølge billedteksten er infrastrukturen omkring den både enorm og
kompleks. Et sted vurderer forfatterne, at et modent system kan ende med at
bestå af højst 5 procent maskinlæringskode og mindst 95 procent limkode, hvis
eneste opgave er at flytte data ind i og ud af generelle softwarepakker.

De påpeger også, at det er dyrere at afhænge af data end af kode. Compilere og
byggesystemer kan analysere, hvilken kode der afhænger af hvilken, men
tilsvarende værktøjer til data er langt sjældnere. En kolonne kan derfor få en
ny betydning, uden at noget som helst opdager det. Forfatterne giver også et
princip et navn, CACE, som står for »Changing Anything Changes Everything«:
ændres fordelingen af ét input, kan modellens måde at bruge alle de andre input
på ændre sig med.

En model er med andre ord billig at bygge og dyr at holde kørende. Det, der
afgør, om den gør nytte, handler for det meste om data: hvor de stammer fra, om
hver kunde står der én eller tre gange, om det registrerede udfald overhovedet
er korrekt, og hvem der opdager det, når et system længere oppe i kæden begynder
at levere noget andet end før.

## Sådan ser det ud i praksis

Forestil dig en virksomhed, der vil vide allerede samme dag, som en faktura
bliver udstedt, om den sandsynligvis bliver betalt for sent. Så kan en
medarbejder ringe til kunden før forfaldsdagen og ikke først bagefter. Et udtræk
har én række pr. tidligere faktura, sorteret efter udstedelsesdato, og fire
kolonner, som alle var kendt den dag, fakturaen blev sendt: beløbet, antallet af
kreditdage, hvor mange fakturaer kunden fik i det foregående år, og hvor mange
af dem der blev betalt for sent. En femte kolonne fortæller, hvordan det gik: 1,
hvis fakturaen blev betalt for sent, og 0, hvis den blev betalt til tiden.

Scriptet herunder er skrevet til denne artikel og gennemgået linje for linje mod
dokumentationen til scikit-learn 1.9.1 og NumPy 2.5 i oktober 2026. Det træner
en model på de ældste tre fjerdedele af fakturaerne og tester den på den nyeste
fjerdedel:

```python
# invoices.csv: én række pr. faktura sorteret efter dato, kun tal.
# Sidste kolonne er 1, hvis fakturaen blev betalt for sent, ellers 0.
import numpy as np
from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import balanced_accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

data = np.loadtxt("invoices.csv", delimiter=",", skiprows=1)
X, y = data[:, :-1], data[:, -1].astype(int)

# Hold den nyeste fjerdedel tilbage: intet lærer af den.
X_old, X_new, y_old, y_new = train_test_split(
    X, y, test_size=0.25, shuffle=False
)

model = make_pipeline(StandardScaler(), LogisticRegression())
baseline = DummyClassifier(strategy="most_frequent")
for name, candidate in [("model", model), ("baseline", baseline)]:
    candidate.fit(X_old, y_old)
    score = balanced_accuracy_score(y_new, candidate.predict(X_new))
    print(name, round(score, 3))
```

Fire beslutninger i scriptet betyder mere end valget af algoritme.

**Den nyeste fjerdedel lægges til side, før noget lærer.** Med `shuffle=False`
bevarer `train_test_split` rækkefølgen og lægger de sidste rækker i testsættet.
Modellen lærer altså af ældre fakturaer og bliver bedømt på nyere, præcis som
den skal bruges i virkeligheden. Lod man standardværdien `shuffle=True` stå,
ville opdelingen blive tilfældig, og modellen ville delvis blive testet på
fakturaer, der er ældre end nogle af dem, den har lært af.

**Skaleringen sidder inde i pipelinen.** `StandardScaler` trækker hver features
middelværdi fra og skalerer den til enhedsvarians. Fordi `make_pipeline` kæder
skaleringen sammen med klassifikatoren, beregnes middelværdi og varians alene ud
fra træningsrækkerne, og intet fra den nyeste fjerdedel siver ind i modellen.

**Målet er balanceret nøjagtighed.** For hver af de to klasser regnes den andel
ud, som modellen ramte rigtigt, altså de sene fakturaer, den fangede, og de
rettidige, den genkendte, og så tages gennemsnittet. Bliver hver tiende faktura
betalt for sent, belønner almindelig nøjagtighed en model, der svarer »til
tiden« hver eneste gang. Det gør balanceret nøjagtighed ikke.

**Modellen skal slå en referencemodel.** `DummyClassifier` med
`strategy="most_frequent"` ser bort fra alle features og svarer altid med den
hyppigste klasse. Målt på balanceret nøjagtighed får den 0,5, når der er to
klasser, for den rammer hver rettidig faktura og misser hver sen. Slår modellen
den ikke tydeligt, har den ikke lært noget brugbart, uanset hvor pæn dens
almindelige nøjagtighed ser ud.

Intet af det redder en model fra dårlige rækker. Står den samme kunde som tre
forskellige poster, bliver antallet af kundens sene fakturaer fordelt på tre, og
modellen lærer af en historik, der aldrig har fundet sted. Vores side om
[Klargøring af data til AI](/da/services/ai-data/) handler om netop det
forarbejde: at hver kunde kun findes én gang, at den enkelte medarbejder kun får
adgang til det, vedkommende skal se, og at ethvert svar kan følges tilbage til
de data, det bygger på. For en model er det sidste punkt det, der gør det muligt
at gå baglæns fra en underlig forudsigelse til de rækker, der gav den.

## Hvor det går galt

**Oplysninger fra fremtiden sniger sig ind i træningen.** Vejledningen om
almindelige faldgruber i scikit-learn kalder det datalækage (data leakage):
modellen bygges med oplysninger, som ikke vil findes, når den skal forudsige. I
fakturaeksemplet er den fristende ekstra kolonne antallet af udsendte rykkere.
Den forudsiger forsinkelse fremragende, fordi rykkere først sendes, når en
faktura allerede er forfalden. Samme læk opstår mere ubemærket, når en skalering
eller en udfyldning af manglende værdier tilpasses på samtlige rækker før
opdelingen, og vejledningens regel er, at man aldrig kalder `fit` på testdata.
Løsningen er at skrive ned, hvornår hver kolonnes værdi bliver kendt, at dele
data først og at lade al forbehandling ligge i pipelinen, så den kun tilpasses
på træningsrækkerne.

**Modellen bedømmes på noget, den allerede har set.** En model, der bare gentog
de svar, den var trænet på, ville få fuld score på sine egne træningsrækker og
være værdiløs over for alt nyt. Vejledningen om krydsvalidering kalder det
overtilpasning (overfitting). En mere lumsk variant er at afprøve mange
indstillinger og beholde den, der scorer bedst på de tilbageholdte rækker, for
så siver viden om netop de rækker ind i valget. Sammenlign i stedet
indstillinger med krydsvalidering på træningsdelen, hvor rækkerne skæres i flere
fold, og hver fold på skift er testsæt. De tilbageholdte rækker ser man på én
gang, til allersidst.

**Høj nøjagtighed, som en konstant kunne matche.** Når den ene klasse er
sjælden, bliver nøjagtigheden domineret af den almindelige. Vejledningen advarer
om, at en fold ved sjældne klasser, som ved afsløring af svindel, kan ende uden
et eneste eksempel på den sjældne klasse. Rapportér balanceret nøjagtighed eller
et andet mål pr. klasse, sammenlign med en `DummyClassifier`, og brug
stratificerede fold, der holder klassernes andele nogenlunde ens i hver fold.
`cross_val_score` bruger dem som standard for en klassifikator, når man giver
den et antal fold.

**En tilfældig opdeling af data, der ændrer sig over tid.** Vejledningen om
krydsvalidering bemærker, at antagelsen om uafhængige og identisk fordelte
observationer sjældent holder i praksis, og at tidsordnede data bør testes på
observationer, der ligger senere end dem, modellen har lært af. En tilfældig
opdeling lader modellen kigge ind i fremtiden og giver en score, den aldrig når
i drift. Del efter dato, som eksemplet gør, og brug ved krydsvalidering en
opdeling, der tager hensyn til tiden, for eksempel `TimeSeriesSplit`, hvis fold
ifølge vejledningen hver bør dække et lige så langt tidsrum.

**Forbehandlingen er ikke den samme i træning og drift.** Har modellen lært af
skalerede features, og sender det kørende system rå tal, bliver forudsigelserne
forkerte, uden at der kommer nogen fejlmeddelelse. Vejledningen om faldgruber
kræver, at enhver transformation, der bruges under træningen, også anvendes på
testdata og på data i produktion. Lever den tilpassede pipeline som ét samlet
objekt frem for en model med en vejledning i, hvad der først skal gøres ved
data.

**Verden ændrer sig, efter modellen er sat i drift.** En model, der er trænet på
sidste års fakturaer, går ud fra, at kunderne stadig opfører sig som dengang.
Sculley og hans medforfattere foreslår at holde øje med forudsigelsesbias
(prediction bias): i et system, der fungerer efter hensigten, bør fordelingen af
de forudsagte kategorier normalt svare til fordelingen af dem, der faktisk
observeres, og et voksende gab mellem de to er ofte det første tegn på, at
verden har flyttet sig. Lad den sammenligning køre fast, træn igen på nyere
data, og hold en fastfrosset, versioneret kopi af hvert inputsignal, som et
andet team ejer, så en velment ændring længere oppe i kæden ikke i al stilhed
ændrer jeres model.

## Læs mere

- [User Guide](https://scikit-learn.org/stable/user_guide.html)
  — scikit-learns opslagsværk om superviseret og ikke-superviseret læring og om
  modelvalg og evaluering, kapitel for kapitel.
- [Getting Started](https://scikit-learn.org/stable/getting_started.html)
  — estimatorernes fælles grænseflade vist i korte eksempler: tilpasning og
  forudsigelse, transformere, sammenkædede trin og krydsvalidering.
- [Common pitfalls and recommended practices](https://scikit-learn.org/stable/common_pitfalls.html)
  — inkonsekvent forbehandling, datalækage og tilfældighed, med den forkerte og
  den rigtige fremgangsmåde side om side i kode.
- [Cross-validation: evaluating estimator performance](https://scikit-learn.org/stable/modules/cross_validation.html)
  — hvorfor en test på træningsdata vildleder, og hvilken opdeling der passer
  til grupperede, skævt fordelte eller tidsordnede data.
- [Hidden Technical Debt in Machine Learning Systems](https://papers.nips.cc/paper_files/paper/2015/hash/86df7dcfd896fcaf2674f757a2463eba-Abstract.html)
  — Sculley og kolleger fra Google, NIPS 2015, om hvad det koster at drive en
  model i produktion ud over selve modellen.

<https://engineer.company/da/glossary/machine-learning/>
