Un Proyecto completo de Machine Learning en Python: Segunda Parte

Cesar Vega
27 septiembre, 2019

. . .

Selección de modelo, Ajuste de Hiperparámetro y Evaluación


Este artículo se basa en el video publicado originalmente en inglés por Will Koehrsen en Medium. Por favor, visita el siguiente enlace y recomienda el artículo original si te gusta el contenido:

Ensamblar todas las piezas de machine learning necesarias para resolver un problema puede ser una tarea desalentadora. En esta serie de artículos, estamos caminando a través de la implementación de un flujo de trabajo de machine learning utilizando un conjunto de datos del mundo real para ver cómo se unen las técnicas individuales.

En el primer post, limpiamos y estructuramos los datos, realizamos un análisis exploratorio de datos, desarrollamos un conjunto de características para utilizar en nuestro modelo y establecimos una línea de base contra la cual podemos medir el desempeño. En este artículo, veremos cómo implementar y comparar varios modelos de machine learning en Python, realizaremos un ajuste de hiperparámetros para optimizar el mejor modelo y evaluaremos el modelo final en el equipo de prueba.

El código completo de este proyecto está en GitHub y el segundo notebook correspondiente a este artículo está aquí. Siéntase libre de usar, compartir y modificar el código de la manera que desee!

. . .

Evaluación y selección de modelos

Como recordatorio, estamos trabajando en una tarea de regresión supervisada: usando los datos de energía de los edificios de la ciudad de Nueva York, queremos desarrollar un modelo que pueda predecir el Energy Star Score de un edificio. Nuestro enfoque se centra tanto en la precisión de las predicciones como en la interpretabilidad del modelo.

Hay un montón de modelos de machine learning para elegir y decidir por dónde empezar puede ser intimidante. Aunque hay algunos gráficos tipo chart que tratan de mostrarle qué algoritmo usar, prefiero probar varios y ver cuál funciona mejor. El machine learning sigue siendo un campo impulsado principalmente por resultados empíricos (experimentales) más que teóricos, y es casi imposible saber de antemano qué modelo será el mejor.

En general, es una buena idea comenzar con modelos simples e interpretables, como la regresión lineal, y si el rendimiento no es adecuado, pasar a métodos más complejos, pero generalmente más precisos. El siguiente gráfico muestra una versión (muy poco científica) de la compensación entre la precisión y la interpretabilidad:

Interpretabilidad versus precisión (fuente)

Evaluaremos cinco modelos diferentes que cubren el espectro de complejidad:

  • * Regresión lineal
  • * Regresión de los vecinos más cercanos a  k
  • * Regresión Aleatoria de Bosques
  • * Regresión con Aumento de Gradiente
  • * Regresión de la Máquina Vectorial de Soporte

En este post nos centraremos en la implementación de estos métodos más que en la teoría que los sustenta. Para cualquiera que esté interesado en aprender el trasfondo, recomiendo An Introduction to Statistical Learning (disponible gratis en línea) o Hands-On Machine Learning with Scikit-Learn and TensorFlow. Ambos libros de texto hacen un gran trabajo al explicar la teoría y mostrar cómo usar efectivamente los métodos en R y Python respectivamente.


Imputando valores perdidos

Mientras que dejamos caer las columnas con más del 50% de valores perdidos cuando limpiamos los datos, todavía faltan bastantes observaciones. Los modelos de Machine learning no pueden tratar con valores ausentes, por lo que tenemos que completarlos, un proceso conocido como imputación.

Primero, leeremos todos los datos y nos recordaremos cómo es:


import pandas as pd
import numpy as np

# Leer datos en dataframes 
train_features = pd.read_csv('data/training_features.csv')
test_features = pd.read_csv('data/testing_features.csv')
train_labels = pd.read_csv('data/training_labels.csv')
test_labels = pd.read_csv('data/testing_labels.csv')

Training Feature Size:  (6622, 64)
Testing Feature Size:   (2839, 64)
Training Labels Size:   (6622, 1)
Testing Labels Size:    (2839, 1)



Cada valor que es NaN representa una observación que falta. Aunque hay varias maneras de completar los datos que faltan, utilizaremos un método relativamente simple, la imputación mediana. Esto reemplaza todos los valores que faltan en una columna por el valor medio de la columna.

En el siguiente código, creamos un objeto Scikit-Learn Imputer  con la estrategia establecida en mediana. Luego entrenamos este objeto en los datos de entrenamiento (usando imputer.fit) y lo usamos para completar los valores que faltan tanto en los datos de entrenamiento como en los de prueba (usando imputer.transform). Esto significa que los valores que faltan en los datos de la prueba se completan con el valor medio correspondiente de los datos de entrenamiento.

(Tenemos que hacer la imputación de esta manera en lugar de la formación sobre todos los datos para evitar el problema de la fuga de datos de prueba, donde la información del conjunto de datos de prueba se derrama sobre los datos de formación.)

# Crear un objeto imputer con una estrategia de relleno mediana
imputer = Imputer(strategy='median')

# Entrenamiento sobre las características de formación
imputer.fit(train_features)

# Transformar tanto los datos de entrenamiento como los datos de pruebas
X = imputer.transform(train_features)
X_test = imputer.transform(test_features)

Missing values in training features:  0
Missing values in testing features:   0

Todas las características ahora tienen valores reales y finitos sin ejemplos faltantes.


Escalado de características

Scaling se refiere al proceso general de cambiar el rango de una característica. Esto es necesario porque las características se miden en diferentes unidades, y por lo tanto cubren diferentes rangos. Métodos como las máquinas vectoriales de apoyo y los vecinos más cercanos, K-nearest, que tienen en cuenta las medidas de distancia entre observaciones se ven afectados significativamente por el rango de las características y la escala les permite aprender. Aunque métodos como la Regresión Lineal y el Bosque Aleatorio no requieren en realidad el escalado de características, sigue siendo una buena práctica dar este paso cuando estamos comparando múltiples algoritmos.

Escalaremos las características poniendo cada una en un rango entre 0 y 1. Esto se hace tomando cada valor de una característica, restando el valor mínimo de la característica y dividiéndola por el máximo menos el mínimo (el rango). Esta versión específica de escalamiento se llama a menudo normalización y la otra versión principal se conoce como estandarización.

Aunque este proceso sería fácil de implementar a mano, podemos hacerlo usando un objeto MinMaxScaler en Scikit-Learn. El código para este método es idéntico al de la imputación, excepto con un escalador en lugar de un imputador. Una vez más, nos aseguramos de entrenar sólo con datos de entrenamiento y luego transformamos todos los datos.

# Crea el objeto escalador con un rango de 0-1
scaler = MinMaxScaler(feature_range=(0, 1))

# Encajar en los datos de entrenamiento
scaler.fit(X)

# Transforma los datos de entrenamiento y prueba
X = scaler.transform(X)
X_test = scaler.transform(X_test)

Cada característica tiene ahora un valor mínimo de 0 y un valor máximo de 1. La imputación de valores faltantes y el escalado de características son dos pasos necesarios en casi cualquier proceso machine learning, por lo que es una buena idea entender cómo funcionan.


Implementación de Modelos de Machine Learning en Scikit-Learn

Después de todo el trabajo que pasamos limpiando y formateando los datos, en realidad crear, entrenar y predecir con los modelos es relativamente simple. Usaremos la biblioteca Scikit-Learn en Python, que tiene una gran documentación y una sintaxis consistente de construcción de modelos. Una vez que sepas cómo hacer un modelo en Scikit-Learn, podrás implementar rápidamente una amplia gama de algoritmos.

Podemos ilustrar un ejemplo de creación de modelos, entrenamiento (usando .fit) y pruebas (usando .predict) con el Gradient Boosting Regressor:

from sklearn.ensemble import GradientBoostingRegressor

# Crea el modelo
gradient_boosted = GradientBoostingRegressor()

# Ajustar el modelo a los datos de entrenamiento.
gradient_boosted.fit(X, y)

# Hacer predicciones sobre los datos de la prueba
predictions = gradient_boosted.predict(X_test)

# Evaluar el modelo
mae = np.mean(abs(predictions - y_test))

print('Gradient Boosted Performance on the test set: MAE = %0.4f' % mae)

Gradient Boosted Performance en el equipo de prueba: MAE = 10.0132


¡La creación de modelos, el entrenamiento y las pruebas son una línea! Para construir los otros modelos, usamos la misma sintaxis, con el único cambio, el nombre del algoritmo. Los resultados se presentan a continuación:

Para poner estas cifras en perspectiva, la línea base ingenua calculada sobre la base del valor medio de la meta fue de 24,5. Claramente, ¡machine learning es aplicable a nuestro problema debido a la mejora significativa sobre la línea de base.

El regresor impulsado por gradiente (MAE = 10.013) supera ligeramente al bosque aleatorio (10.014 MAE). Estos resultados no son del todo justos, ya que en la mayoría de los casos se utilizan los valores por defecto para los hiperparámetros. Especialmente en modelos como la máquina vectorial de soporte, el rendimiento depende en gran medida de estos ajustes. No obstante, a partir de estos resultados seleccionaremos el regresor impulsado por gradiente para la optimización del modelo.


Ajuste de hiperparámetros para la optimización del modelo

En el aprendizaje automático, después de haber seleccionado un modelo, podemos optimizarlo para nuestro problema afinando los hiperparámetros del modelo.

En primer lugar, ¿qué son los hiperparámetros y en qué se diferencian de los parámetros?

  • * Los hiperparámetros de modelo son mejor pensados como ajustes para un algoritmo de machine learning que son establecidos por el científico de datos antes del entrenamiento. Ejemplos serían el número de árboles en un bosque aleatorio o el número de vecinos utilizados en el algoritmo K-nearest neighbors.
  • * Los parámetros del modelo son los que el modelo aprende durante el entrenamiento, como los pesos en una regresión lineal.

El control de los hiperparámetros afecta al rendimiento del modelo al alterar el equilibrio entre el ajuste insuficiente y el ajuste excesivo en un modelo. El Underfitting (ajuste insuficiente) es cuando nuestro modelo no es lo suficientemente complejo (no tiene suficientes grados de libertad) para aprender el mapeo desde las características hasta el objetivo. Un modelo de ajuste tiene un alto sesgo, que podemos corregir haciendo que nuestro modelo sea más complejo.

El Overfitting (ajuste excesivo) es cuando nuestro modelo esencialmente memoriza los datos de entrenamiento. Un modelo de overfit tiene una alta varianza, que podemos corregir limitando la complejidad del modelo a través de la regularización. Tanto un modelo underfit como un modelo overfit no podrán generalizarse bien a los datos de las pruebas.

¡El problema de elegir los hiperparámetros correctos es que el conjunto óptimo será diferente para cada problema de machine learning! Por lo tanto, la única manera de encontrar los mejores ajustes es probar un número de ellos en cada nuevo conjunto de datos. Afortunadamente, Scikit-Learn tiene una serie de métodos que nos permiten evaluar eficientemente los hiperparámetros. Además, proyectos como el TPOT de Epistasis Lab están tratando de optimizar la búsqueda de hiperparámetros utilizando métodos como la programación genética. ¡En este proyecto, seguiremos haciendo esto con Scikit-Learn, pero nos quedamos sintonizados para más trabajo en la escena auto-ML!


Búsqueda aleatoria con validación cruzada

El método particular de ajuste de hiperparámetros que implementaremos se llama búsqueda aleatoria con validación cruzada:

  • * La búsqueda aleatoria se refiere a la técnica que utilizaremos para seleccionar los hiperparámetros. Definimos una grilla y luego tomamos muestras aleatorias de diferentes combinaciones, en lugar de una búsqueda en grilla en la que probamos exhaustivamente cada una de las combinaciones. (Sorprendentemente, la búsqueda aleatoria funciona casi tan bien como la búsqueda en grilla con una reducción drástica del tiempo de ejecución.)
  • * La validación cruzada es la técnica que utilizamos para evaluar una combinación seleccionada de hiperparámetros. En lugar de dividir el conjunto de formación en conjuntos de entrenamiento y validación separados, lo que reduce la cantidad de datos de formación que podemos utilizar, utilizamos K-Fold Cross Validation. Esto implica dividir los datos de entrenamiento en K número de pliegues, y luego pasar por un proceso iterativo donde primero entrenamos en K-1 de los pliegues y luego evaluamos el rendimiento en el pliegue Kth. Repetimos este proceso K veces y al final de la validación cruzada del pliegue K, tomamos el error promedio en cada una de las iteraciones K como la medida final de rendimiento.

La idea de la validación cruzada de K-Fold con K = 5 se muestra a continuación:

K-Fold Cross Validation with K = 5 (Source)

Todo el proceso de búsqueda aleatoria con validación cruzada es:

  • 1. Configurar una grilla de hiperparámetros para evaluar
  • 2. Muestreo aleatorio de una combinación de hiperparámetros
  • 3. Crear un modelo con la combinación seleccionada
  • 4. Evaluar el modelo utilizando la validación cruzada de pliegues en K
  • 5. Decidir qué hiperparámetros funcionaron mejor

¡Por supuesto, no hacemos esto manualmente, sino que dejamos que RandomizedSearchCV  de Scikit-Learn se encargue de todo el trabajo!


Desvío leve: Métodos de aumento de gradiente

Ya que vamos a usar el modelo de Gradient Boosted Regression, ¡debería dar al menos un poco de información de fondo! Este modelo es un método de conjunto, lo que significa que se construye a partir de muchos aprendices débiles, en este caso árboles de decisión individuales. Mientras que un algoritmo de empaquetamiento como el bosque aleatorio entrena a los aprendices débiles en paralelo y los hace votar para hacer una predicción, un método de empuje como el Gradient Boosting, entrena a los aprendices en secuencia, con cada aprendiz «concentrándose» en los errores cometidos por los anteriores.

Los métodos de estimulación se han vuelto populares en los últimos años y con frecuencia ganan concursos de machine learning. El Gradient Boosting Method es una implementación particular que utiliza el Descenso de gradiente para minimizar la función de costo al capacitar secuencialmente a los aprendices sobre los residuos de los anteriores. La implementación de Scikit-Learn de Gradient Boosting es generalmente considerada menos eficiente que otras librerías como XGBoost, pero funcionará lo suficientemente bien para nuestro pequeño conjunto de datos y es bastante precisa.


Volver a Sintonización de hiperparámetros

Hay muchos hiperparámetros para sintonizar en un Regressor Gradient Boosted y puedes ver la documentación de Scikit-Learn para más detalles. Optimizaremos los siguientes hiperparámetros:

  • * loss: la función de pérdida para minimizar
  • * n_estimators: el número de aprendices débiles (árboles de decisión) a utilizar
  • * max_depth: la profundidad máxima de cada árbol de decisión
  • * min_samples_leaf: el número mínimo de ejemplos requeridos en un nodo hoja del árbol de decisión
  • * min_samples_split: el número mínimo de ejemplos necesarios para dividir un nodo del árbol de decisión
  • * max_features: el número máximo de características a utilizar para dividir nodos

No estoy seguro de que haya alguien que realmente entienda cómo interactúan todas estas cosas, y la única manera de encontrar la mejor combinación es probándolas.

En el siguiente código, construimos una grilla de hiperparámetros, creamos un objeto RandomizedSearchCV  y realizamos una búsqueda de hiperparámetros utilizando una validación cruzada cuádruple en 25 combinaciones diferentes de hiperparámetros:

# Función de pérdida a optimizar
loss = ['ls', 'lad', 'huber']

# Número de árboles utilizados en el proceso de cultivo
n_estimators = [100, 500, 900, 1100, 1500]

# Profundidad máxima de cada árbol
max_depth = [2, 3, 5, 10, 15]

# Número mínimo de muestras por hoja
min_samples_leaf = [1, 2, 4, 6, 8]

# Número mínimo de muestras para dividir un nodo
min_samples_split = [2, 4, 6, 10]

# Número máximo de características a tener en cuenta para realizar particiones
max_features = ['auto', 'sqrt', 'log2', None]

# Definir la cuadrícula de hiperparámetros a buscar
hyperparameter_grid = {'loss': loss,
                       'n_estimators': n_estimators,
                       'max_depth': max_depth,
                       'min_samples_leaf': min_samples_leaf,
                       'min_samples_split': min_samples_split,
                       'max_features': max_features}

# Crear el modelo a utilizar para la sintonización de hiperparámetros
model = GradientBoostingRegressor(random_state = 42)

# Configurar la búsqueda aleatoria con validación cruzada cuádruple
random_cv = RandomizedSearchCV(estimator=model,
                               param_distributions=hyperparameter_grid,
                               cv=4, n_iter=25, 
                               scoring = 'neg_mean_absolute_error',
                               n_jobs = -1, verbose = 1, 
                               return_train_score = True,
                               random_state=42)

# Encajar en los datos de entrenamiento
random_cv.fit(X, y)

Después de realizar la búsqueda, podemos inspeccionar el objeto RandomizedSearchCV  para encontrar el mejor modelo:

# Encuentre la mejor combinación de ajustes
random_cv.best_estimator_

GradientBoostingRegressor(loss='lad', max_depth=5,
                          max_features=None,
                          min_samples_leaf=6,
                          min_samples_split=6,
                          n_estimators=500)

A continuación, podemos utilizar estos resultados para realizar la búsqueda de la grilla mediante la elección de parámetros para nuestra grilla que se aproximen a estos valores óptimos. Sin embargo, es poco probable que una mayor afinación mejore significativamente nuestro modelo. Como regla general, la ingeniería de características adecuada tendrá un impacto mucho mayor en el rendimiento del modelo que incluso la sintonización de hiperparámetros más extensa. Es la ley de los rendimientos decrecientes aplicados al machine learning: la ingeniería de características le lleva la mayor parte del camino hasta allí, y el afinamiento de hiperparámetros por lo general sólo proporciona un pequeño beneficio.

Un experimento que podemos intentar es cambiar el número de estimadores (árboles de decisión) mientras mantenemos el resto de los hiperparámetros estables. Esto nos permite observar directamente el efecto de este ajuste en particular. Ver el cuaderno para la implementación, pero aquí están los resultados:

A medida que aumenta el número de árboles utilizados por el modelo, tanto el error de entrenamiento como el de prueba disminuyen. Sin embargo, el error de entrenamiento disminuye mucho más rápidamente que el error de prueba y podemos ver que nuestro modelo está sobreajustado: funciona muy bien en los datos de entrenamiento, pero no es capaz de lograr el mismo rendimiento en el conjunto de pruebas.

Siempre esperamos al menos alguna disminución en el rendimiento del conjunto de pruebas (después de todo, el modelo puede ver las verdaderas respuestas para el conjunto de entrenamiento), pero una brecha significativa indica un ajuste excesivo. Podemos abordar el problema del sobreajuste obteniendo más datos de entrenamiento o disminuyendo la complejidad de nuestro modelo a través de los hiperparámetros. En este caso, dejaremos los hiperparámetros donde están, pero animo a cualquiera a que intente reducir el sobreajuste.

Para el modelo final, usaremos 800 estimadores porque eso resultó en el error más bajo en la validación cruzada. Ahora, ¡es hora de probar este modelo!

. . .

Evaluación en el conjunto de prueba

Como ingenieros responsables del aprendizaje de máquinas, nos aseguramos de no dejar que nuestro modelo viera el conjunto de pruebas en ningún punto de la formación. Por lo tanto, podemos utilizar el rendimiento del conjunto de prueba como un indicador de lo bien que nuestro modelo funcionaría si se desplegara en el mundo real.

Hacer predicciones en el conjunto de prueba y calcular el rendimiento es relativamente sencillo. Aquí, comparamos el rendimiento del Regressor de Gradiente Reforzado por defecto con el modelo ajustado:

# Haga predicciones sobre el conjunto de pruebas usando el modelo predeterminado y el modelo final
default_pred = default_model.predict(X_test)
final_pred = final_model.predict(X_test)

Default model performance on the test set: MAE = 10.0118.
Final model performance on the test set:   MAE = 9.0446.

El ajuste de los hiperparámetros mejoró la precisión del modelo en un 10%. Dependiendo del caso de uso, el 10% podría ser una mejora masiva, ¡pero llegó en un momento de inversión significativa! También podemos calcular el tiempo que se tarda en entrenar los dos modelos usando el comando magico %timeit en Jupyter Notebooks. El primero es el modelo por defecto:

%%timeit -n 1 -r 5
default_model.fit(X, y)

1.09 s ± 153 ms per loop (mean ± std. dev. of 5 runs, 1 loop each)

1 segundo para entrenar parece muy razonable. El modelo afinado final no es tan rápido:

%%timeit -n 1 -r 5
final_model.fit(X, y)

12.1 s ± 1.33 s per loop (mean ± std. dev. of 5 runs, 1 loop each)

Esto demuestra un aspecto fundamental de machine learning: siempre es un juego de compensaciones. Constantemente tenemos que equilibrar la precisión con la interpretación, el sesgo con la varianza, la precisión con el tiempo de ejecución, y así sucesivamente. La mezcla correcta dependerá en última instancia del problema. En nuestro caso, un aumento de 12 veces en el tiempo de ejecución es grande en términos relativos, pero en términos absolutos no es tan significativo.

Una vez que tengamos las predicciones finales, podemos investigarlas para ver si muestran alguna inclinación notable. A la izquierda hay un diagrama de densidad de los valores predichos y reales, y a la derecha un histograma de los residuos:


Gráfico de densidad de valores pronosticados y reales (izquierda) e histograma de residuos (derecha)

Las predicciones del modelo parecen seguir la distribución de los valores reales, aunque el pico de la densidad se aproxima más al valor medio (66) del conjunto de entrenamiento que al verdadero pico de densidad (que está cerca de 100). Los residuales son casi una distribución normal, aunque vemos algunos valores negativos grandes donde las predicciones del modelo estaban muy por debajo de los valores reales. En el próximo post profundizaremos en la interpretación de los resultados del modelo.


Conclusiones

En este artículo cubrimos varios pasos en el flujo de trabajo de machine learning:

  • * Imputación de valores perdidos y escalado de características
  • * Evaluar y comparar varios modelos de machine learning
  • * Ajuste de hiperparámetros mediante búsqueda aleatoria en grilla y validación cruzada
  • * Evaluar el mejor modelo en el equipo de prueba

Los resultados de este trabajo nos mostraron que el machine learning es aplicable a la tarea de predecir el Energy Star Score de un edificio utilizando los datos disponibles. Usando un regresor de gradiente aumentado, pudimos predecir las puntuaciones en el conjunto de pruebas a 9.1 puntos del valor real. Además, hemos visto que el ajuste de hiperparámetros puede aumentar el rendimiento de un modelo a un coste significativo en términos de tiempo invertido. Esta es una de las muchas ventajas y desventajas que tenemos que considerar al desarrollar una solución de machine learning.

En el tercer post (disponible aquí), miraremos la caja negra que hemos creado y trataremos de entender cómo nuestro modelo hace predicciones. También determinaremos los factores más importantes que influyen en el Energy Star Score. Aunque sabemos que nuestro modelo es preciso, queremos saber por qué hace las predicciones que hace y qué nos dice sobre el problema.

Como siempre, agradezco los comentarios y las críticas constructivas y puedo ser contactado en Twitter @koehrsen_will.

8

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Comunidades en Español