Temas a tratar:
Este es el primer post de la serie dedicada a un truco o metodología (si así lo
quisieran llamar) de Sklearn: Pipelines. Una herramienta tremendamente útil de
la librería Sci-kit Learn (sklearn
) que me
gustaría haber conocido hace mucho antes. Para sacar el máximo provecho a estos
posts se sugiere que el lector previamente:
- Haya alguna vez, ajustado un modelo con
sklearn
y que especialmente le sea familiar los métodosfit
ytransform
de esta librería. - Haber utilizado
pandas
como herramienta de preprocesamiento y transformación de data, de manera similar a lo relatado en mi post anterior de preprocesamiento y entendimiento. - Entienda a que nos referimos con los conceptos de validación cruzada. Y, preferiblemente, haya alguna alguna vez utilizado el método GridSearchCV o RandomizedSearchCV
- Entienda como crear Clases en Python.
Introducción
El primer paso para extraer todo el potencial de Pipelines es entender las Transformaciones customizadas, objetivo principal de este post. Sin embargo, estas serán muy dificiles de entender, sin explicar el rol que cumplen las pipelines de sklearn, en las etapas que prácticamente todo proyecto de machine learning deben tener: Lectura, Procesamiento de variables, Ajuste del modelo y Evaluación de resultados.
En concreto, los pipelines nos permiten encapsular cada una de estas etapas (con excepción de la lectura de datos) en un solo gran proceso para así considerar el pipeline resultante como un gran estimador de sklearn. Dejame explicarte a que me refiero con esto.
Para ello me gustaría que imagines el siguiente escenario. Tienes a tu haber un
dataset con dos variables, ambas numéricas, una independiente, a la que
denominaremos x
y una dependiente, y
. Tu objetivo es encontrar un modelo,
donde solo mediante x
podamos predecir o estimar con buena precisión el valor
que tomará y
, en el caso, obviamente, que este último ya no lo tengamos. Ante
tal problema, la principal pregunta que nos gustaría resolver es ¿Qué
combinación de procesamiento y modelo estadístico, nos permitirá obtener el
mejor resultado posible?
Para resolver tal desafío, supongo que utilizarás una metodología muy similar a
lo que la gran mayoría de los que usuarios de Python harían. En específico
utilizarías pandas
para manipular o transformar variables y sklearn
para el
ajuste de modelos. Individualmente, las API de cada uno están pensados para que
cada tarea te demande el menor tiempo posible, reduciendo la cantidad de código
a escribir. Por lo que no sorprende, que ambas sean ampliamente las herramientas
más utilizadas para este desafío.
No obstante, me gustaría convencerte que nuestro problema no radica ahí. Al
contrario, radica principalmente en que tanto para la etapa de procesamiento y
modelos tenemos una serie de opciones posibles, los que de manera combinada, nos
entregan un gran abanico de opciones. Si esto no es suficiente para clarificar
mi idea, volvamos al ejemplo anterior: Imaginando que además de tomar x
en la
misma forma como venía originalmente, nos gustaría probar con diferentes
transformaciones: Específicamente, con $x^2$, $x^{-1}$, $log(x)$ , $e^x$ y
$\sqrt{x}$, esto para la etapa de procesamiento de variables. A su vez, para el
Ajuste del modelo, creemos que tanto Regresión lineal
como Lasso,
Ridge y Elastic
Net parecen
a priori como buenas opciones. Si, consideramos todas las combinaciones de
procesamiento vs ajuste como validas, nos encontramos que debiésemos repetir 20
(5 transformaciones x 4 modelos) veces el proceso combinado de transformación en
pandas
y luego estimación en sklearn
. Como correctamente supondrás, si
utilizas independientemente ambas librerías como la gran mayoría de los usuarios
haría, deberás entonces construir un laaaargo script, donde tendrás que dedicar
especial esfuerzo en organizar y resumir el resultado de cada modelo en forma
ordenada, para así tener cierto control sobre cada experimento (combinación
preprocesamiento-ajuste). Este tipo de situaciones, es justamente lo que las
sklearn pipelines
nos ayudan a solucionar.
En términos sencillos, por medio del uso de pipelines, buscamos modularizar las
etapas mencionadas anteriormente, con una una lógica estándar para luego
concatenarlas de manera secuencial. Esto, finalmente, nos permitirá hacer uso de
las funciones fit
y predict
– los verdaderos motores analíticos de
sklearn
– para el proceso completo y no solamente para el ajuste del modelo.
Como valor adicional, obtendremos un código más legible, donde cada paso del
proceso queda claramente explicitado y donde encontrar la combinación correcta
de estrategia de procesamiento + modelo, sera revelerá de forma más directa.
Custom Transformers
Así, hemos llegado al tema central de este post: Transformaciones de Dataset
Customizadas, elemento clave para poder traducir las transformaciones realizadas
en pandas
a una transformación que sklearn
la pueda entender. Sin este
proceso de traducción, sklearn
no podrá integrar el procesamiento customizado
para nuestro problema al Pipeline y obtener los beneficios anteriormente
mencionados. (Nota: Cabe mencionar, que no necesariamente se debe definir un
nuevo objeto de procesamiento. De hecho, sklearn
dispone de múltiples
funciones de procesamiento, que pueden resolver una gran variedad de problemas.
En efecto, este post busca ilustrar como podemos extender aún más esas
capacidades, generando una propia para ajustarse a nuestras necesidades
particulares).
De forma breve, las Transformaciones Customizadas
son un objeto que nosotros
definimos. La cual, para que pueda conversar con sklearn, debe contar como
mínimo con 3 funciones __init__
, fit
y transform
. Además de ello, por lo
general este objeto lo creamos a partir de un objeto de bajo nivel de
sklearn
: TransformerMixin
. Heredando desde este último, nos permitirá luego
hacer uso del método fit_transform
de manera gratuita. Más adelante explicaré
por que esto es conveniente. Antes de ello, lo mejor es comprender cada función
mencionada elementalmente:
__init__
: Para crear un nuevo objeto de transformación con los atributos que queremos que este posea.fit
: Obtiene los parámetros a partir de la data (cómo mediana, desviación estándar, media, etc), necesarios para poder transformar la data. En las ocasiones en que no es necesario estimar un parámetro a partir de la data (cómo en el caso de $x^2$, $x^{-1}$, $log(x)$ , $e^x$ y $\sqrt{x}$. Esta función debiese devolver los mismos datos de entrada.transform
: Es la función que transforma la data a partir de los parámetros estimados enfit
(o ninguno si es así el caso) y de la transformación específica que nosotros deseemos definir.
Código
A continuación, mostramos como creamos un objeto de transformación simple de
nuestro dataset. A este le llamaremos ModeImputer
. El cuál reemplazará, para
cada columna, las celdas que correspondan a un carácter que nosotros
determinaremos por la moda de cada columna. Cómo procederemos es más bien
simple, importamos las librerías, leemos la data, luego definimos ModeImputer
,
para finalmente ejemplificar como podría utilizarse.
# Importando librerías
import pandas as pd
from sklearn.base import TransformerMixin # el objeto de sklearn en que
# nos basaremos para transformar los datos.
from sklearn.model_selection import train_test_split # metodo de sklearn
# para separar nuestro dataset en entrenamiento y testing
import numpy as np
Leemos los datos, especificando los nombres de columnas. Al igual que en
preprocesamiento y
entendimiento, utilizaremos el mismo dataset de auto
disponible en UCI
Machine Learning Repository
p = './datos/auto.txt'
cols = ['symboling', 'norm_losses', 'make', 'fuel_type', 'aspiration',
'num_of_doors', 'body_style', 'drive_wheels',
'engine_location','wheel_base', 'length', 'width', 'height',
'curb-weight', 'engine_type', 'num_of_cylinders', 'engine_size',
'fuel_system', 'bore', 'stroke', 'compression_ratio', 'horsepower',
'peak_rpm', 'city_mpg', 'highway_mpg', 'price']
df = pd.read_csv(p, names= cols)
Finalmente definimos nuestro objeto de Transformación de Dataset Customizada.
class ModeImputer(TransformerMixin):
def __init__(self, missing_value = '?'):
self.missing_value = missing_value # Espeficamos el atributo con el que
# debe ser inicializado el objeto.
def fit(self, X, y=None):
# En la función fit es donde obtenemos los parametros que nos
# para luego hacer la imputación. En este caso será un diccionario
# de reemplazo.
# primero buscamos las columnas que contengan
#el cáracter especificado.
cols = X.apply(lambda x: x.str.contains('\\' + self.missing_value).any())
# luego reemplazamos las celdas con ese caracter con np.nan
df_tmp = X.loc[:,cols].replace({self.missing_value: np.nan})
# finalmente, para esas columnas obtenemos la moda para cada una
# esto lo guardamos en un diccionario dentro del mismo objeto
# ModeImputer
self.replace_with = dict(zip(df_tmp.columns, df_tmp.mode(axis = 0).values[0]))
return self
def transform(self, X):
# Transform lo único que hace es tomar el diccionario computado y
# devolver el dataframe de vars independientes reemplazado.
replace_with = {k:{self.missing_value: v} for k, v in self.replace_with.items()}
return X.replace(to_replace = replace_with)
Con lo anterior, hemos definido todo lo necesario para poder crear un objeto de
transformación de dataset compatible con sklearn. Como punto importante, es el
lugar en la clase en donde obtenemos los parámetros de la data – el cual en
este caso es el diccionario de remplazo self.replace_with
. Donde se lo
escribimos cómo parte de la función fit
y no de transform
.
Así, la transformación de los datos de tanto entrenamiento como testing para estos parámetros, solo se realizará con los parámetros calculados a partir de los datos de entrenamiento y no los de testing. Este razonamiento, se basa en la distinción que debemos tener entre datos de entrenamiento vs testing. Donde los primeros son utilizados para ajustar el modelo y el segundo para evaluarlo. Buscando siempre que esta evaluación refleje lo que sucede en la práctica, que no tenemos acceso a los datos de entrenamiento, más que para solo evaluar el modelo.
En este sentido, un error común, es utilizar parte de datos de testing, para calcular parámetros con que imputaremos o transformaremos el dataset de entrenamiento,que es el cual donde nos basamos para ajustar el modelo. Este error, da como resultado un modelo contaminado, dado que dentro de los datos utilizados para ajustarlo se incluyeron datos de testing.
A continuación un breve ejemplo de como se utilizaría ModeImputer
. Primero
separamos nuestro dataset df
, en entrenamiento y testing. Luego pasamos
ajustar e imputar (transformar) nuestra data de entrenamiento. Para finalmente
imputar nuestra dataset de testing con los parámetros estimados en la linea
anterior.
# train test split siempre entrega las separaciones en este orden.
X_train, X_test, y_train, y_test = train_test_split(df[df.columns[:-1]],
df['price'] )
# Nuestro objeto para imputar.
mr = ModeImputer(missing_value = '?')
Primero ajustamos a la data para obtener los parametros de reemplazo y luego
imputar el dataset de entrenamiento. Notar cómo ModeImputer
hereda de
TransformerMixin
, también podriamos haber hecho el ajuste y transformación en
una sola línea. Así: X_train_transf = mr.fit_transform(X = X_train)
mr.fit(X = X_train) #Ajuste para obtener los parametros de reemplazo
X_train_transf = mr.transform(X = X_train)# Tranformación
Podemos inspeccionar cuales son los valores de reemplazo para cada columna con:
mr.replace_with
{'norm_losses': '161',
'num_of_doors': 'four',
'bore': '3.62',
'stroke': '3.40'}
Finalmente transformamos nuestro data de testing. En este caso no es necesario
hacer fit
nuevamente, ya que se realizó para el entrenamiento.
X_test_transf = mr.transform(X = X_test)
Cómo ultimo código verifiquemos que no es lo mismo tranformar los datos de
testing con los parametros obtenidos a partir de los datos de entrenamiento
(como lo hicimos en el código anterior) vs hacerlo con los propios datos de
entrenamiento. Esto lo realizaremos utilizando la función de pandas equals
,
que verifica si 2 df son iguales.
X_test_transf.equals(mr.fit_transform(X = X_test))
False
El codigo anterior nos ilustra la claridad que podemos lograr con esta
metodología. Usando mr.fit()
y mr.transform()
, al igual que lo hacemos para
ajustar el modelo. Mirándolos por un par de segundos, no es difícil imaginarse
como podríamos customizar y reutilizarlos para otro tipo de problemas, siguiendo
la misma metodología usada comunmente por sklearn
.
Si aún, para ti, esto no es tan claro, lo entiendo y creo pensar que no eres el
único – de hecho ni siquiera importamos el módulo Pipeline
, que tanto
destacábamos. Aún así, tengo la esperanza que en el siguiente post esto hará más
sentido. Allí, construiremos a partir de la bases que aquí hemos descrito,
describiendo más explícitamente como se combinan las etapas de procesamiento de
variables, ajuste y evaluación del modelo en un solo gran proceso.