Saltar ao contido
5/30Capítulo 5 de 30

Backpropagation desde cero: primeiro o motor, despois a rede

Escribe un motor autodiff de 120 liñas en Python puro, compárao con PyTorch ata dezaseis decimais e aprende que fai zero_grad ao borralo.

Nesta páxina

Catro capítulos despois, hai un oco no medio do curso.

O Capítulo 3 deunos gradient descent: para mellorar un parámetro, atopa a pendente da perda con respecto a el e dá un paso costa abaixo. O Capítulo 4 deunos unha perda pola que paga a pena descender. Pero en ambos os casos, a derivada calculábase á man: un modelo, un parámetro, unha liña de cálculo, e cabía nunha páxina.

Agora amoreas dúas capas. A saída da primeira alimenta a segunda, así que cada peso da primeira afecta á perda a través de cada neurona da segunda. Unha rede con dúas capas ocultas de cen unidades cada unha ten arredor de vinte mil parámetros, e cada un necesita a súa propia derivada parcial da mesma perda. Facelo á man non é tedioso; é imposible, e segue sendo imposible para todas as arquitecturas do resto deste curso.

A saída non é unha notación mellor. É decatarse de que a derivada dunha composición pode calculala mecanicamente un programa a partir da estrutura da propia computación; e de que, se o fas na dirección correcta, obtés as vinte mil derivadas polo custo aproximado de calcular a perda unha soa vez.

Ese mecanismo é a diferenciación automática en modo inverso. Aplicado a unha rede neuronal chámase backpropagation, e ao final deste capítulo terás escrito unha en arredor de 120 liñas de Python sen bibliotecas, comprobaraa contra PyTorch e usaraa para resolver o problema XOR que matou o perceptrón no Capítulo 1.

Primeiro: por que ten que haber unha non linearidade

Ligazón á sección: Primeiro: por que ten que haber unha non linearidade

Antes de construír a máquina, hai que pechar unha pregunta, porque se a resposta fose a contraria non habería nada que construír.

O perceptrón fallou con XOR porque unha liña non pode separar os catro puntos. A corrección obvia é amorear: pasar a entrada por unha capa lineal e despois por outra. Axuda iso?

Non, e a proba son dúas liñas. Unha capa lineal é h=W1x+b1\mathbf{h} = W_1\mathbf{x} + \mathbf{b}_1. Dálle iso a outra, y=W2h+b2\mathbf{y} = W_2\mathbf{h} + \mathbf{b}_2, e substitúe:

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 composición é Wx+bW\mathbf{x} + \mathbf{b} con W=W2W1W = W_2W_1 e b=W2b1+b2\mathbf{b} = W_2\mathbf{b}_1 + \mathbf{b}_2. Unha pila de capas lineais é unha única capa lineal. Dez delas, mil delas: segue sendo unha liña, segue sen poder facer XOR.

Paga a pena velo ocorrer en vez de crelo:

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

Non aproximadamente iguais. Idénticas bit a bit, porque é a mesma aritmética reordenada.

Así que a profundidade por si soa non compra nada. O que compra algo é poñer unha función non lineal entre as capas, e esa é toda a razón de existir das funcións de activación. Non son un adorno biolóxico nin un truco de normalización. Sen unha, a segunda capa é decoración.

A regra da cadea, no papel, cun nodo compartido

Ligazón á sección: A regra da cadea, no papel, cun nodo compartido

Agora as matemáticas, e é unha soa regra que xa coñeces aplicada nun lugar lixeiramente menos familiar.

A regra da cadea dunha soa variable di que se LL depende de cc e cc depende de xx, entón dLdx=dLdcdcdx\frac{dL}{dx} = \frac{dL}{dc} \cdot \frac{dc}{dx}. As derivadas multiplícanse ao longo dunha cadea.

A parte que importa aquí é o que ocorre cando unha variable alimenta máis dun camiño descendente. Se xx inflúe en LL a través de aa e tamén a través de bb, as contribucións súmanse:

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}

Multiplica ao longo dun camiño, suma entre camiños. Iso é todo o backpropagation, e cada detalle de implementación no resto deste capítulo — incluído o += no código e a chamada zero_grad() que fai tropezar a todo o mundo cando escribe o seu primeiro bucle de adestramento — é unha consecuencia directa desa segunda palabra.

Colle un circuíto concreto de cinco operacións, con x=0.5x = 0.5 e 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

Observa que xx aparece tres veces: en aa, en bb e directamente en LL. Fai a pasada cara atrás no papel, de dereita a esquerda, comezando por dLdL=1\frac{dL}{dL} = 1:

L=d+xL = d + x, así que Ld=1\frac{\partial L}{\partial d} = 1 e o camiño directo contribúe Lx=1\frac{\partial L}{\partial x} = 1. A suma distribúe o gradiente entrante sen cambios a ambas as entradas.

d=tanh(c)d = \tanh(c) con c=ab=0.7×1.9=1.33c = ab = 0.7 \times 1.9 = 1.33, así que dLdc=1tanh2(1.33)=0.2444\frac{dL}{dc} = 1 - \tanh^2(1.33) = 0.2444.

c=abc = ab, así que dLda=dLdcb=0.2444×1.9=0.4644\frac{dL}{da} = \frac{dL}{dc} \cdot b = 0.2444 \times 1.9 = 0.4644 e dLdb=dLdca=0.2444×0.7=0.1711\frac{dL}{db} = \frac{dL}{dc} \cdot a = 0.2444 \times 0.7 = 0.1711. A multiplicación intercambia: o gradiente de cada entrada escálase polo valor da outra entrada.

A través de aa: dLday=0.4644×1.4=0.6501\frac{dL}{da} \cdot y = 0.4644 \times 1.4 = 0.6501. A través de bb: dLdb1=0.1711\frac{dL}{db} \cdot 1 = 0.1711. Directamente: 11.

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

Queda con ese número. En poucas páxinas, un programa vaino producir sen que lle contemos nada disto.

A idea que o fai programable: cada un deses pasos era local. Para empurrar un gradiente a través do nodo de multiplicación, necesitabas o gradiente entrante e os dous valores de entrada gardados; nada sobre o resto do circuíto. Cada operación sabe como diferenciarse a si mesma.

Así que crea un número que lembre que o produciu.

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

Catro campos. data é o valor. grad acumula Lself\frac{\partial L}{\partial \text{self}}. _prev é o conxunto de Values a partir dos cales se calculou este: as arestas do grafo. E _backward é un peche que instala cada operación: sabe como empurrar o gradiente deste nodo un paso cara atrás ata as súas entradas.

Cada operador segue a mesma forma: calcula a saída, rexistra os pais, instala a regra local.

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

Le os catro corpos de _backward como unha táboa e os patróns de fluxo da derivación en papel están aí mesmo:

operaciónque lle fai ao gradiente
+distribúe — o mesmo gradiente a cada entrada
*intercambia — cada entrada escalada polo valor da outra
reluencamiña — déixao pasar ou bloquéao por completo
tanhatenúa — escala por 1t21 - t^2, que como moito é 1 e normalmente é menor

Cada unha usa += e nunca =. Esa é a regra de «sumar entre camiños», codificada. Un nodo que alimenta dous consumidores chámase dúas veces, e as dúas contribucións súmanse soas.

Despois vén o controlador, que é a única parte con coñecemento global:

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 produce unha orde topolóxica do grafo: cada nodo aparece despois de todas as súas entradas. Percorrer esa lista ao revés garante que, cando chamas ao _backward dun nodo, o seu propio gradiente xa está completo: todos os consumidores por debaixo del xa contribuíron. Equivócate na orde e empurras cara atrás un gradiente a medio facer, o que produce unha resposta incorrecta sen ningunha mensaxe de erro.

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. O mesmo número, dun programa ao que só se lle contou a regra de +, a regra de *, a regra de tanh, e nada sobre este circuíto.

Dúas comprobacións independentes, porque «coincide co que derivei» é unha proba feble cando a mesma persoa fixo as dúas cousas.

Diferenciación numérica. Move un chisco a entrada e mide. A diferenza centrada L(x+h)L(xh)2h\frac{L(x+h) - L(x-h)}{2h} estima a derivada sen ningún cálculo diferencial:

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

Contra PyTorch, que ten un motor autodiff industrial escrito por xente que se dedica a isto:

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

Acordo en 2×10162 \times 10^{-16}, que é o épsilon de máquina para un float de 64 bits: os dous motores están facendo aritmética idéntica. Garda a comprobación numérica no peto: é a ferramenta para depurar a pasada cara atrás dunha capa nova, e é a razón pola que un gradiente incorrecto se pode atopar.

O mesmo circuíto, entradas distintas. Pon x=2x = 2 e y=3y = -3, o que fai 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

O gradiente que cruza o nodo tanh\tanh caeu por un factor de 9.945. Todo o que hai augas arriba del — nunha rede real, cada capa antes del — recibe practicamente nada. Os dous camiños a través do circuíto calaron; só a conexión directa que salta tanh\tanh segue levando sinal.

Ese é o problema do gradiente que desaparece, nun só nodo. Amorea corenta capas de tanh\tanh e multiplica corenta factores coma ese, e as primeiras capas deixan de aprender por completo. Tamén é, de paso, un argumento a favor das conexións de salto que podes ver aquí en miniatura: o camiño que evitou a non linearidade é o único que sobreviviu.

Que fai realmente zero_grad, e por que o erro se agocha

Ligazón á sección: Que fai realmente zero_grad, e por que o erro se agocha

Cada _backward usa +=. Iso é correcto: é como se suman os camiños. Pero ten unha consecuencia que colle a todo o mundo: os gradientes tamén se acumulan entre chamadas a backward(). O motor non ten nin idea de que a túa segunda chamada é un novo paso de adestramento en vez doutro camiño no mesmo grafo.

Así que un bucle de adestramento ten que limpalos:

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

Isto é optimizer.zero_grad() en PyTorch, e o consello habitual é que esquecelo rompe o adestramento. Así que imos borrar esas dúas liñas e ver canto rompe. Mesmas sementes, todo igual, 200 pasos de XOR:

learning rateseedcon reiniciosen reinicio
0.051337perda 3.255088, 3/4perda 0.000000, 4/4
0.057perda 2.144820, 2/4perda 0.000000, 4/4
0.0542perda 2.126074, 2/4perda 0.000000, 4/4
0.11337perda 0.038597, 4/4perda 0.000000, 4/4
0.17perda 2.055048, 2/4perda 0.000000, 4/4
0.142perda 2.049876, 2/4perda 0.000073, 4/4
0.31337perda 4.512310, 2/4perda 8.000000, 2/4
0.37perda 0.015247, 4/4perda 4.000000, 3/4
0.342perda 0.005478, 4/4perda 4.000000, 3/4

Coas taxas de aprendizaxe pequenas, a versión con erro gaña en todas as filas. Converxe cando a versión correcta queda atascada.

Iso non é casualidade e paga a pena entendelo, porque explica por que este erro é tan difícil de detectar. Se nunca limpas o gradiente, entón no paso kk o parámetro actualízase coa suma de todos os gradientes calculados ata ese momento. Nunha perda que segue apuntando máis ou menos na mesma dirección, esa suma medra de forma constante, e o efecto é unha taxa de aprendizaxe que aumenta soa. En η=0.05\eta = 0.05, onde o algoritmo correcto vai arrastrándose, ese tamaño de paso desbocado parece exactamente unha corrección.

Despois mira as tres filas inferiores. En η=0.3\eta = 0.3 o mesmo mecanismo rebenta o modelo: perda 8.0 é o que puntúa un modelo colapsado a unha constante ±1\pm 1 — a metade dos 16 que custarían catro respostas maximamente erradas — — mentres a versión correcta agora converxe con limpeza.

Así que a afirmación honesta non é «chama sempre a zero_grad ou o teu modelo non se adestrará». É: sen iso xa non estás executando gradient descent. Estás executando algo cuxo tamaño de paso deriva cara arriba a un ritmo que ninguén escolleu, e parecerá funcionar, ás veces mellor que o real, xusto ata que deixe de facelo; nese momento culparás a taxa de aprendizaxe, a inicialización ou os datos. Esta é a forma dos peores erros en machine learning: non fallan, transforman o algoritmo noutro algoritmo distinto que ás veces puntúa mellor.

Co motor rematado, unha rede neuronal é moi pouco código. Unha neurona é un produto escalar, un nesgo e unha activación; unha capa é unha lista de neuronas; unha rede é unha lista de capas.

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

Non hai ningunha pasada cara atrás en nada diso. Nin unha liña. A clase Value xa sabe como diferenciar o que sexa que estas clases constrúan, que é a razón de tela escrito primeiro: un motor autodiff non sabe que se está usando para unha rede neuronal.

Agora o problema do Capítulo 1. Dúas entradas, dúas unidades ocultas, unha saída, nove parámetros:

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

Catro de catro. A función que ningún perceptrón pode calcular — demostrado no Capítulo 1 con catro desigualdades que esixían que bb fose á vez positivo e negativo — calcúlana nove números atopados automaticamente.

O satisfactorio non é que funcione. É poder ver como, porque con dúas unidades ocultas a representación intermedia é un punto nun plano e podes imprimila.

Adestrada ata unha perda de 0.001241, aquí é onde aterra cada entrada despois da capa oculta, e que fai con ela a neurona de saída:

entradasaída da capa ocultapuntuación de saídaetiqueta
(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

Mira a primeira e a cuarta filas. As entradas (0,0)(0,0) e (1,1)(1,1) son esquinas diagonalmente opostas do cadrado — tan afastadas como poden estar dous puntos neste problema — e a capa oculta asígnaas a (0.82,0.85)(0.82, -0.85) e (0.84,0.86)(0.84, -0.86). Case o mesmo punto. A capa dobrou o plano para que as dúas esquinas rexeitadas caian unha enriba da outra, e cando están no mesmo sitio, unha liña sepáraas das outras dúas.

E a neurona de saída é exactamente esa liña. Os seus parámetros aprendidos son w=(3.1153, +3.0893)\mathbf{w} = (-3.1153,\ +3.0893), b=+2.7697b = +2.7697, así que a súa fronteira de decisión é

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

que é unha recta: un perceptrón, o mesmo obxecto do Capítulo 1, sen cambios. Daquela non podía resolver XOR e agora tampouco pode. O que cambiou é que xa non está mirando a entrada; está mirando un espazo que a primeira capa construíu para el, no que o problema é linealmente separable.

Iso é unha representación aprendida, e paga a pena ser preciso porque a frase úsase con laxitude durante o resto deste curso e no resto do campo. Non é unha compresión, un resumo nin un embedding en ningún sentido místico. É un cambio de coordenadas, aprendido en vez de deseñado, cuxo único traballo é facer fácil o traballo da seguinte capa.

O teorema de aproximación universal, e o que non di

Ligazón á sección: O teorema de aproximación universal, e o que non di

Aquí hai un teorema, e adoita citarse mal.

Cybenko en 1989 e Hornik en 1991 demostraron que unha rede feedforward cunha soa capa oculta e unha función de activación axeitada pode aproximar calquera función continua nun conxunto compacto, coa precisión que queiras, se se lle dan unidades ocultas abondas.34 É un resultado real e importante: di que a arquitectura non é a limitación.

Agora le o que omite. Non di cantas unidades: a cota pode ser astronomicamente grande. Non di que os pesos poidan atoparse; afirma existencia, e gradient descent desde un inicio aleatorio non é un oráculo. E non di nada sobre o comportamento en datos que non viches, que é a segunda metade do Capítulo 6.

A fenda entre «existe» e «atopable» non é académica. Aquí está o mesmo problema XOR, 50 inicializacións aleatorias cada unha, 1000 pasos, cambiando só o tamaño da capa oculta:

unidades ocultasinicializacións que chegan a 4/4
238 / 50 (76 %)
349 / 50 (98 %)
450 / 50 (100 %)
847 / 50 (94 %)

Coa arquitectura mínima viable, unha execución de cada catro nunca chega: aséntase nunha configuración da que non pode descender, exactamente o mínimo local que o Capítulo 3 mostrou nunha superficie unidimensional. Engade unha unidade e os fallos case desaparecen, non porque a rede se volvese máis expresiva (dúas unidades xa abondan; 38 execucións demóstrano), senón porque as dimensións extra danlle ao descenso máis direccións polas que escapar.

E despois oito unidades funcionan lixeiramente peor ca catro. Cunha taxa de aprendizaxe e un orzamento de pasos fixos, máis capacidade non é monotonicamente mellor. Calquera que che diga que a solución para unha rede atascada sempre é unha rede máis grande está extrapolando desde o medio desa táboa.

Esta é a mesma lección ca o teorema de converxencia do Capítulo 1, e será a mesma lección no Capítulo 10 sobre leis de escalado, na forma que lle dá ese capítulo: unha predición da perda non é unha predición da capacidade pola que estás pagando, e a distancia entre as dúas é onde vive a enxeñaría.

Mostrar detalles

Opcional: a forma matricial, e por que o código anterior non a usa.

Todo aquí foi escrito un escalar cada vez, que é a forma máis clara de ver o mecanismo e a máis lenta de executalo. Na práctica, unha capa é unha multiplicación de matrices, e a pasada cara atrás de y=Wx\mathbf{y} = W\mathbf{x} é

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

As transpostas non son un truco para lembrar; son o aspecto que ten a regra de sumar entre camiños cando os camiños están indexados polas entradas da matriz. O obxecto xeral é o xacobiano, a matriz de todas as derivadas parciais de todas as saídas con respecto a todas as entradas, e o modo inverso é precisamente o cálculo dun produto vector-xacobiano sen formar nunca o xacobiano, cousa que importa, porque para unha capa con 4096 entradas e 4096 saídas esa matriz ten dezaseis millóns de entradas e nunca paga a pena construíla.

Non necesitas nada disto para seguir os próximos capítulos; a versión escalar fai todo o que fai a versión matricial, máis amodo. Vólvese necesaria no Capítulo 9, onde as formas deixan de ser obvias.

Agora tes unha rede que se adestra. É un logro máis pequeno do que parece, porque a rede que tes adéstrase con catro exemplos e mídese cos mesmos catro.

Executa o mesmo código nun conxunto de datos real e aparece unha nova serie de problemas, ningún dos cales vai de gradientes. A perda baixa durante un tempo e despois para. Ou baixa nos datos de adestramento e sobe en todo o demais. Ou non se move en absoluto desde o primeiro paso, e a causa resulta ser o rango dos pesos aleatorios iniciais. Ou a entrada dunha unidade derivou a negativa en todos os exemplos na terceira época e leva morta desde entón, en silencio, levando con ela un anaco da capacidade do modelo.

Estes non son fallos exóticos; son a condición normal dunha rede que acaba de escribirse, e ningún se anuncia a si mesmo. O gradiente é correcto — comprobáchelo contra PyTorch ata dezaseis cifras decimais — e o modelo segue sen aprender.

O Capítulo 6 vai diso: inicialización, normalización, overfitting e regularización, e o hábito diagnóstico de preguntar cal desas cousas está a pasar antes de cambiar nada. É a diferenza entre unha rede que se executa e unha rede que funciona.


A clase Value deste capítulo descende directamente de micrograd, de Andrej Karpathy, e o seu vídeo The spelled-out intro to neural networks and backpropagation: building micrograd é as mellores tres horas que podes investir neste material se queres que outra persoa cho explique dunha segunda maneira. A súa publicación de 2016 Yes you should understand backprop defende a idea de escribir un ti mesmo e é lectura asignada no CS224n de Stanford. As notas de CS231n sobre backpropagation (cs231n.github.io/optimization-2) son o tratamento canónico dos patróns de fluxo tabulados arriba. Para as matemáticas como cálculo nun grafo, e non como folclore de redes neuronais, o capítulo 5.6 de Mathematics for Machine Learning de Deisenroth, Faisal e Ong é inusualmente claro; e a revisión de Baydin, Pearlmutter, Radul e Siskind Automatic Differentiation in Machine Learning: a Survey (arXiv:1502.05767) é a referencia para o campo no seu conxunto, incluído o compromiso entre modo directo e inverso discutido arriba.

  1. Linnainmaa, S. The representation of the cumulative rounding error of an algorithm as a Taylor expansion of the local rounding errors. Tese de mestrado, Universidade de Helsinqui (1970). Acumulación en modo inverso, dezaseis anos antes de chegar a este campo e cunha motivación completamente distinta.

  2. Rumelhart, D. E., Hinton, G. E. e Williams, R. J. Learning representations by back-propagating errors. Nature 323, pp. 533–536 (1986). O artigo que deu a coñecer o método, e a fonte da lectura das unidades ocultas como representacións aprendidas á que a sección Que fixo a capa oculta deste capítulo dedica as súas medicións.

  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). Xeneraliza Cybenko: o resultado depende de que a activación sexa non polinómica, non de que sexa sigmoidal.

Listo para deixar que LIA escolla por ti?

Crea con todos os modelos de IA nun só sitio: empeza gratis hoxe mesmo.