Siirry sisältöön
5/30Luku 5/30

Backpropagation alusta asti: ensin moottori, sitten verkko

Rakenna 120 rivin autodiff-moottori Pythonilla, vertaa PyTorchiin 16 desimaaliin ja opi, mitä zero_grad tekee poistamalla se.

Tällä sivulla

Neljän luvun jälkeen kurssin keskellä on aukko.

Chapter 3 antoi meille gradient descentin: parantaaksesi parametria etsi lossin kulmakerroin sen suhteen ja astu alamäkeen. Chapter 4 antoi lossin, jota kannattaa laskea. Mutta molemmissa derivaatta laskettiin käsin — yksi malli, yksi parametri, yksi rivi analyysiä, ja se mahtui sivulle.

Pinoa nyt kaksi kerrosta. Ensimmäisen ulostulo syötetään toiseen, joten jokainen ensimmäisen kerroksen paino vaikuttaa lossiin jokaisen toisen kerroksen neuronin kautta. Verkossa, jossa on kaksi sadan yksikön piilokerrosta, on noin kaksikymmentätuhatta parametria, ja jokainen tarvitsee oman osittaisderivaattansa samasta lossista. Sen tekeminen käsin ei ole työlästä; se on mahdotonta, ja se pysyy mahdottomana jokaiselle arkkitehtuurille tämän kurssin loppuosassa.

Tie ulos ei ole parempi merkintätapa. Se on oivallus, että komposition derivaatta voidaan laskea mekaanisesti, ohjelmalla, itse laskennan rakenteesta — ja että jos teet sen oikeaan suuntaan, saat kaikki kaksikymmentätuhatta derivaattaa suunnilleen sillä hinnalla, jolla loss lasketaan kerran.

Tuo mekanismi on reverse-mode automatic differentiation. Neuroverkkoon sovellettuna sitä kutsutaan nimellä backpropagation, ja tämän luvun lopussa olet kirjoittanut sellaisen noin 120 Python-rivillä ilman kirjastoja, tarkistanut sen PyTorchia vasten ja käyttänyt sitä ratkaisemaan XOR-ongelman, joka kaatoi perceptronin Chapter 1:ssa.

First: why there has to be a nonlinearity at all

Linkki osioon: First: why there has to be a nonlinearity at all

Ennen koneen rakentamista yksi kysymys on ratkaistava, koska jos vastaus olisi toisenlainen, mitään rakennettavaa ei olisi.

Perceptron epäonnistui XOR:ssa, koska yksi suora ei voi erottaa neljää pistettä. Ilmeinen korjaus on pinoaminen: aja syöte yhden lineaarisen kerroksen läpi ja sitten toisen. Auttaako se?

Ei, ja todistus on kahden rivin mittainen. Lineaarinen kerros on h=W1x+b1\mathbf{h} = W_1\mathbf{x} + \mathbf{b}_1. Syötä se toiseen, y=W2h+b2\mathbf{y} = W_2\mathbf{h} + \mathbf{b}_2, ja sijoita:

y=W2(W1x+b1)+b2=(W2W1)x+(W2b1+b2)\mathbf{y} = W_2(W_1\mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2 = (W_2W_1)\mathbf{x} + (W_2\mathbf{b}_1 + \mathbf{b}_2)

Kompositio on Wx+bW\mathbf{x} + \mathbf{b}, missä W=W2W1W = W_2W_1 ja b=W2b1+b2\mathbf{b} = W_2\mathbf{b}_1 + \mathbf{b}_2. Lineaaristen kerrosten pino on yksi lineaarinen kerros. Kymmenen niitä, tuhat niitä: silti yksi suora, silti kyvytön tekemään XOR:ää.

Se kannattaa katsoa tapahtuvan eikä vain uskoa:

linear_is_linear.pyPYTHON
import numpy as np
rng = np.random.default_rng(0)
W1, b1 = rng.normal(size=(3, 2)), rng.normal(size=3)
W2, b2 = rng.normal(size=(1, 3)), rng.normal(size=1)
x = rng.normal(size=2)

two_layers = W2 @ (W1 @ x + b1) + b2
one_layer  = (W2 @ W1) @ x + (W2 @ b1 + b2)
print(two_layers[0], one_layer[0], abs(two_layers[0] - one_layer[0]))
TEXT
-4.612963371048  -4.612963371048  0.00e+00

Ei likimain yhtä suuri. Bitti bitiltä identtinen, koska kyse on samasta aritmetiikasta uudelleen järjestettynä.

Syvyys ei siis yksin osta mitään. Se, mikä ostaa jotain, on epälineaarisen funktion sijoittaminen kerrosten väliin — ja se on koko syy siihen, että aktivaatiofunktiot ovat olemassa. Ne eivät ole biologinen koriste tai normalisointikikka. Ilman sellaista toinen kerros on somiste.

The chain rule, on paper, with a shared node

Linkki osioon: The chain rule, on paper, with a shared node

Nyt matematiikkaan, ja kyseessä on yksi sääntö, jonka jo tunnet, sovellettuna hieman vieraampaan paikkaan.

Yhden muuttujan ketjusääntö sanoo, että jos LL riippuu cc:stä ja cc riippuu xx:stä, niin dLdx=dLdcdcdx\frac{dL}{dx} = \frac{dL}{dc} \cdot \frac{dc}{dx}. Derivaatat kerrotaan ketjua pitkin.

Tässä tärkeää on se, mitä tapahtuu, kun muuttuja syöttää useampaa kuin yhtä alavirran polkua. Jos xx vaikuttaa LL:een aa:n kautta ja myös bb:n kautta, kontribuutiot summataan:

dLdx=Laax+Lbbx\frac{dL}{dx} = \frac{\partial L}{\partial a}\frac{\partial a}{\partial x} + \frac{\partial L}{\partial b}\frac{\partial b}{\partial x}

Kerro polkua pitkin, summaa polkujen yli. Siinä on koko backpropagation, ja jokainen toteutusyksityiskohta tämän luvun loppuosassa — mukaan lukien koodin += ja zero_grad()-kutsu, johon jokainen ensimmäistä training loopiaan kirjoittava kompastuu — on suora seuraus tuosta toisesta sanasta.

Otetaan konkreettinen viiden operaation piiri, jossa x=0.5x = 0.5 ja y=1.4y = 1.4:

a=xy,b=x+y,c=ab,d=tanh(c),L=d+xa = xy, \quad b = x + y, \quad c = ab, \quad d = \tanh(c), \quad L = d + x

Huomaa, että xx esiintyy kolme kertaa: aa:ssä, bb:ssa ja suoraan LL:ssa. Tee backward pass paperilla oikealta vasemmalle aloittaen kohdasta dLdL=1\frac{dL}{dL} = 1:

L=d+xL = d + x, joten Ld=1\frac{\partial L}{\partial d} = 1, ja suora polku kontribuoi Lx=1\frac{\partial L}{\partial x} = 1. Yhteenlasku jakaa saapuvan gradientin muuttumattomana molemmille syötteille.

d=tanh(c)d = \tanh(c), missä c=ab=0.7×1.9=1.33c = ab = 0.7 \times 1.9 = 1.33, joten dLdc=1tanh2(1.33)=0.2444\frac{dL}{dc} = 1 - \tanh^2(1.33) = 0.2444.

c=abc = ab, joten dLda=dLdcb=0.2444×1.9=0.4644\frac{dL}{da} = \frac{dL}{dc} \cdot b = 0.2444 \times 1.9 = 0.4644 ja dLdb=dLdca=0.2444×0.7=0.1711\frac{dL}{db} = \frac{dL}{dc} \cdot a = 0.2444 \times 0.7 = 0.1711. Kertolasku vaihtaa: kunkin syötteen gradientti skaalataan toisen syötteen arvolla.

Kohdan aa kautta: dLday=0.4644×1.4=0.6501\frac{dL}{da} \cdot y = 0.4644 \times 1.4 = 0.6501. Kohdan bb kautta: dLdb1=0.1711\frac{dL}{db} \cdot 1 = 0.1711. Suoraan: 11.

dLdx=0.6501+0.1711+1.0000=1.8212\frac{dL}{dx} = 0.6501 + 0.1711 + 1.0000 = 1.8212

Pidä kiinni tuosta luvusta. Muutaman sivun päästä ohjelma tuottaa sen ilman, että sille kerrotaan mitään tästä.

Oivallus, joka tekee tästä ohjelmoitavaa: jokainen noista askelista oli paikallinen. Gradientin työntämiseen kertolaskusolmun läpi tarvitsit saapuvan gradientin ja kaksi tallennettua syötearvoa — et mitään muusta piiristä. Jokainen operaatio osaa derivoida itsensä.

Tehdään siis luku, joka muistaa, mikä sen tuotti.

value.pyPYTHON
class Value:
    """A number that remembers where it came from."""

    def __init__(self, data, _children=(), _op=""):
        self.data = data
        self.grad = 0.0
        self._backward = lambda: None
        self._prev = set(_children)      
        self._op = _op

Neljä kenttää. data on arvo. grad kerää arvon Lself\frac{\partial L}{\partial \text{self}}. _prev on joukko niistä Value-olioista, joista tämä laskettiin — graafin reunat. Ja _backward on sulkeuma, jonka kukin operaatio asentaa: se tietää, miten tämän solmun gradientti työnnetään yksi askel taaksepäin sen syötteisiin.

Jokainen operaattori noudattaa samaa muotoa: laske ulostulo, tallenna vanhemmat, asenna paikallinen sääntö.

value.py (continued)PYTHON
    def __add__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        out = Value(self.data + other.data, (self, other), "+")

        def _backward():
            self.grad += out.grad       
            other.grad += out.grad      

        out._backward = _backward
        return out

    def __mul__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        out = Value(self.data * other.data, (self, other), "*")

        def _backward():
            self.grad += other.data * out.grad   
            other.grad += self.data * out.grad   

        out._backward = _backward
        return out

    def tanh(self):
        t = math.tanh(self.data)
        out = Value(t, (self,), "tanh")

        def _backward():
            self.grad += (1 - t * t) * out.grad

        out._backward = _backward
        return out

    def relu(self):
        out = Value(self.data if self.data > 0 else 0.0, (self,), "relu")

        def _backward():
            self.grad += (1.0 if out.data > 0 else 0.0) * out.grad

        out._backward = _backward
        return out

Lue neljä _backward-runkoa taulukkona, ja paperijohdannosta tutut virtauskuviot ovat siinä suoraan näkyvissä:

operaatiomitä se tekee gradientille
+jakaa — sama gradientti jokaiselle syötteelle
*vaihtaa — kukin syöte skaalataan toisen arvolla
relureitittää — päästää sen läpi tai estää kokonaan
tanhvaimentaa — skaalaa arvolla 1t21 - t^2, joka on enintään 1 ja yleensä pienempi

Jokainen niistä käyttää += eikä koskaan =. Se on ”summaa polkujen yli” -sääntö koodattuna. Solmua, joka syöttää kahta kuluttajaa, kutsutaan kahdesti, ja kaksi kontribuutiota summautuvat itsestään.

Sitten ajuri, joka on ainoa osa, jolla on mitään globaalia tietoa:

value.py (continued)PYTHON
    def backward(self):
        order, seen = [], set()

        def build(v):
            if v in seen:
                return
            seen.add(v)
            for child in v._prev:
                build(child)
            order.append(v)

        build(self)
        self.grad = 1.0
        for v in reversed(order):       
            v._backward()               

build tuottaa graafin topologisen järjestyksen: jokainen solmu ilmestyy kaikkien syötteidensä jälkeen. Listan läpikäynti käänteisessä järjestyksessä takaa, että kun kutsut solmun _backward-funktiota, sen oma gradientti on jo valmis — jokainen sen alavirran kuluttaja on jo kontribuoinut. Jos järjestys on väärä, työnnät puolivalmiin gradientin taaksepäin, mikä tuottaa väärän vastauksen ilman virheilmoitusta.

check.pyPYTHON
x = Value(0.5)
y = Value(1.4)
a = x * y
b = x + y
c = a * b
d = c.tanh()
L = d + x
L.backward()
print(x.grad, y.grad)
TEXT
forward:  a=0.7000  b=1.9000  c=1.3300  d=0.8692  L=1.3692
backward: dL/dd=1.0000  dL/dc=0.2444  dL/da=0.4644  dL/db=0.1711
          dL/dx=1.8212   dL/dy=0.4033

1.8212. Sama luku ohjelmasta, jolle kerrottiin sääntö kohteelle +, sääntö kohteelle *, sääntö kohteelle tanh, eikä mitään tästä piiristä.

Kaksi riippumatonta tarkistusta, koska ”se vastaa johtamaani tulosta” on heikko testi, kun sama henkilö teki molemmat.

Numeerinen derivointi. Tönäise syötettä ja mittaa. Keskitetty erotus L(x+h)L(xh)2h\frac{L(x+h) - L(x-h)}{2h} arvioi derivaatan ilman lainkaan analyysiä:

TEXT
dL/dx:  analytic=1.821202805  numeric=1.821202805  |diff|=1.80e-10
dL/dy:  analytic=0.403269235  numeric=0.403269235  |diff|=7.64e-12

PyTorchia vasten, jossa on teollisuustason autodiff-moottori, jonka ovat kirjoittaneet ihmiset, jotka tekevät tätä työkseen:

TEXT
torch dL/dx=1.821202805316   ours=1.821202805316   |diff|=2.22e-16
torch dL/dy=0.403269234753   ours=0.403269234753   |diff|=1.11e-16

Yhtäpitävyys kohdassa 2×10162 \times 10^{-16}, joka on 64-bittisen liukuluvun machine epsilon: kaksi moottoria suorittaa identtistä aritmetiikkaa. Pidä numeerinen tarkistus taskussa — se on työkalu uuden kerroksen backward passin debuggaamiseen, ja se on syy siihen, että väärä gradientti on ylipäätään löydettävissä.

Sama piiri, eri syötteet. Aseta x=2x = 2 ja y=3y = -3, mikä tekee c=6c = 6:

TEXT
x=0.5, y=1.4:  dL/dc = 0.244400     three paths into x:  0.6501 + 0.1711 + 1.0000 = 1.8212
x=2.0, y=-3.0: dL/dc = 0.000025     three paths into x:  0.0001 + -0.0001 + 1.0000 = 0.9999

tanh\tanh-solmun ylittävä gradientti putosi kertoimella 9 945. Kaikki sen ylävirrassa — oikeassa verkossa jokainen sitä edeltävä kerros — saa käytännössä ei mitään. Piirin kaksi polkua ovat vaienneet; vain suora yhteys, joka ohittaa tanh\tanh:n, kantaa yhä signaalia.

Tämä on katoavan gradientin ongelma yhdessä solmussa. Pinoa neljäkymmentä kerrosta tanh\tanh:ää ja kerro neljäkymmentä tällaista tekijää keskenään, ja varhaiset kerrokset lakkaavat oppimasta kokonaan. Se on myös, sivumennen, argumentti skip connectioneille, jonka näet tässä pienoiskoossa: epälineaarisuuden ohittanut polku on ainoa, joka selvisi.

What zero_grad actually does, and why the bug hides

Linkki osioon: What zero_grad actually does, and why the bug hides

Jokainen _backward käyttää +=. Se on oikein — niin polut summataan. Mutta sillä on seuraus, johon kaikki törmäävät: gradientit kertyvät myös backward()-kutsujen yli. Moottorilla ei ole aavistustakaan, että toinen kutsusi on uusi training-askel eikä toinen polku samassa graafissa.

Siksi training loopin täytyy tyhjentää ne:

train.pyPYTHON
for step in range(steps):
    ys = [model(x) for x, _ in DATA]
    loss = sum((yp - yt) ** 2 for yp, (_, yt) in zip(ys, DATA))

    for p in model.parameters():   
        p.grad = 0.0

    loss.backward()
    for p in model.parameters():
        p.data -= lr * p.grad

Tämä on optimizer.zero_grad() PyTorchissa, ja tavallinen neuvo on, että sen unohtaminen rikkoo trainingin. Poistetaan siis nuo kaksi riviä ja katsotaan, kuinka rikki se on. Samat seedit, sama kaikki, 200 askelta XOR:ää:

learning rateseedresetilläilman resetiä
0.051337häviö 3.255088, 3/4häviö 0.000000, 4/4
0.057häviö 2.144820, 2/4häviö 0.000000, 4/4
0.0542häviö 2.126074, 2/4häviö 0.000000, 4/4
0.11337häviö 0.038597, 4/4häviö 0.000000, 4/4
0.17häviö 2.055048, 2/4häviö 0.000000, 4/4
0.142häviö 2.049876, 2/4häviö 0.000073, 4/4
0.31337häviö 4.512310, 2/4häviö 8.000000, 2/4
0.37häviö 0.015247, 4/4häviö 4.000000, 3/4
0.342häviö 0.005478, 4/4häviö 4.000000, 3/4

Pienillä learning rateilla bugillinen versio voittaa jokaisella rivillä. Se konvergoi, kun oikea versio jumittuu.

Se ei ole sattuma, ja se kannattaa ymmärtää, koska se selittää, miksi tätä bugia on niin vaikea saada kiinni. Jos et koskaan tyhjennä gradienttia, niin askeleella kk parametri päivitetään kaikkien tähän mennessä laskettujen gradienttien summalla. Lossilla, joka osoittaa suunnilleen samaan suuntaan, tuo summa kasvaa tasaisesti, ja vaikutus on learning rate, joka kasvaa itsestään. Kohdassa η=0.05\eta = 0.05, jossa oikea algoritmi matelee, karkaava askelkoko näyttää täsmälleen korjaukselta.

Katso sitten alimmaisia kolmea riviä. Kohdassa η=0.3\eta = 0.3 sama mekanismi repii mallin hajalle — häviö 8.0 on tulos, jonka vakioon ±1\pm 1 romahtanut malli saa — puolet siitä 16:sta, jonka neljä maksimaalisesti väärää vastausta maksaisi — — samalla kun oikea versio nyt konvergoi siististi.

Rehellinen väite ei siis ole ”kutsu aina zero_grad tai mallisi ei trainaa”. Se on: ilman sitä et enää aja gradient descentiä. Aja jotain, jonka askelkoko ajautuu ylöspäin nopeudella, jota kukaan ei valinnut, ja se näyttää toimivan, joskus paremmin kuin oikea asia, juuri siihen asti kun ei enää toimi — jolloin syytät learning ratea, alustusta tai dataa. Tämä on machine learningin pahimpien bugien muoto: ne eivät kaadu, ne muuttavat algoritmin toiseksi algoritmiksi, joka joskus saa paremman tuloksen.

Kun moottori on valmis, neuroverkko on tuskin lainkaan koodia. Neuroni on pistetulo, bias ja aktivaatio; kerros on lista neuroneja; verkko on lista kerroksia.

nn.pyPYTHON
class Neuron:
    def __init__(self, nin):
        self.w = [Value(random.uniform(-1, 1)) for _ in range(nin)]
        self.b = Value(0.0)

    def __call__(self, x):
        act = sum((wi * xi for wi, xi in zip(self.w, x)), self.b)
        return act.tanh()

    def parameters(self):
        return self.w + [self.b]


class Layer:
    def __init__(self, nin, nout):
        self.neurons = [Neuron(nin) for _ in range(nout)]

    def __call__(self, x):
        out = [n(x) for n in self.neurons]
        return out[0] if len(out) == 1 else out

    def parameters(self):
        return [p for n in self.neurons for p in n.parameters()]


class MLP:
    def __init__(self, nin, nouts):
        sizes = [nin] + nouts
        self.layers = [Layer(sizes[i], sizes[i + 1]) for i in range(len(nouts))]

    def __call__(self, x):
        for layer in self.layers:
            x = layer(x)
        return x

    def parameters(self):
        return [p for layer in self.layers for p in layer.parameters()]

Tuossa ei ole backward passia. Ei riviäkään. Value-luokka osaa jo derivoida mitä tahansa nämä luokat sattuvat rakentamaan, mikä on koko syy siihen, että kirjoitimme sen ensin: autodiff-moottori ei tiedä, että sitä käytetään neuroverkkoon.

Nyt Chapter 1:n ongelma. Kaksi syötettä, kaksi piiloyksikköä, yksi ulostulo, yhdeksän parametria:

TEXT
step   1: loss 4.156690
step  10: loss 4.005572
step  50: loss 3.996708
step 100: loss 3.510700
step 200: loss 0.038597

[0, 0] -> -0.9081  (target -1)  ok
[0, 1] -> +0.8934  (target +1)  ok
[1, 0] -> +0.8906  (target +1)  ok
[1, 1] -> -0.9207  (target -1)  ok

Neljä neljästä. Funktio, jota yksikään perceptron ei voi laskea — Chapter 1:ssä todistettu neljällä epäyhtälöllä, jotka vaativat bb:n olevan sekä positiivinen että negatiivinen — lasketaan yhdeksällä luvulla, jotka löytyivät automaattisesti.

Tyydyttävä osa ei ole se, että se toimii. Se on se, että näet miten, koska kahdella piiloyksiköllä väliesitys on piste tasossa ja voit vain tulostaa sen.

Kun malli on trainattu häviöön 0.001241, tässä on, mihin kukin syöte päätyy piilokerroksen jälkeen ja mitä ulostuloneuroni tekee sille:

syötepiilokerroksen ulostuloulostulopisteetlabel
(0,0)(0, 0)(+0.8206, 0.8474)(+0.8206,\ -0.8474)2.4045-2.40451-1
(0,1)(0, 1)(+0.9985, +0.8564)(+0.9985,\ +0.8564)+2.3049+2.3049+1+1
(1,0)(1, 0)(0.8401, 0.9990)(-0.8401,\ -0.9990)+2.3006+2.3006+1+1
(1,1)(1, 1)(+0.8368, 0.8550)(+0.8368,\ -0.8550)2.4786-2.47861-1

Katso ensimmäistä ja neljättä riviä. Syötteet (0,0)(0,0) ja (1,1)(1,1) ovat neliön diagonaalisesti vastakkaiset kulmat — niin kaukana toisistaan kuin kaksi pistettä tässä ongelmassa voi olla — ja piilokerros kuvaa ne pisteisiin (0.82,0.85)(0.82, -0.85) ja (0.84,0.86)(0.84, -0.86). Lähes samaan pisteeseen. Kerros on taittanut tason niin, että kaksi hylättävää kulmaa päätyy päällekkäin, ja kun ne ovat samassa paikassa, yksi suora erottaa ne kahdesta muusta.

Ja ulostuloneuroni on täsmälleen tuo suora. Sen opitut parametrit ovat w=(3.1153, +3.0893)\mathbf{w} = (-3.1153,\ +3.0893), b=+2.7697b = +2.7697, joten sen päätösraja on

3.1153h1+3.0893h2+2.7697=0-3.1153\,h_1 + 3.0893\,h_2 + 2.7697 = 0

joka on suora viiva — perceptron, sama objekti Chapter 1:stä, muuttumattomana. Se ei voinut ratkaista XOR:ää silloin eikä se voi sitä nytkään. Muuttunut asia on, ettei se enää katso syötettä; se katsoo avaruutta, jonka ensimmäinen kerros rakensi sille ja jossa ongelma on lineaarisesti eroteltavissa.

Tätä opittu representaatio tarkoittaa, ja siitä kannattaa olla täsmällinen, koska ilmausta käytetään löyhästi tämän kurssin loppuosassa ja koko alalla. Se ei ole kompressio, tiivistelmä tai embedding missään mystisessä mielessä. Se on koordinaattien muutos, opittu eikä suunniteltu, jonka ainoa tehtävä on tehdä seuraavan kerroksen työ helpoksi.

The universal approximation theorem, and what it does not say

Linkki osioon: The universal approximation theorem, and what it does not say

Tässä on teoreema, ja sitä siteerataan yleensä huonosti.

Cybenko vuonna 1989 ja Hornik vuonna 1991 todistivat, että feedforward-verkko, jossa on yksi piilokerros ja sopiva aktivaatiofunktio, voi approksimoida minkä tahansa jatkuvan funktion kompaktissa joukossa niin tarkasti kuin haluat, kunhan piiloyksiköitä on riittävästi.34 Se on aito ja tärkeä tulos: se sanoo, ettei arkkitehtuuri ole rajoite.

Lue nyt, mitä se jättää pois. Se ei sano montako yksikköä — raja voi olla astronomisen suuri. Se ei sano, että painot voidaan löytää; se väittää olemassaolon, eikä gradient descent satunnaisesta alusta ole oraakkeli. Eikä se sano mitään käyttäytymisestä datalla, jota et ole nähnyt, mikä on Chapter 6:n toinen puolisko.

Ero ”on olemassa” ja ”löydettävissä” välillä ei ole akateeminen. Tässä on sama XOR-ongelma, 50 satunnaista alustusta kussakin, 1000 askelta, vain piilokerroksen koko muuttuu:

piiloyksikötalustuksia, jotka saavuttavat 4/4
238 / 50 (76 %)
349 / 50 (98 %)
450 / 50 (100 %)
847 / 50 (94 %)

Minimikelpoisella arkkitehtuurilla joka neljäs ajo ei koskaan pääse perille — se asettuu konfiguraatioon, josta se ei voi laskeutua ulos, täsmälleen siihen paikalliseen minimiin, jonka Chapter 3 näytti yksiulotteisella pinnalla. Lisää yksi yksikkö ja epäonnistumiset lähes katoavat, ei siksi että verkosta tuli ilmaisuvoimaisempi (kaksi yksikköä riittää jo — 38 ajoa todistaa sen), vaan koska lisäulottuvuudet antavat descentille enemmän pakosuuntia.

Ja sitten kahdeksan yksikköä toimii hieman huonommin kuin neljä. Kiinteällä learning ratella ja askelbudjetilla suurempi kapasiteetti ei ole monotonisesti parempi. Jokainen, joka sanoo sinulle, että jumissa olevan verkon korjaus on aina suurempi verkko, ekstrapoloi tuon taulukon keskikohdasta.

Tämä on sama opetus kuin Chapter 1:n konvergenssiteoreemassa, ja se on sama opetus Chapter 10:ssa scaling laeista siinä muodossa, jonka tuo luku sille antaa: lossin ennuste ei ole ennuste siitä kyvykkyydestä, josta maksat, ja näiden kahden väli on se paikka, jossa engineering elää.

Näytä lisätiedot

Valinnainen: matriisimuoto ja miksi yllä oleva koodi ei käytä sitä.

Kaikki tässä on kirjoitettu yksi skalaari kerrallaan, mikä on selkein tapa nähdä mekanismi ja hitain tapa suorittaa se. Käytännössä kerros on matriisikertolasku, ja kohdan y=Wx\mathbf{y} = W\mathbf{x} backward pass on

LW=Lyx,Lx=WLy\frac{\partial L}{\partial W} = \frac{\partial L}{\partial \mathbf{y}}\mathbf{x}^\top, \qquad \frac{\partial L}{\partial \mathbf{x}} = W^\top\frac{\partial L}{\partial \mathbf{y}}

Transpoosit eivät ole muistettava kikka; ne ovat sitä, miltä polkujen yli summaamisen sääntö näyttää, kun polut indeksoidaan matriisin alkioilla. Yleinen objekti on Jacobian, kaikkien ulostulojen kaikkien osittaisderivaattojen matriisi kaikkien syötteiden suhteen, ja reverse mode on täsmälleen vektori-Jacobian-tulon laskemista muodostamatta Jacobiania koskaan — mikä on olennaista, koska kerroksella, jossa on 4096 syötettä ja 4096 ulostuloa, tuo matriisi sisältää kuusitoista miljoonaa alkiota eikä sitä kannata koskaan rakentaa.

Et tarvitse tätä seurataksesi seuraavia lukuja; skalaariversio tekee kaiken, minkä matriisiversio tekee, vain hitaammin. Se tulee tarpeelliseksi Chapter 9:ssä, jossa muodot lakkaavat olemasta ilmeisiä.

Sinulla on nyt verkko, joka trainaa. Se on pienempi saavutus kuin miltä se tuntuu, koska verkko trainaa neljällä esimerkillä ja sitä mitataan samoilla neljällä.

Aja sama koodi oikealla datasetillä, ja esiin tulee uusi joukko ongelmia, joista mikään ei koske gradientteja. Loss laskee hetken ja pysähtyy sitten. Tai se laskee training-datalla ja nousee kaikella muulla. Tai se ei liiku lainkaan ensimmäisestä askeleesta, ja syyksi paljastuu alkuperäisten satunnaispainojen alue. Tai yhden yksikön syöte ajautui negatiiviseksi jokaisella esimerkillä kolmannessa epochissa, ja se on ollut kuollut siitä lähtien, hiljaa, vieden palan mallin kapasiteetista mukanaan.

Nämä eivät ole eksoottisia epäonnistumisia; ne ovat juuri kirjoitetun verkon normaali tila, eikä yksikään ilmoita itsestään. Gradientti on oikein — tarkistit sen PyTorchia vasten kuudentoista desimaalin tarkkuudella — ja malli ei silti opi.

Chapter 6 käsittelee tätä: alustusta, normalisointia, overfittingiä ja regularisointia sekä diagnostista tapaa kysyä, mikä näistä on käynnissä ennen kuin mitään muutetaan. Se on ero verkon, joka pyörii, ja verkon, joka toimii, välillä.


Tämän luvun Value-luokka polveutuu suoraan Andrej Karpathyn microgradista, ja hänen videonsa The spelled-out intro to neural networks and backpropagation: building micrograd on parhaat kolme tuntia, jotka voit käyttää tähän materiaaliin, jos haluat kuulla sen selitettynä toisella tavalla jonkun muun toimesta. Hänen vuoden 2016 kirjoituksensa Yes you should understand backprop perustelee, miksi sellainen kannattaa kirjoittaa itse, ja se on pakollista luettavaa Stanfordin CS224n-kurssilla. CS231n-muistiinpanot backpropagationista (cs231n.github.io/optimization-2) ovat yllä taulukoitujen virtauskuvioiden kanoninen käsittely. Matematiikasta graafin analyysinä eikä neuroverkkofolklorena Deisenrothin, Faisalin ja Ongin Mathematics for Machine Learning -kirjan luku 5.6 on poikkeuksellisen selkeä; ja Baydinin, Pearlmutterin, Radulin ja Siskindin katsaus Automatic Differentiation in Machine Learning: a Survey (arXiv:1502.05767) on koko alan viite, mukaan lukien yllä käsitelty forward/reverse-vaihtokauppa.

  1. Linnainmaa, S. The representation of the cumulative rounding error of an algorithm as a Taylor expansion of the local rounding errors. Pro gradu -tutkielma, Helsingin yliopisto (1970). Reverse-mode-kertymä, kuusitoista vuotta ennen kuin se saapui tälle alalle ja täysin eri motivaatiolla.

  2. Rumelhart, D. E., Hinton, G. E. ja Williams, R. J. Learning representations by back-propagating errors. Nature 323, s. 533–536 (1986). Artikkeli, joka teki menetelmän tunnetuksi, ja lähde piiloyksiköiden lukemiselle opittuina representaatioina, johon tämän luvun What the hidden layer did -osio käyttää mittauksensa.

  3. Cybenko, G. Approximation by superpositions of a sigmoidal function. Mathematics of Control, Signals and Systems 2, s. 303–314 (1989).

  4. Hornik, K. Approximation capabilities of multilayer feedforward networks. Neural Networks 4(2), s. 251–257 (1991). Yleistää Cybenkon: tulos riippuu siitä, että aktivaatio on epäpolynominen, ei siitä, että se on sigmoidaalinen.


Tekijä

David Vicente Campos

NeuraLIA Labsin perustaja ja MyRealFoodin toinen perustaja

Olen valmistunut tietotekniikan insinööriksi Leónin yliopistosta. Olin mukana perustamassa MyRealFoodia, jossa teknologiajohtajana rakensin sovelluksen, jota miljoonat ihmiset ovat käyttäneet syödäkseen paremmin, ja perustin NeuraLIA Labsin, jossa rakennan tekoälytuotteita. Täällä kirjoitan siitä, mitä minun on pitänyt ymmärtää matkan varrella, niin kuin olisin toivonut jonkun selittävän asiat minulle.

Lisää kirjoittajasta

Julkaisija: NeuraLIA Labs.

Uudet julkaisut suoraan sähköpostiisi

AI-uutisia, oppaita ja tuoteuutisia — lyhyt sähköposti, kun julkaisemme jotain aikasi arvoista.

Kurssin hakemisto

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev9 min lukuaikaa

Jev AI -malli on rakennettu päätöksiä, ei proosaa varten

TypeSafe AI:n Jev herättää huomiota, koska se käsittelee ohjelmistojen älykkyyttä todennäköisyysongelmana: valitse oikea haara, liitä mukaan varmuus ja vältä maksamasta LLM:lle tekstin kirjoittamisesta, kun koodi tarvitsee päätöksen.

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering9 min lukuaikaa

Kontekstisuunnittelu pitkän aikavälin AI-agenteille

Pitkäkestoiset agentit eivät epäonnistu vain siksi, että ikkuna on pieni. Ne epäonnistuvat, kun tiedostot, työkalujen tulosteet ja vanhentunut historia syrjäyttävät tehtävän, joka agentin piti saada valmiiksi.

Valmis antamaan LIA:n valita puolestasi?

Rakenna kaikilla tekoälymalleilla yhdessä paikassa — aloita ilmaiseksi jo tänään.