Un peso era fácil. Ahora son dos 🌸
En el capítulo 13 había un solo peso, así que el valle era una curva y la pregunta era izquierda o derecha.
Con dos pesos el valle es una superficie, como un terreno con lomas. Y la pregunta ya no es izquierda o derecha: es en qué dirección de todas las posibles.
Volvemos a abrir el archivo
Cada capítulo arranca de cero, así que estas líneas se repiten. Son las mismas del capítulo 1 y las vas a ver en todos.
import pandas as pd import numpy as np df = pd.read_csv('ventas-miss-yera.csv') for c in ('unidades', 'monto', 'satisfaccion'): df[c] = pd.to_numeric(df[c], errors='coerce') X = df[['unidades', 'monto', 'satisfaccion']].dropna().to_numpy() print('X.shape:', X.shape)
X.shape: (2238, 3)
Dos pesos: intercepto y unidades
y = X[:, 1] A = np.column_stack([np.ones(X.shape[0]), X[:, 0]]) def error(w): return float(((y - A @ w) ** 2).mean()) def gradiente(w): return -2 * (A.T @ (y - A @ w)) / len(y) w = np.array([0.0, 0.0]) print('error en (0, 0):', round(error(w), 2)) print('gradiente :', np.round(gradiente(w), 4))
error en (0, 0): 1213444.56 gradiente : [ -1606.7725 -18794.463 ]
la derivada parcial respecto a un peso es cuánto cambia el error si mueves ese peso y dejas todos los demás quietos
Ese vector tiene dos números y cada uno contesta una pregunta distinta:
- -1606.77: si muevo el intercepto y dejo el peso de las unidades quieto, el error baja con esta inclinación.
- -18794.46: lo mismo para el peso de las unidades.
Eso es una derivada parcial: la derivada de siempre, pero mirando un solo peso y congelando los demás. La palabra parcial no esconde nada raro, quiere decir "de a uno".
el gradiente es el vector con todas las derivadas parciales juntas, y como vector apunta hacia donde el error sube más rápido
| Símbolo | Qué es |
|---|---|
| ∇E | el gradiente, y ese triangulito al revés se llama nabla |
| ∂E/∂wj | la parcial respecto al peso j |
| ∂ | la de es curva y no recta solo para avisar que hay más variables |
El gradiente es un vector, con todo lo que eso quiere decir desde el capítulo 2: tiene componentes, tiene una dirección y tiene un largo.
Y ahora lo importante: apunta hacia arriba
w = np.array([500.0, 30.0]) g = gradiente(w) paso = 1e-5 print('error donde estoy :', round(error(w), 2)) print('error restando el grad :', round(error(w - paso * g), 2)) print('error sumando el grad :', round(error(w + paso * g), 2))
error donde estoy : 599329.38 error restando el grad : 599235.21 error sumando el grad : 599423.86
Restando baja, sumando sube. El gradiente apunta cuesta arriba.
Y no es un capricho de la definición: el gradiente apunta hacia donde la función crece más rápido, de todas las direcciones posibles. Es la dirección de máxima subida.
Como nosotras queremos bajar, vamos exactamente al revés. De ahí sale el signo menos que vas a ver en todas las fórmulas de entrenamiento, y de ahí sale el nombre del algoritmo 🐣
En el fondo vale cero
optimo = np.linalg.solve(A.T @ A, A.T @ y) print('intercepto :', round(float(optimo[0]), 4)) print('peso unidades:', round(float(optimo[1]), 4)) print('gradiente ahi:', np.round(gradiente(optimo), 6)) print('error ahi :', round(error(optimo), 2))
intercepto : 797.5076 peso unidades: 0.5035 gradiente ahi: [-0. 0.] error ahi : 568006.6
Gradiente cero en las dos componentes. Ninguna dirección baja: estás en el fondo.
Y ese punto es el mismo que salió por las ecuaciones normales del capítulo 6. Tercera vez que dos caminos distintos llegan al mismo sitio en este libro, y no va a ser la última 🌟
Cuidado con leer los pesos
Fíjate en el gradiente de partida: -1606 contra -18794. El segundo es once veces más grande.
La tentación es decir "las unidades importan once veces más". Y no. Ese número es grande porque la columna de unidades tiene números grandes comparada con la columna de unos, exactamente el mismo problema de escala del capítulo 7.
El gradiente no mide importancia, mide inclinación en unidades de esa columna. Y esa diferencia de escala entre componentes es la que hace que el descenso de gradiente zigzaguee. Eso lo trabajamos en el capítulo 16.
Lo que te llevas
- Una parcial es la derivada de siempre moviendo un peso y congelando los demás.
- El gradiente es el vector con todas las parciales juntas.
- Apunta hacia donde la función SUBE más rápido.
- Por eso el descenso de gradiente resta, y de ahí sale el menos de las fórmulas.
- Gradiente cero en todas las componentes quiere decir que estás en el fondo.
- Componentes grandes no quiere decir columnas importantes: quiere decir escalas distintas.
Comprueba que se entendió
Comprueba que lo tienes
El gradiente del error en tu punto actual es [93.79, 3069.82]. ¿Hacia dónde te mueves para bajar el error?
- Restando el gradiente: hacia [-93.79, -3069.82]
- Sumando el gradiente, porque apunta al mínimo
- Solo hacia el segundo peso, porque su número es mucho mayor
- Hacia [0, 0], que es donde el gradiente vale cero
Ejercicios
1. Cada parcial, una por una
Comprueba que el gradiente son dos derivadas separadas.
y = X[:, 1] A = np.column_stack([np.ones(X.shape[0]), X[:, 0]]) def error(w): return float(((y - A @ w) ** 2).mean()) w = np.array([500.0, 30.0]) h = 1e-4 for j in range(2): mas, menos = w.copy(), w.copy() mas[j] += h menos[j] -= h print('parcial', j, ':', round((error(mas) - error(menos)) / (2 * h), 4))
parcial 0 : 93.7905 parcial 1 : 3069.8176
Mover un peso y congelar el otro, dos veces. El gradiente no es un objeto misterioso: es el resultado de hacer eso para cada peso y guardar los números en una lista.
2. Ninguna otra dirección baja más
Compara el gradiente contra direcciones al azar.
y = X[:, 1] A = np.column_stack([np.ones(X.shape[0]), X[:, 0]]) def error(w): return float(((y - A @ w) ** 2).mean()) def gradiente(w): return -2 * (A.T @ (y - A @ w)) / len(y) w = np.array([500.0, 30.0]) g = gradiente(w) d = -g / np.linalg.norm(g) paso = 0.01 print('bajando por el gradiente :', round(error(w + paso * d), 4)) rng = np.random.default_rng(0) peor = 0 for _ in range(200): r = rng.normal(size=2) r = r / np.linalg.norm(r) if error(w + paso * r) < error(w + paso * d): peor += 1 print('direcciones al azar que bajaron mas:', peor, 'de 200')
bajando por el gradiente : 599298.6806 direcciones al azar que bajaron mas: 0 de 200
Doscientas direcciones y ninguna gana. Eso es lo que quiere decir "dirección de máxima pendiente", y es la razón de que el algoritmo se llame así y no de otra manera.
3. Escala las columnas y mira el gradiente
El mismo problema, con las columnas puestas en la misma escala.
y = X[:, 1] u = X[:, 0] uz = (u - u.mean()) / u.std() Az = np.column_stack([np.ones(len(u)), uz]) def grad_z(w): return -2 * (Az.T @ (y - Az @ w)) / len(y) print('sin escalar:', np.round(-2 * (np.column_stack([np.ones(len(u)), u]).T @ (y - np.column_stack([np.ones(len(u)), u]) @ np.array([0.0, 0.0]))) / len(y), 2)) print('escalado :', np.round(grad_z(np.array([0.0, 0.0])), 2))
sin escalar: [ -1606.77 -18794.46] escalado : [-1606.77 -5.83]
Sin escalar las dos componentes se llevaban once veces de diferencia. Con la columna escalada el segundo número se desploma a -5.83, porque ahora está medido en desviaciones y no en unidades sueltas. Los datos son los mismos: lo único que cambió es en qué se miden. Por eso escalar antes de entrenar no es cosmética.
Preguntas frecuentes
¿Qué son las derivadas parciales?
La derivada respecto de una variable dejando las otras quietas. Si tu error depende de cinco parámetros, hay cinco derivadas parciales, una por parámetro.
¿Qué es una derivada parcial, con un ejemplo?
Si el error depende del precio y del descuento, la parcial respecto del precio dice cuánto cambia el error al mover el precio un poquito y no tocar el descuento.
¿Qué es el gradiente?
El vector con todas las derivadas parciales juntas. Apunta hacia donde la función sube más rápido, y por eso entrenar es caminar en contra de él.
¿Para qué sirve el gradiente en machine learning?
Para saber en qué dirección mover cada parámetro para que el error baje. Es lo único que un modelo sabe hacer mientras entrena.
Practica este capítulo 📓
Todo el código de arriba en un cuaderno que corre de principio a fin, y los ejercicios con una celda vacía para que los hagas tú. Se abre en Google Colab de un clic y no hay que instalar nada. Donde veas %%revisa, escribe tu respuesta y el cuaderno te dice si te salió.
¿Prefieres trabajar en tu máquina? Bájate el cuaderno de práctica o el de soluciones. Todos están también en github.com/soymissyera/MissYeraEjercicios.