Automatización de la optimización de hiperparámetros de Keras con Talos
Un modelo Deep Learning NO es una caja negra. Requiere un ajuste para obtener un buen rendimiento.
En los dos posts anteriores os mostré mis primeros pasos con Keras. Utilicé ejemplos encontrados en internet y cambié el conjunto de datos por algo trivial, es decir, que yo mismo genero los datos y conozco los valores esperados. Pero también os dije que no tenía ni idea de por qué parámetros como neuronas, epochs, batch_size tenían esos valores.
Así que lo que tenemos no es realmente una caja negra. Por fuera también hay algunos interruptores y tornillos que necesitan mucho nuestra atención. En este post estoy utilizando Talos, 'Hyperparameter Optimization for Keras, TensorFlow (tf.keras) and PyTorch', ver enlaces más abajo, que está destinado a automatizar el proceso de selección de los parámetros óptimos.
Al final de este post está el código para que puedas probarlo tú mismo.
Hiperparámetros
Los parámetros como las neuronas, las épocas y el tamaño del lote se denominan hiperparámetros, y su ajuste es esencial para un buen rendimiento del modelo. Hay algunos artículos interesantes en Internet sobre cómo afinar estos parámetros. Se puede llamar a esto el santo grial de las redes neuronales: la optimización de los hiperparámetros. Página web de Merriam-Webster: Santo Grial es un objeto o meta que se busca por su gran importancia. A menos que se sepa lo que se está haciendo, es fácil seleccionar parámetros no óptimos, o incluso totalmente erróneos.
Funciones de pérdida
Para realizar la optimización necesitamos valores que indiquen lo bien que funciona nuestro modelo. Estos valores se calculan mediante funciones de pérdida. Utilizamos diferentes funciones de pérdida para la regresión y la clasificación. Véase, por ejemplo, el artículo 'Overview of loss functions for Machine Learning', véanse los enlaces siguientes.
Funciones de pérdida de regresión:
- Mean Squared Error (MSE)
- Mean Absolute Error (MAE)
- Huber
- Log-Cosh
- Quantile
ClassFunciones de pérdida de la clasificación:
- Binary Cross Entropy
- Multi-Class Cross Entropy
Talos resumen
Me gusta la frase del artículo 'Hyperparameter Optimization with Keras', ver enlaces más abajo:
No te equivoques; INCLUSO CUANDO HACEMOS GET LA MÉTRICA DE PERFECCIÓN CORRECTA (sí, estoy gritando), tenemos que considerar lo que sucede en el proceso de optimización de un modelo.
Con Talos parametrizamos nuestro modelo. El número de combinaciones de epochs, batch_size, etc. puede ser enorme. Talos elige al azar un número de combinaciones y crea el nuevo modelo con los datos de entrenamiento y validación. Esto puede llevar minutos pero también horas o incluso días.
Una vez terminado podemos utilizar las puntuaciones, cambiar los valores de los parámetros y/o añadir algunos parámetros y volver a ejecutar. Mientras tanto podemos hacer otras cosas como tomar un café, hablar con un amigo, o incluso mejor, intentar aprender más sobre la optimización de Machine Learning . Veamos cómo funciona esto.
Ejemplo
Ejecutaré Talos para un modelo Neural Network muy sencillo basado en 'Keras 101: A simple (and interpretable) Neural Network model for House Pricing regression', ver enlaces más abajo.
El conjunto de datos se genera con esta función:
# define input sequence
def fx(x0, x1):
y = x0 + 2*x1
return y
El modelo:
model = Sequential()
# add layers
model.add(Dense(100, input_shape=(2,), activation='relu', name='layer_input'))
model.add(Dense(50, activation='relu', name='layer_hidden_1')
model.add(Dense(1, activation='linear', name='layer_output')
# compile
model.compile(optimizer='adam', loss='mse', metrics=['mean_absolute_error'])
Y la función de ajuste:
history = model.fit(X_train, y_train, epochs=100, validation_split=0.05)
Como te dije antes, no tengo ni idea de por qué los parámetros tienen estos valores. Vale, pues un poco.
Talos
Para usar esto en Talos sustituimos los parámetros hardcoded por variables que pueden ser controladas por Talos. Primero creamos un diccionario de parámetros con los valores que queremos variar. No empieces con todos los parámetros que quieras cambiar. Antes de que te des cuenta, estarás esperando y esperando. Además, empecé por ejemplo con el parámetro batch_size con dos valores que están muy alejados para tener una idea. Con los parámetros de abajo Talos hizo 16 ejecuciones y el tiempo total en mi PC (sin GPU) fue de 42 segundos.
# parameters
p = dict(
first_neuron=[24, 192],
activation=['relu', 'elu'],
epochs=[50, 200],
batch_size=[8, 32]
)
Entonces cambiamos el modelo para Talos:
model = Sequential()
# add layers
model.add(Dense(
params['first_neuron'],
input_shape=(2,),
activation=params['activation'],
name='layer_input')
)
model.add(Dense(
50,
activation=params['activation'],
name='layer_hidden_1')
)
model.add(Dense(
1,
activation='linear',
name='layer_output')
)
# compile
model.compile(
optimizer='adam',
loss='mse',
metrics=['mean_absolute_error'],
)
Y la función de ajuste pasa a ser
history = model.fit(
x=x_train,
y=y_train,
validation_data=[x_val, y_val],
epochs=params['epochs'],
batch_size=params['batch_size'],
verbose=0,
)
Ejecutar Talos y analizar
¡Es hora de correr! Después de la ejecución imprimo la tabla completa de resultados.
# perform scan
so = talos.Scan(
x=X,
y=y,
model=dlm.model2scan,
params=p,
experiment_name='model2scan',
val_split=0.3,
)
print('scan details {}'.format(so.details))
print('analyze ...')
a = talos.Analyze(so)
a_table = a.table('val_loss', sort_by='val_loss', exclude=['start', 'end', 'duration'])
print('a_table = \n{}'.format(a_table))
Aquí está la tabla. Tenga en cuenta que está ordenado por val_loss, lo que significa que la última fila es el ganador:
activation epochs round_epochs first_neuron val_mean_absolute_error mean_absolute_error batch_size val_loss loss
4 relu 50 50 24 7.786795 10.516150 32 94.053894 143.701889
12 elu 50 50 24 5.021943 2.713378 32 30.090796 10.598367
13 elu 50 50 192 2.880891 2.392172 32 10.355382 7.829942
5 relu 50 50 192 2.306647 1.435794 32 8.680595 3.228125
8 elu 50 50 24 2.205534 1.599179 8 5.929257 3.529320
9 elu 50 50 192 1.564395 0.991934 8 3.059430 1.329003
1 relu 50 50 192 0.813473 0.418985 8 1.315141 0.284857
14 elu 200 200 24 0.934512 0.557581 32 1.210560 0.448432
0 relu 50 50 24 0.680375 0.463401 8 0.818936 0.343270
15 elu 200 200 192 0.773358 0.466824 32 0.776512 0.313476
6 relu 200 200 24 0.510728 0.256091 32 0.515720 0.105524
7 relu 200 200 192 0.473588 0.219744 32 0.471352 0.076440
2 relu 200 200 24 0.562183 0.213781 8 0.431688 0.075045
3 relu 200 200 192 0.261547 0.062857 8 0.179677 0.006133
10 elu 200 200 24 0.327835 0.207104 8 0.140866 0.063864
11 elu 200 200 192 0.218479 0.116624 8 0.071611 0.028682
Mirando los datos vemos que epochs=200 da resultados mucho mejores que epochs=50. Podemos cambiarlo a 100 para ver si esto también es bueno. El batch_size=8 también da mejores resultados que batch_size=32. Podemos cambiarlo a 16 para ver si esto también es bueno. También podemos intentar reducir first_neuron. Los nuevos parámetros para Talos:
p = dict(
first_neuron=[128, 192],
activation=['relu', 'elu'],
epochs=[100, 200],
batch_size=[8, 16]
)
Vamos a volver a ejecutar con estos valores. El resultado:
batch_size activation epochs val_mean_absolute_error val_loss loss round_epochs first_neuron mean_absolute_error
13 16 elu 100 0.884498 1.053809 0.726826 100 192 0.706334
12 16 elu 100 0.779756 0.834097 0.669265 100 128 0.696979
4 16 relu 100 0.415210 0.235131 0.124713 100 128 0.291552
9 8 elu 100 0.395605 0.204896 0.157592 100 192 0.321956
5 16 relu 100 0.290187 0.109819 0.064319 100 192 0.211642
15 16 elu 200 0.234876 0.108920 0.070559 200 192 0.220270
14 16 elu 200 0.274542 0.107709 0.075080 200 128 0.216383
0 8 relu 100 0.280294 0.104701 0.049495 100 128 0.188951
1 8 relu 100 0.268457 0.100130 0.041598 100 192 0.160658
6 16 relu 200 0.228168 0.079410 0.033531 200 128 0.138397
11 8 elu 200 0.175497 0.070203 0.041988 200 192 0.154247
10 8 elu 200 0.150712 0.057377 0.021882 200 128 0.108372
8 8 elu 100 0.205943 0.055572 0.048045 100 128 0.187398
2 8 relu 200 0.182463 0.046856 0.018731 200 128 0.096890
7 16 relu 200 0.135524 0.025975 0.010142 200 192 0.073783
3 8 relu 200 0.078327 0.009304 0.004181 200 192 0.042721
Los mejores resultados mejoraron pero no tanto. El epochs=200 sigue siendo el mejor, así como el batch_size=8. Los parámetros finales son:
p = dict(
first_neuron=192,
activation='relu',
epochs=200,
batch_size=8,
)
Código completo
Abajo está el código por si quieres probar tú mismo. Seleccione 'run_optimizer' para ejecutar Talos. El conjunto de datos se divide en datos de entrenamiento y datos de prueba. Los datos de entrenamiento se dividen de nuevo en datos de entrenamiento y datos de validación.
Después de ejecutar el optimizador, puede introducir los nuevos valores de los parámetros en el modelo, generar gráficos, ejecutar la evaluación contra los datos de prueba y realizar algunas predicciones.
# optimizing keras hyperparameters with talos
from keras.models import Sequential, load_model
from keras.layers import Dense
import numpy as np
from plotly.subplots import make_subplots
import plotly.graph_objects as go
from sklearn.model_selection import train_test_split
import talos
# use plotly or pyplot
from matplotlib import pyplot
# your input: select talos optimizer or normal operation
run_optimizer = False
#run_optimizer = True
# your input: train model or use saved model
use_saved_model = False
#use_saved_model = True
# create dataset
def fx(x0, x1):
y = x0 + 2*x1
return y
X_items = []
y_items = []
for x0 in range(0, 18, 3):
for x1 in range(2, 27, 3):
y = fx(x0, x1)
X_items.append([x0, x1])
y_items.append(y)
X = np.array(X_items).reshape((-1, 2))
y = np.array(y_items)
print('X = {}'.format(X))
print('y = {}'.format(y))
X_data_shape = X.shape
print('X_data_shape = {}'.format(X_data_shape))
class DLM:
def __init__(
self,
model_name='my_model',
):
self.model_name = model_name
self.layer_input_shape=(2, )
# your input: final model parameters
self.params = dict(
# layers
first_neuron=192,
activation='relu',
# compile
# fit
val_split=0.3,
epochs=200,
batch_size=8,
verbose=0,
)
def data_split_train_test(
self,
X,
y,
):
self.X_train, self.X_test, self.y_train, self.y_test = train_test_split(X, y, test_size=0.3, random_state=1)
print('self.X_train = {}'.format(self.X_train))
print('self.X_test = {}'.format(self.X_test))
print('self.y_train = {}'.format(self.y_train))
print('self.y_test = {}'.format(self.y_test))
print('training data row count = {}'.format(len(self.y_train)))
print('test data row count = {}'.format(len(self.y_test)))
X_train_data_shape = self.X_train.shape
print('X_train_data_shape = {}'.format(X_train_data_shape))
def get_model(
self,
):
self.model = self.get_model_for_params(self.params)
return self.model
def get_model_for_params(self, params):
model = Sequential()
# add layers
model.add(Dense(
params['first_neuron'],
input_shape=self.layer_input_shape,
activation=params['activation'],
name='layer_input')
)
model.add(Dense(
50,
activation=params['activation'],
name='layer_hidden_1')
)
model.add(Dense(
1,
activation='linear',
name='layer_output')
)
# compile
model.compile(
optimizer='adam',
loss='mse',
metrics=['mean_absolute_error'],
)
return model
def model_summary(
self,
model,
):
model.summary()
def fit(
self,
model,
plot=False,
):
# split training data
X_train, X_val, y_train, y_val = train_test_split(self.X_train, self.y_train, test_size=self.params['val_split'], random_state=1)
print('X_train = {}'.format(X_train))
print('y_train = {}'.format(y_train))
print('X_val = {}'.format(X_val))
print('y_val = {}'.format(y_val))
print('training data row count = {}'.format(len(y_train)))
print('validation data row count = {}'.format(len(y_val)))
history = self.fit_model(model, X_train, y_train, X_val, y_val, self.params)
if plot:
fig = go.Figure()
fig.add_trace(go.Scattergl(y=history.history['loss'], name='Train'))
fig.add_trace(go.Scattergl(y=history.history['val_loss'], name='Valid'))
fig.update_layout(height=500, width=700, xaxis_title='Epoch', yaxis_title='Loss')
fig.show()
fig = go.Figure()
fig.add_trace(go.Scattergl(y=history.history['mean_absolute_error'], name='Train'))
fig.add_trace(go.Scattergl(y=history.history['val_mean_absolute_error'], name='Valid'))
fig.update_layout(height=500, width=700, xaxis_title='Epoch', yaxis_title='Mean Absolute Error')
fig.show()
pyplot.plot(history.history['loss'], label='Train')
pyplot.plot(history.history['val_loss'], label='Valid')
pyplot.legend()
#pyplot.show()
pyplot.savefig('ex_dl_loss.png')
return history
def fit_model(self, model, x_train, y_train, x_val, y_val, params):
history = model.fit(
x=x_train,
y=y_train,
validation_data=[x_val, y_val],
epochs=params['epochs'],
batch_size=params['batch_size'],
verbose=0,
)
return history
def model2scan(self, x_train, y_train, x_val, y_val, params):
model = self.get_model_for_params(params)
history = self.fit_model(model, x_train, y_train, x_val, y_val, params)
return history, model
def evaluate(
self,
model,
):
score = model.evaluate(self.X_test, self.y_test)
print('test data - loss = {}'.format(score[0]))
print('test data - mean absolute error = {}'.format(score[1]))
return score
def predict(
self,
model,
x0,
x1,
fx=None,
):
x = np.array([[x0, x1]]).reshape((-1, 2))
predictions = model.predict(x)
expected = ''
if fx is not None:
expected = ', expected = {}'.format(fx(x0, x1))
print('for x = {}, predictions = {}{}'.format(x, predictions, expected))
return predictions
def save_model(
self,
model,
):
model.save(self.model_name)
def load_saved_model(
self,
):
self.model = load_model(self.model_name)
return self.model
dlm = DLM()
if not run_optimizer:
# create & save or used saved
if use_saved_model:
model = dlm.load_saved_model()
else:
dlm.data_split_train_test(X, y)
model = dlm.get_model()
# remove plot=True for no plot
dlm.fit(model, plot=True)
dlm.evaluate(model)
dlm.save_model(model)
# predict
dlm.predict(model, 4, 17, fx=fx)
dlm.predict(model, 23, 79, fx=fx)
dlm.predict(model, 40, 33, fx=fx)
dlm.predict(model, 140, 68, fx=fx)
else:
# talos
# your input: parameters run 1
p = dict(
first_neuron=[24, 192],
activation=['relu', 'elu'],
epochs=[50, 200],
batch_size=[8, 32]
)
# your input: parameters run 2 (change p2 to p)
p2 = dict(
first_neuron=[128, 192],
activation=['relu', 'elu'],
epochs=[100, 200],
batch_size=[8, 16]
)
# perform scan
so = talos.Scan(
x=X,
y=y,
model=dlm.model2scan,
params=p,
experiment_name='model2scan',
val_split=0.3,
)
print('scan details {}'.format(so.details))
print('analyze ...')
a = talos.Analyze(so)
# dump table
a_table = a.table('val_loss', sort_by='val_loss', exclude=['start', 'end', 'duration'])
print('a_table = \n{}'.format(a_table))
Algunas reflexiones
¿Estamos realmente convergiendo en la dirección correcta? Se trata de un problema muy complejo. Tenemos un mundo de N dimensiones con altos y bajos en todas partes. Esto significa que el punto de partida puede ser muy importante. Al final siempre hay que hacer una corrida con tantos parámetros como sea posible. Si tienes un conjunto de datos grande, deberías empezar por reducirlo eligiendo muestras aleatorias.
Espero que quede claro que se necesita una muy buena comprensión de lo que se está haciendo. Pero un sistema que está optimizado para la velocidad (caro GPU) también ayuda mucho.
Resumen
Talos es una herramienta muy agradable que hace mucho trabajo para usted. La documentación podría ser mejorada pero no me quejo. Tiene muchas más funciones pero no las he probado todas. No pude hacer que mis ejemplos de univariate LSTM (de varios pasos) funcionaran con ella debido a la naturaleza más compleja del conjunto de datos. Hay que investigar más sobre esto.
¿Podemos optimizar el optimizador? Por supuesto. Como siguiente paso podríamos tomar los resultados de la tabla y hacer que se analicen automáticamente para hacer una nueva selección de los parámetros para la siguiente ejecución.
Enlaces / créditos
10 Hyperparameter optimization frameworks
https://towardsdatascience.com/10-hyperparameter-optimization-frameworks-8bc87bc8b7e3
5 Regression Loss Functions All Machine Learners Should Know
https://heartbeat.comet.ml/5-regression-loss-functions-all-machine-learners-should-know-4fb140e9d4b0
How do I choose the optimal batch size?
https://ai.stackexchange.com/questions/8560/how-do-i-choose-the-optimal-batch-size
How to Develop LSTM Models for Time Series Forecasting
https://machinelearningmastery.com/how-to-develop-lstm-models-for-time-series-forecasting
How to get reproducible results in keras
https://stackoverflow.com/questions/32419510/how-to-get-reproducible-results-in-keras
How to Manually Optimize Machine Learning Model Hyperparameters
https://machinelearningmastery.com/manually-optimize-hyperparameters
How to tune the number of epochs and batch_size in Keras-tuner?
https://kegui.medium.com/how-to-tune-the-number-of-epochs-and-batch-size-in-keras-tuner-c2ab2d40878d
Hyperparameter Optimization for Keras, TensorFlow (tf.keras) and PyTorch
https://github.com/autonomio/talos
Hyperparameter Optimization with Keras
https://towardsdatascience.com/hyperparameter-optimization-with-keras-b82e6364ca53
Keras 101: A simple (and interpretable) Neural Network model for House Pricing regression
https://towardsdatascience.com/keras-101-a-simple-and-interpretable-neural-network-model-for-house-pricing-regression-31b1a77f05ae
Overview of loss functions for Machine Learning
https://medium.com/analytics-vidhya/overview-of-loss-functions-for-machine-learning-61829095fa8a
What is batch size in neural network?
https://stats.stackexchange.com/questions/153531/what-is-batch-size-in-neural-network
Leer más
Deep Learning Machine Learning
Recientes
- Don't Repeat Yourself (DRY) con Jinja2
- SQLAlchemy, PostgreSQL, número máximo de filas por user
- Mostrar los valores en filtros dinámicos SQLAlchemy
- Transferencia de datos segura con cifrado de Public Key y pyNaCl
- rqlite: una alternativa de alta disponibilidad y dist distribuida SQLite
- ¿Debo migrar mi Docker Swarm a Kubernetes?
Más vistos
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados
- Usando PyInstaller y Cython para crear un ejecutable de Python
- Flask RESTful API validación de parámetros de solicitud con esquemas Marshmallow