6.1. Pipelines y estimadores compuestos¶
Los transformadores generalmente se combinan con clasificadores, regresores u otros estimadores para construir un estimador compuesto. La herramienta más común es un Pipeline. El pipeline se utiliza a menudo en combinación con FeatureUnion que concatena la salida de los transformadores en un espacio compuesto de características. TransformedTargetRegressor trata de transformar el target (i.e. log-transform y). En contraste, los pipelines sólo transforman los datos observados (X).
6.1.1. Pipeline: estimadores encadenados¶
Pipeline
puede ser usado para encadenar múltiples estimadores en uno. Esto es útil ya que a menudo hay una secuencia fija de pasos en el procesamiento de los datos, por ejemplo selección de características, normalización y clasificación. Pipeline
sirve varios propósitos aquí:
- Conveniencia y encapsulación
Solo tienes que llamar a fit y predict una vez en tus datos para ajustar toda una secuencia de estimadores.
- Selección conjunta de parámetros
Puedes utilizar búsqueda en cuadrícula sobre los parámetros de todos los estimadores en el pipeline a la vez.
- Seguridad
Los pipelines ayudan a evitar la fuga de estadísticas de los datos de prueba en el modelo entrenado en la validación cruzada, asegurando que se utilizan las mismas muestras para entrenar los transformadores y los predictores.
Todos los estimadores en un pipeline, excepto el último, deben ser transformadores (es decir, debe tener un método transform). El último estimador puede ser cualquier tipo (transformador, clasificador, etc.).
6.1.1.1. Uso¶
6.1.1.1.1. Construcción¶
El Pipeline
está construido usando una lista de pares (key, value)
, donde la key
es una cadena que contiene el nombre que quieres dar este paso y value
es un objeto de estimación:
>>> from sklearn.pipeline import Pipeline
>>> from sklearn.svm import SVC
>>> from sklearn.decomposition import PCA
>>> estimators = [('reduce_dim', PCA()), ('clf', SVC())]
>>> pipe = Pipeline(estimators)
>>> pipe
Pipeline(steps=[('reduce_dim', PCA()), ('clf', SVC())])
La función de utilidad make_pipeline
es un atajo para construir pipelines; toma un número variable de estimadores y devuelve un pipeline, rellenando los nombres automáticamente:
>>> from sklearn.pipeline import make_pipeline
>>> from sklearn.naive_bayes import MultinomialNB
>>> from sklearn.preprocessing import Binarizer
>>> make_pipeline(Binarizer(), MultinomialNB())
Pipeline(steps=[('binarizer', Binarizer()), ('multinomialnb', MultinomialNB())])
6.1.1.1.2. Acceso a los steps (pasos)¶
Los estimadores de un pipeline se almacenan como una lista en el atributo steps
(pasos), pero se puede acceder por índice o nombre indexando (con [idx]
) el Pipeline:
>>> pipe.steps[0]
('reduce_dim', PCA())
>>> pipe[0]
PCA()
>>> pipe['reduce_dim']
PCA()
El atributo named_steps
del pipeline permite acceder a los pasos por su nombre con una autocompletado con tabulación en entornos interactivos:
>>> pipe.named_steps.reduce_dim is pipe['reduce_dim']
True
También se puede extraer una sub-línea utilizando la notación de corte comúnmente utilizada para secuencias de Python como listas o cadenas (aunque sólo se permite un paso de 1). Esto es conveniente para realizar sólo algunas de las transformaciones (o su inversa):
>>> pipe[:1]
Pipeline(steps=[('reduce_dim', PCA())])
>>> pipe[-1:]
Pipeline(steps=[('clf', SVC())])
6.1.1.1.3. Parámetros anidados¶
Puedes acceder a los parámetros de los estimadores en el pipeline usando la sintaxis <estimator>__<parameter>`
:
>>> pipe.set_params(clf__C=10)
Pipeline(steps=[('reduce_dim', PCA()), ('clf', SVC(C=10))])
Esto es particularmente importante para realizar búsquedas en cuadrículas:
>>> from sklearn.model_selection import GridSearchCV
>>> param_grid = dict(reduce_dim__n_components=[2, 5, 10],
... clf__C=[0.1, 10, 100])
>>> grid_search = GridSearchCV(pipe, param_grid=param_grid)
Los pasos individuales también pueden ser reemplazados como parámetros, y los pasos no finales pueden ser ignorados estableciéndolos a 'passthrough'
:
>>> from sklearn.linear_model import LogisticRegression
>>> param_grid = dict(reduce_dim=['passthrough', PCA(5), PCA(10)],
... clf=[SVC(), LogisticRegression()],
... clf__C=[0.1, 10, 100])
>>> grid_search = GridSearchCV(pipe, param_grid=param_grid)
Los estimadores del pipeline pueden ser recuperados por el índice:
>>> pipe[0]
PCA()
o por el nombre:
>>> pipe['reduce_dim']
PCA()
Ejemplos:
Ejemplo de pipeline para la extracción y evaluación de características de texto
Pipelining: encadenamiento de un PCA y una regresión logística
Aproximación explícita del mapeo de características para los núcleos RBF
SVM-Anova: SVM con selección de características univariantes
Selección de la reducción de la dimensionalidad con Pipeline y GridSearchCV
6.1.1.2. Notas¶
Llamar a fit
en el pipeline es lo mismo que llamar a fit
en cada estimador a su vez, transform
la entrada y pasarla al siguiente paso. El pipeline tiene todos los métodos que tiene el último estimador del pipeline, es decir, si el último estimador es un clasificador, el Pipeline
puede ser utilizado como un clasificador. Si el último estimador es un transformador, también lo es la tubería.
6.1.1.3. Transformadores de caché: evitar cálculos repetidos¶
El ajuste de los transformadores puede ser costoso desde el punto de vista computacional. Con su parámetro memory
ya establecido, Pipeline
almacenará en caché cada transformador después de llamar a fit
. Esta característica se utiliza para evitar el cálculo de los transformadores de ajuste dentro de un pipeline si los parámetros y los datos de entrada son idénticos. Un ejemplo típico es el caso de una búsqueda en cuadrícula en la que los transformadores pueden ajustarse sólo una vez y reutilizarse para cada configuración.
El parámetro memory
es necesario para almacenar en caché los transformadores. memory
puede ser una cadena que contiene el directorio donde guardar los transformadores en caché o un joblib.Memory objeto:
>>> from tempfile import mkdtemp
>>> from shutil import rmtree
>>> from sklearn.decomposition import PCA
>>> from sklearn.svm import SVC
>>> from sklearn.pipeline import Pipeline
>>> estimators = [('reduce_dim', PCA()), ('clf', SVC())]
>>> cachedir = mkdtemp()
>>> pipe = Pipeline(estimators, memory=cachedir)
>>> pipe
Pipeline(memory=...,
steps=[('reduce_dim', PCA()), ('clf', SVC())])
>>> # Clear the cache directory when you don't need it anymore
>>> rmtree(cachedir)
Advertencia
Efecto colateral de los transformadores de caché
Usando un Pipeline
sin caché activado, es posible inspeccionar la instancia original como en:
>>> from sklearn.datasets import load_digits
>>> X_digits, y_digits = load_digits(return_X_y=True)
>>> pca1 = PCA()
>>> svm1 = SVC()
>>> pipe = Pipeline([('reduce_dim', pca1), ('clf', svm1)])
>>> pipe.fit(X_digits, y_digits)
Pipeline(steps=[('reduce_dim', PCA()), ('clf', SVC())])
>>> # The pca instance can be inspected directly
>>> print(pca1.components_)
[[-1.77484909e-19 ... 4.07058917e-18]]
La activación del almacenamiento en caché desencadena un clon de los transformadores antes del ajuste. Por lo tanto, la instancia del transformador del pipeline no puede ser inspeccionada directamente. En el siguiente ejemplo, el acceso a la instancia de PCA
pca2
generará un AttributeError
ya que pca2
será un transformador no ajustado. En su lugar, utiliza el atributo named_steps
para inspeccionar los estimadores dentro del pipeline:
>>> cachedir = mkdtemp()
>>> pca2 = PCA()
>>> svm2 = SVC()
>>> cached_pipe = Pipeline([('reduce_dim', pca2), ('clf', svm2)],
... memory=cachedir)
>>> cached_pipe.fit(X_digits, y_digits)
Pipeline(memory=...,
steps=[('reduce_dim', PCA()), ('clf', SVC())])
>>> print(cached_pipe.named_steps['reduce_dim'].components_)
[[-1.77484909e-19 ... 4.07058917e-18]]
>>> # Remove the cache directory
>>> rmtree(cachedir)
6.1.2. Transformación del objetivo en regresión¶
TransformedTargetRegressor
transforma los objetivos y
antes de ajustar un modelo de regresión. Las predicciones se devuelven al espacio original mediante una transformación inversa. Toma como argumento el regresor que se utilizará para la predicción, y el transformador que se aplicará a la variable objetivo:
>>> import numpy as np
>>> from sklearn.datasets import fetch_california_housing
>>> from sklearn.compose import TransformedTargetRegressor
>>> from sklearn.preprocessing import QuantileTransformer
>>> from sklearn.linear_model import LinearRegression
>>> from sklearn.model_selection import train_test_split
>>> X, y = fetch_california_housing(return_X_y=True)
>>> X, y = X[:2000, :], y[:2000] # select a subset of data
>>> transformer = QuantileTransformer(output_distribution='normal')
>>> regressor = LinearRegression()
>>> regr = TransformedTargetRegressor(regressor=regressor,
... transformer=transformer)
>>> X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
>>> regr.fit(X_train, y_train)
TransformedTargetRegressor(...)
>>> print('R2 score: {0:.2f}'.format(regr.score(X_test, y_test)))
R2 score: 0.61
>>> raw_target_regr = LinearRegression().fit(X_train, y_train)
>>> print('R2 score: {0:.2f}'.format(raw_target_regr.score(X_test, y_test)))
R2 score: 0.59
Para transformaciones simples, en lugar de un objeto Transformador, puedes pasar un par de funciones, para definir la transformación y su mapeo inverso:
>>> def func(x):
... return np.log(x)
>>> def inverse_func(x):
... return np.exp(x)
Posteriormente, el objeto es creado como:
>>> regr = TransformedTargetRegressor(regressor=regressor,
... func=func,
... inverse_func=inverse_func)
>>> regr.fit(X_train, y_train)
TransformedTargetRegressor(...)
>>> print('R2 score: {0:.2f}'.format(regr.score(X_test, y_test)))
R2 score: 0.51
Por defecto, las funciones proporcionadas se comprueban en cada ajuste para ser la inversa de cada una. Sin embargo, es posible evitar esta comprobación estableciendo check_inverse
a False
:
>>> def inverse_func(x):
... return x
>>> regr = TransformedTargetRegressor(regressor=regressor,
... func=func,
... inverse_func=inverse_func,
... check_inverse=False)
>>> regr.fit(X_train, y_train)
TransformedTargetRegressor(...)
>>> print('R2 score: {0:.2f}'.format(regr.score(X_test, y_test)))
R2 score: -1.57
Nota
La transformación puede ser activada estableciendo transformer
o el par de funciones func
y inverse_func
. Sin embargo, ambas opciones provocarán un error.
6.1.3. FeatureUnion: espacios de características compuestas¶
FeatureUnion
combina varios objetos transformadores en un nuevo transformador que combina su salida. Una FeatureUnion
toma una lista de objetos transformadores. Durante el ajuste, cada uno de ellos se ajusta a los datos de forma independiente. Los transformadores se aplican en paralelo, y las matrices de características que producen se concatenan una al lado de la otra en una matriz mayor.
Cuando quieras aplicar diferentes transformaciones a cada campo de los datos, consulta la clase relacionada ColumnTransformer
(ver guía de usuario).
FeatureUnion
tiene los mismos propósitos que Pipeline
- conveniencia y estimación y validación de parámetros conjuntos.
FeatureUnion
y Pipeline
pueden ser combinados para crear modelos complejos.
(Un FeatureUnion
no tiene forma de comprobar si dos transformadores pueden producir características idénticas. Sólo produce una unión cuando los conjuntos de características son disjuntos, y asegurarse de que lo son es responsabilidad de quien invoca dicha clase.)
6.1.3.1. Uso¶
Una FeatureUnion
se construye utilizando una lista de pares (key, value)
, donde key
es el nombre que se quiere dar a una determinada transformación (es una cadena arbitraria; sólo sirve como identificador) y valor
es un objeto estimador:
>>> from sklearn.pipeline import FeatureUnion
>>> from sklearn.decomposition import PCA
>>> from sklearn.decomposition import KernelPCA
>>> estimators = [('linear_pca', PCA()), ('kernel_pca', KernelPCA())]
>>> combined = FeatureUnion(estimators)
>>> combined
FeatureUnion(transformer_list=[('linear_pca', PCA()),
('kernel_pca', KernelPCA())])
Al igual que las pipelines, las uniones de características tienen un constructor abreviado llamado make_union
que no requiere nombrar explícitamente los componentes.
Como Pipeline
, los pasos individuales pueden ser reemplazados usando set_params
, e ignorados con la configuración de 'drop'
:
>>> combined.set_params(kernel_pca='drop')
FeatureUnion(transformer_list=[('linear_pca', PCA()),
('kernel_pca', 'drop')])
6.1.4. ColumnTransformer para datos heterogéneos¶
Muchos conjuntos de datos contienen características de diferentes tipos, por ejemplo, texto, flotantes y fechas, donde cada tipo de característica requiere pasos separados de preprocesamiento o extracción de características. A menudo es más fácil preprocesar los datos antes de aplicar los métodos de scikit-learn, por ejemplo, utilizando pandas. Procesar los datos antes de pasarlos a scikit-learn podría resultar problemático por alguna de las siguientes razones:
La incorporación de las estadísticas de los datos de prueba en los preprocesadores hace que las puntuaciones de la validación cruzada no sean fiables (lo que se conoce como fuga de datos), por ejemplo en el caso de los escaladores o de la imputación de valores faltantes.
Es posible que desees incluir los parámetros de los preprocesadores en una búsqueda de parámetros.
El ColumnTransformer
ayuda a realizar diferentes transformaciones para diferentes columnas de los datos, dentro de un Pipeline
que está a salvo de fugas de datos y que puede ser parametrizado. ColumnTransformer
trabaja en arreglos, matrices dispersas y pandas DataFrames.
Para cada columna, se puede aplicar una transformación diferente, como el preprocesamiento o un método de extracción de características específicas:
>>> import pandas as pd
>>> X = pd.DataFrame(
... {'city': ['London', 'London', 'Paris', 'Sallisaw'],
... 'title': ["His Last Bow", "How Watson Learned the Trick",
... "A Moveable Feast", "The Grapes of Wrath"],
... 'expert_rating': [5, 3, 4, 5],
... 'user_rating': [4, 5, 4, 3]})
Para estos datos, podríamos querer codificar la columna ciudad
como una variable categórica utilizando OneHotEncoder
pero aplicar un CountVectorizer
a la columna título
. Como podemos utilizar varios métodos de extracción de características en la misma columna, damos a cada transformador un nombre único, por ejemplo city_category
y title_bow
. Por defecto, el resto de columnas de valoración se ignoran (remainder='drop'
):
>>> from sklearn.compose import ColumnTransformer
>>> from sklearn.feature_extraction.text import CountVectorizer
>>> from sklearn.preprocessing import OneHotEncoder
>>> column_trans = ColumnTransformer(
... [('city_category', OneHotEncoder(dtype='int'),['city']),
... ('title_bow', CountVectorizer(), 'title')],
... remainder='drop')
>>> column_trans.fit(X)
ColumnTransformer(transformers=[('city_category', OneHotEncoder(dtype='int'),
['city']),
('title_bow', CountVectorizer(), 'title')])
>>> column_trans.get_feature_names()
['city_category__x0_London', 'city_category__x0_Paris', 'city_category__x0_Sallisaw',
'title_bow__bow', 'title_bow__feast', 'title_bow__grapes', 'title_bow__his',
'title_bow__how', 'title_bow__last', 'title_bow__learned', 'title_bow__moveable',
'title_bow__of', 'title_bow__the', 'title_bow__trick', 'title_bow__watson',
'title_bow__wrath']
>>> column_trans.transform(X).toarray()
array([[1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0],
[0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1]]...)
En el ejemplo anterior, el transformador CountVectorizer
espera un arreglo 1D como entrada y por lo tanto las columnas fueron especificadas como una cadena ('title
). Sin embargo, OneHotEncoder
, como la mayoría de los otros transformadores, espera datos 2D, por lo que en ese caso hay que especificar la columna como una lista de cadenas (['city']
).
Aparte de un escalar o una lista de un solo elemento, la selección de columnas puede especificarse como una lista de múltiples elementos, un arreglo de enteros, un intervalo, una máscara booleana, o con un make_column_selector
. El make_column_selector
se utiliza para seleccionar columnas basándose en el tipo de datos o en el nombre de la columna:
>>> from sklearn.preprocessing import StandardScaler
>>> from sklearn.compose import make_column_selector
>>> ct = ColumnTransformer([
... ('scale', StandardScaler(),
... make_column_selector(dtype_include=np.number)),
... ('onehot',
... OneHotEncoder(),
... make_column_selector(pattern='city', dtype_include=object))])
>>> ct.fit_transform(X)
array([[ 0.904..., 0. , 1. , 0. , 0. ],
[-1.507..., 1.414..., 1. , 0. , 0. ],
[-0.301..., 0. , 0. , 1. , 0. ],
[ 0.904..., -1.414..., 0. , 0. , 1. ]])
Las cadenas pueden referenciar columnas si la entrada es de tipo DataFrame, los enteros siempre se interpretan como las columnas posicionales.
Podemos mantener las columnas de valoración restantes estableciendo remainder='passthrough'
. Los valores se añaden al final de la transformación:
>>> column_trans = ColumnTransformer(
... [('city_category', OneHotEncoder(dtype='int'),['city']),
... ('title_bow', CountVectorizer(), 'title')],
... remainder='passthrough')
>>> column_trans.fit_transform(X)
array([[1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 5, 4],
[1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 3, 5],
[0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 4],
[0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 5, 3]]...)
El parámetro remainder
puede establecerse en un estimador para transformar las columnas de valoración restantes. Los valores transformados se añaden al final de la transformación:
>>> from sklearn.preprocessing import MinMaxScaler
>>> column_trans = ColumnTransformer(
... [('city_category', OneHotEncoder(), ['city']),
... ('title_bow', CountVectorizer(), 'title')],
... remainder=MinMaxScaler())
>>> column_trans.fit_transform(X)[:, -2:]
array([[1. , 0.5],
[0. , 1. ],
[0.5, 0.5],
[1. , 0. ]])
La función make_column_transformer
está disponible para crear más fácilmente un objeto de tipo ColumnTransformer
. Específicamente, los nombres serán generados automáticamente. El equivalente para el ejemplo anterior sería:
>>> from sklearn.compose import make_column_transformer
>>> column_trans = make_column_transformer(
... (OneHotEncoder(), ['city']),
... (CountVectorizer(), 'title'),
... remainder=MinMaxScaler())
>>> column_trans
ColumnTransformer(remainder=MinMaxScaler(),
transformers=[('onehotencoder', OneHotEncoder(), ['city']),
('countvectorizer', CountVectorizer(),
'title')])
6.1.5. Visualización de estimadores compuestos¶
Los estimadores pueden ser mostrados con una representación HTML cuando se muestran en un cuaderno de Jupyter. Esto puede ser útil para diagnosticar o visualizar un pipeline con muchos estimadores. Esta visualización se activa estableciendo la opción display
en set_config
:
>>> from sklearn import set_config
>>> set_config(display='diagram')
>>> # diplays HTML representation in a jupyter context
>>> column_trans
Un ejemplo de la salida HTML puede verse en la sección Representación HTML del pipeline de Transformador de columna con tipos mixtos. Como alternativa, el HTML se puede escribir en un archivo utilizando estimator_html_repr
:
>>> from sklearn.utils import estimator_html_repr
>>> with open('my_estimator.html', 'w') as f:
... f.write(estimator_html_repr(clf))