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

LSTM mehrstufige hyperparameter Optimierung mit Keras Tuner

Unsere Blackbox wird von jemand anderem eingestellt - schön. Aber das kostet auch eine Menge Zeit und hat einen großen Kohlenstoff-Fußabdruck, nicht so schön.

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

In einem früheren Beitrag ging es um die Optimierung von Hyperparameter mit Talos. Ich konnte dies nicht mit meinem LSTM -Modell für univariate -Mehrschritt-Zeitreihenvorhersage zum Laufen bringen, wegen der 3D-Eingabe, also wechselte ich zu Keras Tuner.
In diesem Beitrag versuche ich, die nächste Periode einer Sinuswelle mit dem Hyperband -Abstimmungsalgorithmus vorherzusagen. Um die Abstimmzeit zu reduzieren, habe ich die Anzahl der hyperparameters, die optimiert werden können, reduziert und auch die möglichen Werte für jeden Parameter begrenzt.
Spoiler-Alarm: Nehmen Sie immer einen Optimierer wie Keras Tuner in Ihren Code auf, er wird Ihnen viel Zeit sparen.

Behalten Sie die hyperparameters an einem Ort

In den meisten Beispielen befinden sich die Parameter irgendwo im Code des Modells. Ich möchte, dass sie an einer Stelle stehen und habe einige Klassen hinzugefügt, um dies zu handhaben. Im Moment verwende ich nur die Option 'Choice' für die hyperparameters, die ein Array mit diskreten Werten annimmt. So habe ich das gemacht:

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

Jetzt können wir uns auf die Parameter beziehen, zum Beispiel als:

tpars.learning_rate.args

Hyperband und der Parameter batch_size

Standardmäßig können Sie für den Parameter batch_size keine "Wahl" angeben. Um dies zu tun, müssen wir die Klasse Hyperband unterklassifizieren, wie in 'How to tune the number of epochs and batch_size? #122' beschrieben, siehe Links unten. Wir stimmen die Epochen nicht ab, da Hyperband die Epochen für das Training über seine eigene Logik festlegt. Hyperparameters, die wir hinzugefügt und zur Abstimmung übergeben haben, müssen aus den Parametern, die an Hyperband übergeben werden, entfernt werden.

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

Erzeugung und Verarbeitung von Eingabedaten

Wir generieren die Zeitreihendaten mit der Funktion Python sin(). In diesem Beispiel werden die Eingabedaten in Trainingsdaten und Testdaten aufgeteilt. Wir erzeugen 4 Perioden für die Trainingsdaten und 2 Perioden für die Testdaten. Dann wird die nächste Periode vorhergesagt. Die Trainingsdaten werden auch zur Validierung verwendet (validation_split=0.33). Beachten Sie, dass wir die Eingabedaten beim Splitten nicht (!) mischen.

Wir verwenden n_steps_in=5 Eingabewerte. Um eine ganze Periode vorherzusagen, setzen wir die Anzahl der Vorhersageschritte, n_steps_out, gleich der Anzahl der Datenpunkte in einer Periode. Hier verwenden wir 20 Datenpunkte pro Periode. Weitere Informationen über LSTM univariate Mehrschrittprognosen finden Sie in dem Beitrag 'How to Develop LSTM Models for Time Series Forecasting', siehe Links unten.

Mit n_steps_in=5 und n_steps_out=20 haben wir einen "Block von 25 (n_steps_in=5 + n_steps_out=20) Datenpunkten", der sich über alle Datenpunkte erstreckt. Das bedeutet, dass wir nach der Konvertierung 120 - 25 = 95 Werte als Eingabedaten haben, und der Beginn des ersten Wertes liegt beim Offset 5 Schritte.

Wir machen eine Vorhersage mit den letzten n_steps_in-Werten unseres Datensatzes, dies ist unten dargestellt.

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

Der Code

Es gibt nicht viel mehr über den Code zu sagen. Wir verwenden EarlyStopping, ich weiß immer noch nicht, ob das eine gute oder schlechte Praxis ist. Natürlich verkürzt sich die Gesamtzeit eines Durchlaufs, aber wir verpassen vielleicht die besten Parameter. Aber so machen wir es, wenn wir die Deep Learning -Optimierung verwenden. Wir geben einige Werte vor und verwenden ein Ergebnis, das "gut genug" ist. Vergessen Sie nicht, den Code mit anderen Eingaben auszuführen.

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

Ergebnisse

Ich habe diesem Beitrag keine Diagramme beigefügt. Wenn Sie sie sehen möchten, führen Sie den Code aus. Beachten Sie, dass im letzten Diagramm die für die Trainingsdaten und die Testdaten vorhergesagten Werte angezeigt werden. Die vorhergesagten Trainingsdaten beginnen bei Schritt n_steps=5 und die vorhergesagten Testdaten enden bei (n_periods * period_points)=120 - n_steps_out=20, was korrekt ist. Die angeforderte Vorhersage beginnt bei Schritt 120 und hat 20 Schritte.

Das Ergebnis der Sinuswellenvorhersage ist für einen so kleinen Datensatz nicht schlecht. Als ich eine Steigung ausprobierte (siehe Code), war das Ergebnis der Vorhersage viel stärker verrauscht. Ich vermute, dass dies mit der begrenzten Anzahl von Parametern in der Optimierung und der Tatsache zu tun hat, dass die vorhergesagten Werte der Sinuswelle im Bereich der Trainings- und Testdaten liegen. Dennoch sieht die Vorhersage für die Steigung gut aus, und bei Verwendung von Mehrschrittverfahren erhalten wir mehr Werte und können einen Trend erkennen.

Energieverbrauch und Abstimmung des neuronalen Netzes

Ein Großteil des heutigen Energieverbrauchs entfällt auf Systeme, die neuronale Netze abstimmen. In dem Artikel "Maschinen brauchen viel Energie, um zu lernen - warum AI so stromhungrig ist" (siehe Links unten) wird erwähnt, dass das einmalige Trainieren von BERT (Bidirectional Encoder Representations from Transformers) den Kohlenstoff-Fußabdruck eines Passagiers verursacht, der einen Hin- und Rückflug zwischen New York und San Francisco unternimmt. Durch mehrmaliges Trainieren und Abstimmen wurden die Kosten zum Äquivalent von 315 Passagieren oder einem ganzen 747er-Jet.

Wie viele neuronale Netze werden täglich trainiert? Und wie viele sind wie BERT? Gehen wir von 12.000 BERT-ähnlichen Netzen aus, die alle zwei Monate optimiert werden (ich glaube, es sind viel mehr). Dann haben wir (12.000/60=) 200 volle 747, die jeden Tag zwischen New York und San Francisco hin und her fliegen! Und dann sind da noch die kleineren Netze. Hmmm ....

Zusammenfassung

Hyperparameter Tuning erschien mit Keras Tuner nicht schwierig, ich mag es sogar sehr, weil ich möchte, dass das neuronale Netz eine Blackbox ist. Bei Keras Tuner sind die Schalter und Schrauben immer noch da, aber jemand anderes stellt sie ein. Das ist schön. Aber der Abstimmungsprozess kostet viel Zeit und Energie. Leider gibt es im Moment keine andere Möglichkeit.

Links / Impressum

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

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.