Descenso del gradiente
En resumen: El descenso del gradiente encuentra los pesos de un modelo paso a paso: calcula hacia dónde crece el error y mueve los pesos un poco en la dirección contraria, tanto como diga la tasa de aprendizaje.
Problema
La mejor recta, sin adivinar
Los compañeros de la lección sobre regresión lineal eligieron una recta a ojo. Ahora el almacén quiere que el programa encuentre la mejor por sí solo. Solo hay una fórmula exacta para la recta, y los siguientes modelos (las redes neuronales) no tienen ninguna. Necesitamos un camino que funcione siempre.
Por ahora la tarea es más pequeña: el almacén es conocido (6 horas), y solo buscamos cuántas horas añade cada 100 km, es decir, w. La distancia está en cientos de km para que los números sean pequeños:
// distancia en cientos de km (0.3 son 30 km) y horas hasta la entregaconst DIST = [0.3, 0.5, 0.8, 1.0, 1.3, 1.6, 2.0, 2.4, 2.7, 3.0];const HOURS = [10, 11, 17, 17, 23, 24, 31, 33, 40, 44];const B = 6; // el almacén: 6 horas, se sabeconst NUDGE = 0.0001;La pérdida es el mismo MSE de la regresión lineal, ahora solo como función de w:
// el MSE de la recta horas = w · distancia + 6function loss(w) { let sum = 0; for (let i = 0; i < DIST.length; i++) { const error = w * DIST[i] + B - HOURS[i]; sum += error * error; } return sum / DIST.length;}Podríamos probar w = 0, 1, 2, … hasta 100 y quedarnos con la pérdida más pequeña. Con un solo w funciona. Con 10 pesos son 100¹⁰ intentos, y una red neuronal tiene millones de pesos.
La idea
Empujar y mirar
En lugar de probarlo todo, te pones en un w y preguntas: si me muevo un poco a la derecha, ¿la pérdida baja o sube?
const before = loss(0);const after = loss(0.01);console.log(`w = 0: pérdida ${before.toFixed(2)}`);console.log(`w = 0.01: pérdida ${after.toFixed(2)}`);console.log(`pendiente: ${((after - before) / 0.01).toFixed(2)}`);w = 0: pérdida 485.00w = 0.01: pérdida 484.21pendiente: -78.97El empujón de 0,01 bajó la pérdida en 0,79. Dividido entre el empujón, esa es la pendiente: −78,97, cuánto cambia la pérdida por unidad de w. El menos significa «a la derecha es cuesta abajo».
Con un empujón menor (0,0001) la pendiente es más exacta. Aquí en cuatro puntos:
// la pendiente: empuja w un poco y mira cuánto cambia la pérdidafunction slope(w) { return (loss(w + NUDGE) - loss(w)) / NUDGE;}for (const w of [0, 6, 12, 18]) { console.log(`w = ${w}: pérdida ${loss(w).toFixed(2)}, pendiente ${slope(w).toFixed(2)}`);}w = 0: pérdida 485.00, pendiente -79.00w = 6: pérdida 127.21, pendiente -40.26w = 12: pérdida 1.83, pendiente -1.53w = 18: pérdida 108.87, pendiente 37.21La pendiente dice dos cosas. El signo es la dirección: menos significa que w debe crecer, más que debe disminuir (en w = 18 ya hemos pasado el fondo). El tamaño dice lo lejos que estamos: −79 está lejos del fondo, −1,53 está casi en el fondo.
La idea
Un paso contra la pendiente
La regla es una línea: w -= tasa · pendiente. Contra la pendiente, porque la pendiente apunta cuesta arriba. Con pendiente negativa, w aumenta; con positiva, disminuye. Cuanto más empinado, mayor el paso.
Una pasada (calcular la pendiente, dar un paso) es una época. Repetimos:
let w = 0;const rate = 0.1;for (let epoch = 1; epoch <= 10; epoch++) { w -= rate * slope(w); console.log(`época ${epoch}: w = ${w.toFixed(2)}, pérdida ${loss(w).toFixed(2)}`);}época 1: w = 7.90, pérdida 62.36época 2: w = 10.70, pérdida 9.28época 3: w = 11.69, pérdida 2.61época 4: w = 12.04, pérdida 1.77época 5: w = 12.17, pérdida 1.67época 6: w = 12.21, pérdida 1.65época 7: w = 12.23, pérdida 1.65época 8: w = 12.23, pérdida 1.65época 9: w = 12.24, pérdida 1.65época 10: w = 12.24, pérdida 1.65El primer paso es grande (de 0 a 7,90) porque la curva es empinada. Cerca del fondo la pendiente se hace menor, y con ella los pasos: a partir de la época 7, w casi no se mueve. La pérdida de 1,65 es menor que la 1,83 de la recta que los compañeros eligieron a ojo.
Eso es el descenso del gradiente: paso a paso por la pendiente de la pérdida hasta llegar al fondo. «Gradiente» es la pendiente cuando hay más de un parámetro.
Míralo
La bola en la curva
function slope(w) { return (loss(w + NUDGE) - loss(w)) / NUDGE;}
let w = 0;const rate = 0.1;for (let epoch = 1; epoch <= 10; epoch++) { w -= rate * slope(w);}function slope(w) { return (loss(w + NUDGE) - loss(w)) / NUDGE;}
let w = 0;const rate = 0.1;for (let epoch = 1; epoch <= 10; epoch++) { w -= rate * slope(w);}function slope(w) { return (loss(w + NUDGE) - loss(w)) / NUDGE;}
let w = 0;const rate = 0.01;for (let epoch = 1; epoch <= 10; epoch++) { w -= rate * slope(w);}function slope(w) { return (loss(w + NUDGE) - loss(w)) / NUDGE;}
let w = 0;const rate = 0.35;for (let epoch = 1; epoch <= 10; epoch++) { w -= rate * slope(w);}Paso a paso, en palabras
- Abajo: la pérdida para cada w; la bola parte de w = 0. Arriba: la recta con este w.
- Empuja w un poco: la pérdida baja. La pendiente es -79,00: cuesta abajo es hacia la derecha.
- Un paso: w -= 0,1 · (-79,00), así que w pasa a ser 7,90. Contra la pendiente, es decir, cuesta abajo.
- Cada pasada es una época. Cerca del fondo la pendiente se hace menor, y con ella los pasos. Tras 10: w = 12,24.
- Tasa 0,01: la bola gris se arrastra. Tras 10 épocas está solo en w = 5,96.
- Tasa 0,35: la bola roja salta por encima del fondo, cada vez más arriba. La pérdida crece: diverge.
- Con 0,1 la bola se queda en el fondo: w = 12,24, pérdida 1,65. Nadie adivinó w: se encontró.
Abajo está la pérdida para cada w, y la bola es el w actual. Arriba está la recta con ese w sobre los pedidos: cuando la bola llega al fondo, la recta pasa por los puntos. La bola gris y la roja son la misma regla con otra tasa.
La idea
La tasa de aprendizaje
El número por el que se multiplica la pendiente es la tasa de aprendizaje. Decide lo largo que es el paso. Tres tasas, 20 épocas cada una (w tras las tres primeras y tras la vigésima):
for (const rate of [0.01, 0.1, 0.35]) { let w = 0; const seen = []; for (let epoch = 1; epoch <= 20; epoch++) { w -= rate * slope(w); if (epoch <= 3 || epoch === 20) seen.push(w.toFixed(2)); } console.log(`tasa ${rate.toFixed(2)}: w = ${seen.join(", ")}, pérdida ${loss(w).toFixed(2)}`);}tasa 0.01: w = 0.79, 1.53, 2.22, 9.02, pérdida 35.14tasa 0.10: w = 7.90, 10.70, 11.69, 12.24, pérdida 1.65tasa 0.35: w = 27.65, -7.18, 36.69, -1224.61, pérdida 4938139.70- 0,01 es muy pequeña. La dirección es correcta, pero tras 20 épocas w es solo 9,02. Llega al fondo, pero tras cientos de épocas.
- 0,1 es la justa. Tres pasos y w está cerca, luego se asienta en 12,24.
- 0,35 es muy grande. El primer paso se pasa del fondo hasta 27,65. Allí la pendiente es aún mayor, el siguiente paso es aún más largo y w vuela a −7,18, luego a 36,69. La pérdida llega a millones: el descenso diverge.
Una buena tasa no se calcula, se prueba. Lo habitual es empezar con 0,1 o 0,01 y mirar la pérdida por época: si crece o salta, la tasa es muy grande; si apenas baja, es muy pequeña.
La idea
w y b juntos
Hasta ahora b era conocido. Ahora lo buscamos también: cada parámetro tiene su propia pendiente (empujas solo ese), y cada uno da su propio paso en la misma época.
// el MSE de la recta horas = w · distancia + bfunction loss(w, b) { let sum = 0; for (let i = 0; i < DIST.length; i++) { const error = w * DIST[i] + b - HOURS[i]; sum += error * error; } return sum / DIST.length;}let w = 0, b = 0;const rate = 0.1;for (let epoch = 1; epoch <= 1000; epoch++) { const slopeW = (loss(w + NUDGE, b) - loss(w, b)) / NUDGE; const slopeB = (loss(w, b + NUDGE) - loss(w, b)) / NUDGE; w -= rate * slopeW; b -= rate * slopeB; if ([1, 10, 100, 1000].includes(epoch)) { console.log(`época ${epoch}: w = ${w.toFixed(2)}, b = ${b.toFixed(2)}, pérdida ${loss(w, b).toFixed(2)}`); }}época 1: w = 9.77, b = 5.00, pérdida 29.77época 10: w = 12.21, b = 6.03, pérdida 1.66época 100: w = 12.41, b = 5.65, pérdida 1.62época 1000: w = 12.41, b = 5.64, pérdida 1.62Tras 1000 épocas, w = 12,41 horas por 100 km y b = 5,64 horas. La fórmula de la mejor recta de la regresión lineal da 0,1241 horas por km y 5,64 horas: la misma recta. El descenso no conoce la fórmula; solo va cuesta abajo.
La idea
La escala de las características
¿Por qué estaba la distancia en cientos de km? Aquí están los mismos pedidos en km:
// los mismos pedidos, pero la distancia está en kmconst KM = [30, 50, 80, 100, 130, 160, 200, 240, 270, 300];const HOURS = [10, 11, 17, 17, 23, 24, 31, 33, 40, 44];const NUDGE = 0.0001;Con una tasa de 0,1, el descenso diverge desde la primera época:
let w = 0, b = 0;const rate = 0.1;for (let epoch = 1; epoch <= 3; epoch++) { const slopeW = (loss(w + NUDGE, b) - loss(w, b)) / NUDGE; const slopeB = (loss(w, b + NUDGE) - loss(w, b)) / NUDGE; w -= rate * slopeW; b -= rate * slopeB; console.log(`tasa 0.1, época ${epoch}: w = ${w.toFixed(0)}`);}tasa 0.1, época 1: w = 977tasa 0.1, época 2: w = -6304921tasa 0.1, época 3: w = 40699231079La pendiente de w crece con el tamaño de la característica: cada w se multiplica por 300 en vez de por 3, y con w = 0 la pendiente es casi −10 000. Así que bajamos la tasa hasta que deje de divergir:
let w = 0, b = 0;const rate = 0.00002;for (let epoch = 1; epoch <= 1000; epoch++) { const slopeW = (loss(w + NUDGE, b) - loss(w, b)) / NUDGE; const slopeB = (loss(w, b + NUDGE) - loss(w, b)) / NUDGE; w -= rate * slopeW; b -= rate * slopeB; if ([10, 100, 1000].includes(epoch)) { console.log(`época ${epoch}: w = ${w.toFixed(4)}, b = ${b.toFixed(4)}, pérdida ${loss(w, b).toFixed(2)}`); }}época 10: w = 0.1513, b = 0.0013, pérdida 9.44época 100: w = 0.1513, b = 0.0063, pérdida 9.42época 1000: w = 0.1510, b = 0.0563, pérdida 9.28Ahora no diverge, pero b apenas se mueve: tras 1000 épocas vale 0,06 en vez de 5,64, y w está en 0,151 en vez de 0,124. Para b el paso es demasiado pequeño. Una sola tasa no sirve para los dos. La solución es la escala: las características se llevan a un rango parecido antes de entrenar (aquí dividiendo entre 100; a menudo se resta la media y se divide entre la dispersión). Sin eso ninguna tasa resuelve el problema.
Pruébalo
Predice la salida
w vale 2, la pendiente allí es −20 y la tasa es 0,1. ¿Qué imprimirá un paso?
let w = 2;const slope = -20;const rate = 0.1;w -= rate * slope;console.log(w.toFixed(1));0,1 · (−20) = −2, y w -= −2 suma 2: w pasa a ser 4.0. Una pendiente negativa significa que w crece. 0.0 sería un paso a favor de la pendiente (cuesta arriba), y −18.0 sería restar la pendiente sin la tasa.
Pruébalo
Completa
¿Qué operación da el paso contra la pendiente?
for (let epoch = 1; epoch <= 10; epoch++) {
w rate * slope(w);
}-=: restamos la tasa por la pendiente. La pendiente apunta hacia donde crece la pérdida, y queremos lo contrario.
Pruébalo
Encuentra el error
Un compañero escribe un descenso, pero la pérdida por época crece cada vez más deprisa. ¿En qué línea está el error?
Elige la línea con el error.
+= da un paso a favor de la pendiente, es decir, cuesta arriba en la curva. Debe ser -=: contra la pendiente.
La idea
Cuando el descenso duele
- La tasa equivocada. Si es muy grande, diverge; si es muy pequeña, se arrastra. Si la pérdida por época crece o salta, baja primero la tasa.
- Características en escalas distintas. Km y número de paquetes en una misma línea: las pendientes difieren cientos de veces y ninguna tasa sirve. Escala las características antes de entrenar.
- No solo un valle. La pérdida de una recta es un cuenco liso con un fondo. En las redes neuronales la curva tiene muchos valles y mesetas: el descenso llega a algún fondo, no necesariamente al más bajo, y el resultado depende del w inicial.
- Cuándo parar. Aquí las épocas son fijas. En la práctica paras cuando la pérdida deja de bajar, y si el modelo es bueno para pedidos nuevos se mide aparte (evaluar el modelo).
- El coste. Cada época recorre todos los datos. Con millones de ejemplos y millones de pesos, el entrenamiento dura horas en servidores especiales; esa infraestructura (máquinas para entrenar, poner el modelo en producción) se enseña en el curso de DevOps ZeroToOps.
Más precisamente, para curiosos
La pendiente con fórmula. Empujar es una aproximación. Para el MSE la pendiente tiene una fórmula exacta (la derivada): 2 · la media de error · distancia. Aquí están las dos formas lado a lado:
// la pendiente con la fórmula: 2 · la media de error · distanciafunction slopeByFormula(w) { let sum = 0; for (let i = 0; i < DIST.length; i++) { const error = w * DIST[i] + B - HOURS[i]; sum += error * DIST[i]; } return 2 * sum / DIST.length;}for (const w of [0, 12]) { console.log(`con w = ${w}: con números ${slope(w).toFixed(2)}, con fórmula ${slopeByFormula(w).toFixed(2)}`);}con w = 0: con números -79.00, con fórmula -79.00con w = 12: con números -1.53, con fórmula -1.53Coinciden hasta el segundo decimal. La fórmula es más rápida (una pasada en lugar de dos por parámetro), por eso las bibliotecas calculan las derivadas automáticamente a partir del código del modelo. Eso es la retropropagación, de la lección sobre entrenar una red.
El gradiente. Con muchos parámetros, sus pendientes juntas son el gradiente (un vector). El paso es el mismo para cada uno: parámetro -= tasa · su pendiente.
Partes de los datos. Con millones de ejemplos una época es lenta. Por eso la pendiente se calcula en una parte pequeña al azar (un mini-batch) y hay muchos más pasos. Es el descenso del gradiente estocástico (SGD). Sus versiones mejoradas (Adam, por ejemplo) guardan además un «impulso» de los pasos anteriores.
Ejercicio
El mensajero, aprendido solo
Resuelve la tarea en tu editor.
En la lección sobre regresión lineal los compañeros compararon tres rectas para el precio del mensajero. Ahora haz que el programa encuentre la mejor por sí solo:
// paquetes: peso en kg y precio del mensajero en €const KG = [0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 6.0];const EUR = [4.10, 4.60, 5.00, 5.60, 6.40, 7.50, 8.30, 9.40];const NUDGE = 0.0001;Escribe la función de pérdida (MSE) para precio = w · kg + b, y luego 1000 épocas de descenso del gradiente para w y b juntos, desde w = 0 y b = 0, con una tasa de 0,05 y un empujón de 0,0001. Imprime w, b y la pérdida con 4 decimales cada uno.
w = 0.9538, b = 3.6204, MSE 0.0038Más tareas
- La tasa. Ejecútalo con una tasa de 0,1. ¿Qué pasa? Encuentra la mayor tasa con la que la pérdida todavía baja.
- Parar. En lugar de 1000 épocas, para cuando las dos pendientes sean menores que 0,001 en valor absoluto. ¿Después de cuántas épocas?
- Comparación. ¿Cuánto menor es la pérdida que la de la mejor recta de los compañeros (A, MSE 0,0144)?
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: static: lo que comparten todos los objetos · Arrays: push, splice y sort · Qué es el aprendizaje automático · Regresión lineal
- Continúa en: Clasificación: fraude o no · Evaluación: la exactitud miente · Sobreajuste: el modelo que memoriza · Retropropagación
- Ejercicios sobre el tema: Cómo aprende un modelo