13. Cadenas de Algoritmos y Pipelines#

  • El rendimiento de los algoritmos de aprendizaje automático depende de la representación de los datos, desde el escalado hasta el aprendizaje de características no supervisado. La mayoría de las aplicaciones requieren encadenar múltiples transformaciones y modelos.

  • La clase Pipeline simplifica este proceso, permitiendo combinar pasos de preprocesamiento y modelos en un solo flujo. Además, Pipeline puede integrarse con GridSearchCV para optimizar todos los parámetros a la vez. Un ejemplo muestra que el uso de MinMaxScaler mejora significativamente el desempeño de un SVM en el conjunto de datos cancer.

import warnings
warnings.filterwarnings('ignore')
from sklearn.svm import SVC
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, random_state=0)
scaler = MinMaxScaler().fit(X_train)
X_train_scaled = scaler.transform(X_train)
svm = SVC()
svm.fit(X_train_scaled, y_train)
X_test_scaled = scaler.transform(X_test)
print("Test score: {:.2f}".format(svm.score(X_test_scaled, y_test)))
Test score: 0.97

13.1. Selección de parámetros con preprocesamiento#

  • Ahora digamos que queremos encontrar mejores parámetros para el SVC usando GridSearchCV, como se ha discutido en secciones anteriores. ¿Cómo podemos hacerlo? Un enfoque ingenuo podría ser el siguiente

from sklearn.model_selection import GridSearchCV

param_grid = {'C': [0.001, 0.01, 0.1, 1, 10, 100],
              'gamma': [0.001, 0.01, 0.1, 1, 10, 100]}
grid = GridSearchCV(SVC(), param_grid=param_grid, cv=5)
grid.fit(X_train_scaled, y_train)
print("Best cross-validation accuracy: {:.2f}".format(grid.best_score_))
print("Best set score: {:.2f}".format(grid.score(X_test_scaled, y_test)))
print("Best parameters: ", grid.best_params_)
Best cross-validation accuracy: 0.98
Best set score: 0.97
Best parameters:  {'C': 1, 'gamma': 1}
  • Se realiza una búsqueda en red (grid-search) sobre los parámetros de SVC utilizando los datos escalados. Al escalar, se usa todo el conjunto de entrenamiento, y los datos escalados se emplean en la búsqueda en red con validación cruzada (cv=5). En cada división, una parte del conjunto de entrenamiento se usa para entrenar y otra para evaluar, simulando cómo el modelo clasificaría nuevos datos.

  • Es clave notar que, en la validación cruzada, la parte de prueba sigue perteneciendo al conjunto de entrenamiento y el escalado se calcula con todos los datos de entrenamiento. Sin embargo, en la evaluación final con datos nuevos, estos pueden tener un mínimo y un máximo diferentes, afectando el escalado. El siguiente ejemplo ilustra este proceso en la validación cruzada y la evaluación final.

import mglearn
mglearn.plots.plot_improper_processing()
_images/a1b41286a25d60e4e204917a4bce51c8c79c59200849bd5db5c3974d677b9774.png
  • La validación cruzada puede dar resultados demasiado optimistas si se filtra información antes de la división de los datos, lo que puede llevar a la selección de parámetros subóptimos. Para evitarlo, la división debe hacerse antes del preprocesamiento, asegurando que cualquier conocimiento extraído solo afecte el conjunto de entrenamiento. La validación cruzada debe ser el “bucle más externo” del proceso.

  • En scikit-learn, esto se maneja con Pipeline, que permite combinar varios pasos de preprocesamiento y modelado en un solo estimador. Pipeline facilita el ajuste, la predicción y la evaluación, integrando transformaciones como el escalado junto con modelos supervisados.

Observacion

La fuga de datos ocurre cuando el modelo usa información ajena al conjunto de entrenamiento, afectando su evaluación. Si hay datos compartidos entre entrenamiento y prueba, los resultados pueden ser artificialmente altos, ya que el modelo ha sido expuesto previamente a los datos de prueba.

13.2. Construyendo Pipelines#

  • Veamos cómo podemos utilizar la clase Pipeline para expresar el flujo de trabajo para entrenar una SVM después de escalar los datos con MinMaxScaler (por ahora sin grid-search). Primero, construimos un objeto pipeline proporcionándole una lista de pasos. Cada paso es una tupla que contiene un nombre (cualquier cadena de su elección) y una instancia de un estimador

from sklearn.pipeline import Pipeline
pipe = Pipeline([("scaler", MinMaxScaler()), ("svm", SVC())])
  • Aquí, creamos dos pasos: el primero, llamado "scaler", es una instancia de MinMaxScaler y el segundo, llamado "svm", es una instancia de SVC. Ahora, podemos ajustar nuestro pipeline, como cualquier otro estimador de scikit-learn

pipe.fit(X_train, y_train)
Pipeline(steps=[('scaler', MinMaxScaler()), ('svm', SVC())])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
  • Aquí, pipe.fit primero llama a fit en el primer paso (el escalador), luego transforma los datos de entrenamiento usando el escalador, y finalmente ajusta la SVM con los datos escalados. Para evaluar en los datos de prueba, simplemente llamamos a pipe.score

print("Test score: {:.2f}".format(pipe.score(X_test, y_test)))
Test score: 0.97
  • El pipeline primero transforma los datos de prueba con scaler y luego aplica SVM sobre los datos escalados, obteniendo el mismo resultado que al transformar manualmente. Su principal ventaja es simplificar el proceso de preprocesamiento + clasificación y permitir su uso directo en cross_val_score o GridSearchCV.

13.3. Utilizando Pipeline en GridSearch#

  • El uso de una tubería (pipeline) en una búsqueda de red (grid-search) es similar a cualquier otro estimador. Definimos una red de parámetros y creamos un GridSearchCV con la tubería y la red. Al especificar los parámetros, debemos indicar el paso al que pertenecen. En este caso, los parámetros C y gamma corresponden a SVC, nombrado "svm". La sintaxis para definirlos en el diccionario de la red es "svm__C" y "svm__gamma", usando __ (doble guión bajo) para separar el nombre del paso y el parámetro.

param_grid = {'svm__C': [0.001, 0.01, 0.1, 1, 10, 100], 
              'svm__gamma': [0.001, 0.01, 0.1, 1, 10, 100]}
  • Con este parámetro grid podemos utilizar GridSearchCV como siempre

grid = GridSearchCV(pipe, param_grid=param_grid, cv=5)
grid.fit(X_train, y_train)
print("Best cross-validation accuracy: {:.2f}".format(grid.best_score_))
print("Test set score: {:.2f}".format(grid.score(X_test, y_test)))
print("Best parameters: {}".format(grid.best_params_))
Best cross-validation accuracy: 0.98
Test set score: 0.97
Best parameters: {'svm__C': 1, 'svm__gamma': 1}
  • A diferencia de la búsqueda en red que hicimos antes, ahora para cada división en la validación cruzada el MinMaxScaler se reajusta sólo con las divisiones de entrenamiento y no se filtra información de la división de prueba en la búsqueda de parámetros

mglearn.plots.plot_proper_processing()
_images/d1dcf2fda5f9f88977acc506f13827838447508306759b260d38989c688c7ee5.png
  • El impacto de la fuga de información en la validación cruzada depende del preprocesamiento. La estimación de escala en el pliegue de prueba tiene un impacto menor, pero usarlo en la extracción o selección de características puede distorsionar significativamente los resultados.

13.4. Data Leakage#

  • La fuga de datos ocurre cuando se usa información en la construcción del modelo que no estaría disponible al predecir, lo que genera estimaciones optimistas y peor rendimiento en producción.

  • Una causa común es no separar correctamente los datos de entrenamiento y prueba. Los datos de prueba nunca deben influir en el ajuste del modelo, ni directa ni indirectamente, como al aplicar transformaciones de preprocesamiento.

  • Las transformaciones deben aprenderse solo de los datos de entrenamiento. Por ejemplo, si se normaliza dividiendo por la media, esta debe calcularse solo con el entrenamiento; incluir los datos de prueba introduciría sesgo.

  • En la selección de características, solo deben usarse los datos de entrenamiento. Incluir los de prueba sesgaría el modelo. Se demostrará este efecto con un problema de clasificación binaria con 10,000 características aleatorias.

import numpy as np
n_samples, n_features, n_classes = 200, 10000, 2
rng = np.random.RandomState(42)
X = rng.standard_normal((n_samples, n_features))
y = rng.choice(n_classes, n_samples)

Forma incorrecta

  • El uso de todos los datos para la selección de características genera una precisión artificialmente alta, incluso cuando X e y son independientes, lo que debería dar una precisión cercana a 0.5. Esto ocurre porque la selección de características “ve” los datos de prueba, otorgando una ventaja injusta.

  • En el enfoque incorrecto, se seleccionan características antes de dividir los datos, lo que infla la precisión del modelo. SelectKBest elige las k mejores características según una función de puntuación (X, y), reteniendo aquellas con mayor relación con y. Si se usa chi2, se evalúa la dependencia entre cada característica e y: valores bajos indican independencia, valores altos sugieren relación no aleatoria. Por defecto, SelectKBest emplea f_regression F-value para medir la relevancia de cada característica.

from sklearn.model_selection import train_test_split
from sklearn.feature_selection import SelectKBest
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import accuracy_score
  • Preprocesamiento incorrecto: se transforman todos los datos

X_selected = SelectKBest(k=25).fit_transform(X, y) #(Dependencia entre cada X[:,i] e y.) -> Reg: F de ANOVA o Class: Chi-cuadrado 

X_train, X_test, y_train, y_test = train_test_split(X_selected, y, random_state=42)
gbc = GradientBoostingClassifier(random_state=1)
gbc.fit(X_train, y_train)

y_pred = gbc.predict(X_test)
accuracy_score(y_test, y_pred)
0.76

Forma correcta

  • Para evitar la fuga de datos, primero se deben dividir los datos en entrenamiento y prueba. La selección de características debe realizarse solo con el conjunto de entrenamiento. Al usar fit o fit_transform, se aplica exclusivamente al entrenamiento. Esto garantiza que la puntuación refleje el desempeño real del modelo.

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
select = SelectKBest(k=25)
X_train_selected = select.fit_transform(X_train, y_train)

gbc = GradientBoostingClassifier(random_state=1)
gbc.fit(X_train_selected, y_train)

X_test_selected = select.transform(X_test)
y_pred = gbc.predict(X_test_selected)
accuracy_score(y_test, y_pred)
0.46
  • Se recomienda usar un pipeline para encadenar la selección de características y los estimadores, asegurando que solo los datos de entrenamiento se usen en el ajuste y los de prueba solo para evaluar la precisión.

from sklearn.pipeline import make_pipeline
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
pipeline = make_pipeline(SelectKBest(k=25), GradientBoostingClassifier(random_state=1))
pipeline.fit(X_train, y_train)

y_pred = pipeline.predict(X_test)
accuracy_score(y_test, y_pred)
0.46

13.5. Ilustración de la fuga de datos#

  • Un ejemplo clásico de fuga de información en validación cruzada se encuentra en The Elements of Statistical Learning de Hastie, Tibshirani y Friedman. Reproducimos una versión adaptada: en una tarea de regresión sintética con 100 muestras y 1.000 características, todas se generan de una distribución gaussiana independiente, al igual que la variable respuesta.

import numpy as np
rnd = np.random.RandomState(seed=0)
X = rnd.normal(size=(100, 10000))
y = rnd.normal(size=(100,))
  • No hay relación entre los datos \(X\) y el objetivo \(y\) (son independientes), por lo que no debería ser posible aprender nada del conjunto. Se selecciona la característica más informativa con SelectPercentile (elige variables en el percentil más alto de scores) y se evalúa un regresor Ridge mediante validación cruzada.

from sklearn.feature_selection import SelectPercentile, f_regression
select = SelectPercentile(score_func=f_regression, percentile=5).fit(X, y)
X_selected = select.transform(X)
print("X_selected.shape: {}".format(X_selected.shape))
X_selected.shape: (100, 500)
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import Ridge
cross_val_score(Ridge(), X_selected, y, cv=5)
array([0.84834054, 0.94084243, 0.88541709, 0.94012139, 0.91425508])
print("Cross-validation accuracy (cv only on ridge): {:.2f}".format(np.mean(cross_val_score(Ridge(), X_selected, y, cv=5))))
Cross-validation accuracy (cv only on ridge): 0.91
  • El score de validación cruzada es 0.91, lo que sugiere un modelo excelente, pero esto es erróneo, ya que los datos son aleatorios. La selección de características eligió, por azar, algunas altamente correlacionadas con el objetivo. Como la selección ocurrió fuera de la validación cruzada, filtró información de los pliegues de prueba, generando resultados poco realistas. Para evitar esto, es necesario aplicar una validación cruzada adecuada con una tubería.

pipe = Pipeline([("select", SelectPercentile(score_func=f_regression, percentile=5)), 
                 ("ridge", Ridge())])
cross_val_score(pipe, X, y, cv=5)
array([-0.97502994, -0.03166358, -0.03989415,  0.03018385, -0.2163673 ])
print("Cross-validation accuracy (pipeline): {:.2f}".format(np.mean(cross_val_score(pipe, X, y, cv=5))))
Cross-validation accuracy (pipeline): -0.25
  • La API de scoring siempre maximiza la puntuación. Por ello, las métricas a minimizar (como MSE) se devuelven en negativo, mientras que las que deben maximizarse se mantienen en positivo.

  • Pipeline garantiza que la selección de características ocurra dentro de la validación cruzada, evitando la fuga de datos. Esto impide que el modelo seleccione características basadas en información del conjunto de prueba, lo que podría generar resultados engañosos.

  • Consideremos el siguiente ejemplo en el que aplicaremos la siguiente cadena al conjunto de datos digits

\[ \textsf{StandardScaler}() \Longrightarrow \textsf{PCA}() \Longrightarrow \textsf{LogisticRegression}() \]
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import StandardScaler
from sklearn import datasets
import matplotlib.pyplot as plt
  • Definimos un pipeline para buscar la mejor combinación de truncamiento PCA y la regularización del clasificador. Definir un escalador estándar para normalizar las entradas

pca = PCA()
scaler = StandardScaler()
  • Establecemos la tolerancia a un valor grande para que el ejemplo sea más rápido

logistic = LogisticRegression(max_iter=10000, tol=0.1)
  • Definimos nuestro Pipeline

pipe = Pipeline(steps=[("scaler", scaler), ("pca", pca), ("logistic", logistic)])
  • Cargamos el conjunto de datos digits a manera de ejemplo

X_digits, y_digits = datasets.load_digits(return_X_y=True)
  • Recuerde que los parámetros del pipeline se pueden establecer utilizando nombres de parámetros separados por '__' (doble guión bajo)

param_grid = {
    "pca__n_components": [5, 15, 30, 45, 60],
    "logistic__C": np.logspace(-4, 4, 4)
    }
  • Aplicamos GridSearchCV para encontrar los mejores parámetros en nuestra cadena. Recuerde que Pipeline automáticamente ajusta el modelo sin caer en data leakage. Asiganmos n_jobs=-1 para utilizar todos los núcleos del procesador

search = GridSearchCV(pipe, param_grid, n_jobs=-1)
search.fit(X_digits, y_digits)
print("Best parameter (CV score=%0.3f):" % search.best_score_)
print(search.best_params_)
Best parameter (CV score=0.874):
{'logistic__C': 21.54434690031882, 'pca__n_components': 60}
  • Nótese que los siguientes son los méjores parámetros para la clasificación utilizando regresión logística, basados en el análisis de componentes principales {'logistic__C': 0.046415888336127774, 'pca__n_components': 60}. Dibujemos el espectro PCA para poder visualizar los parámetros encontrados:

pca.fit(X_digits)

plt.plot(np.arange(1, pca.n_components_ + 1), pca.explained_variance_ratio_, "+", linewidth=2)
plt.ylabel("PCA explained variance ratio")
plt.xlabel("PCA n_components")
plt.axvline(
    search.best_estimator_.named_steps["pca"].n_components,
    color='k', linewidth=1.0, linestyle='--',
    label="n_components chosen",
);
_images/c1e1b1921dd4d42274314c200616c7cc9ef156b5ad8fcaa5dbc19a1c80479c4f.png

13.6. La interfaz general del Pipeline#

  • La clase Pipeline no se limita al preprocesamiento y la clasificación, sino que puede combinar múltiples estimadores, como extracción de características, selección, escalado y clasificación, en una secuencia de pasos. También puede incluir regresión o agrupación en lugar de clasificación. El único requisito es que todos los pasos, excepto el último, deben tener un método transform para encadenar su salida con el siguiente paso.

  • Durante Pipeline.fit, cada paso aplica fit y transform, excepto el último, que solo usa fit. Internamente, la tubería ejecuta los métodos en secuencia, con pipeline.steps[i][1] representando cada estimador en la lista de pasos.

def fit(self, X, y):
    X_transformed = X
    for name, estimator in self.steps[:-1]:
        # Iterar sobre todo excepto el último paso 
        # Ajustar y transformar los datos
        X_transformed = estimator.fit_transform(X_transformed, y)
    # Ajuste en el último paso
    self.steps[-1][1].fit(X_transformed, y)
    return self
  • Cuando se predice utilizando Pipeline, transformamos los datos de forma similar utilizando todos los pasos menos el último paso, y luego llamamos a predict en el último paso

def predict(self, X):
    X_transformed = X
    for step in self.steps[:-1]:
        # Iterar sobre todo excepto el último paso 
        # Ajustar y transformar los datos
        X_transformed = step[1].transform(X_transformed)
    # Ajuste en el último paso
    return self.steps[-1][1].predict(X_transformed)
  • El proceso se ilustra en la Fig. 13.1 para dos transformadores, T1, T2, y un classifier (llamado Classifier)

_images/pipeline_predict.png

Fig. 13.1 Visión general del proceso de entrenamiento y predicción de la tubería.#

  • En realidad, la tubería es aún más general que esto. No es necesario que el último paso de una tubería tenga una función de predicción, y podríamos crear una tubería que sólo contenga, por ejemplo, un escalador y un PCA. Entonces, como el último paso (PCA) tiene un método transform, podríamos llamar a transform en el pipeline para obtener el resultado de PCA.transform aplicado a los datos que fueron procesados por el paso anterior. Sólo se requiere que el último paso de un pipeline tenga un método de ajuste.

13.7. Creación cómoda de pipelines con make_pipeline#

  • La creación de un pipeline utilizando la sintaxis descrita anteriormente es a veces un poco engorrosa, y a menudo no necesitamos nombres especificados por el usuario para cada paso. Existe una función conveniente make_pipeline, que creará una tubería por nosotros y nombrará automáticamente cada paso basándose en su clase. La sintaxis de make_pipeline es la siguiente

from sklearn.pipeline import make_pipeline
pipe_long = Pipeline([("scaler", MinMaxScaler()), ("svm", SVC(C=100))])
pipe_short = make_pipeline(MinMaxScaler(), SVC(C=100))
  • Los objetos pipeline pipe_long y pipe_short hacen exactamente lo mismo, pero pipe_short tiene pasos que fueron nombrados automáticamente. Podemos ver los nombres de los pasos mirando el atributo steps

print("Pipeline steps:\n{}".format(pipe_short.steps))
Pipeline steps:
[('minmaxscaler', MinMaxScaler()), ('svc', SVC(C=100))]
  • Los pasos se denominan minmaxscaler y svc. En general, los nombres de los pasos son sólo versiones de los nombres de las clases. Si varios pasos tienen la misma clase, se añade un número

from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
pipe = make_pipeline(StandardScaler(), PCA(n_components=2), StandardScaler())
print("Pipeline steps:\n{}".format(pipe.steps))
Pipeline steps:
[('standardscaler-1', StandardScaler()), ('pca', PCA(n_components=2)), ('standardscaler-2', StandardScaler())]
  • Como puede ver, el primer paso de StandardScaler llamó standardscaler-1 y el segundo standardscaler-2. Sin embargo, en este tipo de configuraciones podría ser mejor utilizar la construcción de tuberías con nombres explícitos, para dar nombres más semánticos a cada paso

13.8. Acceso a los atributos de los pasos#

  • A menudo querrá inspeccionar los atributos de uno de los pasos de la tubería, por ejemplo, los coeficientes de un modelo lineal o los componentes extraídos por PCA. La forma más fácil de acceder a los pasos de un pipeline es a través del atributo named_steps, que es un diccionario de los nombres de los pasos a los estimadores. A continuación, se ajusta el pipeline definido anteriormente, al conjunto de datos cancer y extrae las dos primeros componentes principales del paso pca.

pipe.fit(cancer.data)
components = pipe.named_steps["pca"].components_
print("components.shape: {}".format(components.shape))
components.shape: (2, 30)

13.9. Acceso a los atributos en un Pipeline con GridSearch#

  • Como hemos comentado anteriormente en este capítulo, una de las principales razones para utilizar pipelines, es para realizar búsquedas en la red. Una tarea común es acceder a algunos de los pasos de una tubería dentro de dentro de una búsqueda en red. Busquemos en la red un clasificador LogisticRegression en el conjunto de datos cancer, utilizando Pipeline. Usamos StandardScaler, para escalar los datos antes de pasarlos al clasificador LogisticRegresión. Primero creamos una instancia de nuestro pipeline usando la función make_pipeline.

from sklearn.linear_model import LogisticRegression
pipe = make_pipeline(StandardScaler(), LogisticRegression())
  • A continuación, creamos un grid de parámetros. Como se explica en la sección anterior, el parámetro de regularización para LogisticRegression es el parámetro C. Utilizamos una red logarítmica para este parámetro, buscando entre 0.01 y 100. Como utilizamos la función make_pipeline, el nombre del paso LogisticRegression en el pipeline es el nombre de la clase en minúsculas, logisticregression. Para afinar el parámetro C, tenemos que especificar una red de parámetros para logisticregression__C

param_grid = {'logisticregression__C': [0.01, 0.1, 1, 10, 100]}
  • Como es habitual, dividimos el conjunto de datos cancer en conjuntos de entrenamiento y de prueba, y ajustamos una búsqueda en red

X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, random_state=4)
grid = GridSearchCV(pipe, param_grid, cv=5)
grid.fit(X_train, y_train)
GridSearchCV(cv=5,
             estimator=Pipeline(steps=[('standardscaler', StandardScaler()),
                                       ('logisticregression',
                                        LogisticRegression())]),
             param_grid={'logisticregression__C': [0.01, 0.1, 1, 10, 100]})
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
  • Entonces, ¿cómo accedemos a los coeficientes del mejor modelo LogisticRegression que fue encontrado por GridSearchCV? De los capítulos anteriores sabemos que el mejor modelo encontrado por GridSearchCV, entrenado con todos los datos de entrenamiento, se almacena en grid.best_estimator_

print("Best estimator:\n{}".format(grid.best_estimator_))
Best estimator:
Pipeline(steps=[('standardscaler', StandardScaler()),
                ('logisticregression', LogisticRegression(C=1))])
  • Este best_estimator_ en nuestro caso es un pipeline con dos pasos, standardcaler y logisticregression. Para acceder al paso de logisticregression, podemos utilizar el atributo named_steps de la tubería, como se ha explicado anteriormente

print("Logistic regression step:\n{}".format(grid.best_estimator_.named_steps["logisticregression"]))
Logistic regression step:
LogisticRegression(C=1)
  • Ahora que tenemos la instancia de LogisticRegression entrenada, podemos acceder a los coeficientes (pesos) asociados a cada característica de entrada. Para mas información sobre los parámetros que podemos visualizar de este modelo clasificador (ver LogisticRegression). En este caso visualizaremos sus coeficientes usando .coef_. Puede que sea una expresión algo larga, pero a menudo resulta útil para entender los modelos.

print("Logistic regression coefficients:\n{}".format(grid.best_estimator_.named_steps["logisticregression"].coef_))
Logistic regression coefficients:
[[-0.4475566  -0.34609376 -0.41703843 -0.52889408 -0.15784407  0.60271339
  -0.71771325 -0.78367478  0.04847448  0.27478533 -1.29504052  0.05314385
  -0.69103766 -0.91925087 -0.14791795  0.46138699 -0.1264859  -0.10289486
   0.42812714  0.71492797 -1.08532414 -1.09273614 -0.85133685 -1.04104568
  -0.72839683  0.07656216 -0.83641023 -0.64928603 -0.6491432  -0.42968125]]

13.10. Pasos de preprocesamiento GridSearch y Parámetros del Modelo#

  • Usando pipelines, podemos encapsular todos los pasos de procesamiento en nuestro flujo de trabajo de aprendizaje automático en un único estimador de scikit-learn. Otro beneficio de hacer esto es que ahora podemos ajustar los parámetros de preprocesamiento usando el output de una tarea supervisada, como la regresión o la clasificación. En los capítulos anteriores, utilizamos las funciones polinómicas en el dataset boston antes de aplicar el regresor ridge. Vamos a modelar esto usando un pipeline en su lugar. El proceso contiene tres pasos: escalar los datos, calcular las características polinómicas y la regresión ridge

import pandas as pd
data_url = "http://lib.stat.cmu.edu/datasets/boston"
raw_df = pd.read_csv(data_url, sep="\s+", skiprows=22, header=None)
data = np.hstack([raw_df.values[::2, :], raw_df.values[1::2, :2]])
target = raw_df.values[1::2, 2]
X_train, X_test, y_train, y_test = train_test_split(data, target, random_state=0)
from sklearn.preprocessing import PolynomialFeatures
pipe = make_pipeline(StandardScaler(), PolynomialFeatures(), Ridge())
  • PolynomialFeatures genera características polinómicas e interacciones hasta un grado especificado. Por ejemplo, para una entrada [a, b] y grado 2, se obtiene [1, a, b, a², ab, b²]. La elección del grado adecuado depende del desempeño del modelo. Se puede optimizar junto con alpha de Ridge usando un param_grid en un pipeline.

param_grid = {'polynomialfeatures__degree': [1, 2, 3], 
              'ridge__alpha': [0.001, 0.01, 0.1, 1, 10, 100]}
  • Ahora podemos volver a ejecutar nuestra búsqueda en la red utilizando nuestro param_grid

grid = GridSearchCV(pipe, param_grid=param_grid, cv=5, n_jobs=-1)
grid.fit(X_train, y_train)
GridSearchCV(cv=5,
             estimator=Pipeline(steps=[('standardscaler', StandardScaler()),
                                       ('polynomialfeatures',
                                        PolynomialFeatures()),
                                       ('ridge', Ridge())]),
             n_jobs=-1,
             param_grid={'polynomialfeatures__degree': [1, 2, 3],
                         'ridge__alpha': [0.001, 0.01, 0.1, 1, 10, 100]})
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
  • Podemos visualizar el resultado de la validación cruzada utilizando un mapa de calor (ver matshow), como ya se hizo en anteriores secciones

import matplotlib.pyplot as plt
plt.matshow(grid.cv_results_['mean_test_score'].reshape(3, -1), vmin=0, cmap="viridis")
plt.xlabel("ridge__alpha")
plt.ylabel("polynomialfeatures__degree")
plt.xticks(range(len(param_grid['ridge__alpha'])), param_grid['ridge__alpha'])
plt.yticks(range(len(param_grid['polynomialfeatures__degree'])),
param_grid['polynomialfeatures__degree'])
plt.colorbar();
_images/baa06a8b76e7cf2266f4c5268990e3e5c273482bc361252c943d5884cfb4dd8f.png
  • Observando los resultados producidos por la validación cruzada, podemos ver que el uso de polinomios de grado dos ayuda, pero que los polinomios de grado tres son mucho peores que los de grado uno o dos. Esto se refleja en los mejores parámetros encontrados

print("Best parameters: {}".format(grid.best_params_))
Best parameters: {'polynomialfeatures__degree': 2, 'ridge__alpha': 10}
  • Lo que lleva al siguiente score

print("Test-set score: {:.2f}".format(grid.score(X_test, y_test)))
Test-set score: 0.77
  • Hagamos una búsqueda en red sin características polinómicas para comparar

param_grid = {'ridge__alpha': [0.001, 0.01, 0.1, 1, 10, 100]}
pipe = make_pipeline(StandardScaler(), Ridge())
grid = GridSearchCV(pipe, param_grid, cv=5)
grid.fit(X_train, y_train)
print("Score without poly features: {:.2f}".format(grid.score(X_test, y_test)))
Score without poly features: 0.63
  • El no usar características polinómicas empeora los resultados. Buscar simultáneamente los parámetros del preprocesamiento y del modelo es una estrategia poderosa. Sin embargo, GridSearchCV evalúa todas las combinaciones posibles, por lo que añadir más parámetros incrementa exponencialmente el número de modelos a construir.

13.11. Grid-Searching Qué Modelo Usar#

  • Se puede ampliar GridSearchCV combinándolo con Pipeline para optimizar no solo los hiperparámetros, sino también los pasos del preprocesamiento, como elegir entre StandardScaler o MinMaxScaler. Esto aumenta el espacio de búsqueda y debe usarse con precaución.

  • Por ejemplo, al comparar RandomForestClassifier y SVC en el conjunto iris, sabemos que SVC puede requerir escalado, por lo que buscamos si usar StandardScaler o prescindir de preprocesamiento. RandomForestClassifier, en cambio, no lo necesita. Definimos un pipeline con dos pasos: preprocesamiento y clasificación, usando SVC con StandardScaler.

pipe = Pipeline([('preprocessing', StandardScaler()), ('classifier', SVC())])
  • Definimos parameter_grid para buscar entre RandomForestClassifier y SVC, ajustando sus parámetros y preprocesamiento según el modelo. Usamos una lista de búsqueda para gestionar configuraciones distintas. Para asignar un estimador a un paso, usamos su nombre como parámetro. Si un modelo no requiere preprocesamiento (como RandomForest), configuramos ese paso como None.

from sklearn.ensemble import RandomForestClassifier
param_grid = [{'classifier': [SVC()], 
               'preprocessing': [StandardScaler(), None],
               'classifier__gamma': [0.001, 0.01, 0.1, 1, 10, 100],
               'classifier__C': [0.001, 0.01, 0.1, 1, 10, 100]},
              {'classifier': [RandomForestClassifier(n_estimators=100)],
               'preprocessing': [None], 
               'classifier__max_features': [1, 2, 3]}]
  • Ahora podemos instanciar y ejecutar grid search como de costumbre, aquí en el conjunto de datos cancer. Nótes que solo es necesario inicializar pipe con cualquiera de los dos estimadores, luego GridSearchCV se encargará de seleccionar el mejor modelo, basado en el param_grid suministrado.

X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, random_state=0)
grid = GridSearchCV(pipe, param_grid, cv=5)
grid.fit(X_train, y_train)
print("Best params:\n{}\n".format(grid.best_params_))
print("Best cross-validation score: {:.2f}".format(grid.best_score_))
print("Test-set score: {:.2f}".format(grid.score(X_test, y_test)))
Best params:
{'classifier': SVC(), 'classifier__C': 10, 'classifier__gamma': 0.01, 'preprocessing': StandardScaler()}

Best cross-validation score: 0.99
Test-set score: 0.98
  • El resultado de la búsqueda en la red es que el SVC con el preprocesamiento StandardScaler, C=10 y gamma=0.01 dio el mejor resultado de clasificación, en comparación del el RandomForestClassifier.

Resumen y conclusiones

  • La clase Pipeline permite encadenar múltiples pasos de procesamiento en un flujo de trabajo de aprendizaje automático, encapsulándolos en un solo objeto que sigue la interfaz de scikit-learn (fit, predict, transform). Su uso es clave para una evaluación adecuada mediante validación cruzada y búsqueda en red.

  • Además, Pipeline simplifica el código y minimiza errores, como olvidar aplicar transformaciones en el conjunto de prueba o desordenar los pasos. La elección de preprocesamiento y modelos requiere ensayo y error, pero Pipeline facilita la experimentación sin excesiva complejidad.