3.2. Ajustar los hiperparámetros de un estimador

Los hiperparámetros son parámetros que no se aprenden directamente dentro de los estimadores. En scikit-learn se pasan como argumentos al constructor de las clases de estimadores. Los ejemplos típicos incluyen C, kernel y gamma para el Clasificador de Vectores de Soporte, alpha para Lasso, etc.

Es posible y recomendable buscar en el espacio de hiperparámetros la mejor puntuación de validación cruzada.

Cualquier parámetro proporcionado al construir un estimador puede ser optimizado de esta manera. Específicamente, para encontrar los nombres y valores actuales de todos los parámetros de un estimador determinado, utiliza:

estimator.get_params()

Una búsqueda consiste en:

  • un estimador (regresor o clasificador como sklearn.svm.SVC());

  • un espacio de parámetros;

  • un método de búsqueda o muestreo de candidatos;

  • un esquema de validación cruzada; y

  • una función de puntuación.

En scikit-learn se proporcionan dos enfoques genéricos para la búsqueda de parámetros: para valores dados, GridSearchCV considera exhaustivamente todas las combinaciones de parámetros, mientras que RandomizedSearchCV puede muestrear un número determinado de candidatos de un espacio de parámetros con una distribución especificada. Ambas herramientas tienen sus homólogos sucesivos HalvingGridSearchCV y HalvingRandomSearchCV, que pueden ser mucho más rápidos a la hora de encontrar una buena combinación de parámetros.

Después de describir estas herramientas, detallamos las mejores prácticas aplicables a estos enfoques. Algunos modelos permiten estrategias de búsqueda de parámetros especializadas y eficientes, descritas en Alternativas a la búsqueda de parámetros por fuerza bruta.

Ten en cuenta que es habitual que un pequeño subconjunto de esos parámetros pueda tener un gran impacto en el rendimiento predictivo o de cálculo del modelo, mientras que otros pueden dejarse con sus valores por defecto. Se recomienda leer la cadena de documentación de la clase del estimador para obtener una comprensión más fina de su comportamiento esperado, posiblemente leyendo la referencia adjunta a la literatura.

3.2.2. Optimización Aleatoria de Parámetros

Aunque el uso de una cuadrícula de ajustes de parámetros es el método más utilizado actualmente para la optimización de parámetros, otros métodos de búsqueda tienen propiedades más favorables. RandomizedSearchCV implementa una búsqueda aleatoria sobre los parámetros, donde cada ajuste se muestrea a partir de una distribución sobre los posibles valores de los parámetros. Esto tiene dos ventajas principales sobre una búsqueda exhaustiva:

  • Se puede elegir un presupuesto independientemente del número de parámetros y de los valores posibles.

  • Añadir parámetros que no influyen en el rendimiento no disminuye la eficiencia.

La especificación de cómo se deben muestrear los parámetros se realiza mediante un diccionario, de forma muy similar a la especificación de parámetros para GridSearchCV. Además, se especifica un presupuesto de cálculo, que es el número de candidatos muestreados o iteraciones de muestreo, utilizando el parámetro n_iter. Para cada parámetro, se puede especificar una distribución sobre posibles valores o una lista de opciones discretas (que se muestrearán uniformemente):

{'C': scipy.stats.expon(scale=100), 'gamma': scipy.stats.expon(scale=.1),
  'kernel': ['rbf'], 'class_weight':['balanced', None]}

Este ejemplo utiliza el módulo scipy.stats, que contiene muchas distribuciones útiles para el muestreo de parámetros, como expon, gamma, uniform o randint.

En principio, se puede pasar cualquier función que proporcione un método rvs (muestra de variante aleatoria) para muestrear un valor. Una llamada a la función rvs debe proporcionar muestras aleatorias independientes de los posibles valores de los parámetros en llamadas consecutivas.

Advertencia

Las distribuciones en scipy.stats anteriores a la versión scipy 0.16 no permiten especificar un estado aleatorio. En su lugar, utilizan el estado aleatorio global de numpy, que puede ser sembrado a través de np.random.seed o establecido utilizando np.random.set_state. Sin embargo, a partir de scikit-learn 0.18, el módulo sklearn.model_selection establece el estado aleatorio proporcionado por el usuario si scipy >= 0.16 también está disponible.

Para los parámetros continuos, como C arriba, es importante especificar una distribución continua para aprovechar al máximo la aleatorización. De esta manera, el aumento de n_iter siempre conducirá a una búsqueda más fina.

Una variable aleatoria continua log-uniforme está disponible a través de loguniform. Esta es una versión continua de los parámetros log-espaciados. Por ejemplo, para especificar C arriba, se puede usar loguniform(1, 100) en lugar de [1, 10, 100] o np.logspace(0, 2, num=1000). Este es un alias de stats.reciprocal de SciPy.

Reflejando el ejemplo anterior en la búsqueda en cuadrícula, podemos especificar una variable aleatoria continua que se distribuye log-uniformemente entre 1e0 y 1e3:

from sklearn.utils.fixes import loguniform
{'C': loguniform(1e0, 1e3),
 'gamma': loguniform(1e-4, 1e-3),
 'kernel': ['rbf'],
 'class_weight':['balanced', None]}

Ejemplos:

Referencias:

  • Bergstra, J. y Bengio, Y., Random search for hyper-parameter optimization, The Journal of Machine Learning Research (2012)

3.2.3. Búsqueda de los parámetros óptimos con la reducción sucesiva a la mitad

Scikit-learn también proporciona los estimadores HalvingGridSearchCV y HalvingRandomSearchCV que se pueden utilizar para buscar un espacio de parámetros utilizando la reducción sucesiva a la mitad 1 2. La reducción sucesiva a la mitad (successive halving, SH) es como un torneo entre combinaciones de parámetros candidatos. SH es un proceso de selección iterativo en el que todos los candidatos (las combinaciones de parámetros) se evalúan con una pequeña cantidad de recursos en la primera iteración. Sólo algunos de estos candidatos se seleccionan para la siguiente iteración, a la que se asignan más recursos. Para el ajuste de parámetros, el recurso suele ser el número de muestras de entrenamiento, pero también puede ser un parámetro numérico arbitrario como n_estimators en un bosque aleatorio.

Como se ilustra en la siguiente figura, sólo un subconjunto de candidatos “sobrevive” hasta la última iteración. Se trata de los candidatos que se han clasificado sistemáticamente entre los candidatos con mayor puntuación en todas las iteraciones. A cada iteración se le asigna una cantidad creciente de recursos por candidato, en este caso el número de muestras.

../_images/sphx_glr_plot_successive_halving_iterations_001.png

Aquí describimos brevemente los parámetros principales, pero cada parámetro y sus interacciones se describen con más detalle en las siguientes secciones. El parámetro factor (> 1) controla la tasa de crecimiento de los recursos y la tasa de disminución del número de candidatos. En cada iteración, el número de recursos por candidato se multiplica por factor y el número de candidatos se divide por el mismo factor. Junto con resource y min_resources, factor es el parámetro más importante para controlar la búsqueda en nuestra implementación, aunque un valor de 3 suele funcionar bien. factor controla efectivamente el número de iteraciones en HalvingGridSearchCV y el número de candidatos (por defecto) e iteraciones en HalvingRandomSearchCV. También se puede utilizar aggressive_elimination=True si el número de recursos disponibles es pequeño. Hay más control disponible mediante el ajuste del parámetro min_resources.

Estos estimadores son todavía experimentales: sus predicciones y su API podrían cambiar sin ningún ciclo de obsolescencia. Para utilizarlos, es necesario importar explícitamente enable_halving_search_cv:

>>> # explicitly require this experimental feature
>>> from sklearn.experimental import enable_halving_search_cv  # noqa
>>> # now you can import normally from model_selection
>>> from sklearn.model_selection import HalvingGridSearchCV
>>> from sklearn.model_selection import HalvingRandomSearchCV

3.2.3.1. Elegir min_resources y el número de candidatos

Además de factor, los dos parámetros principales que influyen en el comportamiento de una búsqueda sucesiva a la mitad son el parámetro min_resources y el número de candidatos (o combinaciones de parámetros) que se evalúan. min_resources es la cantidad de recursos asignados en la primera iteración para cada candidato. El número de candidatos se especifica directamente en HalvingRandomSearchCV, y se determina a partir del parámetro param_grid de HalvingGridSearchCV.

Consideremos un caso en el que el recurso es el número de muestras, y en el que tenemos 1000 muestras. En teoría, con min_resources=10 y factor=2, podemos ejecutar como máximo 7 iteraciones con el siguiente número de muestras: [10, 20, 40, 80, 160, 320, 640].

Pero dependiendo del número de candidatos, podríamos realizar menos de 7 iteraciones: si empezamos con un pequeño número de candidatos, la última iteración podría utilizar menos de 640 muestras, lo que significa no utilizar todos los recursos disponibles (muestras). Por ejemplo, si empezamos con 5 candidatos, sólo necesitaremos 2 iteraciones: 5 candidatos para la primera iteración, y luego 5 // 2 = 2 candidatos en la segunda iteración, después de la que sabremos cuál es el mejor candidato (por lo que no necesitamos una tercera). Sólo estaríamos utilizando como máximo 20 muestras, lo cual es un desperdicio ya que tenemos 1000 muestras a nuestra disposición. Por otro lado, si empezamos con un número elevado de candidatos, podríamos acabar con muchos candidatos en la última iteración, lo que no siempre es ideal: significa que muchos candidatos correrán con todos los recursos, reduciendo básicamente el procedimiento a una búsqueda estándar.

En el caso de HalvingRandomSearchCV, el número de candidatos se establece por defecto de forma que la última iteración utilice la mayor cantidad posible de recursos disponibles. Para HalvingGridSearchCV, el número de candidatos viene determinado por el parámetro param_grid. Cambiar el valor de min_resources afectará al número de iteraciones posibles, y como resultado también tendrá un efecto en el número ideal de candidatos.

Otra consideración a la hora de elegir min_resources es si es fácil o no discriminar entre candidatos buenos y malos con una pequeña cantidad de recursos. Por ejemplo, si necesitas muchas muestras para distinguir entre parámetros buenos y malos, se recomienda un min_resources alto. Por otro lado, si la distinción es clara incluso con una pequeña cantidad de muestras, entonces puede ser preferible un min_resources pequeño, ya que aceleraría el cálculo.

Observa en el ejemplo anterior que la última iteración no utiliza el máximo de recursos disponibles: Hay 1000 muestras disponibles, pero sólo se utilizan 640, como máximo. Por defecto, tanto HalvingRandomSearchCV como HalvingGridSearchCV intentan utilizar la mayor cantidad de recursos posible en la última iteración, con la restricción de que esta cantidad de recursos debe ser un múltiplo tanto de min_resources como de factor (esta restricción quedará clara en la siguiente sección). HalvingRandomSearchCV logra esto mediante el muestreo de la cantidad correcta de candidatos, mientras que HalvingGridSearchCV lo logra estableciendo adecuadamente min_resources. Por favor, consulta Agotar los recursos disponibles para más detalles.

3.2.3.2. Cantidad de recursos y número de candidatos en cada iteración

En cualquier iteración i, a cada candidato se le asigna una cantidad determinada de recursos que denotamos n_resources_i. Esta cantidad está controlada por los parámetros factor y min_resources como sigue (factor es estrictamente mayor que 1):

n_resources_i = factor**i * min_resources,

o, de forma equivalente:

n_resources_{i+1} = n_resources_i * factor

donde min_resources == n_resources_0 es la cantidad de recursos utilizados en la primera iteración. factor también define las proporciones de candidatos que se seleccionarán para la siguiente iteración:

n_candidates_i = n_candidates // (factor ** i)

o, de forma equivalente:

n_candidates_0 = n_candidates
n_candidates_{i+1} = n_candidates_i // factor

Así que en la primera iteración, utilizamos min_resources recursos n_candidates veces. En la segunda iteración, utilizamos min_resources * factor recursos n_candidates // factor veces. La tercera vuelve a multiplicar los recursos por candidato y divide el número de candidatos. Este proceso se detiene cuando se alcanza la cantidad máxima de recursos por candidato, o cuando hemos identificado al mejor candidato. El mejor candidato se identifica en la iteración que está evaluando factor o menos candidatos (ver justo debajo para una explicación).

Aquí hay un ejemplo con min_resources=3 y factor=2, empezando con 70 candidatos:

n_resources_i

n_candidates_i

3 (=min_resources)

70 (=n_candidates)

3 * 2 = 6

70 // 2 = 35

6 * 2 = 12

35 // 2 = 17

12 * 2 = 24

17 // 2 = 8

24 * 2 = 48

8 // 2 = 4

48 * 2 = 96

4 // 2 = 2

Podemos notar que:

  • el proceso se detiene en la primera iteración que evalúa factor=2 candidatos: el mejor candidato es el mejor de estos 2 candidatos. No es necesario ejecutar una iteración adicional, ya que sólo evaluaría un candidato (el mejor, que ya hemos identificado). Por esta razón, en general, queremos que la última iteración ejecute como máximo factor candidatos. Si la última iteración evalúa más de factor candidatos, entonces esta última iteración se reduce a una búsqueda regular (como en RandomizedSearchCV o GridSearchCV).

  • cada n_resources_i es un múltiplo tanto de factor como de min_resources (lo cual se confirma por su definición anterior).

La cantidad de recursos que se utilizan en cada iteración se puede encontrar en el atributo n_resources_.

3.2.3.3. Elegir un recurso

Por defecto, el recurso se define en términos del número de muestras. Es decir, cada iteración utilizará una cantidad creciente de muestras para entrenar. Sin embargo, puedes especificar manualmente un parámetro para utilizarlo como recurso con el parámetro resource. Aquí hay un ejemplo donde el recurso se define en términos del número de estimadores de un bosque aleatorio:

>>> from sklearn.datasets import make_classification
>>> from sklearn.ensemble import RandomForestClassifier
>>> from sklearn.experimental import enable_halving_search_cv  # noqa
>>> from sklearn.model_selection import HalvingGridSearchCV
>>> import pandas as pd
>>>
>>> param_grid = {'max_depth': [3, 5, 10],
...               'min_samples_split': [2, 5, 10]}
>>> base_estimator = RandomForestClassifier(random_state=0)
>>> X, y = make_classification(n_samples=1000, random_state=0)
>>> sh = HalvingGridSearchCV(base_estimator, param_grid, cv=5,
...                          factor=2, resource='n_estimators',
...                          max_resources=30).fit(X, y)
>>> sh.best_estimator_
RandomForestClassifier(max_depth=5, n_estimators=24, random_state=0)

Ten en cuenta que no es posible presupuestar en un parámetro que es parte de la cuadrícula del parámetro.

3.2.3.4. Agotar los recursos disponibles

Como se ha mencionado anteriormente, el número de recursos que se utiliza en cada iteración depende del parámetro min_resources. Si tienes muchos recursos disponibles pero empiezas con un número bajo de recursos, algunos de ellos podrían ser desperdiciados (es decir, no utilizados):

>>> from sklearn.datasets import make_classification
>>> from sklearn.svm import SVC
>>> from sklearn.experimental import enable_halving_search_cv  # noqa
>>> from sklearn.model_selection import HalvingGridSearchCV
>>> import pandas as pd
>>> param_grid= {'kernel': ('linear', 'rbf'),
...              'C': [1, 10, 100]}
>>> base_estimator = SVC(gamma='scale')
>>> X, y = make_classification(n_samples=1000)
>>> sh = HalvingGridSearchCV(base_estimator, param_grid, cv=5,
...                          factor=2, min_resources=20).fit(X, y)
>>> sh.n_resources_
[20, 40, 80]

El proceso de búsqueda sólo utilizará 80 recursos como máximo, mientras que nuestra cantidad máxima de recursos disponibles es n_samples=1000. Aquí, tenemos min_resources = r_0 = 20.

Para HalvingGridSearchCV, por defecto, el parámetro min_resources se establece en “exhaust”. Esto significa que min_resources se establece automáticamente de forma que la última iteración pueda utilizar tantos recursos como sea posible, dentro del límite de max_resources:

>>> sh = HalvingGridSearchCV(base_estimator, param_grid, cv=5,
...                          factor=2, min_resources='exhaust').fit(X, y)
>>> sh.n_resources_
[250, 500, 1000]

min_resources se estableció aquí automáticamente en 250, lo que hace que la última iteración utilice todos los recursos. El valor exacto que se utiliza depende del número de parámetros candidatos, en max_resources y en factor.

Para HalvingRandomSearchCV, el agotamiento de los recursos puede hacerse de dos maneras:

  • estableciendo min_resources='exhaust', al igual que para HalvingGridSearchCV;

  • estableciendo n_candidates='exhaust'.

Ambas opciones son mutuamente excluyentes: usar min_resources='exhaust' requiere conocer el número de candidatos, y simétricamente n_candidates='exhaust' requiere conocer min_resources.

En general, agotar el número total de recursos conduce a un mejor parámetro candidato final, y requiere ligeramente más tiempo.

3.2.3.5. Eliminación agresiva de candidatos

Idealmente, queremos que la última iteración evalúe factor candidatos (ver Cantidad de recursos y número de candidatos en cada iteración). Luego sólo tenemos que elegir el mejor. Cuando el número de recursos disponibles es pequeño respecto al número de candidatos, es posible que la última iteración tenga que evaluar más de factor candidatos:

>>> from sklearn.datasets import make_classification
>>> from sklearn.svm import SVC
>>> from sklearn.experimental import enable_halving_search_cv  # noqa
>>> from sklearn.model_selection import HalvingGridSearchCV
>>> import pandas as pd
>>>
>>>
>>> param_grid = {'kernel': ('linear', 'rbf'),
...               'C': [1, 10, 100]}
>>> base_estimator = SVC(gamma='scale')
>>> X, y = make_classification(n_samples=1000)
>>> sh = HalvingGridSearchCV(base_estimator, param_grid, cv=5,
...                          factor=2, max_resources=40,
...                          aggressive_elimination=False).fit(X, y)
>>> sh.n_resources_
[20, 40]
>>> sh.n_candidates_
[6, 3]

Como no podemos utilizar más de max_resources=40 recursos, el proceso tiene que detenerse en la segunda iteración que evalúa más de factor=2 candidatos.

Usando el parámetro aggressive_elimination, puedes forzar el proceso de búsqueda para que termine con menos de factor candidatos en la última iteración. Para ello, el proceso eliminará tantos candidatos como sea necesario utilizando min_resources recursos:

>>> sh = HalvingGridSearchCV(base_estimator, param_grid, cv=5,
...                            factor=2,
...                            max_resources=40,
...                            aggressive_elimination=True,
...                            ).fit(X, y)
>>> sh.n_resources_
[20, 20,  40]
>>> sh.n_candidates_
[6, 3, 2]

Observa que terminamos con 2 candidatos en la última iteración ya que hemos eliminado suficientes candidatos durante las primeras iteraciones, utilizando n_resources = min_resources = 20.

3.2.3.6. Analizar resultados con el atributo cv_results_

El atributo cv_results_ contiene información útil para analizar los resultados de una búsqueda. Se puede convertir en un dataframe de pandas con df = pd.DataFrame(est.cv_results_). El atributo cv_results_ de HalvingGridSearchCV y HalvingRandomSearchCV es similar al de GridSearchCV y RandomizedSearchCV, con información adicional relacionada con el proceso de reducción sucesiva a la mitad.

A continuación se muestra un ejemplo con algunas de las columnas de un dataframe (truncado):

iter

n_resources

mean_test_score

params

0

0

125

0.983667

{“criterion”: “entropy”, “max_depth”: None, “max_features”: 9, “min_samples_split”: 5}

1

0

125

0.983667

{“criterion”: “gini”, “max_depth”: None, “max_features”: 8, “min_samples_split”: 7}

2

0

125

0.983667

{“criterion”: “gini”, “max_depth”: None, “max_features”: 10, “min_samples_split”: 10}

3

0

125

0.983667

{“criterion”: “entropy”, “max_depth”: None, “max_features”: 6, “min_samples_split”: 6}

15

2

500

0.951958

{“criterion”: “entropy”, “max_depth”: None, “max_features”: 9, “min_samples_split”: 10}

16

2

500

0.947958

{“criterion”: “gini”, “max_depth”: None, “max_features”: 10, “min_samples_split”: 10}

17

2

500

0.951958

{“criterion”: “gini”, “max_depth”: None, “max_features”: 10, “min_samples_split”: 4}

18

3

1000

0.961009

{“criterion”: “entropy”, “max_depth”: None, “max_features”: 9, “min_samples_split”: 10}

19

3

1000

0.955989

{“criterion”: “gini”, “max_depth”: None, “max_features”: 10, “min_samples_split”: 4}

Cada fila corresponde a una combinación de parámetros dada (un candidato) y a una iteración determinada. La iteración viene dada por la columna iter. La columna n_resources indica el número de recursos utilizados.

En el ejemplo anterior, la mejor combinación de parámetros es {'criterion': 'entropy', 'max_depth': None, 'max_features': 9, 'min_samples_split': 10} ya que ha alcanzado la última iteración (3) con la mayor puntuación: 0.96.

Referencias:

1

K. Jamieson, A. Talwalkar, Non-stochastic Best Arm Identification and Hyperparameter Optimization, in proc. of Machine Learning Research, 2016.

2

L. Li, K. Jamieson, G. DeSalvo, A. Rostamizadeh, A. Talwalkar, Hyperband: A Novel Bandit-Based Approach to Hyperparameter Optimization, in Machine Learning Research 18, 2018.