784 128 10

MNIST · NumPy · Manim

Jeg bygget et nevralt nettverk fra bunnen av.

Ingen TensorFlow, ingen PyTorch, ingen scikit-learn — bare NumPy til matrisematematikken. Backprop er derivert for hånd og verifisert mot numeriske gradienter. Så laget jeg en film om hvordan det virker, animert i Manim. Nettverket under er ikke et bilde av en demo: det er de ekte vektene, og de kjører i nettleseren din.

97,47 %
treffsikkerhet på testsettet
101 770
parametre
0
ML-bibliotek brukt
1,0e−11
avvik i gradientsjekken

Tegn et siffer

Bruk musa eller fingeren. Nettverket regner mens du tegner. Ingenting sendes noe sted — de 101 770 parametrene lastes ned én gang (199 KB) og hele utregningen skjer på din egen maskin.

Tegn her
Kjører lokalt i nettleseren

Eller prøv et ekte MNIST-siffer:

Disse kommer fra treningsbatchen som ligger lagret i run.npz.

Laster vektene …

Utgangsaktivering

00.000
10.000
20.000
30.000
40.000
50.000
60.000
70.000
80.000
90.000

Slik ser nettverket det

Tegningen din beskjæres, skaleres til 20×20 og sentreres etter tyngdepunkt i et 28×28-felt — nøyaktig slik MNIST-bildene ble normalisert. Hopper man over dette steget, bommer nettverket systematisk, og det er forbehandlingen som er feil, ikke modellen.

Det skjulte laget

128 nevroner. Hver rute er ett av dem, og lysstyrken er hvor sterkt det fyrer akkurat nå. Dette er alt nettverket «vet» om sifferet ditt før det bestemmer seg.

Merk: utgangene summerer seg ikke til 1. Nettverket bruker sigmoid og kvadratfeil, ikke softmax og kryssentropi, så hver utgang er et uavhengig svar mellom 0 og 1. Det er et bevisst valg — mer om det nederst.

Filmen

Hvordan nettverket virker, fra én piksel til ferdig trent modell. Hver animasjon er skrevet i Manim, og hvert tall som vises på skjermen er hentet fra den samme treningskjøringen som driver demoen over.

Hvordan det virker

Hele nettverket er to matrisemultiplikasjoner og en sigmoid. Det er ingen skjult kompleksitet — alt annet er trening.

784
piksler
100 352 vekter
128
skjulte nevroner
1 280 vekter
10
siffer
01

Bildet blir en liste

28×28 piksler brettes ut til én kolonne med 784 tall, hver delt på 255 så de ligger mellom 0 og 1. Den skaleringen er ikke pynt: uten den blir summene inn i sigmoid så store at den flater ut og gradientene forsvinner.

02

784 → 128

Hvert av de 128 skjulte nevronene ser på alle 784 pikslene, ganger hver med sin egen vekt, legger sammen og legger til et bias. Resultatet presses gjennom sigmoid til noe mellom 0 og 1. Det er 100 352 vekter i dette ene steget.

03

128 → 10

Samme operasjon en gang til, ned til ti tall — ett per siffer. Den høyeste vinner. Ingen softmax: nettverket er trent til å presse riktig utgang mot 1 og de andre mot 0, hver for seg.

04

Og så bakover

Feilen deriveres bakover gjennom begge lagene med kjerneregelen, og alle 101 770 parametrene flyttes et lite steg i retningen som gjør feilen mindre. 10 epoker, 32 bilder om gangen, 18 750 slike steg.

Hele forward-passet, slik det står i nn/network.py:

# nn/network.py

def forward(X, Ws, bs):
    As = [X]
    Zs = []
    for W, b in zip(Ws, bs):
        Z = As[-1] @ W + b
        Zs.append(Z)
        As.append(sigmoid(Z))
    return As[-1], Zs, As

Men var backprop riktig?

Dette er spørsmålet et ML-bibliotek svarer på for deg. Når man deriverer for hånd, må man svare selv — og en backprop med fortegnsfeil trener fortsatt, bare litt dårligere, så feilen kan gjemme seg lenge.

W[1][2,1]  num=0.10971767  ana=0.10971767  rel=1.01e-11
W[0][1,0]  num=-0.07089210  ana=-0.07089210  rel=2.49e-11
W[0][0,0]  num=-0.07931584  ana=-0.07931584  rel=7.50e-11
W[1][2,2]  num=-0.02275238  ana=-0.02275238  rel=6.52e-11
W[1][3,0]  num=0.07062142  ana=0.07062142  rel=5.07e-11

Testen: flytt én enkelt vekt et bittelite steg opp og ned, mål hvordan tapet faktisk endrer seg, og sammenlign med det backprop påstod. Fem tilfeldige vekter, begge lag.

Sifrene stemmer til ellevte desimal. Det er ikke «omtrent riktig» — det er så nær som flyttall kommer, og det betyr at derivasjonen er korrekt.

Treningen

10 epoker over 60 000 bilder. Den første epoken gjør nesten hele jobben; resten henter inn de siste prosentpoengene.

0.00.51.0018 750Tap
Tapet per batch gjennom hele treningen — 18 750 steg, vist som median i vinduer. Støyen er ekte: hver batch er 32 tilfeldige bilder, og noen batcher er vanskeligere enn andre.
92.5%95.0%97.5%100.0%97.47%13579Treffsikkerhet
Treffsikkerhet på testsettet etter hver epoke.

Hva den lærte

Hvert skjult nevron har 784 vekter — én per piksel. Bretter man dem tilbake til 28×28, ser man hva nevronet leter etter. Her er de tolv med sterkest utslag.

#9
#55
#10
#43
#26
#122
#28
#59
#124
#52
#80
#14
vil ha mørktvil ha lys

Grønt betyr «jeg vil ha lys her», oransje betyr «jeg vil ha mørkt her». De fleste ser ut som strukturert støy, ikke som pene strekdetektorer — og det er det ærlige svaret på hva et lite nettverk faktisk lærer.

Hva den bommer på

De 20 bildene nettverket tok mest selvsikkert feil på. Sortert etter hvor sikkert det var — de øverste er de ubehagelige, for der er modellen skråsikker og feil på én gang.

Gjettet 6Fasit 599.6 % sikker
Gjettet 5Fasit 699.5 % sikker
Gjettet 2Fasit 799.3 % sikker
Gjettet 4Fasit 898.7 % sikker
Gjettet 7Fasit 298.6 % sikker
Gjettet 2Fasit 798.5 % sikker
Gjettet 9Fasit 498.0 % sikker
Gjettet 2Fasit 197.8 % sikker
Gjettet 5Fasit 897.5 % sikker
Gjettet 2Fasit 796.7 % sikker
Gjettet 8Fasit 396.7 % sikker
Gjettet 9Fasit 496.2 % sikker
Gjettet 3Fasit 596.0 % sikker
Gjettet 2Fasit 795.7 % sikker
Gjettet 1Fasit 695.2 % sikker
Gjettet 5Fasit 695.1 % sikker
Gjettet 9Fasit 794.9 % sikker
Gjettet 2Fasit 394.4 % sikker
Gjettet 9Fasit 794.0 % sikker
Gjettet 9Fasit 493.9 % sikker

Alle bommene, sortert

Dette er ikke en vanlig konfusjonsmatrise: den teller bare de 253 bildene av 10 000 som ble feilklassifisert, så diagonalen er tom med vilje. Raden er hva sifferet var, kolonnen er hva nettverket sa. Mønsteret er ikke tilfeldig — det er sifrene som faktisk ligner på hverandre når de er håndskrevet.

0123456789
0335111
1311214
24151267
353463
413141214
54111016123
65311255
717124114
8215653223
944992151

Fasit ↓ · Gjettet → · tom diagonal: treff telles ikke her

Vanligste forvekslinger

4914 ganger7914 ganger7212 ganger5310 ganger939 ganger

Hva jeg ville gjort annerledes

Sigmoid i det siste laget med kvadratfeil er ikke det man ville valgt i dag. Softmax med kryssentropi gir gradienter som ikke forsvinner når nettverket er veldig sikkert og veldig feil, og ville trolig trent raskere til et bedre resultat. Jeg valgte sigmoid fordi det var den jeg kunne derivere for hånd og forstå fullt ut, og det var poenget med prosjektet.

ReLU i det skjulte laget ville løst det samme problemet der. Med sigmoid i begge lag kryper gradienten sammen på vei bakover, og det er en stor del av grunnen til at læringsraten måtte helt opp i 1,0 for å komme noen vei.

97,47 % er et godt resultat for et fullt tilkoblet nettverk på MNIST, men det er også taket for arkitekturen. Et konvolusjonsnettverk kommer over 99 % fordi det vet at nabopiksler hører sammen — informasjon som forsvinner i det øyeblikket jeg bretter bildet ut til en liste på 784 tall.