Búsqueda de hiperparámetros – Hyperparameter Tuning

Tienes todo tu pipeline de ML montado, pero los resultados no son muy buenos… Aprende a mejorar los resultados de tu modelo con la búsquedas de hiperparámetros con la ayuda de este post!

En Machine Learning hay infinidad de modelos de todo tipo. Desde lineales, hasta basados en densidad de datos, pasando por árboles. Eso sin olvidarnos de los cientos de tipos de arquitecturas de redes neuronales que se utilizan en el Deep Learning. Pero lo que todos los modelos tienen en común es que tienen hiperparámetros. Es decir, parámetros predefinidos que no mejoran según entrena nuestro modelo. Ejemplos pueden ser el número de capas de una red neuronal, la profundidad de un árbol, el tipo de kernel en un SVM…

Os preguntaréis, ¿pero cómo sé cuál es el óptimo a escoger? Muy fácil, no lo sabrás a priori, pero hay técnicas para encontrar la combinación óptima. Además, la implementación en Scikit-Learn es súper sencilla de usar. Te lo explicamos en este post.

Hiperparámetro

Como he dicho en la intro, los hiperparámetros son parámetros de un estimador que no se van aprendiendo según se entrena el modelo, si no que son predefinidos antes de empezar el entrenamiento. 

Estos hiperparámetros normalmente se definen cuando se crea el objeto del estimador. Os dejo el ejemplo de un RandomForest con unos cuantos hiperparámetros, aunque aquí tenéis todos los hiperparámetros que tiene un RandomForest de clasificación.

from sklearn.ensemble import RandomForestClassifier
rfc = RandomForestClassifier(n_estimators=100,
                             max_features="auto",
                             min_samples_split=2,
                             min_samples_leaf=1,
                             bootstrap=True)

Como véis en el código, hemos creado un objeto de Random Forest con 5 hiperparámetros. Aunque es muy probable que no sea la combinación perfecta para nuestros datos.

Pero os preguntaréis, ¿cómo sé yo cuáles son los mejores? Aquí es donde entra en acción la cross-validation y la búsqueda de hiperparámetros.

Qué es la Cross Validation

Como bien sabéis, cuando estamos creando un modelo de predicción, necesitamos partir los datos en entrenamiento en test. Uno para ajustar el modelo (entrenarlo) y otro para comprobar cómo de bien predice el modelo (muestra de test). Pero esto no es todo. Ahora que queremos introducir la búsqueda de hiperparámetros, es necesario introducir otra muestra para validar la combinación de hiperparámetros y encontrar la óptima. Esta muestra se llama muestra de validación, y la técnica se llama cross validation. Es decir, validamos la combinación con datos con los que no hemos entrenado, y después testamos el modelo entero con otros datos para medir cómo de bueno es el modelo.

Búsqueda de Hiperparámetros o Hyperparameter tuning

Existen principalmente dos técnicas de búsqueda de hiperparámetros que encuentran una combinación de manera eficiente y especializada para cada modelo. Se trata de GridSearch (búsqueda exhaustiva) y RandomSearch (búsqueda aleatoria) que son muy parecidas pero con un toque diferente que hace que una de ellas sea mucho más rápida que la otra a expensas de no encontrar la mejor combinación de todas.

GridSearch o Búsqueda Exhaustiva.

Este tipo de búsqueda prueba todas las posibles combinaciones de valores que se le proporcione en el grid de parámetros. Por ejemplo, para un SVM queremos optimizar la C, el kernel y gamma propondremos el siguiente grid:

param_grid = 
                     {'kernel': ['linear'], 'C':[1, 10, 100, 1000],
                     'gamma': [1e-3, 1e-4]}

Lo que especifica que queremos explorar por completo todas las combinaciones.

from sklearn import svm, datasets
from sklearn.model_selection import GridSearchCV
import pandas as pd
iris = datasets.load_iris()
param_grid = {'kernel': ['linear'], 'C':[1, 100, 1000],
              'gamma': [1e-3, 1e-4]}
svc = svm.SVC(gamma="scale")
clf = GridSearchCV(svc, param_grid, cv=5)
clf.fit(iris.data, iris.target)
df = pd.concat([pd.DataFrame(clf.cv_results_["params"]),
                pd.DataFrame(clf.cv_results_["mean_test_score"],
                              columns=["Accuracy"])],
               axis=1)

Obtenemos los siguientes resultados:

CGammaKernelAccuracy
10.001linear0.98
10.0001linear0.98
1000.001linear0.966667
1000.0001linear0.966667
10000.001linear0.966667
10000.0001linear0.966667

Como se ve en la tabla anterior, se ha entrenado un SVM por cada combinación de hiperparámetros y se ha medido su accuracy. Al entrenar un modelo de SkLearn con GridsearchCV, se guardará el modelo con mejor score, en este caso la primera fila.

Búsqueda Aleatoria – RandomSearch

Este tipo de búsqueda prueba combinaciones de valores al azar que se le proporcione en el grid de parámetros. Al ser una búsqueda aleatoria, es muy posible que no nos quedemos con la combinación de hiperparámetros óptima para nuestros datos. Pero ahorraremos mucho tiempo, sobre todo cuanto más tarde el modelo en entrenar y más posibles hiperparámetros especifiquemos.

Pero si no encuentra el modelo óptimo, ¿para qué lo queremos?

Muy fácil, para ahorrar tiempo. Cuando tenemos un grid de parámetros muy grande y se trata de un modelo que tarde mucho en ser entrenado, confiamos en que la búsqueda aleatoria dé en el clavo y encuentre una combinación (pseudo) óptima sin tener que ejecutar todas las iteraciones que tendríamos que hacer con un grid search tradicional.

A continuación tenéis una comparativa de puntuación y de tiempos de lo que tardan ambas soluciones:

import numpy as np
from time import time
import scipy.stats as stats
from sklearn.utils.fixes import loguniform
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
from sklearn.datasets import load_digits
from sklearn.linear_model import SGDClassifier
# Sacamos datos
X, y = load_digits(return_X_y=True)
# Construimos un modelo clasificador
clf = SGDClassifier(loss='hinge', penalty='elasticnet',
                    fit_intercept=True)

# Funcion para reportar los resultados
def report(results, n_top=3):
    for i in range(1, n_top + 1):
        candidates = np.flatnonzero(results['rank_test_score'] == i)
        for candidate in candidates:
            print("Modelo con ranking: {0}".format(i))
            print("Puntuacion media en validacion: {0:.3f} (std: {1:.3f})"
                  .format(results['mean_test_score'][candidate],
                          results['std_test_score'][candidate]))
            print("Parametros: {0}".format(results['params'][candidate]))
            print("")

# Creamos la distribucion de parametros
param_dist = {'average': [True, False],
              'l1_ratio': stats.uniform(0, 1),
              'alpha': loguniform(1e-4, 1e0)}
# ejecucion de la busqueda aleatoria
n_iter_search = 20
random_search = RandomizedSearchCV(clf, param_distributions=param_dist,
                                   n_iter=n_iter_search, n_jobs=-1)
start = time()
random_search.fit(X, y)
print("RandomizedSearchCV tardo %.2f segundos para %d combinaciones"
      " de parametros." % ((time() - start), n_iter_search))
report(random_search.cv_results_)
# Parametros propuestos en la busqueda exhaustiva
param_grid = {'average': [True, False],
              'l1_ratio': np.linspace(0, 1, num=10),
              'alpha': np.power(10, np.arange(-4, 1, dtype=float))}
# ejecucion de la busqueda exhaustiva
grid_search = GridSearchCV(clf, param_grid=param_grid, n_jobs=-1)
start = time()
grid_search.fit(X, y)
print("GridSearchCV tardo %.2f segundos para %d combinaciones de parametros."
      % (time() - start, len(grid_search.cv_results_['params'])))
report(grid_search.cv_results_)

Resultados

RandomizedSearchCV tardó 11.09 segundos para 20 combinaciones de parámetros.
Modelo con ranking: 1
Puntuación media en validación: 0.926 (std: 0.030)
Parámetros: {‘alpha’: 0.00015325700553216163, ‘average’: True, ‘l1_ratio’: 0.12002318852835192}

Modelo con ranking: 2
Puntuación media en validación: 0.925 (std: 0.023)
Parámetros: {‘alpha’: 0.23881024490431232, ‘average’: False, ‘l1_ratio’: 0.00720651004884687}

Modelo con ranking: 3
Puntuación media en validación: 0.923 (std: 0.032)
Parámetros: {‘alpha’: 0.0008599704729242684, ‘average’: True, ‘l1_ratio’: 0.22318138156955203}

GridSearchCV tardó 58.87 segundos para 100 combinaciones de parámetros.
Modelo con ranking: 1
Puntuación media en validacion: 0.929 (std: 0.026)
Parámetros: {‘alpha’: 0.001, ‘average’: True, ‘l1_ratio’: 0.0}

Modelo con ranking: 2
Puntuación media en validación: 0.928 (std: 0.025)
Parámetros: {‘alpha’: 0.0001, ‘average’: True, ‘l1_ratio’: 0.1111111111111111}

Modelo con ranking: 3
Puntuación media en validación: 0.925 (std: 0.031)
Parametros: {‘alpha’: 0.0001, ‘average’: True, ‘l1_ratio’: 0.4444444444444444}

Como vemos en el ejemplo anterior, la búsqueda aleatoria realiza 80 iteraciones menos que la búsqueda exhaustiva y ha tardado la quinta parte de lo que tarda la búsqueda exhaustiva, para tener un Accuracy sólo 0.003 menor.

Recordad, que en este ejemplo son datos muy simples, con un dataset más grande es muy probable que la diferencia de tiempos de ejecución crezcan mucho.

¿Todavía no tienes tu entorno de desarrollo favorito? A lo mejor nuestro post sobre Google Colab te convence.