Retropropagación
En resumen: La retropropagación calcula con la regla de la cadena la pendiente del error de todos los pesos a la vez, yendo desde la salida hacia atrás, hacia la entrada.
Problema
Nueve números. ¿Cuál mueves?
La red de la lección sobre capas resolvió la regla de los países distintos porque sus pesos estaban elegidos a mano. Una red real empieza con pesos que no significan nada, y tiene que encontrarlos ella misma.
Aquí está la misma red con pesos iniciales. Un cliente en el extranjero paga con una tarjeta extranjera desde una IP extranjera, caso (1, 1). Está bien, la etiqueta es 0. La red dice 0,4489.
La red tiene nueve números: cuatro pesos y dos sesgos en la capa oculta, dos pesos y un sesgo en la salida. ¿Cuál mueves, en qué dirección y cuánto? Si pruebas a ciegas, cada intento es una pasada hacia adelante. Con una red de un millón de pesos (un ejemplo de escala) son millones de intentos para un solo paso.
La idea
Empuja el peso y mira el error
El error de un pago es (p − y)², el cuadrado de la diferencia. Toma un peso, v[0], y muévelo un poco hacia arriba y un poco hacia abajo, 0,0001. Cada vez calculas el error de nuevo.
La red como una clase con los nueve números, las funciones de la lección anterior y el error:
class Net { double[][] w; // la capa oculta: pesos y sesgos double[] b; double[] v; // la salida: pesos y sesgo double c;
Net(double[][] w, double[] b, double[] v, double c) { this.w = w; this.b = b; this.v = v; this.c = c; }
static Net zeros() { return new Net(new double[2][2], new double[2], new double[2], 0); }}static double[] hidden(Net net, double[] x) { return new double[] {neuron(x, net.w[0], net.b[0]), neuron(x, net.w[1], net.b[1])};}
static double forward(Net net, double[] x) { return neuron(hidden(net, x), net.v, net.c);}
static double loss(Net net, double[] x, double y) { double d = forward(net, x) - y; return d * d;}Los pesos iniciales y el pago:
// los pesos iniciales: la red todavía no ha aprendidoNet net = new Net(new double[][] {{1.5, 1.0}, {2.5, 2.0}}, new double[] {-0.5, -3.0}, new double[] {1.5, -1.5}, -0.3);double[] x = {1, 1}; // una tarjeta extranjera desde una IP extranjera: un cliente en el extranjerodouble y = 0;System.out.printf("p = %.4f, error = %.6f%n", forward(net, x), loss(net, x, y));double old = net.v[0];net.v[0] = old + 0.0001;double up = loss(net, x, y);net.v[0] = old - 0.0001;double down = loss(net, x, y);net.v[0] = old;System.out.printf("v[0] + 0.0001: error = %.6f%n", up);System.out.printf("v[0] - 0.0001: error = %.6f%n", down);System.out.printf("pendiente = %.4f%n", (up - down) / 0.0002);p = 0.4489, error = 0.201500v[0] + 0.0001: error = 0.201520v[0] - 0.0001: error = 0.201481pendiente = 0.1956Hacia arriba el error crece; hacia abajo baja. La diferencia dividida por la distancia entre los dos puntos (0,0002) es la pendiente: 0,1956. Positiva significa «un v[0] mayor da un error mayor». Así que v[0] tiene que bajar. Es la pendiente del descenso del gradiente, solo que para uno de los nueve pesos.
Funciona, pero necesita dos pasadas hacia adelante por cada peso. Necesitamos una forma de obtener la pendiente sin tocar el peso.
La idea
La regla de la cadena con números
v[0] no toca el error directamente. Toca la suma z de la salida, z toca p, y p toca el error. Una cadena de tres eslabones. Si sabes cuánto se mueve cada eslabón cuando mueves el anterior, la pendiente es su producto:
- El error (p − y)² se mueve 2·(p − y) cuando mueves p.
- p = σ(z) se mueve p·(1 − p) cuando mueves z. Esa es la pendiente de la sigmoide.
- z = v[0]·h[0] + v[1]·h[1] + c se mueve h[0] cuando mueves v[0].
double[] h = hidden(net, x);double p = neuron(h, net.v, net.c);double a = 2 * (p - y); // cómo cambia el error con pdouble s = p * (1 - p); // cómo cambia p con su suma zdouble dz = h[0]; // cómo cambia z con v[0]System.out.printf("2(p - y) = %.4f%n", a);System.out.printf("p(1 - p) = %.4f%n", s);System.out.printf("h[0] = %.4f%n", dz);System.out.printf("pendiente de v[0] = %.4f%n", a * s * dz);2(p - y) = 0.8978p(1 - p) = 0.2474h[0] = 0.8808pendiente de v[0] = 0.19560,8978 × 0,2474 × 0,8808 = 0,1956. El mismo número que con el empujón, pero sin un solo cálculo nuevo de la red. Esta es la regla de la cadena: la pendiente a lo largo de una cadena es el producto de las pendientes de sus eslabones.
La idea
El error viaja hacia atrás
Los dos primeros eslabones son comunes a todo lo que hay antes de la salida. Su producto es la parte de la salida, dOut = 2·(p − y)·p·(1 − p) = 0,2221. A partir de ahí:
- Los pesos de la salida: la pendiente de v[j] es dOut × h[j].
- El sesgo de la salida: la pendiente de c es el propio dOut, porque z se mueve 1 cuando mueves c.
- Las neuronas ocultas: h[j] entra en z con el peso v[j]. Su parte es dH = dOut × v[j] × h[j]·(1 − h[j]): la responsabilidad de la salida en el error, por la fuerza de la arista, por la pendiente de su propia sigmoide.
- Los pesos ocultos: la pendiente de w[j][i] es dH × x[i], y la de b[j] es dH.
La responsabilidad en el error viaja hacia atrás por las mismas aristas por las que la señal fue hacia adelante, y cada peso recibe su parte. Esta es la retropropagación del error (backprop, para abreviar): una pasada hacia adelante, para conocer h y p, y una hacia atrás, para todas las pendientes a la vez.
static Net backward(Net net, double[] x, double y) { double[] h = hidden(net, x); double out = neuron(h, net.v, net.c); Net g = Net.zeros(); // la parte de la salida double dOut = 2 * (out - y) * out * (1 - out); g.c = dOut; for (int j = 0; j < 2; j++) { g.v[j] = dOut * h[j]; // la parte de la neurona oculta j double dH = dOut * net.v[j] * h[j] * (1 - h[j]); g.b[j] = dH; for (int i = 0; i < 2; i++) { g.w[j][i] = dH * x[i]; } } return g;}Net.zeros() es una red de ceros con la misma forma: hay una pendiente por cada peso, así que las pendientes se guardan en la misma clase.Míralo
Empujón contra retropropagación, para los nueve pesos
Para recorrer los nueve números con un ciclo, get y set los numeran de 0 a 8. nudge es el empujón de arriba, para el peso número k:
static final String[] NAMES = {"w[0][0]", "w[0][1]", "w[1][0]", "w[1][1]", "b[0]", "b[1]", "v[0]", "v[1]", "c"};
// k: w[0][0], w[0][1], w[1][0], w[1][1], b[0], b[1], v[0], v[1], cstatic double get(Net n, int k) { if (k < 4) return n.w[k / 2][k % 2]; if (k < 6) return n.b[k - 4]; if (k < 8) return n.v[k - 6]; return n.c;}
static void set(Net n, int k, double value) { if (k < 4) n.w[k / 2][k % 2] = value; else if (k < 6) n.b[k - 4] = value; else if (k < 8) n.v[k - 6] = value; else n.c = value;}static double nudge(Net net, int k, double[] x, double y) { double old = get(net, k); set(net, k, old + 0.0001); double up = loss(net, x, y); set(net, k, old - 0.0001); double down = loss(net, x, y); set(net, k, old); return (up - down) / 0.0002;}Net g = backward(net, x, y);System.out.println(" empujón backprop");for (int k = 0; k < 9; k++) { System.out.printf("%-8s%10.4f%10.4f%n", NAMES[k], nudge(net, k, x, y), get(g, k));} empujón backpropw[0][0] 0.0350 0.0350w[0][1] 0.0350 0.0350w[1][0] -0.0497 -0.0497w[1][1] -0.0497 -0.0497b[0] 0.0350 0.0350b[1] -0.0497 -0.0497v[0] 0.1956 0.1956v[1] 0.1816 0.1816c 0.2221 0.2221Los nueve coinciden hasta el cuarto decimal. Las entradas son (1, 1), así que los dos pesos y el sesgo de una neurona oculta tienen la misma pendiente: cada uno mueve el z de esa neurona en 1. Y las dos neuronas ocultas tienen signos opuestos, porque v[0] = 1,5 y v[1] = −1,5 entran en la salida en direcciones opuestas.
Míralo
La señal hacia adelante, el error hacia atrás
static Net backward(Net net, double[] x, double y) { double[] h = hidden(net, x); double out = neuron(h, net.v, net.c); Net g = Net.zeros(); // la parte de la salida double dOut = 2 * (out - y) * out * (1 - out); g.c = dOut; for (int j = 0; j < 2; j++) { g.v[j] = dOut * h[j]; // la parte de la neurona oculta j double dH = dOut * net.v[j] * h[j] * (1 - h[j]); g.b[j] = dH; for (int i = 0; i < 2; i++) { g.w[j][i] = dH * x[i]; } } return g;}Paso a paso, en palabras
- Los pesos iniciales y el caso x = (1, 1): los dos del extranjero, así que está bien, y = 0.
- Primero hacia adelante: h1 = 0,8808, h2 = 0,8176, p = 0,4489.
- El error: (0,4489 − 0)² = 0,2015. La red debería decir menos.
- Hacia atrás desde la salida: la parte es 2(p − y) · p(1 − p) = 0,8978 × 0,2474 = 0,2221, y también la pendiente de c.
- La pendiente de v[j] es la parte por h[j]: 0,2221 × 0,8808 = 0,1956 y 0,2221 × 0,8176 = 0,1816.
- Hacia atrás por v y a través de σ: h1 recibe 0,2221 × 1,5 × 0,1050 = 0,0350, h2 recibe −0,0497. También son las pendientes de b.
- La pendiente de w[j][i] es la parte de h[j] por x[i]. Las dos entradas son 1: 0,0350 y −0,0497.
- Nueve pendientes de una sola pasada hacia atrás, las mismas que con el empujón. Un peso con pendiente positiva baja.
Los valores viajan hacia adelante, las pendientes viajan hacia atrás. La pendiente de cada arista es la parte de la neurona a la que lleva la arista, por el valor que pasó por ella.
Pruébalo
Predice la salida
La parte de la salida es 0,4, el peso hacia la salida es 1,5, y la neurona oculta dio 0,5. ¿Qué imprimirá: la pendiente de ese peso y la parte de la neurona oculta?
double dOut = 0.4; // la parte de la salidadouble v = 1.5;double h = 0.5;double dH = dOut * v * h * (1 - h);System.out.printf("%.2f %.2f%n", dOut * h, dH);La pendiente del peso es dOut × h = 0,4 × 0,5 = 0,20. La parte de la neurona oculta es 0,4 × 1,5 × 0,5 × (1 − 0,5) = 0,15. Sin el factor h·(1 − h), la pendiente de la sigmoide, saldría 0,30.
Pruébalo
Encuentra el error
Un colega escribe backward por su cuenta. La red aprende, pero despacio y de forma rara. La comprobación con el empujón muestra que las pendientes de la salida coinciden, mientras que las ocultas son de 7 a 10 veces demasiado grandes. ¿En qué línea está el error?
Elige la línea con el error.
A la parte de la neurona oculta le falta la pendiente de su sigmoide, h[j]·(1 − h[j]). A la cadena le falta un eslabón. Aquí h·(1 − h) es de 0,1 a 0,15, así que las pendientes salen de 7 a 10 veces demasiado grandes. La red aprende igual, porque la dirección es más o menos correcta, y por eso un error así vive mucho tiempo. La comprobación con el empujón lo detecta de inmediato.
Míralo
Un paso de aprendizaje
Las pendientes dan la dirección. El paso es el del descenso del gradiente: cada número se mueve en contra de su pendiente, por la tasa de aprendizaje. La pendiente es la media de los cuatro pagos, porque la red tiene que acertar en todos:
static double meanLoss(Net net, double[][] cases, double[] labels) { double s = 0; for (int n = 0; n < cases.length; n++) { s += loss(net, cases[n], labels[n]); } return s / cases.length;}double[][] cases = {{0, 0}, {1, 0}, {0, 1}, {1, 1}};double[] labels = {0, 1, 1, 0};// la pendiente media de los cuatro pagosNet mean = Net.zeros();for (int n = 0; n < 4; n++) { Net g = backward(net, cases[n], labels[n]); for (int k = 0; k < 9; k++) { set(mean, k, get(mean, k) + get(g, k) / 4); }}double rate = 2.0; // la tasa de aprendizajedouble before = meanLoss(net, cases, labels);for (int k = 0; k < 9; k++) { double next = get(net, k) - rate * get(mean, k); System.out.printf("%-8s%8.4f → %.4f%n", NAMES[k], get(net, k), next); set(net, k, next);}double after = meanLoss(net, cases, labels);System.out.printf("error medio: %.4f → %.4f%n", before, after);w[0][0] 1.5000 → 1.5147w[0][1] 1.0000 → 1.0210w[1][0] 2.5000 → 2.4863w[1][1] 2.0000 → 1.9926b[0] -0.5000 → -0.4947b[1] -3.0000 → -3.0367v[0] 1.5000 → 1.4987v[1] -1.5000 → -1.5266c -0.3000 → -0.3285error medio: 0.2236 → 0.2218El error medio baja de 0,2236 a 0,2218. Poco, pero en la dirección correcta. Entrenar es este mismo paso repetido miles de veces: pasada hacia adelante, pasada hacia atrás, paso. El ciclo completo y cómo baja el error está en la próxima lección.
Pruébalo
Completa
¿Cuál es el paso para cada número de la red?
for (int k = 0; k < 9; k++) {
set(net, k, get(net, k) rate * get(g, k));
}Menos: una pendiente positiva significa que el error crece con el peso, así que el peso baja. Con un más la red sube la cuesta y el error crece con cada paso.
La idea
Cuándo perjudica la retropropagación
- La pendiente se desvanece. La pendiente de la sigmoide p·(1 − p) es como máximo 0,25. A través de diez capas con sigmoide los factores dan como máximo 0,25¹⁰, alrededor de 0,000001. Las primeras capas casi no aprenden. Por eso las capas ocultas de hoy usan ReLU: su pendiente es 1 para todo z positivo.
- La pendiente explota. Si los pesos son grandes, los productos crecen a través de las capas, y el paso lanza los pesos lejos. Una tasa de aprendizaje menor y la normalización ayudan.
- Un error en las pendientes no hace fallar el programa, hace aprender mal. La red se mueve igual, solo que más despacio o hacia algo peor. Por eso tu propia retropropagación se compara primero con el empujón, como en el programa de los nueve pesos.
- El empujón es solo una comprobación. Es exacto, pero necesita dos pasadas por cada peso. Para aprender es demasiado lento.
- La pendiente solo dice «dónde está abajo aquí». El paso puede ser demasiado grande y pasarse, y uno pequeño es lento. Esa es la tasa de aprendizaje del descenso del gradiente.
Más precisamente, para curiosos
La fórmula. Con L = (p − y)² y p = σ(z):
∂L/∂v₀ = ∂L/∂p · ∂p/∂z · ∂z/∂v₀ = 2(p − y) · p(1 − p) · h₀
∂ se lee «derivada parcial»: cuánto se mueve el lado izquierdo cuando mueves solo el derecho y todo lo demás se queda quieto. Para la capa oculta la cadena es dos eslabones más larga: ∂z/∂h₀ = v₀ y ∂h₀/∂z₀ = h₀(1 − h₀), luego ∂z₀/∂w₀ᵢ = xᵢ.
Por qué la pendiente de la sigmoide es p(1 − p). Si σ(z) = 1 / (1 + e^(−z)), su derivada es σ(z)·(1 − σ(z)). Es cómodo: el valor p ya está calculado en la pasada hacia adelante, y la pendiente sale sin un exp nuevo.
El empujón, con más precisión. El programa mueve el peso hacia arriba y hacia abajo (una diferencia central). El error de esta estimación es del orden de h², con h = 0,0001 alrededor de 0,00000001. Si mueves solo hacia arriba, el error es del orden de h y los números coinciden en menos decimales.
En las bibliotecas reales. Nadie escribe backward a mano para cada red. PyTorch y similares registran cada operación de la pasada hacia adelante y luego recorren ese registro hacia atrás por sí mismos (diferenciación automática): en PyTorch es loss.backward(). La regla de dentro es la misma regla de la cadena.
Entrenar redes grandes (con tarjetas gráficas y muchas máquinas) y ponerlas en servicio es infraestructura. Eso está en el curso ZeroToOps.
Ejercicio
Retropropagación para una neurona
Resuélvelo en tu editor.
El pago de la lección de la neurona: 230 €, una cuenta nueva, una tarjeta extranjera, pesos 0,9, 1,4 y 1,8 y un sesgo de −4. El pago resultó ser fraude: y = 1. La neurona dijo 0,7807. Calcula con la regla de la cadena la pendiente del error (p − y)² para cada peso y para el sesgo: dz = 2·(p − y)·p·(1 − p), la pendiente de w[i] es dz × x[i], y la de b es dz. Comprueba los pesos con el empujón (± 0,0001).
p = 0.7807, error = 0.0481w[0]: backprop -0.1727, empujón -0.1727w[1]: backprop -0.0751, empujón -0.0751w[2]: backprop -0.0751, empujón -0.0751b: backprop -0.0751Las pendientes son negativas: pesos mayores dan una p mayor, más cerca de 1, y el error baja. El peso del monto recibe la mayor parte del error, porque el monto es la entrada más grande (2,3).
Más tareas
- Un paso. Da un paso con una tasa de aprendizaje de 0,5 e imprime la nueva p. Debería estar más cerca de 1.
- Otro pago. Haz lo mismo con el pago de 40 €, que está bien (y = 0). ¿Qué pendientes se vuelven cero, y por qué?
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Descenso del gradiente · Clasificación: fraude o no · La neurona · Capas
- Continúa en: El entrenamiento de principio a fin · Los dígitos del recibo
- Ejercicios sobre el tema: Una red neuronal por dentro