Machine learning desde cero · Lección 4.6 · 25 de 33 lecciones listas · 14 min
Pipeline: de la tabla a la predicción
En pocas palabras
El escalador es parte del modelo y aprende solo con el entrenamiento. Ajustado en la propia prueba da 44,1% en vez de 99,0% con pingüinos que llegan uno a uno.
Calentamiento Dos preguntas de lecciones anteriores. Un minuto, y no bloquea la lección.
En la lección 4.3 escalamos medidas y en la lección 4.5 buscamos fugas. Hoy lo juntamos en una sola máquina. Un pipeline es una lista de pasos que convierte una tabla en bruto en una predicción: escalado, codificación, modelo. En scikit-learn es un solo objeto: lo ajustas como un modelo y le preguntas como a un modelo. La pregunta clave es: ¿en qué orden?
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: Primero dividir, luego ajustar el escalador solo con el entrenamiento y convertir la prueba con él
Dividir, y ajustar el escalador solo con el entrenamiento; en la validación cruzada, en cada fold por separado. Con los pingüinos, un escalador ajustado con todos los datos da por casualidad lo mismo: 99,0%, las mismas predicciones para 102 de 102 pingüinos de prueba. Pero la media de la prueba entra igualmente en el escalador. Y escalar la prueba con su propia media estropea el modelo: cuando los pingüinos llegan de uno en uno, la exactitud cae al 44,1%.
Pruébalo
- Empieza con el escalador de entrenamiento. Cambia al escalador de todos los datos. ¿Cuántas predicciones cambian y por qué?
- Elige el escalador ajustado en la prueba y cambia cómo llegan los pingüinos. ¿Cuándo deja de funcionar el modelo?
- Arma el pipeline con los bloques. ¿Cuál de las tres trampas tienta más?
Cuando lo hayas comprobado en el lab, pongamos nombre a lo que ves.
La idea en palabras sencillas
Un escalador no es preparación de datos «antes del modelo». Es la primera parte del modelo. Para cada columna guarda dos números: una media y una desviación típica. Primero los aprende y luego convierte con ellos. Como toda parte de un modelo, aprende solo con el entrenamiento. La prueba se convierte con los mismos números, igual que después cada pingüino nuevo.
Un escalador ajustado en la propia prueba rompe esta regla: la prueba recibe su propia media y su propia dispersión. Cuando toda la prueba llega a la vez y se parece al entrenamiento, el resultado parece bueno: 98,0%. Cuando llegan lotes de una sola isla, ya solo 82,4%. En los datos, Torgersen solo tiene pingüinos Adelie. Al restarles su propia media, el modelo reconoce 6 de 13. Cuando los pingüinos llegan de uno en uno, la exactitud cae al 44,1%. Un solo pingüino no tiene dispersión, así que tras la conversión es cero en todas las columnas. El modelo llama «Adelie» a todos, que es justo lo que hace «siempre Adelie».
Un pipeline une los pasos en un solo objeto, aquí el escalador y kNN. Ajustado con el entrenamiento, aprende a la vez las medias, las desviaciones y el propio modelo, y al predecir solo convierte y pregunta. La validación cruzada con un pipeline ajusta el escalador de nuevo en cada fold, así que ningún fold de prueba toca el aprendizaje. Cómo se ve esto en scikit-learn lo muestra la capa «En código» al final de la lección.
Jak to policzono
- Pingüinos: Palmer Penguins (Gorman, Williams y Fraser, 2014; licencia CC0), 342 pingüinos con cuatro medidas. División estratificada: 240 para aprender, 102 para la prueba. Los puntos del lab son 20 divisiones con otras semillas.
- Modelo: kNN con k = 5 sobre las medidas estandarizadas (media 0, desviación 1). El escalador se ajusta con el entrenamiento, con los 342 pingüinos o con la propia prueba: toda a la vez, por separado en cada isla o por separado en cada pingüino.
Por qué funciona
En el uso real, un modelo recibe pingüinos que no estaban en los datos, a menudo de uno en uno. Entonces no hay «todos los datos» ni «toda la prueba». Hay pingüinos nuevos y los números guardados durante el aprendizaje. Una evaluación es honesta cuando la prueba sigue exactamente el mismo camino. Un pipeline lo impone: aprende solo con el entrenamiento, y todo lo demás solo se convierte y se evalúa.
Un escalador ajustado con todos los datos también rompe esta regla, solo que en silencio. Con los pingüinos casi no cambia nada: la longitud media de la aleta de 342 y de 240 pingüinos difiere en 0,2 mm, y en cada una de las 20 divisiones del lab las predicciones son las mismas. El resultado aparente y el honesto difieren aquí en 0. En el código de abajo, scikit-learn da en 50 divisiones las mismas predicciones en 50 de 50. Pero el escalador no ve las etiquetas. Un paso que sí las ve puede filtrar mucho en el mismo lugar: en la lección 4.5, elegir variables con todos los datos sacó un «modelo» de ruido puro. Un pipeline te protege de ambas cosas, así que no tienes que comprobar cada vez si esta vez la fuga es pequeña.
Un error frecuente
Verdadero o falso: «El escalador también se puede ajustar en la propia prueba; al fin y al cabo, la prueba y el entrenamiento tienen entonces la misma escala». Falso. La prueba recibe su propia media y su propia dispersión, mientras que el modelo aprendió con los números del entrenamiento. Con los pingüinos parece inofensivo solo cuando toda la prueba llega a la vez: 98,0%. Por lotes de una isla es 82,4%, de uno en uno 44,1%. En las 20 divisiones del lab, de media 76,9% y 42,9%, mientras que el pipeline da 99,2%. En el código de abajo: a la vez 0,971, por isla 0,786, de uno en uno 0,437, lo mismo que «siempre Adelie».
La regla es sencilla: el escalador aprende con el entrenamiento, y la prueba y los datos nuevos solo se convierten con los mismos números. Un pipeline lo hace por ti.
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
- Un escalador, un codificador y cualquier otro paso que aprende algo son parte del modelo: aprenden solo con el entrenamiento.
- Un pipeline une los pasos en un solo objeto, así que la validación cruzada los ajusta de nuevo en cada fold.
- Un escalador ajustado en la propia prueba estropea el modelo: 44,1% en vez de 99,0% con pingüinos que llegan de uno en uno.
- Un escalador ajustado con todos los datos filtra poco (aquí 0 predicciones cambiadas), pero un paso que ve las etiquetas puede filtrar mucho en el mismo lugar.
Más a fondo: fórmula, código, práctica
Las matemáticas
Escalador: z = (x − μ) / σ, donde μ y σ son la media y la desviación típica de la columna calculadas con el entrenamiento. Pipeline P = (S, M): P.fit(X, y) calcula μ y σ con X y luego entrena el modelo M con S(X). La predicción es P.predict(x) = M(S(x)) para cada x nuevo, siempre con los mismos μ y σ.
fit_transform con una sola fila: μ = x y σ = 0 (scikit-learn sustituye una desviación nula por 1), así que z = (x − x) / 1 = 0 en todas las columnas, sea cual sea el pingüino.
En código (Python)
En código el escalador aprende con fit, convierte con transform, y fit_transform hace las dos cosas a la vez. El mismo experimento en scikit-learn: un pipeline con el entrenamiento, fit_transform en la prueba a la vez, por isla y pingüino a pingüino, y al final un escalador ajustado con todos los datos antes de dividir, en 50 divisiones.
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
df = pd.read_csv("data/raw/penguins.csv")
cols = ["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"]
d = df.dropna(subset=cols)
X, y = d[cols], d["species"]
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, stratify=y, random_state=1)
pipe = make_pipeline(StandardScaler(), KNeighborsClassifier(5)) # el escalador es parte del modelo
print(f"train {len(y_tr)}, test {len(y_te)}; CV on the training part: {cross_val_score(pipe, X_tr, y_tr, cv=5).mean():.3f}")
pipe.fit(X_tr, y_tr)
print(f"test, scaler fitted on the training part: {pipe.score(X_te, y_te):.3f}")
knn = pipe[-1] # el kNN ajustado, sin su escalador
batch = knn.score(StandardScaler().fit_transform(X_te), y_te) # fit_transform en la prueba
right = [knn.predict(StandardScaler().fit_transform(part)) == y_te[part.index] for _, part in X_te.groupby(d["island"])]
one = [knn.predict(StandardScaler().fit_transform(X_te.loc[[i]]))[0] == y_te[i] for i in X_te.index]
print(f"fit_transform on the test: all at once {batch:.3f}, by island {np.concatenate(right).mean():.3f}, one at a time {np.mean(one):.3f}")
print(f"always Adelie: {(y_te == 'Adelie').mean():.3f}")
same = equal = 0
for s in range(50):
a, b, ya, yb = train_test_split(X, y, test_size=0.3, stratify=y, random_state=s)
honest = make_pipeline(StandardScaler(), KNeighborsClassifier(5)).fit(a, ya).predict(b)
scaler = StandardScaler().fit(X) # scaler.fit(X): todos los pingüinos, antes de dividir
leaky = KNeighborsClassifier(5).fit(scaler.transform(a), ya).predict(scaler.transform(b))
same += (honest == leaky).all()
equal += (leaky == yb).sum() == (honest == yb).sum()
print(f"scaler.fit(X) before the split, 50 splits: the same predictions in {same} of 50, the same test accuracy in {equal} of 50")
Resultado
train 239, test 103; CV on the training part: 0.983
test, scaler fitted on the training part: 0.961
fit_transform on the test: all at once 0.971, by island 0.786, one at a time 0.437
always Adelie: 0.437
scaler.fit(X) before the split, 50 splits: the same predictions in 50 of 50, the same test accuracy in 50 of 50
La división es distinta de la del lab (103 pingüinos de prueba en vez de 102), así que los números también difieren un poco. La imagen es la misma: fit_transform en toda la prueba a la vez da por suerte incluso 0,971, por isla 0,786 y de uno en uno 0,437, lo mismo que «siempre Adelie». Un escalador ajustado con todos los datos no cambia ni una predicción en estas 50 divisiones.
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:
make_pipelineoPipeline([...]). Cuando las columnas son de tipos distintos, el primer paso es unColumnTransformer: escalar los números, codificar las categorías en one-hot, rellenar los huecos. - Ajuste de hiperparámetros:
GridSearchCV(pipe, param_grid)ajusta todo el pipeline dentro de los folds. Los parámetros de un paso llevan su nombre como prefijo, p. ej.kneighborsclassifier__n_neighbors. - Guarda el pipeline entero (
joblib.dump(pipe, "model.joblib")), no solo el modelo. Así en producción nadie puede olvidar el escalado ni convertir datos nuevos con otra media. - Rellenar huecos (lección 4.2), codificar categorías (lección 4.3), variables nuevas (lección 4.4) y selección de variables (lección 4.5): mete cada uno de estos pasos dentro del pipeline.
¿Te ha sido útil?
¡Gracias! Nos ayuda a mejorar.
Profundiza en el Atlas
Entradas de la enciclopedia sobre las ideas de esta lección: