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 conGridSearchCV
para optimizar todos los parámetros a la vez. Un ejemplo muestra que el uso deMinMaxScaler
mejora significativamente el desempeño de unSVM
en el conjunto de datoscancer
.
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
usandoGridSearchCV
, 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 deSVC
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()

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 conPipeline
, 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 unaSVM
después de escalar los datos conMinMaxScaler
(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 descikit-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.
Pipeline(steps=[('scaler', MinMaxScaler()), ('svm', SVC())])
MinMaxScaler()
SVC()
Aquí,
pipe.fit
primero llama afit
en el primer paso (el escalador), luego transforma los datos de entrenamiento usando el escalador, y finalmente ajusta laSVM
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 conscaler
y luego aplicaSVM
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 encross_val_score
oGridSearchCV
.
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 unGridSearchCV
con la tubería y la red. Al especificar los parámetros, debemos indicar el paso al que pertenecen. En este caso, los parámetrosC
ygamma
corresponden aSVC
, 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 utilizarGridSearchCV
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()

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
ey
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 lask
mejores características según una función de puntuación(X, y)
, reteniendo aquellas con mayor relación cony
. Si se usachi2
, se evalúa la dependencia entre cada característica ey
: valores bajos indican independencia, valores altos sugieren relación no aleatoria. Por defecto,SelectKBest
empleaf_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
ofit_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 regresorRidge
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
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 truncamientoPCA
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 quePipeline
automáticamente ajusta el modelo sin caer endata leakage
. Asiganmosn_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 espectroPCA
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",
);

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étodotransform
para encadenar su salida con el siguiente paso.Durante
Pipeline.fit
, cada paso aplicafit
ytransform
, excepto el último, que solo usafit
. Internamente, la tubería ejecuta los métodos en secuencia, conpipeline.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 apredict
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 unclassifier
(llamadoClassifier
)

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étodotransform
, podríamos llamar atransform
en elpipeline
para obtener el resultado dePCA.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 demake_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
ypipe_short
hacen exactamente lo mismo, peropipe_short
tiene pasos que fueron nombrados automáticamente. Podemos ver los nombres de los pasos mirando el atributosteps
print("Pipeline steps:\n{}".format(pipe_short.steps))
Pipeline steps:
[('minmaxscaler', MinMaxScaler()), ('svc', SVC(C=100))]
Los pasos se denominan
minmaxscaler
ysvc
. 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 segundostandardscaler-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 elpipeline
definido anteriormente, al conjunto de datoscancer
y extrae las dos primeros componentes principales del pasopca
.
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 clasificadorLogisticRegression
en el conjunto de datoscancer
, utilizandoPipeline
. UsamosStandardScaler
, para escalar los datos antes de pasarlos al clasificadorLogisticRegresión
. Primero creamos una instancia de nuestropipeline
usando la funciónmake_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ámetroC
. Utilizamos una red logarítmica para este parámetro, buscando entre 0.01 y 100. Como utilizamos la funciónmake_pipeline
, el nombre del pasoLogisticRegression
en elpipeline
es el nombre de la clase en minúsculas,logisticregression
. Para afinar el parámetroC
, tenemos que especificar una red de parámetros paralogisticregression__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.
GridSearchCV(cv=5, estimator=Pipeline(steps=[('standardscaler', StandardScaler()), ('logisticregression', LogisticRegression())]), param_grid={'logisticregression__C': [0.01, 0.1, 1, 10, 100]})
Pipeline(steps=[('standardscaler', StandardScaler()), ('logisticregression', LogisticRegression(C=1))])
StandardScaler()
LogisticRegression(C=1)
Entonces, ¿cómo accedemos a los coeficientes del mejor modelo
LogisticRegression
que fue encontrado porGridSearchCV
? De los capítulos anteriores sabemos que el mejor modelo encontrado porGridSearchCV
, entrenado con todos los datos de entrenamiento, se almacena engrid.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 unpipeline
con dos pasos,standardcaler
ylogisticregression
. Para acceder al paso delogisticregression
, podemos utilizar el atributonamed_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 descikit-learn
. Otro beneficio de hacer esto es que ahora podemosajustar 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 datasetboston
antes de aplicar elregresor 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 conalpha
deRidge
usando unparam_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.
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]})
Pipeline(steps=[('standardscaler', StandardScaler()), ('polynomialfeatures', PolynomialFeatures()), ('ridge', Ridge(alpha=10))])
StandardScaler()
PolynomialFeatures()
Ridge(alpha=10)
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();

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 conPipeline
para optimizar no solo los hiperparámetros, sino también los pasos del preprocesamiento, como elegir entreStandardScaler
oMinMaxScaler
. Esto aumenta el espacio de búsqueda y debe usarse con precaución.Por ejemplo, al comparar
RandomForestClassifier
ySVC
en el conjuntoiris
, sabemos queSVC
puede requerir escalado, por lo que buscamos si usarStandardScaler
o prescindir de preprocesamiento.RandomForestClassifier
, en cambio, no lo necesita. Definimos unpipeline
con dos pasos: preprocesamiento y clasificación, usandoSVC
conStandardScaler
.
pipe = Pipeline([('preprocessing', StandardScaler()), ('classifier', SVC())])
Definimos
parameter_grid
para buscar entreRandomForestClassifier
ySVC
, 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 (comoRandomForest
), configuramos ese paso comoNone
.
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 datoscancer
. Nótes que solo es necesario inicializarpipe
con cualquiera de los dos estimadores, luegoGridSearchCV
se encargará de seleccionar el mejor modelo, basado en elparam_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 preprocesamientoStandardScaler
,C=10
ygamma=0.01
dio el mejor resultado de clasificación, en comparación del elRandomForestClassifier
.
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 descikit-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, peroPipeline
facilita la experimentación sin excesiva complejidad.