angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

LSTM optimización multipaso hyperparameter con Keras Tuner

Nuestra caja negra es ajustada por otra persona, lo cual es agradable. Pero esto también lleva mucho tiempo y tiene una gran huella de carbono, no tan agradable.

13 febrero 2022
post main image
https://www.pexels.com/nl-nl/@life-of-pix

Un post anterior trataba de la optimización de Hyperparameter con Talos. No pude conseguir que esto funcionara con mi modelo LSTM para la previsión de series temporales multipaso univariate , debido a la entrada 3D, así que cambié a Keras Tuner.
En este post intento predecir el próximo periodo de una onda sinusoidal utilizando el algoritmo de sintonización Hyperband . Para reducir el tiempo de sintonización reduje el número de hyperparameters que pueden ser optimizados y también limité los valores posibles para cada parámetro.
Alerta de spoiler: Incluye siempre un optimizador como Keras Tuner en tu código, te ahorrará mucho tiempo.

Mantén los hyperparameter en un solo lugar

Los ejemplos en la mayoría de las veces tienen los parámetros en algún lugar en el código del modelo. Quiero que estén en un solo lugar y he añadido algunas clases para manejar esto. Por el momento sólo estoy usando la opción 'Choice' para el hyperparameters, que toma un array con valores discretos. Aquí es cómo lo hice:

# tuner parameters
class TunerParameter:
    def __init__(self, name, val):
        self.name = name
        self.val = val
        self.args = (name, val)

class TunerParameters:
    def __init__(self, pars=None):
        self.tpars = []
        for p in pars:
            tpar = TunerParameter(p[0], p[1])
            self.tpars.append(tpar)
            setattr(self, p[0], tpar)
    def get_pars(self):
        return self.tpars

# hyperparameters, name and value, are the inputs for hp.Choice()
tpars = TunerParameters(
    pars=[
        ('first_neuron', [64, 128]),
        ('second_neuron', [64, 128]),
        ('third_neuron', [64, 128]),
        ('learning_rate', [1e-4, 1e-2]),
        ('batch_size', [8, 32]),
    ],
)

Ahora podemos referirnos a los parámetros por ejemplo como:

tpars.learning_rate.args

Hyperband y el parámetro batch_size

Por defecto no se puede especificar una 'Elección' para el parámetro batch_size . Para ello debemos subclasificar la clase Hyperband , como se describe en 'How to tune the number of epochs and batch_size? #122', ver enlaces más abajo. No ajustamos las épocas porque Hyperband establece las épocas para las que entrenar a través de su propia lógica. Los Hyperparameter que añadimos y pasamos para el ajuste deben ser eliminados de los parámetros que se pasan a Hyperband.

# subclass tuner to add hyperparameters (here, batch_size)
class MyTuner(kt.tuners.Hyperband):
    def __init__(self, *args, **kwargs):
        self.tpars = None
        if 'tpars' in kwargs:
            self.tpars = kwargs.pop('tpars')
        super(MyTuner, self).__init__(*args, **kwargs)

    def run_trial(self, trial, *args, **kwargs):
        if self.tpars is not None:
            for tpar in self.tpars:
                kwargs[tpar.name] = trial.hyperparameters.Choice(tpar.name, tpar.val)
        return super(MyTuner, self).run_trial(trial, *args, **kwargs)

Generación y procesamiento de los datos de entrada

Generamos los datos de la serie temporal utilizando la función Python sin(). En el ejemplo, dividimos los datos de entrada en datos de entrenamiento y datos de prueba. Generamos 4 periodos para los datos de entrenamiento y 2 periodos para los datos de prueba. Y luego predecimos el siguiente periodo. Los datos de entrenamiento también se utilizan para la validación (validation_split=0,33). Tenga en cuenta que no (!) barajamos los datos de entrada al dividirlos.

Utilizamos n_steps_in=5 valores de entrada. Para predecir un periodo completo, establecemos el número de pasos de predicción, n_pasos_fuera, igual al número de puntos de datos de un periodo. Aquí utilizamos 20 puntos de datos por período. Para obtener más información sobre LSTM univariate previsión de varios pasos, consulte el post 'Cómo desarrollar modelos LSTM para la previsión de series temporales', consulte los enlaces siguientes.

Con n_pasos_in=5 y n_pasos_out=20 tenemos un 'bloque de 25 (n_pasos_in=5 + n_pasos_out=20) puntos de datos' que se desplaza sobre todos los puntos de datos. Esto significa que después de la conversión, tenemos 120 - 25 = 95 valores de datos de entrada, y el inicio del primer valor está en el desplazamiento 5 pasos.

Hacemos una predicción con los últimos valores de n_pasos_in de nuestro conjunto de datos, esto se ilustra a continuación.

<--------------------- input data -------------------->

<----------- training data ---------><---test data --->

    0        1        2        3        4        5    
|--------|--------|--------|--------|--------|--------|
                                       
0                                                    120 datapoints


blocks of n_steps_in + n_steps_out

.....iiiioooooooooo....................................


to make a prediction we need the last n_steps_in values: 

                                                           6
                                                      |--------|
                                       
                                                  iiii

El código

No hay mucho más que contar sobre el código. Usamos EarlyStopping, todavía no sé si esto es una buena o mala práctica. Por supuesto, se reduce el tiempo total de una ejecución, pero podemos perder los mejores parámetros. Pero así es como lo hacemos cuando usamos la optimización Deep Learning . Damos algunos valores y utilizamos un resultado que sea "suficientemente bueno". No olvides ejecutar el código con otras entradas.

# optimizing hyperparameters with keras tuner
from keras.callbacks import EarlyStopping
from keras.models import Sequential
from keras.layers import Dense, LSTM
import keras_tuner as kt
import numpy as np
import plotly.graph_objects as go
from tensorflow.keras.optimizers import Adam
from sklearn.model_selection import train_test_split
import sys

# tuner parameters
class TunerParameter:
    def __init__(self, name, val):
        self.name = name
        self.val = val
        self.args = (name, val)

class TunerParameters:
    def __init__(self, pars=None):
        self.tpars = []
        for p in pars:
            tpar = TunerParameter(p[0], p[1])
            self.tpars.append(tpar)
            setattr(self, p[0], tpar)
    def get_pars(self):
        return self.tpars

# hyperparameters name and value, are the inputs for hp.Choice()
tpars = TunerParameters(
    pars=[
        ('first_neuron', [64, 128]),
        ('second_neuron', [64, 128]),
        ('third_neuron', [64, 128]),
        ('learning_rate', [1e-4, 1e-2]),
        ('batch_size', [8, 32]),
    ],
)

# split a univariate sequence into samples
# see:
# How to Develop LSTM Models for Time Series Forecasting
# https://machinelearningmastery.com/how-to-develop-lstm-models-for-time-series-forecasting
def split_sequence(sequence, n_steps_in, n_steps_out):
    X, y = list(), list()
    for i in range(len(sequence)):
        # find the end of this pattern
        end_ix = i + n_steps_in
        out_end_ix = end_ix + n_steps_out
        # check if we are beyond the sequence
        if out_end_ix > len(sequence):
            break
        # gather input and output parts of the pattern
        seq_x, seq_y = sequence[i:end_ix], sequence[end_ix:out_end_ix]
        X.append(seq_x)
        y.append(seq_y)
    return np.array(X), np.array(y)

# choose a number of time steps, prediction = 20 steps
n_steps_in = 5
n_steps_out = 20

# train:test = 2:1 (0.33 split)
n_periods_train = 4
n_periods_test = 2
# total periods
n_periods = n_periods_train + n_periods_test
# data points per period, predict a full period (n_steps_out)
period_points = n_steps_out
# generate sine wave data points
xs = np.linspace(0, n_periods * 2 * np.pi, n_periods * period_points)
raw_seq = np.sin(xs)

# plot the input data
dp = [i for i in range(len(raw_seq))]
fig = go.Figure()
fig.add_trace(go.Scattergl(y=raw_seq, x=dp, name='Sin'))
fig.update_layout(height=500, width=700, xaxis_title='datapoints', yaxis_title='Sine wave')
fig.show()

'''
# another dataset you may want try 
raw_seq = []
for n in range(0, n_periods * period_points):
    raw_seq.append(n)
print('raw_seq = {}'.format(raw_seq))
print('len(raw_seq) = {}'.format(len(raw_seq)))
'''

# split into samples
X, y = split_sequence(raw_seq, n_steps_in, n_steps_out)
# reshape from [samples, timesteps] into [samples, timesteps, features]
n_features = 1
X = X.reshape((X.shape[0], X.shape[1], n_features))

X_len = len(X)
y_len = len(y)
print('X_len = {}'.format(X_len))
print('y_len = {}'.format(y_len))

x_pred = np.array(raw_seq[-1*n_steps_in:])
x_pred = x_pred.reshape((1, n_steps_in, n_features))

class DLM:
    
    def __init__(
        self,
        n_steps_in=None,
        n_steps_out=None,
        n_features=None,
        tpars=None,
    ):
        self.n_steps_in = n_steps_in
        self.n_steps_out = n_steps_out
        self.n_features = n_features
        self.tpars = tpars
        # input_shape
        self.layer_input_shape = (self.n_steps_in, self.n_features)
        print('layer_input_shape = {}'.format(self.layer_input_shape))

    def data_split(self, X, y):
        X0, X1, y0, y1 = train_test_split(X, y, test_size=0.33, shuffle=False)
        return X0, X1, y0, y1

    def get_model(self, hp):
        model = Sequential()
        # add layers
        a = ('first_neuron', [64, 128])

        model.add(LSTM(
            hp.Choice(*self.tpars.first_neuron.args),
            activation='relu',
            return_sequences=True,
            input_shape=self.layer_input_shape,
        ))
        model.add(LSTM(
            hp.Choice(*self.tpars.second_neuron.args),
            activation='relu',
            return_sequences=True,
        ))
        model.add(LSTM(
            hp.Choice(*self.tpars.third_neuron.args),
            activation='relu',
        ))
        model.add(Dense(n_steps_out))

        # compile
        model.compile(
            optimizer=Adam(learning_rate=hp.Choice(*self.tpars.learning_rate.args)),
            loss='mean_absolute_error',
            metrics=['mean_absolute_error'],
            run_eagerly=True,
        )
        return model

    def get_X_pred(self, x):
        a = []
        for i in range(0, self.n_steps_in):
            a.append(x[i][0])
        a = [x[i][0] for i in range(0, self.n_steps_in)]
        X_pred = np.array(a)
        X_pred = X_pred.reshape((1, self.n_steps_in, self.n_features))
        return X_pred

    def get_plot_data(self, model, X, y, x_offset):
        x_plot = []
        y_plot = []
        y_predict_plot = []
        for i, x in enumerate(X):
            y_plot.append(y[i][0])
            X_pred = self.get_X_pred(x)
            predictions = model.predict(X_pred)
            y_predict_plot.append(predictions[0][0])
        x_plot = [x + x_offset for x in range(len(X))]
        return x_plot, y_plot, y_predict_plot


# subclass tuner to add hyperparameters (here, batch_size)
class MyTuner(kt.tuners.Hyperband):
    def __init__(self, *args, **kwargs):
        self.tpars = None
        if 'tpars' in kwargs:
            self.tpars = kwargs.pop('tpars')
        super(MyTuner, self).__init__(*args, **kwargs)

    def run_trial(self, trial, *args, **kwargs):
        if self.tpars is not None:
            for tpar in self.tpars:
                kwargs[tpar.name] = trial.hyperparameters.Choice(tpar.name, tpar.val)
        return super(MyTuner, self).run_trial(trial, *args, **kwargs)


dlm = DLM(
    n_steps_in=n_steps_in,
    n_steps_out=n_steps_out,
    n_features=n_features,
    tpars=tpars,
)

# split data 
X_train, X_test, y_train, y_test = dlm.data_split(X, y)
print('len(X_train) = {}, len(X_test) = {}, X0_shape = {}'.format(len(X_train), len(X_test), X_train.shape))

# use subclassed HyperBand tuner
tuner = MyTuner(
    dlm.get_model,
    objective=kt.Objective('val_mean_absolute_error', direction='min'),
    allow_new_entries=True,
    tune_new_entries=True,
    hyperband_iterations=2,
    max_epochs=260,
    directory='keras_tuner_dir',
    project_name='keras_tuner_demo',
    tpars=[tpars.batch_size],
)

print('\n{}\ntuner.search_space_summary()\n{}\n'.format('-'*60, '-'*60))
tuner.search_space_summary()

tuner.search(
    X_train,
    y_train,
    validation_split=0.33,
    callbacks=[EarlyStopping('val_loss', patience=3)]
)

print('\n{}\ntuner.results_summary()\n{}\n'.format('-'*60, '-'*60))
tuner.results_summary()

print('\n{}\nbest_hps\n{}\n'.format('-'*60, '-'*60))
best_hps = tuner.get_best_hyperparameters()[0]
for tpar in tpars.get_pars():
    print('- {} = {}'.format(tpar.name, best_hps[tpar.name]))

h_model = tuner.hypermodel.build(best_hps)
print('\n{}\nh_model.summary()\n{}\n'.format('-'*60, '-'*60))
h_model.summary()

# plot test data performance
num_epochs = 100
history = h_model.fit(X_test, y_test, epochs=num_epochs, validation_split=0.33)

fig = go.Figure()
fig.add_trace(go.Scattergl(y=history.history['mean_absolute_error'], name='Test'))
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()

# plot train using predict(), test using predict(), and actual prediction
x_offset = n_steps_in
x_train_plot, y_train_plot, y_train_predict_plot = dlm.get_plot_data(h_model, X_train, y_train, x_offset)
x_offset += len(x_train_plot)
x_test_plot, y_test_plot, y_test_predict_plot = dlm.get_plot_data(h_model, X_test, y_test, x_offset)
x_offset += len(x_test_plot)
# use x_pred to get the prediction and show the n_steps_out
predictions = h_model.predict(x_pred)
y_predict_plot = predictions[0]
x_offset = n_periods * period_points
x_predict_plot = [x + x_offset for x in range(n_steps_out)]

fig = go.Figure()
fig.add_trace(go.Scattergl(y=y_train_predict_plot, x=x_train_plot, name='Train-pred'))
fig.add_trace(go.Scattergl(y=y_test_predict_plot, x=x_test_plot, name='Test-pred'))
fig.add_trace(go.Scattergl(y=y_predict_plot, x=x_predict_plot, name='Prediction'))
fig.update_layout(height=500, width=700, xaxis_title='Epoch', yaxis_title='Train, Test and Prediction')
fig.show()

h_eval_dict = h_model.evaluate(X_test, y_test, return_dict=True)
print('h_eval_dict = {}.'.format(h_eval_dict))

Resultados

No he añadido gráficos a este post, si quieres verlos, ejecuta el código. Ten en cuenta que en el gráfico final mostramos los valores predichos para los datos de entrenamiento y los datos de prueba. Los datos de entrenamiento predichos comienzan en el paso n_pasos=5 y los datos de prueba predichos terminan en (n_periodos * puntos_periodos)=120 - n_pasos_fuera=20 lo cual es correcto. La predicción solicitada comienza en el paso 120 y tiene 20 pasos.

El resultado de la predicción de la onda sinusoidal no está mal para un conjunto de datos tan pequeño. Cuando probé una pendiente, ver código, el resultado de la predicción tiene mucho más ruido. Supongo que esto tiene que ver con el número limitado de parámetros en la optimización y el hecho de que los valores predichos de la onda sinusoidal están en el rango de los datos de entrenamiento y prueba. Aun así, la predicción de la pendiente parece buena, ya que utilizando el paso múltiple obtenemos más valores y podemos ver una tendencia.

Consumo de energía y ajuste de la red neuronal

Gran parte del consumo de energía actual se lo llevan los sistemas que afinan las redes neuronales. El artículo 'It takes a lot of energy for machines to learn - here's why AI is so power-hungry', ver enlaces más abajo, menciona que entrenar BERT (Bidirectional Encoder Representations from Transformers) sólo una vez tiene la huella de carbono de un pasajero volando un viaje de ida y vuelta entre Nueva York y San Francisco. Al entrenar y afinar múltiples veces, el coste se convirtió en el equivalente a 315 pasajeros, o un jet 747 entero.

¿Cuántas redes neuronales se entrenan cada día? ¿Y cuántas son como BERT? Hagamos un cálculo basado en que 12.000 redes similares al BERT se optimizan cada dos meses (creo que es mucho más). Entonces tenemos (12.000/60=) ¡200 747 llenos volando de ida y vuelta entre Nueva York y San Francisco cada día! Y también están las redes más pequeñas. Hmmm ....

Resumen

Hyperparameter la sintonización no parecía difícil con Keras Tuner, de hecho me gusta mucho porque quiero que la red neuronal sea una caja negra. Con el Keras Tuner los interruptores y tornillos de la misma siguen estando ahí, pero es otra persona la que los ajusta. Eso está bien. Pero el proceso de ajuste requiere mucho tiempo y energía. Desgraciadamente, no hay otra manera por el momento.

Enlaces / créditos

How to Develop LSTM Models for Time Series Forecasting
https://machinelearningmastery.com/how-to-develop-lstm-models-for-time-series-forecasting

How to tune the number of epochs and batch_size? #122
https://github.com/keras-team/keras-tuner/issues/122

It takes a lot of energy for machines to learn – here's why AI is so power-hungry
https://theconversation.com/it-takes-a-lot-of-energy-for-machines-to-learn-heres-why-ai-is-so-power-hungry-151825

KerasTuner API
https://keras.io/api/keras_tuner

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.