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.
MNIST · NumPy · Manim
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.
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.
Eller prøv et ekte MNIST-siffer:
Disse kommer fra treningsbatchen som ligger lagret i run.npz.
Utgangsaktivering
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.
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.
Hele nettverket er to matrisemultiplikasjoner og en sigmoid. Det er ingen skjult kompleksitet — alt annet er trening.
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.
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.
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.
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, AsDette 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.
10 epoker over 60 000 bilder. Den første epoken gjør nesten hele jobben; resten henter inn de siste prosentpoengene.
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.
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.
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.
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.
Fasit ↓ · Gjettet → · tom diagonal: treff telles ikke her
Vanligste forvekslinger
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.