Usando pipelines para ahorrar tiempo
Tags: python, machinelearning, datascience, tutorial
Published: 2020-09-15 20:27:18

Existe una forma muy sencilla de ahorrarnos tiempo en la etapa de pre-procesamiento y de ajuste de parámetros para nuestros modelos de aprendizaje automático. 
---
> Nota: Por lo que entiendo, en español se le llama tubería, pero como no me siento cómodo hablando de plomería, la llamaré con su nombre en ingles; pipeline.

Ahora sin más, comenzemos:

Lo primero es importar el objeto `pipeline` de Scikit-Learn:
```python
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import MinMaxScaler
from sklearn.svm import SVC

pipe = Pipeline([('scaler', MinMaxScaler()), ('svm', SVC())])
```
Aquí hemos creado dos pasos: el primero, llamado "scaler", es una instancia de `MinMaxScaler`, y el segundo, llamado "svm", es una instancia de `SVC`. Ahora, podemos entrenar nuestra pipeline, como cualquier otro estimador de Scikit-Learn:
```python
pipe.fit(X_train, y_train)
```
---

# Usando pipelines con Grid Search
Primero, creamos una *grilla de parámetros* (parameter grid):
```python
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 esta grilla, usamos `GridSearchCV` como siempre:
```python
grid = GridSearchCV(pipe, param_grid=param_grid, cv=5) # we call the pipeline as an attribute
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_))
```

> Código completo en este [link](https://gist.github.com/ruizleandro/c2c3a0d768bdfd26f62740535e98111c).

---
## Una mejor froma de hacer pipelines

Usando `make_pipeline` de Scikit-Learn, es mucho más sencillo crear una pipeline:
```python
from sklearn.pipeline import make_pipeline
# sintaxis estandar
pipe_long = Pipeline([("scaler", MinMaxScaler()), ("svm", SVC(C=100))])
# sintaxis abreviada
pipe_short = make_pipeline(MinMaxScaler(), SVC(C=100))
```

---

## Accediendo a los atributos de los pasos

En algún momento querrás inspeccionar los atributos de uno de los pasos de la pipeline. Por ejemplo, los coeficientes de un modelo lineal o los componentes extraidos por PCA (Principal Component Analysis). La forma mas facil de acceder a estos pasos en una pipeline es a través del atributo `named_steps`, que es un diccionario con los nombres de los atributos como estimadores:
```python
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))

pipe.fit(cancer.data)
# extrae los primeros dos componentes principales del paso de "pca"
components = pipe.named_steps['pca'].components_

print("components.shape: {}".format(components.shape))
```
---

## Accediendo a atributos en una pipeline con Grid Search

Vamos a realizar una grid search en un clasificador de regresión lineal, usando una pipeline y un `StandardScaler` para escalar los datos antes de pasarlos al clasificador.

```python
from sklearn.linear_model import LogisticRegression

pipe = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))
```

El parametro de la regularización para ajustar en regresión logistica es el parámetro `c`:

```python
param_grid = {'logisticregression__C': [0.01, 0.1, 1, 10, 100]}
```

> Recuerda: La sintaxis para definir una cuadrícula de parámetros para una pipeline es especificar para cada parámetro el nombre del paso, seguido por `__` (doble guion bajo), seguido por el nombre del parámetro. Por ejemplo, para acceder al parámetro `gamma` de un modelo `svm`, llamamos a `svm__gamma`, para el parametro `C` de un modelo `LogisticRegression`, llamamos a `logisticregression__C`.

Divide el dataset y entrena el modelo:
```python
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)
```

¿Así que cómo accedemos a los coeficientes del mejor modelo de regresión logistica que fue encontrado por `GridSearchCV`?

```python
print('Best estimator:\n{}'.format(grid.best_estimator_))
```

En este caso, el `best_estimator_` es una pipeline con dos pasos, `StandardScaler` y `LogisticRegression`. Para acceder al paso `LogisticRegression`, podemos usar el atributo `named_steps` de la pipeline:

```python
print("Logistic regression step:\n".format(grid.best_estimator_.named_steps['logisticregression']))
```

Ahora que ya tenemos la regresión logística entrenada, podemos acceder a los coeficientes asociados a cada atributo de entrada:

```python
print("Logistic regression coefficients:\n{}".format(grid.best_estimator_.named_steps['logisticregression'].coef_)) 
```

---

![GitHub Button](https://dev-to-uploads.s3.amazonaws.com/i/sexlw2wpl24igzp1gdnc.png)
<figure>
  <figcaption>[Link a GitHub](https://github.com/ruizleandro/Usando-Pipelines)</figcaption>
</figure>

---

## Obteniendo pasos de pre-procesamiento con Grid-Search

Usando pipelines, podemos encapsular todos los pasos de pre-procesamiento en nuestro proceso de aprendizaje automático es un solo estimador de Scikit-Learn. Otro beneficio de hacer esto es que podemos ajustar los parámetros del pre-procesamiento usando el resultado de una tarea supervisada como regresión o clasificación. 

```python
from sklearn.datasets import load_boston
boston = load_boston()
from sklearn.linear_model import Ridge

X_train, X_test, y_train, y_test = train_test_split(
    boston.data, boston.target, random_state=0)

from sklearn.preprocessing import PolynomialFeatures
pipe = make_pipeline(StandardScaler(), PolynomialFeatures(), Ridge())
```

¿Cómo sabemos que grados de polinomios elegir, o si elegir polinomios o interacciones? Idealmente, queremos seleccionar el parámetro de grado en función del resultado de la clasificación. Usando nuestra pipeline, podemos buscar sobre el parámetro de grado junto con el parámetro alpha o Ridge. 
Para hacer esto, definimos una `param_grid` que contiene ambos, pre-fijados apropiadamente por el nombre de los pasos:

```python
param_grid = {'polynomialfeatures__degree': [1, 2, 3],
              'ridge__alpha': [0.001, 0.01, 0.1, 1, 10, 100]}
```

Ahora podemos correr de nuevo la grilla de parametros:


```python
grid = GridSearchCV(pipe, param_grid=param_grid, cv=5, n_jobs=-1)
grid.fit(X_train, y_train)
```

Ahora podemos visualizar los resultados de la *validacion cruzada*(cross-validation) usando un heat-map:

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

---

## Usando Grid-Search para saber que modelo usar

Aquí dejo un ejemplo comparando un `RandomForestClassifier` y un `SVC`. Sabemos que el `SVC` quizás necesite datos escalados, así que tambien buscamos sobre usar `StandardScaler` o no pre-procesar los datos. Para el `RandomForestClassifier`, ya sabemos que no necesita ningun tipo de pre-procesamiento.
Comenzamos definiendo la pipeline. Aquí, nombramos los pasos explicitamente. Queremos dos pasos, uno de pre-procesamiento y despues el clasificador. Podemos instanciar esto usando `SVC` y `StandardScaler`:

```python
pipe = Pipeline([('preprocessing', StandardScaler()), ('classifier', SVC())])
```

Ahora podemos definir la grilla de parámetros para realizar la busqueda. Queremos que el clasificador sea un `RandomForestClassifier` o `SVC`. Como tienen diferentes parámetros a ajustar, y necesitan diferentes pre-procesamientos, podemos hacer una lista de grillas de búsqueda: 

```python
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 instanciamos y corremos la grilla como siempre:

```python
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)))
```

Y de esta sencilla manera, obtenemos el mejor modelo con sus mejores parámetros incluidos. 

# Conclusión

Crear pipelines es una gran manera de ahorrar mucho tiempo en nuestros proyectos. Espero que este articulo les haya servido, y si llegaste hasta aquí, muchas gracias. ¡Hasta la proxima!