Ugrás a tartalomra
5/305/30. fejezet

Backpropagation nulláról: előbb a motor, aztán a hálózat

Írj 120 soros autodiff motort tiszta Pythonban, ellenőrizd PyTorch ellen 16 tizedesig, és értsd meg a zero_grad szerepét.

Ezen az oldalon

Négy fejezettel később van egy lyuk a kurzus közepén.

A 3. fejezet megadta nekünk a gradient descentet: egy paraméter javításához keresd meg a veszteség meredekségét az adott paraméter szerint, és lépj lefelé a lejtőn. A 4. fejezet adott egy veszteséget, amelyen érdemes leereszkedni. De mindkettőben a deriváltat kézzel számoltuk ki — egy modell, egy paraméter, egy sor kalkulus, és elfért egy oldalon.

Most rakj egymásra két réteget. Az első kimenete táplálja a másodikat, így az első réteg minden súlya a második réteg minden neuronján keresztül hat a veszteségre. Egy két rejtett rétegű, rétegenként száz egységes hálózatnak nagyjából húszezer paramétere van, és mindegyiknek kell a saját parciális deriváltja ugyanabból a veszteségből. Ezt kézzel elvégezni nem fárasztó; lehetetlen, és a kurzus hátralévő részének minden architektúrájára lehetetlen is marad.

A kiút nem egy jobb jelölés. Hanem annak felismerése, hogy egy kompozíció deriváltja mechanikusan, programmal kiszámítható magának a számításnak a szerkezetéből — és hogy ha a megfelelő irányban csinálod, mind a húszezer deriváltat nagyjából annyi költséggel kapod meg, mint amennyibe a veszteség egyszeri kiszámítása kerül.

Ez a mechanizmus a reverse-mode automatic differentiation. Neurális hálózatra alkalmazva backpropagationnek hívják, és a fejezet végére megírsz egyet körülbelül 120 sor Pythonban, könyvtárak nélkül, ellenőrzöd PyTorch ellen, és megoldod vele az XOR-problémát, amely megölte a perceptront az 1. fejezetben.

Először: miért kell egyáltalán nemlinearitás

Link a szakaszhoz: Először: miért kell egyáltalán nemlinearitás

Mielőtt megépítenénk a gépet, egy kérdést tisztázni kell, mert ha a válasz más lenne, nem lenne mit megépíteni.

A perceptron azért bukott el XOR-on, mert egyetlen egyenes nem tudja elválasztani a négy pontot. A kézenfekvő javítás a rétegezés: futtasd át a bemenetet egy lineáris rétegen, aztán egy másikon. Segít ez?

Nem, és a bizonyítás két sor. Egy lineáris réteg h=W1x+b1\mathbf{h} = W_1\mathbf{x} + \mathbf{b}_1. Add be egy másiknak, y=W2h+b2\mathbf{y} = W_2\mathbf{h} + \mathbf{b}_2, és helyettesíts:

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)

A kompozíció Wx+bW\mathbf{x} + \mathbf{b}, ahol W=W2W1W = W_2W_1 és b=W2b1+b2\mathbf{b} = W_2\mathbf{b}_1 + \mathbf{b}_2. Lineáris rétegek egymásra rakása egyetlen lineáris réteg. Tíz darab, ezer darab: még mindig egy egyenes, még mindig képtelen XOR-ra.

Érdemes ezt látni, nem csak elhinni:

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

Nem közelítőleg egyenlők. Bitenként azonosak, mert ugyanaz az aritmetika átrendezve.

A mélység tehát önmagában semmit nem ad. Ami ad valamit, az egy nemlineáris függvény beillesztése a rétegek közé — és pontosan ezért léteznek az aktivációs függvények. Nem biológiai díszítés vagy normalizációs trükk. Nélkülük a második réteg csak dekoráció.

A láncszabály papíron, megosztott csomóponttal

Link a szakaszhoz: A láncszabály papíron, megosztott csomóponttal

Most jön a matematika, és ez egyetlen, már ismert szabály, csak kissé szokatlan helyen alkalmazva.

Az egyváltozós láncszabály azt mondja, hogy ha LL függ cc-tól, és cc függ xx-től, akkor dLdx=dLdcdcdx\frac{dL}{dx} = \frac{dL}{dc} \cdot \frac{dc}{dx}. A deriváltak egy lánc mentén szorzódnak.

Itt az a rész számít, amikor egy változó több mint egy lefelé vezető útba táplál. Ha xx hat LL-re aa-on keresztül, és bb-en keresztül is, akkor a hozzájárulások összeadódnak:

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}

Szorozz az út mentén, összegezz az utak között. Ez a backpropagation egésze, és a fejezet hátralévő részének minden implementációs részlete — beleértve a kódbeli +=-t és a zero_grad() hívást, amely mindenkit megakaszt az első training loop megírásakor — ennek az „összegezésnek” a közvetlen következménye.

Vegyünk egy konkrét, öt műveletből álló áramkört, ahol x=0.5x = 0.5 és 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

Figyeld meg, hogy xx háromszor jelenik meg: aa-ben, bb-ben, és közvetlenül LL-ban. Végezd el a backward passt papíron, jobbról balra, dLdL=1\frac{dL}{dL} = 1-tól indulva:

L=d+xL = d + x, tehát Ld=1\frac{\partial L}{\partial d} = 1, és a közvetlen út hozzájárulása Lx=1\frac{\partial L}{\partial x} = 1. Az összeadás szétosztja a bejövő gradientet változatlanul mindkét bemenetre.

d=tanh(c)d = \tanh(c), ahol c=ab=0.7×1.9=1.33c = ab = 0.7 \times 1.9 = 1.33, tehát dLdc=1tanh2(1.33)=0.2444\frac{dL}{dc} = 1 - \tanh^2(1.33) = 0.2444.

c=abc = ab, tehát dLda=dLdcb=0.2444×1.9=0.4644\frac{dL}{da} = \frac{dL}{dc} \cdot b = 0.2444 \times 1.9 = 0.4644 és dLdb=dLdca=0.2444×0.7=0.1711\frac{dL}{db} = \frac{dL}{dc} \cdot a = 0.2444 \times 0.7 = 0.1711. A szorzás felcserél: minden bemenet gradientje a másik bemenet értékével skálázódik.

aa-en keresztül: dLday=0.4644×1.4=0.6501\frac{dL}{da} \cdot y = 0.4644 \times 1.4 = 0.6501. bb-on keresztül: dLdb1=0.1711\frac{dL}{db} \cdot 1 = 0.1711. Közvetlenül: 11.

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

Tartsd meg ezt a számot. Néhány oldal múlva egy program fogja előállítani anélkül, hogy bármit is elmondanánk neki ebből.

A felismerés, amely programozhatóvá teszi: ezeknek a lépéseknek mindegyike lokális volt. Ahhoz, hogy egy gradientet átjuttass a szorzási csomóponton, csak a bejövő gradientre és a két eltárolt bemeneti értékre volt szükséged — semmire az áramkör többi részéből. Minden művelet tudja, hogyan kell saját magát deriválni.

Készíts tehát egy számot, amely emlékszik arra, mi hozta létre.

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

Négy mező. data az érték. grad gyűjti Lself\frac{\partial L}{\partial \text{self}}-t. _prev azoknak a Value-oknak a halmaza, amelyekből ezt kiszámoltuk — a gráf élei. _backward pedig egy closure, amelyet minden művelet telepít: tudja, hogyan kell ennek a csomópontnak a gradientjét egy lépéssel visszatolni a bemeneteihez.

Minden operátor ugyanazt a formát követi: kiszámítja a kimenetet, feljegyzi a szülőket, telepíti a lokális szabályt.

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

Olvasd a négy _backward törzsét táblázatként, és a papíros levezetés áramlási mintái ott vannak előttünk:

műveletmit tesz a gradienttel
+szétosztja — ugyanazt a gradientet minden bemenetnek
*felcseréli — minden bemenetet a másik értéke skáláz
reluroute-olja — átengedi vagy teljesen blokkolja
tanhcsillapítja1t21 - t^2-vel skálázza, ami legfeljebb 1, és általában kisebb

Mindegyik +=-t használ, soha nem =-t. Ez a „összegzés az utak között” szabály kódolva. Egy csomópont, amely két fogyasztót táplál, kétszer lesz meghívva, és a két hozzájárulás magától összeadódik.

Aztán jön a driver, az egyetlen rész, amelynek globális tudása van:

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 topologikus sorrendet állít elő a gráfból: minden csomópont az összes bemenete után jelenik meg. Ha ezt a listát fordítva járjuk be, az garantálja, hogy amikor egy csomópont _backward-ját hívod, a saját gradientje már teljes — minden alatta lévő fogyasztó hozzájárult. Ha elrontod a sorrendet, félig kész gradientet tolsz visszafelé, ami hibás választ ad hibaüzenet nélkül.

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. Ugyanaz a szám, egy olyan programból, amelynek csak + szabályát, * szabályát és tanh szabályát mondtuk el, erről az áramkörről pedig semmit.

Két független ellenőrzés, mert az „egyezik azzal, amit levezettem” gyenge teszt, ha mindkettőt ugyanaz az ember csinálta.

Numerikus deriválás. Mozdítsd meg kicsit a bemenetet, és mérj. A centrális differencia L(x+h)L(xh)2h\frac{L(x+h) - L(x-h)}{2h} minden kalkulus nélkül becsüli a deriváltat:

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

PyTorch ellen, amelynek ipari autodiff motorját olyan emberek írták, akik ebből élnek:

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

Egyezés 2×10162 \times 10^{-16} szinten, ami a 64 bites lebegőpontos szám machine epsilonja: a két motor azonos aritmetikát végez. Tartsd zsebben a numerikus ellenőrzést — ez az eszköz egy új réteg backward passának hibakereséséhez, és ez az oka annak, hogy egy rossz gradient egyáltalán megtalálható.

Ugyanaz az áramkör, más bemenetek. Állítsd x=2x = 2-t és y=3y = -3-t, amitől 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

A tanh\tanh csomóponton áthaladó gradient 9.945-ös faktorral esett vissza. Minden, ami előtte van — egy valódi hálózatban minden korábbi réteg — lényegében semmit sem kap. Az áramkörön átmenő két út elnémult; csak az a közvetlen kapcsolat visz még jelet, amely kihagyja tanh\tanh-t.

Ez az eltűnő gradient problémája egyetlen csomópontban. Rakj egymásra negyven tanh\tanh réteget, szorozz össze negyven ilyen faktort, és a korai rétegek teljesen abbahagyják a tanulást. Mellékesen ez egy érv a skip connectionök mellett is, miniatűrben látható formában: a nemlinearitást megkerülő út volt az egyetlen, amely túlélte.

Mit csinál valójában zero_grad, és miért rejtőzik el a hiba

Link a szakaszhoz: Mit csinál valójában zero_grad, és miért rejtőzik el a hiba

Minden _backward +=-t használ. Ez helyes — így adódnak össze az utak. De van egy következménye, amely mindenkit elkap: a gradientek a backward() hívások között is felhalmozódnak. A motor nem tudja, hogy a második hívásod egy új training step, nem pedig egy másik út ugyanabban a gráfban.

Ezért a training loopnak törölnie kell őket:

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

Ez PyTorch-ban optimizer.zero_grad(), és a szokásos tanács szerint ha elfelejted, elromlik a training. Töröljük tehát ezt a két sort, és nézzük meg, mennyire romlik el. Ugyanazok a seedek, minden ugyanaz, 200 lépés XOR-on:

learning rateseedresettelreset nélkül
0.051337loss 3.255088, 3/4loss 0.000000, 4/4
0.057loss 2.144820, 2/4loss 0.000000, 4/4
0.0542loss 2.126074, 2/4loss 0.000000, 4/4
0.11337loss 0.038597, 4/4loss 0.000000, 4/4
0.17loss 2.055048, 2/4loss 0.000000, 4/4
0.142loss 2.049876, 2/4loss 0.000073, 4/4
0.31337loss 4.512310, 2/4loss 8.000000, 2/4
0.37loss 0.015247, 4/4loss 4.000000, 3/4
0.342loss 0.005478, 4/4loss 4.000000, 3/4

A kis learning rate-eknél a hibás verzió minden egyes sorban nyer. Konvergál, miközben a helyes verzió beragad.

Ez nem véletlen, és érdemes megérteni, mert megmagyarázza, miért olyan nehéz elkapni ezt a hibát. Ha soha nem törlöd a gradientet, akkor a kk lépésnél a paramétert az összes addig kiszámított gradient összege frissíti. Egy olyan veszteségen, amely nagyjából ugyanabba az irányba mutat, ez az összeg folyamatosan nő, és a hatása egy magától növekvő learning rate. η=0.05\eta = 0.05 mellett, ahol a helyes algoritmus vánszorog, az elszabaduló lépésméret pontosan úgy néz ki, mint egy javítás.

Aztán nézd meg az alsó három sort. η=0.3\eta = 0.3 mellett ugyanez a mechanizmus szétszedi a modellt — a loss 8.0 azt kapja, amit egy konstans ±1\pm 1-re összeomlott modell pontoz — fele annak a 16-nak, amennyibe négy maximálisan rossz válasz kerülne — —, miközben a helyes verzió ekkor már tisztán konvergál.

Az őszinte állítás tehát nem az, hogy „mindig hívd meg zero_grad-t, különben a modelled nem fog trainelni”. Hanem ez: nélküle már nem gradient descentet futtatsz. Valami olyat futtatsz, amelynek lépésmérete senki által nem választott ütemben felfelé sodródik, és úgy fog tűnni, hogy működik, néha jobban is, mint az igazi — egészen addig, amíg már nem. Ekkor majd a learning rate-et, az inicializálást vagy az adatot fogod hibáztatni. Ez a machine learning legrosszabb hibáinak alakja: nem omlanak össze, hanem egy másik algoritmussá változtatják az algoritmust, amely időnként jobb pontszámot ér el.

Miután a motor elkészült, egy neurális hálózat alig pár sor kód. Egy neuron egy dot product, egy bias és egy aktiváció; egy réteg neuronok listája; egy hálózat rétegek listája.

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()]

Ebben nincs backward pass. Egyetlen sor sem. A Value osztály már tudja, hogyan kell deriválni bármit, amit ezek az osztályok éppen megépítenek — éppen ez volt a lényege annak, hogy előbb azt írtuk meg: egy autodiff motor nem tudja, hogy neurális hálózathoz használják.

Most az 1. fejezet problémája. Két bemenet, két rejtett egység, egy kimenet, kilenc paraméter:

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

Négyből négy. A függvényt, amelyet egyetlen perceptron sem tud kiszámítani — amit az 1. fejezetben négy egyenlőtlenséggel bizonyítottunk, amelyek azt követelték, hogy bb egyszerre legyen pozitív és negatív — kilenc automatikusan megtalált szám számítja ki.

A kielégítő rész nem az, hogy működik. Hanem hogy látható, hogyan, mert két rejtett egységnél a köztes reprezentáció egy pont a síkban, és egyszerűen ki lehet nyomtatni.

0.001241 veszteségre trainelve, itt látható, hová kerül minden bemenet a rejtett réteg után, és mit csinál vele a kimeneti neuron:

bemenetrejtett réteg kimenetekimeneti pontszámcímke
(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

Nézd az első és a negyedik sort. A (0,0)(0,0) és (1,1)(1,1) bemenetek a négyzet átlósan szemközti sarkai — ebben a problémában két pont ennél távolabb nem lehet egymástól —, a rejtett réteg pedig (0.82,0.85)(0.82, -0.85)-re és (0.84,0.86)(0.84, -0.86)-ra képezi le őket. Majdnem ugyanarra a pontra. A réteg összehajtotta a síkot úgy, hogy a két elutasított sarok egymás tetejére kerüljön, és miután ugyanott vannak, egyetlen egyenes elválasztja őket a másik kettőtől.

A kimeneti neuron pontosan ez az egyenes. A tanult paraméterei w=(3.1153, +3.0893)\mathbf{w} = (-3.1153,\ +3.0893), b=+2.7697b = +2.7697, így a döntési határa

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

ami egy egyenes — egy perceptron, ugyanaz az objektum az 1. fejezetből, változatlanul. Akkor nem tudta megoldani az XOR-t, és most sem tudja. Ami megváltozott, az az, hogy már nem a bemenetet nézi; azt a teret nézi, amelyet az első réteg épített neki, és amelyben a probléma lineárisan szeparálható.

Ez a tanult reprezentáció, és érdemes pontosnak lenni, mert a kifejezést a kurzus hátralévő részében, és az egész területen, lazán használják. Nem tömörítés, nem összefoglaló, és nem embedding valamilyen misztikus értelemben. Koordinátaváltás, amelyet nem megterveztek, hanem megtanultak, és amelynek egyetlen dolga, hogy megkönnyítse a következő réteg munkáját.

Az univerzális approximációs tétel, és amit nem mond ki

Link a szakaszhoz: Az univerzális approximációs tétel, és amit nem mond ki

Van itt egy tétel, és általában rosszul idézik.

Cybenko 1989-ben és Hornik 1991-ben bizonyította, hogy egyetlen rejtett rétegű feedforward hálózat megfelelő aktivációs függvénnyel bármely folytonos függvényt tetszőleges pontossággal tud approximálni kompakt halmazon, ha elég rejtett egységet kap.34 Ez valódi és fontos eredmény: azt mondja, hogy nem az architektúra a korlát.

Most olvasd el, mit hagy ki. Nem mondja meg, hány egység kell — a korlát csillagászatilag nagy lehet. Nem mondja, hogy a súlyok megtalálhatók; létezést állít, a véletlen indulásból végzett gradient descent pedig nem orákulum. És semmit nem mond a viselkedésről olyan adatokon, amelyeket nem láttál — ez a 6. fejezet második fele.

A „létezik” és a „megtalálható” közti rés nem akadémikus. Íme ugyanaz az XOR-probléma, 50 véletlen inicializálással, 1000 lépéssel, csak a rejtett réteg mérete változik:

rejtett egységek4/4-et elérő inicializálások
238 / 50 (76 %)
349 / 50 (98 %)
450 / 50 (100 %)
847 / 50 (94 %)

A minimálisan életképes architektúrával minden negyedik futás soha nem jut el odáig — olyan konfigurációban állapodik meg, amelyből nem tud leereszkedni, pontosan abban a lokális minimumban, amelyet a 3. fejezet mutatott meg egydimenziós felületen. Adj hozzá egy egységet, és a kudarcok szinte eltűnnek — nem azért, mert a hálózat kifejezőbb lett (két egység már elég — 38 futás bizonyítja), hanem mert az extra dimenziók több menekülési irányt adnak a descentnek.

Aztán nyolc egység kissé rosszabbul teljesít, mint négy. Rögzített learning rate és lépésszám mellett a nagyobb kapacitás nem monoton jobb. Aki azt mondja, hogy egy beragadt hálózat javítása mindig egy nagyobb hálózat, az ennek a táblázatnak a közepéből extrapolál.

Ez ugyanaz a lecke, mint az 1. fejezet konvergenciatétele, és ugyanaz lesz a 10. fejezetben a scaling law-król, abban a formában, ahogy az a fejezet megfogalmazza: a veszteség előrejelzése nem annak a képességnek az előrejelzése, amiért fizetsz, és a kettő közti távolságban él az engineering.

Részletek megjelenítése

Opcionális: a mátrixos forma, és miért nem azt használja a fenti kód.

Itt mindent skaláronként írtunk fel, ami a mechanizmus meglátásának legtisztább módja, és a végrehajtás leglassabb módja. A gyakorlatban egy réteg mátrixszorzás, és y=Wx\mathbf{y} = W\mathbf{x} backward passa

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}}

A transzponálások nem megjegyzendő trükkök; így néz ki az utak közti összegzés szabálya, amikor az utakat mátrixelemek indexelik. Az általános objektum a Jacobian, az összes kimenet összes bemenet szerinti parciális deriváltjának mátrixa, a reverse mode pedig pontosan egy vector-Jacobian product kiszámítása anélkül, hogy valaha is felépítené a Jacobiant — ami számít, mert egy 4096 bemenetű és 4096 kimenetű rétegnél ez a mátrix tizenhatmillió elemes, és soha nem éri meg megépíteni.

A következő fejezetek követéséhez erre nincs szükséged; a skaláris verzió mindent megcsinál, amit a mátrixos, csak lassabban. A 9. fejezetben válik szükségessé, ahol az alakok már nem magától értetődők.

Most már van egy hálózatod, amely trainel. Ez kisebb eredmény, mint amekkorának érződik, mert a hálózatod négy példán trainel, és ugyanazon a négyen méred.

Futtasd ugyanezt a kódot egy valódi adathalmazon, és új problémakészlet jelenik meg, amelyek közül egyik sem a gradientekről szól. A veszteség egy ideig csökken, aztán megáll. Vagy csökken a training adaton, és nő minden máson. Vagy az első lépéstől kezdve meg sem mozdul, és az ok végül a kezdeti véletlen súlyok tartománya. Vagy az egyik egység bemenete a harmadik epochban minden példán negatívra sodródott, és azóta halott, csendben, magával vive a modell kapacitásának egy darabját.

Ezek nem egzotikus hibák; ez egy éppen megírt hálózat normál állapota, és egyik sem jelenti be magát. A gradient helyes — PyTorch ellen tizenhat tizedesjegyig ellenőrizted —, és a modell mégsem tanul.

A 6. fejezet erről szól: inicializálásról, normalizálásról, overfittingről és regularizációról, valamint arról a diagnosztikai szokásról, hogy mielőtt bármit megváltoztatnál, megkérdezed: melyik történik ezek közül. Ez a különbség egy futó hálózat és egy működő hálózat között.


A fejezet Value osztálya közvetlenül Andrej Karpathy micrograd projektjéből származik, és az ő The spelled-out intro to neural networks and backpropagation: building micrograd című videója a legjobb három óra, amit erre az anyagra szánhatsz, ha szeretnéd, hogy valaki más másodszor is elmagyarázza. 2016-os Yes you should understand backprop posztja amellett érvel, hogy írj egyet magadnak, és kötelező olvasmány Stanford CS224n kurzusán. A CS231n backpropagation jegyzetei (cs231n.github.io/optimization-2) a fent táblázatba foglalt áramlási minták kanonikus tárgyalását adják. A matematika gráfon végzett kalkulusként, nem neurális hálózati folklórként való megértéséhez Deisenroth, Faisal és Ong Mathematics for Machine Learning című könyvének 5.6. fejezete szokatlanul világos; Baydin, Pearlmutter, Radul és Siskind Automatic Differentiation in Machine Learning: a Survey című áttekintése (arXiv:1502.05767) pedig a teljes terület alapreferenciája, beleértve a fent tárgyalt forward/reverse kompromisszumot is.

  1. Linnainmaa, S. The representation of the cumulative rounding error of an algorithm as a Taylor expansion of the local rounding errors. Mesterdolgozat, University of Helsinki (1970). Reverse-mode accumulation, tizenhat évvel azelőtt, hogy elérte volna ezt a területet, teljesen más motivációval.

  2. Rumelhart, D. E., Hinton, G. E. és Williams, R. J. Learning representations by back-propagating errors. Nature 323, pp. 533–536 (1986). A cikk, amely ismertté tette a módszert, és a forrása annak, hogy a rejtett egységeket tanult reprezentációkként olvassuk — amire ennek a fejezetnek a Mit csinált a rejtett réteg szakasza a méréseit fordítja.

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

  4. Hornik, K. Approximation capabilities of multilayer feedforward networks. Neural Networks 4(2), pp. 251–257 (1991). Általánosítja Cybenkót: az eredmény attól függ, hogy az aktiváció nem polinomiális, nem attól, hogy szigmoidális.


Készítette

David Vicente Campos

A NeuraLIA Labs alapítója és a MyRealFood társalapítója

Mérnökinformatikus vagyok, a Leóni Egyetemen végeztem. Társalapítottam a MyRealFoodot, ahol CTO-ként felépítettem azt az alkalmazást, amelyet emberek milliói használtak arra, hogy egészségesebben táplálkozzanak, és megalapítottam a NeuraLIA Labst, ahol AI-termékeket fejlesztek. Itt arról írok, amit menet közben meg kellett értenem, úgy, ahogy szerettem volna, hogy valaki elmagyarázza nekem.

Továbbiak a szerzőről

Közzétette a NeuraLIA Labs.

Kapj új bejegyzéseket a postaládádba

AI-hírek, útmutatók és termékfrissítések — rövid email, amikor valami igazán hasznosat publikálunk.

Kurzusindex

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev11 perc olvasás

A Jev AI-modell döntésekre készült, nem prózára

A TypeSafe AI Jev modellje azért kap figyelmet, mert a szoftveres intelligenciát valószínűségi problémaként kezeli: válaszd ki a megfelelő ágat, rendelj hozzá bizalmi szintet, és ne fizess egy LLM-nek szövegírásért, amikor a kódnak döntésre van szüksége.

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering11 perc olvasás

Kontextustervezés hosszú távú AI-ügynökökhöz

A hosszú ideig futó ügynökök nem csak azért vallanak kudarcot, mert kicsi az ablak. Akkor hibáznak, amikor a fájlok, eszközkimenetek és elavult előzmények kiszorítják azt a feladatot, amelyet az ügynöknek be kellett volna fejeznie.

Készen állsz, hogy a LIA válasszon helyetted?

Építs az összes AI-modellel egy helyen – kezdd el ma, ingyen.