Este capítulo es corto a propósito 🌸
Jacobiana y hessiana tienen fama de tema avanzado, y para lo que tú necesitas son dos ideas simples. Te las doy y seguimos.
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)
La jacobiana: cuando la función devuelve varios números
El gradiente del capítulo 14 es de una función que devuelve un número, el error. Si la función devuelve varios, hace falta una derivada por cada entrada y cada salida. Eso es una matriz, y se llama jacobiana.
s = X.std(axis=0) J = np.diag(1 / s) print('desviaciones:', np.round(s, 4)) print('jacobiana del escalado:') print(np.round(J, 6))
desviaciones: [ 5.7856 753.6678 1.424 ] jacobiana del escalado: [[0.172842 0. 0. ] [0. 0.001327 0. ] [0. 0. 0.702235]]
Esa es la jacobiana de escalar tus columnas: entran tres números y salen tres. Sale diagonal porque cada columna se escala sola, sin mirar a las otras.
Y hay un caso que ya conoces: la jacobiana de multiplicar por una matriz es esa misma matriz. Si tu función es Wx, su jacobiana es W. Por eso en una red los pesos aparecen tal cual en la retropropagación del capítulo 15: la cadena va multiplicando jacobianas, y las jacobianas de las capas lineales son los propios pesos 🐣
La hessiana: la curvatura del valle
La derivada dice la pendiente. La derivada segunda dice cuánto cambia esa pendiente, o sea si el valle es cerrado y empinado o abierto y plano.
y = X[:, 1] A = np.column_stack([np.ones(len(y)), X[:, 0]]) H = 2 * (A.T @ A) / len(y) print('hessiana:') print(np.round(H, 4))
hessiana: [[ 2. 23.3521] [ 23.3521 339.6077]]
la hessiana guarda las derivadas segundas: cuánto cambia la pendiente en una dirección cuando te mueves en otra, o sea la curvatura del valle
Es simétrica, como X⊤X en el capítulo 6, y por el mismo motivo. Y eso quiere decir que le
podemos sacar autovalores con eigh, como en el capítulo 10.
Sus autovalores dicen dos cosas
vals = np.linalg.eigvalsh(H) print('autovalores :', np.round(vals, 4)) print('todos positivos :', bool(np.all(vals > 0))) print('numero de condicion:', round(float(vals.max() / vals.min()), 2))
autovalores : [ 0.3924 341.2153] todos positivos : True numero de condicion: 869.55
Todos positivos quiere decir que en todas las direcciones la superficie sube. O sea que ahí hay un fondo de verdad y no una silla de montar. Cuando eso pasa en todo el espacio, el problema se llama convexo, y convexo quiere decir que hay un solo mínimo y no te puedes quedar atrapada.
El cociente entre el mayor y el menor es el número de condición, el mismo del capítulo 8. Aquí vale 869, y traducido quiere decir que el valle es 869 veces más empinado en una dirección que en la otra.
Imagínate un cañón largo y estrecho. Si caminas siempre en la dirección de máxima pendiente, rebotas de una pared a la otra y avanzas poquito hacia el fondo. Eso es el zigzag del descenso de gradiente, y ese 869 es el número que lo predice antes de que pase 🙃
Y por eso existe el método de Newton
w = np.array([500.0, 30.0]) g = -2 * (A.T @ (y - A @ w)) / len(y) paso = np.linalg.solve(H, g) nuevo = w - paso optimo = np.linalg.solve(A.T @ A, A.T @ y) print('gradiente :', np.round(g, 4)) print('un paso da :', round(float(nuevo[0]), 4), round(float(nuevo[1]), 4)) print('el optimo es :', round(float(optimo[0]), 4), round(float(optimo[1]), 4))
gradiente : [ 93.7905 3069.8176] un paso da : 797.5076 0.5035 el optimo es : 797.5076 0.5035
el paso de Newton corrige el gradiente con la curvatura, y por eso llega al fondo mucho más rápido, aunque calcular la hessiana cuesta caro
Un paso. No cien, ni mil: uno, y clavado.
Newton corrige el gradiente con la curvatura, así que sabe estirar el paso donde el valle es plano y acortarlo donde es empinado. Sin zigzag.
Y entonces la pregunta obvia: si esto es tan bueno, ¿por qué las redes se entrenan con descenso de gradiente y no con Newton?
Por el tamaño. La hessiana de un modelo con un millón de pesos tiene un billón de casillas, y además hay que resolver un sistema con ella en cada paso. No cabe ni en la memoria ni en el presupuesto.
Por eso existen los métodos que están en medio, como L-BFGS, que se inventan una hessiana aproximada, o como Adam, que guarda un resumen barato de la curvatura por cada peso. Todos intentan lo mismo: algo de Newton sin pagar Newton 🌟
Lo que te llevas
- La jacobiana es la matriz de derivadas primeras cuando salen varios números.
- La jacobiana de multiplicar por W es W.
- La hessiana son las derivadas segundas y mide la curvatura.
- Autovalores todos positivos: hay un mínimo de verdad y el problema es convexo.
- El número de condición dice cuánto vas a zigzaguear.
- Newton llega en un paso y no se usa porque la hessiana no cabe.
Comprueba que se entendió
Comprueba que lo tienes
Los autovalores de la hessiana de tu error son [0.39, 341.22]. ¿Qué esperas que le pase al descenso de gradiente?
- Que zigzaguee, porque el valle es 869 veces más empinado en una dirección que en la otra
- Que vaya derecho al fondo, porque los dos son positivos
- Que no converja nunca, porque uno es muy grande
- Que se quede en un mínimo local
Ejercicios
1. La hessiana a lo bruto
Sácala midiendo, como hicimos con la derivada.
y = X[:, 1] A = np.column_stack([np.ones(len(y)), X[:, 0]]) def error(w): return float(((y - A @ w) ** 2).mean()) w = np.array([500.0, 30.0]) h = 1e-4 Hn = np.zeros((2, 2)) for i in range(2): for j in range(2): pp, pm, mp, mm = w.copy(), w.copy(), w.copy(), w.copy() pp[i] += h; pp[j] += h pm[i] += h; pm[j] -= h mp[i] -= h; mp[j] += h mm[i] -= h; mm[j] -= h Hn[i, j] = (error(pp) - error(pm) - error(mp) + error(mm)) / (4 * h * h) print(np.round(Hn, 4)) print('coincide con la exacta:', np.allclose(Hn, 2 * (A.T @ A) / len(y), rtol=1e-3))
[[ 1.9994 23.35 ] [ 23.35 339.6097]] coincide con la exacta: True
Cuatro evaluaciones por casilla. Para dos pesos son dieciséis evaluaciones y se aguanta; para un millón de pesos, saca la cuenta. Ahí tienes la razón práctica de que Newton no se use en redes.
2. Escala y mira cómo se arregla el cañón
El número de condición no es del problema: es de cómo lo escribiste.
y = X[:, 1] u = X[:, 0] A = np.column_stack([np.ones(len(y)), u]) uz = (u - u.mean()) / u.std() Az = np.column_stack([np.ones(len(y)), uz]) for nombre, M in (('sin escalar', A), ('escalado ', Az)): H = 2 * (M.T @ M) / len(y) v = np.linalg.eigvalsh(H) print(nombre, '-> autovalores', np.round(v, 4), '| condicion', round(float(v.max() / v.min()), 2))
sin escalar -> autovalores [ 0.3924 341.2153] | condicion 869.55 escalado -> autovalores [2. 2.] | condicion 1.0
Condición 1. El cañón se convirtió en un tazón redondo, y en un tazón redondo el descenso de gradiente va derecho al fondo sin rebotar. La misma pregunta, los mismos datos, y el entrenamiento pasa de lento a inmediato solo por escalar. Es la tercera vez en este libro que escalar arregla algo, y no es casualidad.
3. Una silla de montar
Cuando los autovalores tienen signos distintos, no hay ni mínimo ni máximo.
Silla = np.array([[2.0, 0.0], [0.0, -2.0]]) print('autovalores:', np.linalg.eigvalsh(Silla)) print('hay minimo :', bool(np.all(np.linalg.eigvalsh(Silla) > 0)))
autovalores: [-2. 2.] hay minimo : False
Sube en una dirección y baja en la otra: es la forma de una silla de montar. El gradiente ahí vale cero y no estás en ningún fondo. En redes profundas esto pasa muchísimo más que los mínimos locales, y es la razón de que el gradiente se quede quieto sin que el modelo haya terminado de aprender.
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.