Capítulo 7 de 21 10 secciones 7 min

Compartir

Norma y distancia: cómo mide un modelo que se equivocó

Qué es la norma de un vector, la diferencia entre L1 y L2, y por qué el error de un modelo es literalmente una distancia.

La norma de un vector es cuánto mide. La L2 es Pitágoras estirado: cuadrados, suma y raíz. La L1 suma valores absolutos. La distancia entre dos filas es la norma de su resta, y el error de un modelo es esa misma distancia entre lo que predijo y lo que pasó.

El capítulo donde el error deja de ser un misterio 🌸

En el capítulo 3 te dije que restar dos vectores es cómo se mide una diferencia. Y quedó pendiente algo: la resta te da otro vector, no un número. Para decir "me equivoqué tanto" hace falta convertir ese vector en un número solo.

Eso es la norma.

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)

Cuánto mide un vector

a = X[0]
print('a        :', np.round(a, 2))
print('norma L2 :', round(float(np.linalg.norm(a)), 2))
print('a mano   :', round(float(np.sqrt((a ** 2).sum())), 2))
a        : [ 10.   480.37   4.  ]
norma L2 : 480.49
a mano   : 480.49
𝐯2=v12+v22++vd2

la norma L2 eleva cada componente al cuadrado, las suma y saca la raíz, que es el teorema de Pitágoras estirado a las dimensiones que hagan falta

Si en el colegio hiciste Pitágoras, ya sabías esto: cateto al cuadrado más cateto al cuadrado, raíz, y sale la hipotenusa. La norma L2 es eso mismo pero sin parar en dos componentes.

Y mira el resultado: 480.49, que es casi el monto solo. Los otros dos números casi no se notan. Guárdate ese detalle porque es el corazón del capítulo.

La otra norma

print('norma L1 :', round(float(np.linalg.norm(a, 1)), 2))
print('a mano   :', round(float(np.abs(a).sum()), 2))
norma L1 : 494.37
a mano   : 494.37
𝐯1=|v1|+|v2|++|vd|

la norma L1 suma los valores absolutos sin elevar nada al cuadrado, así que un componente enorme no pesa más que su tamaño

La L1 no eleva al cuadrado: suma los valores absolutos. Parece un detalle tonto y cambia el comportamiento entero.

Al elevar al cuadrado, la L2 castiga mucho más los números grandes: una diferencia de 10 pesa cien, una de 100 pesa diez mil. La L1 los trata a todos con su tamaño y ya.

Esa diferencia es la que hace que L1 y L2 se comporten distinto como penalización dentro de un modelo, y es lo que explica por qué una hace ceros y la otra no. Eso es el capítulo de regularización entero.

La distancia es la norma de la resta

a, b = X[0], X[1]
print('a - b     :', np.round(a - b, 2))
print('distancia :', round(float(np.linalg.norm(a - b)), 2))
a - b     : [  0.   -44.53   1.  ]
distancia : 44.54
d(𝐚,𝐛)=𝐚𝐛2

la distancia entre dos puntos es la norma de su resta, o sea primero se mide la diferencia y después se mide cuánto vale esa diferencia

Dos pasos y ninguno es nuevo: restas, y mides cuánto mide la resta.

Esto es lo que hay debajo de "clientes parecidos". Cuando un algoritmo te agrupa clientes o te recomienda el producto de alguien parecido a ti, está midiendo distancias entre filas 🐣

Y aquí está el error de un modelo

El modelo más tonto posible predice siempre el promedio. Vamos a medir cuánto se equivoca:

media = X.mean(axis=0)
err = X - media
print('RMSE por columna :', np.round(np.sqrt((err ** 2).mean(axis=0)), 2))
print('a mano, el monto :',
      round(float(np.linalg.norm(err[:, 1]) / np.sqrt(X.shape[0])), 2))
RMSE por columna : [  5.79 753.67   1.42]
a mano, el monto : 753.67

Ese 753.67 es el RMSE, el error cuadrático medio con raíz, que es la métrica que más vas a ver en regresión.

Y mira lo que acaba de pasar: lo calculé de dos formas y salió lo mismo. El RMSE es la norma L2 del vector de errores dividida por la raíz del número de filas. No es una métrica que alguien se inventó: es una distancia con un nombre comercial.

Predecir siempre el promedio te deja con 753 soles de error típico. Cualquier modelo que hagas tiene que bajar de ahí, y si no baja, el modelo sobra.

El problema que nadie te avisa

a, b = X[0], X[1]
print('solo unidades y satisfaccion:',
      round(float(np.linalg.norm((a - b)[[0, 2]])), 2))
print('con el monto dentro         :',
      round(float(np.linalg.norm(a - b)), 2))
solo unidades y satisfaccion: 1.0
con el monto dentro         : 44.54

Mismo par de ventas. Sin el monto, la distancia es 1. Con el monto, 44.54.

O sea que el monto se come el 98% de la distancia, y no porque importe más, sino porque sus números son más grandes. Una diferencia de 44 soles es insignificante para el negocio y aplasta a una diferencia de un punto entero de satisfacción, que sí importa.

Toda distancia sin escalar es una mentira educada. Por eso antes de medir distancias se escalan las columnas, y es el paso que más se olvida. En mi libro de machine learning está trabajado entero.

Lo que te llevas

  • La norma es cuánto mide un vector. La L2 es Pitágoras sin límite de dimensiones.
  • La L2 castiga los números grandes al cuadrado, la L1 no.
  • Distancia entre dos filas = norma de su resta.
  • El RMSE es la norma L2 de los errores entre la raíz de n. Es una distancia con otro nombre.
  • Sin escalar, la columna de números más grandes se come la distancia.

Comprueba que se entendió

Comprueba que lo tienes

Mides la distancia entre dos clientes con columnas "monto" en soles y "satisfacción" del 1 al 5. ¿Qué va a pasar?

  • El monto va a mandar casi del todo, porque sus números son mucho más grandes
  • Las dos columnas van a pesar igual, porque la fórmula las trata igual
  • La satisfacción va a mandar, porque va del 1 al 5 y es más limpia
  • Va a dar error porque las unidades son distintas

Ejercicios

1. La venta más lejos del promedio

Encuentra la fila más rara de la tabla.

media = X.mean(axis=0)
distancias = np.linalg.norm(X - media, axis=1)
i = int(distancias.argmax())
print('fila      :', i)
print('unidades  :', X[i, 0], '| promedio:', round(float(media[0]), 2))
print('monto     :', X[i, 1], '| promedio:', round(float(media[1]), 2))
print('satisfacc.:', X[i, 2], '| promedio:', round(float(media[2]), 2))
print('distancia :', round(float(distancias[i]), 2))
fila      : 2130
unidades  : 6.0 | promedio: 11.68
monto     : 4236.71 | promedio: 803.39
satisfacc.: 2.0 | promedio: 3.02
distancia : 3433.33

El axis=1 ahí sí es el correcto: quieres una norma por fila. Y fíjate en que la fila más lejos lo es por el monto, otra vez. Detectar outliers con distancias sin escalar es detectar la columna más grande.

2. L1 contra L2 en el mismo vector

Mira cuánto se separan cuando hay un componente grande.

a = X[0]
print('L1 :', round(float(np.linalg.norm(a, 1)), 2))
print('L2 :', round(float(np.linalg.norm(a, 2)), 2))
print('L1 - L2 :', round(float(np.linalg.norm(a, 1) - np.linalg.norm(a, 2)), 2))
L1 : 494.37
L2 : 480.49
L1 - L2 : 13.88

La L1 es más grande porque suma todo tal cual. La L2 casi ignora las componentes chicas, porque al cuadrado se hacen todavía más chicas frente a la grande. Esa es la intuición que hay que llevarse a regularización.

3. Escala y mira cómo cambia todo

Divide cada columna por su desviación y vuelve a medir.

Z = (X - X.mean(axis=0)) / X.std(axis=0)
a, b = Z[0], Z[1]
print('sin escalar :', round(float(np.linalg.norm(X[0] - X[1])), 2))
print('escalado    :', round(float(np.linalg.norm(a - b)), 2))
print('aporte por columna, escalado:', np.round(np.abs(a - b), 2))
sin escalar : 44.54
escalado    : 0.7
aporte por columna, escalado: [0.   0.06 0.7 ]

Ahora la satisfacción aporta 0.7 y el monto 0.06, o sea justo al revés que antes. Ninguna de las dos distancias es "la correcta": lo que hay que entender es que escalar es una decisión, y que no escalar también lo es, solo que tomada sin darte cuenta.

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.

¿Le sirve a alguien que conoces?

Pásale el libro. Es gratis, está entero y no pide registro 🐣

Instagram y TikTok no dejan compartir enlaces desde la web: esos dos copian la URL para que la pegues en tu historia.

¿Tienes alguna duda o consulta?