El capítulo donde se juntan casi todos los anteriores 🌸
Acá vuelven las normas del capítulo 7, la redundancia entre columnas del capítulo 8 y la inversa del capítulo 6. Si algo de eso te quedó suelto, este es el capítulo que lo amarra.
Montamos el desastre a propósito
Necesito dos columnas casi iguales, así que fabrico una: el monto escalado más un poquito de ruido.
import pandas as pd import numpy as np COLS = ['unidades', 'monto', 'descuento', 'satisfaccion', 'monto_final_facturado'] df = pd.read_csv('ventas-miss-yera.csv') for c in COLS: df[c] = pd.to_numeric(df[c], errors='coerce') d = df[COLS].dropna() y = d['monto_final_facturado'].to_numpy() F = d[['unidades', 'monto', 'descuento', 'satisfaccion']].to_numpy() Z = (F - F.mean(axis=0)) / F.std(axis=0) rng = np.random.default_rng(0) Z = np.column_stack([Z, Z[:, 1] + rng.normal(0, 0.01, len(Z))]) NOMBRES = ['unidades', 'monto', 'descuento', 'satisfaccion', 'monto_casi_igual'] yc = y - y.mean() n, dd = Z.shape print('Z.shape:', Z.shape) print('correlacion monto vs monto_casi_igual:', round(float(np.corrcoef(Z[:, 1], Z[:, 4])[0, 1]), 6))
Z.shape: (1793, 5) correlacion monto vs monto_casi_igual: 0.99995
0.99995. Prácticamente la misma columna. En el capítulo 8 te dije que el caso feo no era la copia exacta sino la casi copia, porque no revienta. Acá está.
Sin regularizar
def ridge(alpha): return np.linalg.solve(Z.T @ Z + alpha * np.eye(dd), Z.T @ yc) for al in (0.0, 1.0, 10.0, 100.0): print('alpha', str(al).rjust(6), np.round(ridge(al), 3))
alpha 0.0 [ 2.075 -565.082 -20.407 58.96 1043.946] alpha 1.0 [ 2.205 173.668 -20.613 58.902 304.725] alpha 10.0 [ 2.194 231.558 -20.577 58.589 245.608] alpha 100.0 [ 2.004 232.08 -20.07 55.639 233.405]
Mira la primera fila. El monto tiene coeficiente -565 y su copia casi idéntica tiene +1044.
Eso, leído en una reunión, dice "cuanto más factura una venta, menos factura". Es un disparate, y sin embargo el modelo predice bien: las dos columnas se cancelan entre sí. El modelo encontró una solución que funciona y no significa nada 🙃
Y ahora mira cómo con alpha 1 se convierten en 174 y 305. Los dos positivos, los dos razonables, y sumando parecido a lo que aporta la plata.
ridge suma al error el cuadrado del largo de los pesos, así que pesos grandes salen caros y el modelo prefiere repartirlos
Le sumas al error el largo de los pesos al cuadrado. A partir de ahí, una solución con pesos de 565 y 1044 cuesta cara, así que el modelo prefiere repartir.
for al in (0.0, 1.0, 10.0, 100.0): print('alpha', str(al).rjust(6), 'norma de los pesos:', round(float(np.linalg.norm(ridge(al))), 3))
alpha 0.0 norma de los pesos: 1188.712 alpha 1.0 norma de los pesos: 356.254 alpha 10.0 norma de los pesos: 343.225 alpha 100.0 norma de los pesos: 334.427
De 1188 a 356 con alpha 1. Eso es lo que hace ridge y no hace nada más: acortar el vector de pesos.
Y fíjate dónde está el alpha
la solución de ridge es la de siempre con alfa sumado en la diagonal, y ese sumando es justo lo que arregla que la matriz no se pueda invertir
Es la fórmula del capítulo 6 con un αI sumado en la diagonal.
Y eso no es un detalle de implementación: es exactamente lo que arregla el problema. Ahí donde el determinante estaba pegado a cero y la inversa devolvía basura, sumarle algo a la diagonal lo separa del borde. Por eso ridge funciona con matrices que sin él no se pueden invertir 🌟
Ahora la otra: lasso
lasso suma la norma L1, que es la suma de valores absolutos, y esa forma es la que hace que algunos pesos acaben exactamente en cero
def blando(x, t): return np.sign(x) * np.maximum(np.abs(x) - t, 0.0) def lasso(alpha, iters=3000, tasa=0.001): w = np.zeros(dd) for _ in range(iters): g = -2 * (Z.T @ (yc - Z @ w)) / n w = blando(w - tasa * g, tasa * alpha) return w for al in (0.0, 1.0, 10.0, 100.0): w = lasso(al) print('alpha', str(al).rjust(6), np.round(w, 3), '| ceros:', int((np.abs(w) < 1e-8).sum()))
alpha 0.0 [ 2.18 239.058 -20.644 58.768 239.431] | ceros: 0 alpha 1.0 [ 1.669 238.814 -20.175 58.248 239.186] | ceros: 0 alpha 10.0 [ 0. 236.617 -15.919 53.674 236.985] | ceros: 1 alpha 100.0 [ 0. 214.141 -0. 8.264 214.417] | ceros: 2
Ceros exactos. No 0.0001: cero pelado. Con alpha 10 las unidades desaparecen, y con alpha 100 también el descuento.
Eso ridge no lo hace nunca. Encoge y encoge y encoge, y no llega. Y la diferencia está en la geometría.
Por qué L1 hace ceros y L2 no
Acuérdate de las dos normas del capítulo 7. Piensa en el conjunto de todos los pesos que tienen norma 1, con dos pesos para poder imaginarlo:
- Con la L2, ese conjunto es una circunferencia. Cuadrados y raíz: lisa por todos lados.
- Con la L1, es un rombo con las puntas justo sobre los ejes. Suma de valores absolutos, que es lo que hace las esquinas.
Ahora: regularizar es encontrar el punto donde el error es más chico tocando esa figura. Y una figura con esquinas se toca por la esquina, porque las esquinas sobresalen.
Estar en una esquina del rombo quiere decir que uno de los pesos vale cero, porque las puntas están sobre los ejes. En la circunferencia no hay esquinas, así que el punto de contacto cae en cualquier sitio y ninguna coordenada tiene motivo para ser cero exacto.
Ahí está toda la diferencia. No es que lasso sea más agresivo: es que su figura tiene puntas 🐣
Cuál usar
| Ridge, L2 | Lasso, L1 | |
|---|---|---|
| Qué hace | encoge todos | pone algunos en cero |
| Con columnas parecidas | reparte entre las dos | elige una y tira la otra |
| Cuándo | todas aportan un poco | sospechas que muchas sobran |
| Solución | fórmula cerrada | hay que iterar |
Y las dos a la vez existe, se llama elastic net, y es lo que se usa cuando no sabes cuál de las dos historias es la tuya.
Una advertencia sobre lasso con columnas casi iguales: elige una y no te dice por qué. Cambia una fila y puede elegir la otra. Si vas a presentar "estas son las cinco variables que importan", saber eso te ahorra un mal rato.
Lo que te llevas
- Regularizar es sumarle al error un castigo por pesos grandes.
- Sin regularizar, dos columnas parecidas dan coeficientes enormes y de signos contrarios que se cancelan.
- Ridge usa L2 y encoge todos; su fórmula es la de siempre con alpha en la diagonal, y por eso arregla la inversa.
- Lasso usa L1 y pone ceros exactos.
- La razón es geométrica: el rombo de la L1 tiene puntas sobre los ejes.
- Lasso elige una entre columnas parecidas y no te dice por qué.
Comprueba que se entendió
Comprueba que lo tienes
Sin regularizar, dos columnas casi idénticas te dan coeficientes -565 y +1044. Con ridge y alpha=1 se convierten en 174 y 305. ¿Qué pasó?
- El modelo dejó de compensar una columna con la otra, porque ahora los pesos grandes cuestan
- Ridge eliminó una de las dos columnas
- Ridge corrigió los datos, que estaban mal
- El modelo predice peor, y por eso los coeficientes son más chicos
Ejercicios
1. Predice igual de bien, y eso es lo grave
Compara el error de los coeficientes locos contra los sanos.
def ridge(alpha): return np.linalg.solve(Z.T @ Z + alpha * np.eye(dd), Z.T @ yc) for al in (0.0, 1.0, 100.0): w = ridge(al) rmse = float(np.sqrt(((yc - Z @ w) ** 2).mean())) print('alpha', str(al).rjust(6), '| RMSE', round(rmse, 2), '| peso mayor', round(float(np.abs(w).max()), 1))
alpha 0.0 | RMSE 433.68 | peso mayor 1043.9 alpha 1.0 | RMSE 433.74 | peso mayor 304.7 alpha 100.0 | RMSE 433.96 | peso mayor 233.4
El RMSE apenas se mueve: 433.68 contra 433.96. Si solo miraras la métrica, los tres modelos son el mismo y el primero gana por un pelo. Lo que cambia brutalmente es si los coeficientes se pueden leer. Por eso la regularización no se elige mirando solo el error.
2. El alpha que arregla la inversa
Mira el número de condición según el alpha.
for al in (0.0, 0.01, 1.0, 100.0): M = Z.T @ Z + al * np.eye(dd) print('alpha', str(al).rjust(6), '| condicion', round(float(np.linalg.cond(M)), 1))
alpha 0.0 | condicion 40462.2 alpha 0.01 | condicion 36364.7 alpha 1.0 | condicion 3299.0 alpha 100.0 | condicion 36.9
De 40.462 a 36.9. Ese número es el mismo del capítulo 8 y el mismo del capítulo 16: dice cuánto se amplifica el error de redondeo. Ridge lo baja a golpe de diagonal, y de paso deja el problema mucho mejor condicionado para entrenar.
3. Sigue el camino de lasso
Sube el alpha poco a poco y mira caer las columnas.
def blando(x, t): return np.sign(x) * np.maximum(np.abs(x) - t, 0.0) def lasso(alpha, iters=3000, tasa=0.001): w = np.zeros(dd) for _ in range(iters): g = -2 * (Z.T @ (yc - Z @ w)) / n w = blando(w - tasa * g, tasa * alpha) return w NOMBRES = ['unidades', 'monto', 'descuento', 'satisfaccion', 'monto_casi_igual'] for al in (1.0, 5.0, 20.0, 60.0, 200.0): w = lasso(al) vivas = [NOMBRES[i] for i in range(dd) if abs(w[i]) > 1e-8] print('alpha', str(al).rjust(6), '| quedan', len(vivas), ':', vivas)
alpha 1.0 | quedan 5 : ['unidades', 'monto', 'descuento', 'satisfaccion', 'monto_casi_igual'] alpha 5.0 | quedan 4 : ['monto', 'descuento', 'satisfaccion', 'monto_casi_igual'] alpha 20.0 | quedan 4 : ['monto', 'descuento', 'satisfaccion', 'monto_casi_igual'] alpha 60.0 | quedan 3 : ['monto', 'satisfaccion', 'monto_casi_igual'] alpha 200.0 | quedan 2 : ['monto', 'monto_casi_igual']
Las columnas se van cayendo de menos a más útiles, y eso se llama el camino de lasso. Las unidades caen primero, después el descuento, después la satisfacción.
Y ahora mira la última línea, que es la que quiero que te lleves: las dos que
sobreviven son monto y monto_casi_igual, o sea
la misma columna dos veces. Lasso no las desempató. Se supone
que L1 elige una entre columnas parecidas, y con una correlación de 0.99995
prefirió quedarse con las dos y repartirse el peso.
Si tú hubieras usado esta lista para decir "estas son las variables que importan", habrías presentado la misma variable dos veces. El alpha se elige con validación cruzada, y eso está en mi libro de machine learning, pero la lista de variables que sale de lasso siempre hay que mirarla con los ojos abiertos.
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.