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

LSTM optimisation en plusieurs étapes hyperparameter avec Keras Tuner

Notre boîte noire est réglée par quelqu'un d'autre, c'est bien. Mais cela prend aussi beaucoup de temps et a une grande empreinte carbone, ce qui n'est pas si bien.

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

Un message précédent traitait de l'optimisation de Hyperparameter avec Talos. Je n'ai pas réussi à faire fonctionner ce modèle avec mon modèle LSTM pour la prévision de séries temporelles à plusieurs étapes univariate , à cause de l'entrée 3D, et je suis donc passé à Keras Tuner.
Dans ce billet, j'essaie de prévoir la prochaine période d'une onde sinusoïdale en utilisant l'algorithme d'accord Hyperband . Pour réduire le temps d'accord, j'ai réduit le nombre de hyperparameter pouvant être optimisés et j'ai également limité les valeurs possibles pour chaque paramètre.
Spoiler alert : Incluez toujours un optimiseur comme Keras Tuner dans votre code, il vous fera gagner beaucoup de temps.

Gardez les hyperparameters au même endroit

Dans la plupart des exemples, les paramètres se trouvent quelque part dans le code du modèle. Je veux qu'ils soient à un seul endroit et j'ai ajouté quelques classes pour gérer cela. Pour l'instant, je n'utilise que l'option "Choix" pour les hyperparameters, qui prend un tableau avec des valeurs discrètes. Voici comment j'ai procédé :

# 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]),
    ],
)

Maintenant nous pouvons nous référer aux paramètres par exemple comme :

tpars.learning_rate.args

Hyperband et le paramètre batch_size .

Par défaut, vous ne pouvez pas spécifier un "Choix" pour le paramètre batch_size . Pour ce faire, nous devons sous-classer la classe Hyperband , comme décrit dans 'How to tune the number of epochs and batch_size? #122', voir les liens ci-dessous. Nous ne réglons pas les époques car Hyperband définit les époques d'entraînement par sa propre logique. Les paramètres de Hyperparameter que nous avons ajoutés et que nous transmettons pour le réglage doivent être retirés des paramètres transmis à 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)

Génération et traitement des données d'entrée

Nous générons les données de la série temporelle en utilisant la fonction sin() de Python . Dans l'exemple, nous divisons les données d'entrée en données d'entraînement et en données de test. Nous générons 4 périodes pour les données de formation et 2 périodes pour les données de test. Puis nous prédisons la période suivante. Les données d'apprentissage sont également utilisées pour la validation (validation_split=0.33). Notez que nous ne mélangeons pas ( !) les données d'entrée lors du fractionnement.

Nous utilisons n_steps_in=5 valeurs d'entrée. Pour prédire une période complète, nous fixons le nombre de pas de prédiction, n_steps_out, égal au nombre de points de données dans une période. Pour plus d'informations sur la prévision multi-étapes LSTM univariate , consultez le post 'Comment développer des modèles LSTM pour la prévision des séries temporelles', voir les liens ci-dessous.

Avec n_steps_in=5 et n_steps_out=20, nous avons un 'bloc de 25 (n_steps_in=5 + n_steps_out=20) points de données' qui se déplace sur tous les points de données. Cela signifie qu'après la conversion, nous avons 120 - 25 = 95 valeurs de données d'entrée, et le début de la première valeur est à un décalage de 5 pas.

Nous faisons une prédiction avec les dernières valeurs n_steps_in de notre jeu de données, ceci est illustré ci-dessous.

<--------------------- 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

Le code

Il n'y a pas grand chose de plus à dire sur le code. Nous utilisons EarlyStopping, je ne sais toujours pas si c'est une bonne ou une mauvaise pratique. Bien sûr, le temps total pris par une exécution est réduit, mais nous pouvons manquer les meilleurs paramètres. Mais c'est ainsi que nous procédons lorsque nous utilisons l'optimisation Deep Learning . Nous donnons quelques valeurs et utilisons un résultat qui est "suffisamment bon". N'oubliez pas d'exécuter le code avec d'autres entrées.

# 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))

Résultats

Je n'ai pas ajouté de graphiques à ce post, si vous voulez les voir, exécutez le code. Notez que dans le graphique final, nous montrons les valeurs prédites pour les données d'entraînement et les données de test. Les données d'entraînement prédites commencent à l'étape n_steps=5 et les données de test prédites se terminent à (n_periods * period_points)=120 - n_steps_out=20, ce qui est correct. La prédiction demandée commence à l'étape 120 et comporte 20 étapes.

Le résultat de la prédiction de l'onde sinusoïdale n'est pas mauvais pour un si petit ensemble de données. Lorsque j'ai essayé une pente, voir le code, le résultat de la prédiction a beaucoup plus de bruit. Je suppose que cela est dû au nombre limité de paramètres dans l'optimisation et au fait que les valeurs prédites de l'onde sinusoïdale sont dans la même fourchette que les données d'entraînement et de test. Pourtant, la prédiction de la pente semble bonne. En utilisant le multi-pas, nous obtenons plus de valeurs et pouvons voir une tendance.

Consommation d'énergie et réglage du réseau neuronal

Une grande partie de l'énergie consommée aujourd'hui l'est par les systèmes de réglage des réseaux neuronaux. L'article "Il faut beaucoup d'énergie aux machines pour apprendre - voici pourquoi AI est si gourmand en énergie", voir les liens ci-dessous, mentionne que la formation de BERT (Bidirectional Encoder Representations from Transformers) une seule fois a l'empreinte carbone d'un passager effectuant un aller-retour entre New York et San Francisco. En formant et en réglant plusieurs fois, le coût est devenu l'équivalent de 315 passagers, ou d'un jet 747 entier.

Combien de réseaux neuronaux sont formés chaque jour ? Et combien sont comme BERT ? Faisons un calcul basé sur 12.000 réseaux de type BERT optimisés tous les deux mois (je crois que c'est beaucoup plus). Nous avons alors (12.000/60=) 200 pleins 747 volant aller-retour entre New York et San Francisco chaque jour ! Et puis il y a aussi les petits réseaux. Hmmm ....

Résumé

Le réglage de Hyperparameter n'a pas semblé difficile avec Keras Tuner, en fait je l'aime beaucoup car je veux que le réseau neuronal soit une boîte noire. Avec Keras Tuner , les interrupteurs et les vis sont toujours là, mais quelqu'un d'autre les ajuste. C'est une bonne chose. Mais le processus de réglage prend beaucoup de temps et d'énergie. Malheureusement, il n'y a pas d'autre moyen pour le moment.

Liens / crédits

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

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.