Machine learning desde cero · Lección 4.5 · 24 de 33 lecciones listas · 14 min
Fuga de datos
En pocas palabras
Una fuga es información del test o del futuro que llega al entrenamiento. Con ruido puro finge un 82 % en validación. Conoce 8 tipos y encuéntralos tú.
Calentamiento Dos preguntas de lecciones anteriores. Un minuto, y no bloquea la lección.
En la lección 3.2 la validación cruzada daba una estimación honesta del modelo. Honesta con una condición: nada de la parte de test puede tocar antes el entrenamiento. Cuando lo toca, hablamos de fuga de datos. El modelo parece excelente en validación y se hunde en el uso real. Kapoor y Narayanan reunieron los informes de otros investigadores sobre fugas en 294 artículos científicos de 17 campos. Empecemos por el ejemplo más limpio: datos en los que no hay nada que aprender.
Elige una respuesta y tu grado de seguridad.
Responde para desbloquear el experimento.
Experimento interactivo
Este experimento interactivo funciona en un navegador con JavaScript activado. El texto que lo rodea describe lo que muestra.
Ver la respuesta
Respuesta correcta: Más del 80 %, aunque estos datos no tienen ninguna señal
La validación da un 81,7 %. Si eliges las columnas en cada pliegue solo con los datos de entrenamiento, la nota baja al 43,3 %, una moneda al aire. En 20 conjuntos de ruido nuevos la media es del 86,6 % con fuga y del 49,8 % de forma honesta.
Prueba esto
- Con las variables elegidas antes de dividir, mueve k de 5 a 50. ¿Hasta dónde sube la nota con ruido puro?
- Con la misma k, cambia a elegir dentro de la validación. ¿Dónde cae la barra y dónde los puntos de los 20 conjuntos de ruido nuevos?
- En la segunda parte del lab, encuentra el paso con fuga en cada uno de los 8 procesos.
Cuando lo hayas comprobado en el lab, pongamos nombre a lo que ves.
La idea en pocas palabras
Una fuga de datos es cualquier información que el modelo no tendría en el momento de una predicción real y que aun así llegó a su entrenamiento o a su evaluación. La validación tiene que hacer de futuro: datos que el modelo nunca vio. La fuga estropea ese papel, así que la nota sube y el modelo no es mejor en absoluto.
En el lab, entre 1000 columnas de ruido siempre hay algunas que encajan con las etiquetas por azar. La mejor tiene una correlación de 0,48 con la etiqueta, cuando lo típico para el ruido es alrededor de 0,13. Elegir con las 60 muestras ajusta las columnas también a las muestras que después hacen de test. La validación cruzada comprueba entonces el modelo con datos que ya lo moldearon, y muestra un 81,7 %. Elegir dentro de un pliegue solo ve 48 muestras de entrenamiento. Las columnas que encajan con ellas por azar no dicen nada de las 12 muestras de test, así que la nota baja al 43,3 %, dentro de una moneda al aire.
Kapoor y Narayanan clasificaron las fugas en 8 tipos dentro de tres grupos:
- sin una división limpia: sin conjunto de test separado, preprocesamiento ajustado a todos los datos, selección de variables con todos los datos, duplicados a ambos lados de la división;
- una variable que no conoces al predecir, por ejemplo los días en el hospital cuando el modelo debe predecir el ingreso;
- un test de otra distribución que la del uso: una división al azar de datos temporales, las mismas personas en entrenamiento y test, un test elegido de otra forma que los datos con los que trabajará el modelo.
Encontrarás cada uno en la segunda parte del lab.
Jak to policzono
- Ruido: 60 muestras, 1000 columnas de una distribución normal, 30 etiquetas 0 y 30 etiquetas 1 en orden aleatorio. Lo genera el navegador con la semilla 1; los puntos son 20 conjuntos más con otras semillas.
- Modelo: las k columnas con mayor correlación con la etiqueta (en valor absoluto), un clasificador de centroide más cercano, validación cruzada estratificada de 5 pliegues (6 muestras de cada clase en cada pliegue).
- Biopsias en «Error frecuente»: Breast Cancer Wisconsin (Diagnostic), UCI, licencia CC BY 4.0, 569 biopsias. Tipos de fuga: S. Kapoor, A. Narayanan, «Leakage and the reproducibility crisis in machine-learning-based science», Patterns, 2023.
Por qué funciona
Una evaluación es honesta solo cuando los datos de test no influyeron en ninguna decisión sobre el modelo. Seleccionar variables también es una decisión, igual que rellenar valores faltantes, escalar, elegir un umbral o quedarse con el mejor de varios modelos. Si alguna de ellas vio las etiquetas del test, el test deja de ser independiente. Cuanto mayor es la elección (10 columnas de 1000), más ajuste al azar pasa a la nota.
Un solo conjunto de ruido podría salir así por azar. Pero en ninguno de los 20 conjuntos nuevos del lab la nota con fuga con k = 10 baja del 81,7 %. En el código de abajo el mismo experimento se hace con 50 conjuntos de ruido en scikit-learn: elegir antes de dividir da de media un 86,4 % (como mínimo un 73,3 %), y elegir en cada pliegue un 50,0 %.
Error frecuente
¿Verdadero o falso?: «la fuga es solo la etiqueta escondida en las variables». Falso. En el lab ninguna columna contiene la etiqueta, y aun así la validación muestra un 81,7 % con ruido puro. Lo que se filtra es una decisión tomada con todos los datos. En las biopsias, sobremuestrear las malignas antes de la validación cruzada da una sensibilidad del 97,1 %, y sobremuestrear solo en los datos de entrenamiento un 92,1 %, en 50 particiones de 50. Las copias caen a ambos lados de la división, así que el modelo reconoce biopsias que ya vio. Los duplicados en los datos funcionan igual, y una división al azar de datos temporales mete el futuro en el entrenamiento.
No todas las fugas son igual de peligrosas. Un escalador ajustado con todas las biopsias cambia la exactitud de un modelo de vecino más cercano en 0,08 puntos de media: no ve las etiquetas, así que transmite muy poco. La selección de variables y el sobremuestreo usan las etiquetas, así que transmiten mucho. La regla es una: ajusta cada paso que aprende algo de los datos solo con la parte de entrenamiento.
Test
Tres preguntas para comprobar lo aprendido. Elige una respuesta o despliega la solución bajo cada pregunta. Con el teclado: las flechas eligen, Enter o Espacio confirma.
Ideas clave
- Una fuga es información que el modelo no tendría al predecir y que aun así tocó su entrenamiento o su evaluación.
- Ajusta cada paso que aprende de los datos (selección, imputación, escalado, sobremuestreo) solo con la parte de entrenamiento, mejor dentro de un pipeline.
- Divide los datos como se usará el modelo: por tiempo, por persona, sin duplicados.
- Cuando una nota parece demasiado buena, busca primero una fuga.
Más a fondo: fórmula, código, práctica
Las matemáticas
Adivinar con n muestras da una exactitud de media 0,5 y desviación típica √(0,5 · 0,5 / n). Con n = 60 son 0,065, así que una moneda al aire es un 50 % ± 13 puntos (dos desviaciones).
Con ruido puro, la correlación de una columna con la etiqueta tiene media 0 y una desviación de alrededor de 1 / √n, unos 0,13 con n = 60. Entre 1000 columnas la mejor queda varias desviaciones lejos del cero: 0,48 en el lab.
La validación cruzada honesta calcula  = media sobre k de A(f_k, T_k), donde el modelo f_k (incluida la selección de variables) se construye solo con los datos fuera del pliegue T_k. Si las variables se eligen con todos los datos, la selección depende de las etiquetas de T_k, así que f_k no es independiente de T_k y  deja de estimar la exactitud con datos nuevos.
En código (Python)
El mismo experimento en scikit-learn: elegir 10 columnas en 50 conjuntos de ruido y, en las biopsias, escalar y sobremuestrear las malignas antes de dividir y dentro de cada pliegue, con 50 particiones.
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.model_selection import StratifiedKFold, cross_val_score, cross_val_predict
from sklearn.neighbors import NearestCentroid, KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import recall_score
def noise(seed): # 60 muestras x 1000 columnas de ruido puro, etiquetas 30/30 en orden aleatorio
rng = np.random.default_rng(seed)
X, y = rng.normal(size=(60, 1000)), rng.permutation(np.repeat([0, 1], 30))
cv = StratifiedKFold(5, shuffle=True, random_state=seed)
leak = cross_val_score(NearestCentroid(), SelectKBest(f_classif, k=10).fit_transform(X, y), y, cv=cv).mean() # 10 columnas elegidas con las 60
honest = cross_val_score(make_pipeline(SelectKBest(f_classif, k=10), NearestCentroid()), X, y, cv=cv).mean() # elegidas dentro de cada pliegue
return leak, honest
r = np.array([noise(s) for s in range(50)])
print(f"pure noise, 50 datasets: picked before the split {r[:, 0].mean():.1%} (lowest {r[:, 0].min():.1%}), inside CV {r[:, 1].mean():.1%}")
X, y = load_breast_cancer(return_X_y=True)
y = 1 - y # 1 = maligna
knn = lambda: KNeighborsClassifier(1)
scale = []
for s in range(50):
cv = StratifiedKFold(5, shuffle=True, random_state=s)
before = cross_val_score(knn(), StandardScaler().fit_transform(X), y, cv=cv).mean() # escalador ajustado con todas las biopsias
inside = cross_val_score(make_pipeline(StandardScaler(), knn()), X, y, cv=cv).mean() # ajustado con cada parte de entrenamiento
scale.append(abs(before - inside))
print(f"breast cancer, 1-NN, 50 splits: scaling before the split changes accuracy by {np.mean(scale) * 100:.2f} points on average")
def oversample(rows, rng): # copia malignas al azar hasta que las dos clases sean iguales
mal, ben = rows[y[rows] == 1], rows[y[rows] == 0]
return np.r_[rows, rng.choice(mal, len(ben) - len(mal))]
rec = []
for s in range(50):
rng, cv = np.random.default_rng(s), StratifiedKFold(5, shuffle=True, random_state=s)
rows = oversample(np.arange(len(y)), rng) # las copias hechas ANTES de dividir también caen en los pliegues de test
before = recall_score(y[rows], cross_val_predict(make_pipeline(StandardScaler(), knn()), X[rows], y[rows], cv=cv))
pred = np.empty(len(y))
for tr, te in cv.split(X, y): # copias solo dentro de cada parte de entrenamiento
t = oversample(tr, rng)
pred[te] = make_pipeline(StandardScaler(), knn()).fit(X[t], y[t]).predict(X[te])
rec.append((before, recall_score(y, pred)))
rec = np.array(rec)
print(f"oversampling malignant before the split: recall {rec[:, 0].mean():.1%}, inside each training part: {rec[:, 1].mean():.1%}; higher before in {(rec[:, 0] > rec[:, 1]).sum()} of 50")
Resultado
pure noise, 50 datasets: picked before the split 86.4% (lowest 73.3%), inside CV 50.0%
breast cancer, 1-NN, 50 splits: scaling before the split changes accuracy by 0.08 points on average
oversampling malignant before the split: recall 97.1%, inside each training part: 92.1%; higher before in 50 of 50
El lab muestra un conjunto de ruido y 20 más, el código otros 50 conjuntos, así que los números difieren un poco: en el lab la fuga da un 81,7 % en el primer conjunto y un 86,6 % de media en los 20 siguientes, en el código un 86,4 % de media. La conclusión es la misma en cada conjunto.
Descargar el notebook (.ipynb). Ábrelo en Jupyter o súbelo a Google Colab; descarga por sí solo los datos que falten.
Para profesionales
- En scikit-learn mete cada paso que aprende de los datos en un
Pipelinejunto con el modelo, y solo entonces evalúa el conjunto concross_val_score: así elfitde cada paso ve solo la parte de entrenamiento. - Divide los datos temporales con
TimeSeriesSplity los datos con grupos (pacientes, clientes, fotos de una misma cámara) conGroupKFold. - Usa el sobremuestreo y SMOTE dentro de un
Pipelinede imbalanced-learn, solo en las partes de entrenamiento (lección 2.5). - En Kaggle la fuga a veces está en el propio archivo: identificadores, orden de las filas, marcas de tiempo. Una nota mucho mejor que la de los demás es una señal para buscar una fuga antes de celebrarlo.
¿Te ha sido útil?
¡Gracias! Nos ayuda a mejorar.
Profundiza en el Atlas
Entradas de la enciclopedia sobre las ideas de esta lección: