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

LSTM meerstappen-optimalisatie hyperparameter met Keras Tuner

Onze zwarte doos wordt door iemand anders afgesteld, leuk. Maar dit kost ook veel tijd en heeft een grote koolstofvoetafdruk, niet zo leuk.

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

Een eerdere post ging over Hyperparameter optimalisatie met Talos. Ik kon dit niet werkend krijgen met mijn LSTM model voor univariate multi-step tijdreeksvoorspelling, vanwege de 3D invoer, dus ben ik overgestapt op Keras Tuner.
In deze post probeer ik de volgende periode van een sinusgolf te voorspellen met behulp van het Hyperband afstemalgoritme. Om de afstemtijd te verkorten heb ik het aantal hyperparameters dat geoptimaliseerd kan worden verminderd en ook de mogelijke waarden voor elke parameter beperkt.
Spoiler alert: Neem altijd een optimizer zoals Keras Tuner in je code op, het zal je een hoop tijd besparen.

Hou de hyperparameters op één plaats

Voorbeelden hebben meestal de parameters ergens in de code van het model. Ik wil ze op één plaats hebben en heb een aantal klassen toegevoegd om dit af te handelen. Op dit moment gebruik ik alleen de 'Choice' optie voor de hyperparameters, die een array met discrete waarden neemt. Hier is hoe ik dit gedaan heb:

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

Nu kunnen we naar de parameters verwijzen als bijvoorbeeld:

tpars.learning_rate.args

Hyperband en de parameter batch_size

Standaard kan geen "Keuze" worden opgegeven voor de parameter batch_size . Om dit te doen moeten we de Hyperband klasse subklassen, zoals beschreven in 'How to tune the number of epochs and batch_size? #122', zie onderstaande links. We stemmen de epochs niet af omdat Hyperband de epochs om te trainen via zijn eigen logica instelt. Hyperparameters die we hebben toegevoegd en doorgeven voor afstemming moeten worden verwijderd uit de parameters die worden doorgegeven aan 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)

Inputgegevens genereren en verwerken

Wij genereren de tijdreeksgegevens met behulp van de functie Python sin(). In het voorbeeld splitsen we de invoergegevens in trainingsgegevens en testgegevens. We genereren 4 periodes voor de trainingsdata en 2 periodes voor de testdata. En voorspellen dan de volgende periode. De training data wordt ook gebruikt voor validatie (validation_split=0.33). Merk op dat we de invoergegevens niet (!) husselen bij het splitsen.

Wij gebruiken n_steps_in=5 invoerwaarden. Om een volledige periode te voorspellen stellen we het aantal voorspellingsstappen, n_steps_out, gelijk aan het aantal datapunten in een periode. Hier gebruiken we 20 datapunten per periode. Voor meer informatie over LSTM univariate multi-step forecasting, zie de post 'How to Develop LSTM Models for Time Series Forecasting', zie links hieronder.

Met n_steps_in=5 en n_steps_out=20 hebben we een 'blok van 25 (n_steps_in=5 + n_steps_out=20) datapunten' dat verschuift over alle datapunten. Dit betekent dat we na conversie 120 - 25 = 95 waarden invoergegevens hebben, en dat het begin van de eerste waarde op offset 5 stappen ligt.

We maken een voorspelling met de laatste n_steps_in waarden van onze dataset, dit wordt hieronder geïllustreerd.

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

De code

Er is niet veel meer te vertellen over de code. We gebruiken EarlyStopping, ik weet nog steeds niet of dit een goede of slechte praktijk is. Natuurlijk wordt de totale tijd van een run korter, maar we missen misschien de beste parameters. Maar dit is hoe we dit doen wanneer we Deep Learning optimalisatie gebruiken. We geven enkele waarden op en gebruiken een resultaat dat 'goed genoeg' is. Vergeet niet de code uit te voeren met andere invoer.

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

Resultaten

Ik heb geen plots toegevoegd aan deze post, als je ze wil zien, moet je de code uitvoeren. Merk op dat we in de laatste plot voorspelde waarden tonen voor de trainingsdata en de testdata. De voorspelde trainingsdata begint bij stap n_steps=5 en en de voorspelde testdata eindigt bij (n_periods * period_points)=120 - n_steps_out=20 wat correct is. De gevraagde voorspelling begint bij stap 120 en heeft 20 stappen.

Het resultaat van de sinusvoorspelling is niet slecht voor zo'n kleine dataset. Toen ik een helling probeerde, zie code, heeft het voorspellingsresultaat veel meer ruis. Ik denk dat dit te maken heeft met het beperkte aantal parameters in de optimalisatie en het feit dat de voorspelde waarden van de sinus in het bereik liggen van de training en test data. Toch ziet de voorspelling voor de helling er goed uit, bij gebruik van multi-step krijgen we meer waarden en kunnen we een trend zien.

Energieverbruik en neuraal netwerk tuning

Een groot deel van het energieverbruik wordt tegenwoordig gebruikt door systemen die neurale netwerken afstemmen. Het artikel 'It takes a lot of energy for machines to learn - here's why AI is so power-hungry', zie links hieronder, vermeldt dat het slechts eenmaal trainen van BERT (Bidirectional Encoder Representations from Transformers) de CO2-voetafdruk heeft van een passagier die een retourtje vliegt tussen New York en San Francisco. Door meerdere keren te trainen en af te stellen, werden de kosten het equivalent van 315 passagiers, of een hele 747 jet.

Hoeveel neurale netwerken worden er elke dag getraind? En hoeveel zijn er zoals BERT? Laten we een berekening maken op basis van 12.000 BERT-achtige netwerken die elke twee maanden worden geoptimaliseerd (ik geloof dat het veel meer is). Dan hebben we (12.000/60=) 200 volle 747's die elke dag heen en weer vliegen tussen New York en San Francisco! En dan zijn er ook nog de kleinere netwerken. Hmmm ....

Samenvatting

Hyperparameter tuning bleek niet moeilijk met Keras Tuner, in feite vind ik het erg prettig omdat ik het neurale netwerk een black box wil laten zijn. Bij Keras Tuner zitten de schakelaars en schroefjes er nog op maar iemand anders stelt ze in. Dat is prettig. Maar het afstemmen kost veel tijd en energie. Helaas is er op dit moment geen andere manier.

Links / credits

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

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.