ML Atlas

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.
De la lección 4.5: Fuga de datos Rellenas los faltantes con la media de todo el archivo, divides en entrenamiento y test, entrenas con el entrenamiento y evalúas en el test. ¿Qué paso tiene fuga?

Ver la respuesta

Respuesta correcta: Rellenar los huecos con la media calculada con todo el archivo

La media incluye los datos de test, así que el test influye en el entrenamiento. Ajusta la imputación solo con el entrenamiento, mejor dentro de un pipeline.

De la lección 4.4: Ingeniería de características Verdadero o falso: «más variables dan un modelo mejor».

Ver la respuesta

Respuesta correcta: Falso, porque las variables sin información añaden ruido y pueden bajar la exactitud

200 columnas de ruido bajan kNN del 98,1% al 77,4%. Lo que ayuda es una variable con información en una buena forma, como x² para la variedad 2.

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?

Predice y comprueba Tienes una tabla de pingüinos y quieres evaluar kNN en una prueba apartada. Antes hay que estandarizar las medidas: restar a cada columna su media y dividirla por su desviación típica. Un escalador es la herramienta que calcula esa media y esa desviación. ¿Primero escalas y luego divides los datos en entrenamiento y prueba, o al revés?
¿Qué tan seguro estás?

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.

1. ¿Por qué ajustas el escalador solo con la parte de entrenamiento?
Ver la respuesta

Respuesta correcta: Porque el escalador es parte del modelo y la prueba debe imitar datos que el modelo no vio

La prueba imita pingüinos nuevos. La media y la dispersión también son algo que el modelo aprende, así que se calculan solo con el entrenamiento.

2. Los pingüinos llegan al modelo de uno en uno y alguien ajusta el escalador con cada uno por separado. ¿Qué pasa?
Ver la respuesta

Respuesta correcta: Cada pingüino se vuelve cero en todas las columnas y el modelo dice lo mismo de cada uno

Un solo pingüino no tiene dispersión, así que tras restarle su propia media solo quedan ceros. El modelo llama «Adelie» a todos: 44,1%, lo mismo que «siempre Adelie».

3. ¿Qué hace la validación cruzada en el entrenamiento cuando el escalador está dentro del pipeline?
Ver la respuesta

Respuesta correcta: Ajusta el escalador de nuevo en cada fold, solo con la parte para aprender de ese fold

Un pipeline es un solo modelo, así que en cada fold toda la máquina aprende desde cero. El fold que hace de prueba no toca ni el escalador ni kNN.

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.

Para profesionales
  • En scikit-learn: make_pipeline o Pipeline([...]). Cuando las columnas son de tipos distintos, el primer paso es un ColumnTransformer: 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.

Profundiza en el Atlas

Entradas de la enciclopedia sobre las ideas de esta lección: