ML clásico · Fácil · 15 min

Regresión por descenso de gradiente

Una recta que aprende sola: el error cuadrático, su gradiente y el bucle que baja por él, comprobado al final contra la fórmula cerrada.

FLATS son 40 pisos sintéticos con metros cuadrados y precio. Vas a ajustar precio ≈ w · z + b, donde z son los metros estandarizados, y a hacerlo de dos formas que deben coincidir. Cinco funciones:

standardize(xs): { z, mean, sd }, con z = (x - media) / sd y la desviación dividida entre n. Si sd es 0, todas las z son 0.

mse(w, b, z, ys): la media de (w · z[i] + b - ys[i])².

gradients(w, b, z, ys): { dw, db }. Con error = w · z[i] + b - ys[i], dw es la media de 2 · error · z[i] y db la media de 2 · error.

descend(z, ys, lr, steps): parte de w = 0, b = 0; steps veces, resta lr por cada gradiente y guarda el mse. Devuelve { w, b, history }.

closedForm(z, ys): w = cov(z, ys) / var(z), b = media(ys) - w · media(z). La solución exacta de mínimos cuadrados.

La vista previa pinta los puntos (puedes arrastrarlos), la recta exacta y la del descenso tras N pasos, con su curva de error. El w dividido entre sd es el precio por metro cuadrado que aprende el modelo.

Retos 0/4

  • standardize: media 0, desviación 1, y ceros si nada varía
  • mse y gradients en un ejemplo de dos puntos
  • descend baja el error en cada paso y llega a la solución exacta
  • closedForm: exacta en una recta perfecta, y el precio por m² en EUR

function standardize(xs) {
  // z = (x - media) / sd, con sd entre n; devuelve { z, mean, sd } (todo ceros si sd es 0)
  return { z: xs.map(() => 0), mean: 0, sd: 1 };
}

function mse(w, b, z, ys) {
  // media de (w * z[i] + b - ys[i])^2
  return 0;
}

function gradients(w, b, z, ys) {
  // dw = media de 2 * error * z[i], db = media de 2 * error, con error = w * z[i] + b - ys[i]
  return { dw: 0, db: 0 };
}

function descend(z, ys, lr, steps) {
  // desde w = 0, b = 0: steps veces, muévete contra el gradiente por lr y guarda el mse; devuelve { w, b, history }
  return { w: 0, b: 0, history: [] };
}

function closedForm(z, ys) {
  // w = covarianza(z, ys) / varianza(z), b = media(ys) - w * media(z)
  return { w: 0, b: 0 };
}

const { z, sd } = standardize(M2);
console.log('EUR per m2:', closedForm(z, PRICE).w / sd, descend(z, PRICE, 0.1, 100).w / sd);
La consola aparece aquí (console.log).

La idea

Casi todo el aprendizaje automático es esto: una función con parámetros, una medida de error y un bucle que mueve los parámetros hacia donde el error baja. La regresión lineal es el caso en el que puedes verlo entero en una pantalla. El error cuadrático medio penaliza los fallos grandes más que los pequeños, y su derivada te dice hacia dónde girar w y b.

Estandarizar los metros antes de empezar no es una manía. Si z está en metros y el precio en cientos de miles, el gradiente de b es enorme comparado con el de w y un mismo paso o explota o no avanza. Con z a media 0 y desviación 1, lr = 0,1 funciona siempre y el error baja en cada paso.

Para una recta existe la solución exacta, closedForm. Entonces, ¿para qué el descenso? Porque la fórmula cerrada no existe para una red neuronal ni para casi nada más, y el bucle sí. Aquí lo aprendes donde puedes comprobar que llega al mismo sitio.

El w entre sd es el precio por metro cuadrado que el modelo ha aprendido, en euros. Es el número que puedes explicar en una reunión. Los datos son inventados: no lo uses para tasar nada.

Hazlo en tu máquina

Con Python y NumPy, sobre el CSV de este ejercicio (CC BY 4.0). La forma cerrada y el descenso deben coincidir:

curl -O https://vorluxai.com/data/labs/pisos-valencia.csv
python3 -c "
import numpy as np
m2, price = np.loadtxt('pisos-valencia.csv', delimiter=',', skiprows=1).T
z = (m2 - m2.mean()) / m2.std()
w = b = 0.0
for step in range(300):
    err = w * z + b - price
    w -= 0.1 * (2 * err * z).mean(); b -= 0.1 * (2 * err).mean()
print('descent', w / m2.std(), 'closed form', np.polyfit(m2, price, 1)[0])
"

Comprueba lo que has entendido

1. ¿Por qué se estandarizan los metros antes del descenso?
2. Subes `lr` a 1,5 y el error crece en cada paso. ¿Qué pasa?
3. Si existe `closedForm`, ¿para qué aprender el descenso de gradiente?
4. ¿Qué significa `w / sd` en este ejercicio?

Para profundizar: la referencia de Google Machine Learning Crash Course (en inglés) →

¿Esto en producción, con tus datos? Hablemos 15 minutos →