In [ ]:
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt

Encadenando los estimadores con tuberías (pipelines)

En esta sección, estudiamos cómo encadenar los estimadores.

Un ejemplo sencillo: extracción de características y selección antes de aplicar un estimador

Extracción de características: vectorizer

Para algunos tipos de datos, por ejemplo, para datos de tipo texto, debemos aplicar un paso de extracción de características para convertirlos en características numéricas. Para demostrarlo, vamos a cargar el dataset de spam que utilizamos previamente.


In [ ]:
import os

with open(os.path.join("datasets", "smsspam", "SMSSpamCollection")) as f:
    lines = [line.strip().split("\t") for line in f.readlines()]
text = [x[1] for x in lines]
y = [x[0] == "ham" for x in lines]

In [ ]:
from sklearn.model_selection import train_test_split

text_train, text_test, y_train, y_test = train_test_split(text, y)

Hemos aplicado extracción de características manualmente del siguiente modo:


In [ ]:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression

vectorizer = TfidfVectorizer()
vectorizer.fit(text_train)

X_train = vectorizer.transform(text_train)
X_test = vectorizer.transform(text_test)

clf = LogisticRegression()
clf.fit(X_train, y_train)

clf.score(X_test, y_test)

El aprender una transformación y luego aplicarla a los datos de test es muy común en aprendizaje automático. Por tanto, scikit-learn tiene una forma cómoda de hacer esto, llamada tuberías (pipelines):


In [ ]:
from sklearn.pipeline import make_pipeline

pipeline = make_pipeline(TfidfVectorizer(), LogisticRegression())
pipeline.fit(text_train, y_train)
pipeline.score(text_test, y_test)

Como puedes comprobar, esto hace el código mucho más corto y fácil de manejar. Realmente, está ejecutándose exactamente lo mismo. Cuando llamamos a fit en la tubería, se llamará a cada método de forma sucesiva.

Después del primer ajuste, se utilizará el método transform para crear una nueva representación. Ésta se utilizará como entrada en el método fit del siguiente paso, y así sucesivamente. En el último paso, no se llamará a transform.

Si llamamos a score, solo se llamará a transform en cada paso, generando lo que serían los datos de test. Al final aplicaremos score sobre la representación final obtenida. Lo mismo pasa para predict.

Construir tuberías no solo nos permite simplificar el código, también es muy importante para el ajuste de parámetros. Imaginemos que queremos ajustar el parámetro $C$ de la regresión logística anterior.

Lo podríamos hacer de la siguiente forma (incorrecta):


In [ ]:
# Este código ilustra un error común, ¡no lo utilices!
from sklearn.model_selection import GridSearchCV

vectorizer = TfidfVectorizer()
vectorizer.fit(text_train)

X_train = vectorizer.transform(text_train)
X_test = vectorizer.transform(text_test)

clf = LogisticRegression()
grid = GridSearchCV(clf, param_grid={'C': [.01, .1, 1, 10, 100]}, cv=5)
grid.fit(X_train, y_train)
grid.score(X_test, y_test)

2.1.2 ¿Qué hemos hecho mal?

Hemos aplicado búsqueda grid con validación cruzada utilizando X_train. Sin embargo, cuando aplicamos TfidfVectorizer estamos considerando el conjunto X_train completo, no solo los folds de entrenamiento. Por tanto, podríamos estar usando conocimiento acerca de la frecuencia de las palabras en los folds de test. Esto se llama "contaminación" del conjunto de test y lleva a estimaciones demasiado optimistas del rendimiento de generalización o a parámetros seleccionados de forma incorrecta. Sin embargo, podemos arreglar esto con una tubería:


In [ ]:
from sklearn.model_selection import GridSearchCV

pipeline = make_pipeline(TfidfVectorizer(), 
                         LogisticRegression())

grid = GridSearchCV(pipeline,
                    param_grid={'logisticregression__C': [.01, .1, 1, 10, 100]}, cv=5)

grid.fit(text_train, y_train)
grid.score(text_test, y_test)

Observa que tenemos que indicar en que parte de la tubería aparece el parámetro $C$. Esto lo hacemos con la sintaxis especial __. El nombre que hay antes del __ es simplemente el nombre de la clase en minúscula, la parte que hay después de __ es el parámetro que queremos ajustar por búsqueda grid.

Otro beneficio de considerar tuberías es que podemos buscar sobre los propios parámetros de los algoritmos de extracción de características mediante el uso de GridSearchCV:


In [ ]:
#Este código puede tardar bastante tiempo
from sklearn.model_selection import GridSearchCV

pipeline = make_pipeline(TfidfVectorizer(), LogisticRegression())

params = {'logisticregression__C': [.1, 1, 10, 100],
          "tfidfvectorizer__ngram_range": [(1, 1), (1, 2), (2, 2)]}
grid = GridSearchCV(pipeline, param_grid=params, cv=5)
grid.fit(text_train, y_train)
print(grid.best_params_)
grid.score(text_test, y_test)
EJERCICIO:
  • Crea una tubería utilizando un ``StandardScaler`` y una regresión ``RidgeRegression`` y aplícala al dataset Boston housing (que se puede cargar con ``sklearn.datasets.load_boston``). Añade el transformador ``sklearn.preprocessing.PolynomialFeatures`` como una segunda fase de pre-procesamiento, y haz una búsqueda *grid* sobre el grado de los polinomios (prueba con grados 1, 2 y 3).

In [ ]: