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()

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:

  1. 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.

  2. 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))