Machine Learning
Cómo una máquina ajusta una regla a partir de ejemplos.
Machine Learning: las bases
Un modelo que acierta el 99 % de las veces puede ser totalmente inútil. Un modelo que reconoce a la perfección todos los ejemplos con los que se ha construido puede ser el peor de todos los candidatos. Ambas afirmaciones parecen absurdas, pero forman parte del día a día del aprendizaje automático, e ignorarlas es la forma más habitual de entregar un sistema que falla desde el momento en que entra en producción.
La razón es que el aprendizaje automático no busca memorizar ejemplos: busca generalizar a casos que nunca ha visto. Y esa capacidad no se puede medir directamente, porque, por definición, los casos futuros no están disponibles. Todo el arte consiste, por tanto, en organizar una medición indirecta honesta y, después, elegir el indicador adecuado para leerla.
Esta lección construye esas salvaguardas en el orden en que se necesitan. Veremos primero qué significa en concreto «aprender» para una máquina, lo cual se reduce a ajustar números. Después, las tres grandes familias de problemas, según la información de la que se dispone. A continuación, el protocolo que permite medir la generalización y el fenómeno que revela: el sobreajuste. Por último, la elección de las métricas, donde se producen la mayoría de los malentendidos entre los equipos técnicos y el negocio.
Todos los ejemplos utilizan scikit-learn y conjuntos de datos incluidos en la biblioteca: se pueden ejecutar tal cual, sin descargar nada.
Aprender es ajustar parámetros
Un modelo es una función parametrizada. Aprender consiste en buscar los valores de los parámetros que minimizan la diferencia entre las predicciones y las respuestas conocidas. No hay nada más misterioso que eso.
import numpy as np
from sklearn.linear_model import LinearRegression
# Cinco apartamentos: superficie en m², alquiler en euros.
surface = np.array([[30], [45], [60], [75], [90]])
loyer = np.array([620, 780, 1010, 1150, 1400])
modele = LinearRegression().fit(surface, loyer)
print(round(float(modele.coef_[0]), 2)) # 12.87
print(round(float(modele.intercept_), 2)) # 220.0
print(modele.predict([[50]]).round(1)) # [863.3]
El modelo ha encontrado dos números. La regla aprendida se lee en lenguaje natural: unos 12,87 € por metro cuadrado, más 220 € de gastos fijos. No se ha escrito a mano: se ha deducido de cinco observaciones minimizando la suma de los cuadrados de los errores.
Lo importante aquí es la naturaleza de la predicción para 50 m²: 863,3 €. Ningún apartamento de 50 m² figura en los datos. El modelo ha interpolado entre el de 45 m² y el de 60 m². Es legítimo. En cambio, pedirle el alquiler de un piso de 400 m² sería una extrapolación muy alejada del dominio observado: la fórmula respondería sin inmutarse y no habría ninguna razón para creerla.
Conviene fijar tres términos, porque aparecen constantemente:
| Término | Significado | En el ejemplo |
|---|---|---|
| Variable explicativa (feature) | lo que se da como entrada | la superficie |
| Objetivo (target, label) | lo que se quiere predecir | el alquiler |
| Parámetros | los números que ajusta el aprendizaje | 12,87 y 220 |
| Hiperparámetros | los ajustes que fija el humano antes del aprendizaje | aquí ninguno; en otros casos, la profundidad de un árbol |
Las tres familias de aprendizaje
La familia depende de la información de la que se dispone, no del algoritmo.
Aprendizaje supervisado
Se dispone de ejemplos etiquetados: para cada entrada, la respuesta correcta. Es el caso de la regresión anterior y de cualquier clasificación. Es, con diferencia, el marco más utilizado en producción, porque es el que mejor se puede medir, y también el más costoso, porque hay que producir las etiquetas.
Aprendizaje no supervisado
Se dispone únicamente de las entradas. El algoritmo busca una estructura: grupos, ejes de variación, anomalías. La evaluación se vuelve delicada, al no haber una verdad de referencia.
import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import load_iris
iris = load_iris()
# NO se usa iris.target: el algoritmo no ve ninguna etiqueta.
groupes = KMeans(n_clusters=3, n_init=10, random_state=0).fit_predict(iris.data)
print(np.bincount(groupes)) # [62 50 38]
# Comparación a posteriori con las especies reales, para juzgar el resultado.
import collections
print(collections.Counter(zip(groupes.tolist(), iris.target.tolist())))
El recuento final muestra exactamente lo que hay que entender sobre el aprendizaje no supervisado. Un grupo contiene las 50 setosa y nada más: esa especie está claramente separada de las otras dos. Los dos grupos restantes mezclan versicolor y virginica: 14 virginica acaban con las versicolor, y 2 versicolor con las virginica. Dieciséis flores de ciento cincuenta están mal agrupadas, es decir, algo más del 10 %.
El algoritmo no ha fallado: ha encontrado los tres grupos más compactos en el sentido de la distancia euclídea. Esos grupos no coinciden perfectamente con las especies porque nada lo obligaba a ello. Es el límite intrínseco del aprendizaje no supervisado: revela una estructura, no la estructura que usted tenía en mente.
Aprendizaje por refuerzo
Ni entradas etiquetadas ni simple observación: un agente actúa, recibe una recompensa numérica y debe descubrir la estrategia que maximiza la suma acumulada de esas recompensas. La dificultad propia de este marco es el equilibrio entre explotar lo que parece bueno y explorar lo que no se ha probado lo suficiente.
import numpy as np
# Tres palancas, cada una con una probabilidad de ganar que el agente desconoce.
rng = np.random.default_rng(0)
vraies = np.array([0.2, 0.5, 0.75])
estimations = np.zeros(3)
tirages = np.zeros(3)
for t in range(2000):
if rng.random() < 0.1: # 10 % del tiempo: se explora
bras = rng.integers(3)
else: # si no: se explota la mejor conocida
bras = int(np.argmax(estimations))
recompense = 1.0 if rng.random() < vraies[bras] else 0.0
tirages[bras] += 1
# Media actualizada de forma incremental.
estimations[bras] += (recompense - estimations[bras]) / tirages[bras]
print(estimations.round(2)) # [0.19 0.46 0.75]
print(tirages.astype(int)) # [ 81 72 1847]
Las estimaciones finales se aproximan a las probabilidades reales y, sobre todo, el reparto de los intentos cuenta el aprendizaje: 1847 intentos de 2000 se dedicaron a la mejor palanca, frente a unos 75 para cada una de las otras dos. El agente no solo ha estimado, sino que ha concentrado su presupuesto. El precio que se paga se llama arrepentimiento (regret): los 153 intentos en las palancas mediocres eran necesarios para estar seguro de que lo eran.
Entrenamiento y prueba: por qué se ocultan datos
Evaluar un modelo con los ejemplos que sirvieron para ajustarlo mide su memoria, no su capacidad de generalizar. La regla es, por tanto, reservar una parte de los datos antes de cualquier aprendizaje y consultarla solo al final.
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
donnees = load_breast_cancer() # 569 tumores, 30 mediciones, 2 clases
X_train, X_test, y_train, y_test = train_test_split(
donnees.data, donnees.target, test_size=0.3, random_state=42
)
print(X_train.shape, X_test.shape) # (398, 30) (171, 30)
for profondeur in (1, 3, None):
arbre = DecisionTreeClassifier(max_depth=profondeur, random_state=0)
arbre.fit(X_train, y_train)
print(profondeur,
round(arbre.score(X_train, y_train), 3),
round(arbre.score(X_test, y_test), 3))
El parámetro random_state fija el sorteo: sin él, dos ejecuciones darían dos divisiones diferentes y cifras no comparables.
El sobreajuste, medido
La tabla que produce el bucle anterior es el resultado más instructivo de esta lección:
| Profundidad máxima | Exactitud en entrenamiento | Exactitud en prueba | Diferencia |
|---|---|---|---|
| 1 | 0,925 | 0,895 | 0,030 |
| 3 | 0,970 | 0,965 | 0,005 |
| ilimitada | 1,000 | 0,924 | 0,076 |
El árbol de profundidad ilimitada clasifica perfectamente los 398 ejemplos de entrenamiento. Sin embargo, es peor en prueba que el árbol de profundidad 3. Ha construido ramas que describen particularidades de esos 398 pacientes concretos: ruido, no una regularidad. Es el sobreajuste (overfitting).
En el otro extremo, el árbol de profundidad 1 solo plantea una pregunta: es demasiado rígido para captar lo que realmente hay. Es el subajuste (underfitting). El ajuste adecuado está entre ambos y se encuentra midiendo, no por intuición.
error
│
│╲ ╱ ← error de prueba
│ ╲ ╱
│ ╲ ╱
│ ╲__________________________╱ ← zona útil
│ ╲
│ ╲___________________ ← error de entrenamiento
└──────────────────────────────────────► complejidad del modelo
subajuste sobreajuste
Una diferencia de 0,076 entre entrenamiento y prueba es la señal de alarma que hay que vigilar. Pero una única división sigue siendo un sorteo: medir sobre 171 ejemplos deja un margen de incertidumbre apreciable. La validación cruzada repite la operación con varias divisiones:
from sklearn.model_selection import cross_val_score
scores = cross_val_score(DecisionTreeClassifier(max_depth=3, random_state=0),
donnees.data, donnees.target, cv=5)
print(scores.round(3)) # [0.912 0.895 0.939 0.947 0.894]
print(scores.mean().round(3)) # 0.917
print(scores.std().round(3)) # 0.022
La media (0,917) es una estimación más sobria que el 0,965 obtenido con una división favorable, y la desviación típica (0,022) cuantifica el margen. Anunciar «96,5 %» a partir de una sola división no es mentir, pero es una medición demasiado frágil para basar en ella una decisión.
Elegir la métrica
Aquí está la trampa anunciada al principio. En una enfermedad que afecta al 1 % de la población, un modelo que responde «sano» a todo el mundo obtiene un 99 % de exactitud.
import numpy as np
from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score
# 1000 pacientes, 10 enfermos.
y_vrai = np.array([0] * 990 + [1] * 10)
# Modelo perezoso: «nadie está enfermo».
y_paresseux = np.zeros(1000, dtype=int)
print((y_vrai == y_paresseux).mean()) # 0.99
print(recall_score(y_vrai, y_paresseux, zero_division=0)) # 0.0
99 % de exactitud, 0 % de exhaustividad (recall): el modelo no detecta a ningún enfermo. En un cribado, es un sistema sin ningún valor. La exactitud engaña en cuanto las clases están desequilibradas, que es el caso de casi todos los problemas interesantes: fraude, avería, enfermedad rara, defecto de fabricación.
Comparemos con un modelo imperfecto pero útil:
y_predit = np.zeros(1000, dtype=int)
y_predit[:20] = 1 # 20 personas sanas señaladas por error
y_predit[990:998] = 1 # 8 enfermos de 10 detectados correctamente
print(confusion_matrix(y_vrai, y_predit))
# [[970 20]
# [ 2 8]]
print(round(precision_score(y_vrai, y_predit), 3)) # 0.286
print(round(recall_score(y_vrai, y_predit), 3)) # 0.8
print(round(f1_score(y_vrai, y_predit), 3)) # 0.421
La matriz de confusión se lee así: 970 sanos correctamente clasificados, 20 falsas alarmas, 2 enfermos no detectados y 8 enfermos detectados.
| Métrica | Definición | Valor | Pregunta a la que responde |
|---|---|---|---|
| Exactitud | respuestas correctas / total | 0,978 | ¿Se equivoca a menudo el modelo, sumando todas las clases? |
| Precisión | 8 / (8 + 20) | 0,286 | Cuando da una alerta, ¿tiene razón? |
| Exhaustividad (recall) | 8 / (8 + 2) | 0,800 | ¿Cuántos casos reales encuentra? |
| F1 | media armónica | 0,421 | Compromiso único entre las dos anteriores |
Este modelo tiene una exactitud inferior a la del modelo perezoso (0,978 frente a 0,990) y es incomparablemente más útil: encuentra 8 enfermos de 10. El precio son 20 falsas alarmas.
Este compromiso no tiene una solución universal. Se resuelve fuera de las matemáticas:
- Cribado de una enfermedad grave y tratable: priorizar la exhaustividad. Una falsa alarma cuesta una prueba de control; un caso no detectado puede costar una vida.
- Bloqueo automático de una cuenta bancaria: priorizar la precisión. Cada falso positivo es un cliente legítimo bloqueado.
Por eso la métrica debe elegirse con el negocio antes del entrenamiento. Optimizar primero y discutir después lleva a rehacer el trabajo.
Conclusiones
- Aprender equivale a ajustar parámetros para minimizar un error medido sobre ejemplos. Lo que no está en los datos no estará en el modelo.
- Las tres familias se distinguen por la información disponible: etiquetas (supervisado), solo entradas (no supervisado), recompensas (refuerzo). El no supervisado revela una estructura, no necesariamente la que usted buscaba; 16 iris de 150 mal agrupados lo recuerdan.
- Una puntuación sobre los datos de entrenamiento no mide nada útil. El árbol con 1,000 en entrenamiento y 0,924 en prueba es peor que el de 0,970 / 0,965: es sobreajuste.
- La validación cruzada sustituye una puntuación frágil por una media y una desviación típica. Es el mínimo exigible antes de anunciar una cifra a nadie.
- La exactitud engaña con clases desequilibradas: 99 % sin detectar a ningún enfermo. La precisión y la exhaustividad se equilibran según el coste real de una falsa alarma y el de un caso no detectado, una decisión que corresponde al negocio.
Comentarios
Los comentarios funcionan con GitHub Discussions
Inicia sesión con GitHub para participar en la discusión