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

LSTM многоэтапная оптимизация hyperparameter с Keras Tuner

Наш "черный ящик" настраивает кто-то другой, что приятно. Но это также занимает много времени и имеет большой углеродный след, что не так приятно.

13 февраля 2022
post main image
https://www.pexels.com/nl-nl/@life-of-pix

Предыдущее сообщение было посвящено оптимизации Hyperparameter с помощью Talos. Я не смог заставить это работать с моей моделью LSTM для многоэтапного прогнозирования временных рядов univariate из-за 3D-входа, поэтому я перешел на Keras Tuner.
В этом посте я пытаюсь предсказать следующий период синусоиды с помощью алгоритма настройки Hyperband . Чтобы уменьшить время работы тюнера, я уменьшил количество оптимизируемых hyperparameter, а также ограничил возможные значения для каждого параметра.
Спойлер: всегда включайте в свой код оптимизатор типа Keras Tuner , он сэкономит вам много времени.

Храните hyperparameter в одном месте.

В большинстве примеров параметры находятся где-то в коде модели. Я хочу, чтобы они были в одном месте, и добавил несколько классов для обработки этого. На данный момент я использую только параметр 'Choice' для hyperparameters, который принимает массив с дискретными значениями. Вот как я это сделал:

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

Теперь мы можем ссылаться на параметры, например, как:

tpars.learning_rate.args

Hyperband и параметр batch_size .

По умолчанию вы не можете указать 'Choice' для параметра batch_size . Для этого мы должны подклассифицировать класс Hyperband , как описано в 'How to tune the number of epochs and batch_size? #122', см. ссылки ниже. Мы не настраиваем эпохи, поскольку Hyperband устанавливает эпохи для обучения с помощью своей собственной логики. Параметры Hyperparameter, которые мы добавили и передали для настройки, должны быть удалены из параметров, передаваемых 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)

Генерация и обработка входных данных

Мы генерируем данные временного ряда с помощью функции Python sin(). В примере мы разделили входные данные на обучающие и тестовые. Мы генерируем 4 периода для обучающих данных и 2 периода для тестовых данных. А затем предсказываем следующий период. Обучающие данные также используются для валидации (validation_split=0.33). Обратите внимание, что мы не (!) перемешиваем входные данные при разбиении.

Мы используем n_steps_in=5 входных значений. Для прогнозирования полного периода мы задаем количество шагов прогнозирования, n_steps_out, равное количеству точек данных в периоде. Здесь мы используем 20 точек данных за период. Более подробную информацию о многошаговом прогнозировании LSTM univariate смотрите в статье "Как разработать LSTM модели для прогнозирования временных рядов", см. ссылки ниже.

При n_steps_in=5 и n_steps_out=20 мы имеем "блок из 25 (n_steps_in=5 + n_steps_out=20) точек данных", который смещается по всем точкам данных. Это означает, что после преобразования мы имеем 120 - 25 = 95 значений входных данных, а начало первого значения находится на смещении 5 шагов.

Мы делаем предсказание с последними 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

Код

Больше о коде рассказывать не нужно. Мы используем EarlyStopping, я до сих пор не знаю, хорошая это практика или плохая. Конечно, общее время, затрачиваемое на прогон, уменьшается, но мы можем упустить лучшие параметры. Но именно так мы поступаем при использовании оптимизации Deep Learning . Мы задаем некоторые значения и используем результат, который "достаточно хорош". Не забудьте запустить код с другими входными данными.

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

Результаты

Я не добавил графики в этот пост, если вы хотите их увидеть, запустите код. Обратите внимание, что на последнем графике мы показываем значения, предсказанные для данных обучения и тестовых данных. Прогнозируемые данные обучения начинаются на шаге n_steps=5, а прогнозируемые данные тестирования заканчиваются на (n_periods * period_points)=120 - n_steps_out=20, что правильно. Запрошенное предсказание начинается с шага 120 и имеет 20 шагов.

Результат предсказания синусоиды неплох для такого маленького набора данных. Когда я попробовал наклонную волну, см. код, результат предсказания имеет гораздо больше шума. Думаю, это связано с ограниченным количеством параметров в оптимизации и с тем, что предсказанные значения синусоиды находятся в том же диапазоне, что и обучающие и тестовые данные. Тем не менее, предсказание для наклона выглядит хорошо, используя многошаговый метод, мы получаем больше значений и можем видеть тенденцию.

Энергопотребление и настройка нейронной сети

В наше время много энергии потребляют системы, настраивающие нейронные сети. В статье "Для обучения машин требуется много энергии - вот почему AI так энергоемка" (см. ссылки ниже) упоминается, что обучение BERT (Bidirectional Encoder Representations from Transformers) только один раз имеет углеродный след пассажира, совершающего перелет в обе стороны между Нью-Йорком и Сан-Франциско. При многократном обучении и настройке затраты становятся эквивалентными 315 пассажирам или целому самолету 747.

Сколько нейронных сетей обучается каждый день? И сколько из них подобны BERT? Давайте сделаем расчет, исходя из того, что 12.000 сетей типа BERT оптимизируются каждые два месяца (я считаю, что это гораздо больше). Тогда у нас есть (12.000/60=) 200 полных 747-х самолетов, летающих в обе стороны между Нью-Йорком и Сан-Франциско каждый день! А ведь есть еще и более мелкие сети. Хммм ....

Резюме

Настройка Hyperparameter оказалась несложной, а Keras Tuner, наоборот, мне очень нравится, потому что я хочу, чтобы нейронная сеть была черным ящиком. С Keras Tuner переключатели и винты на ней все еще на месте, но настраивает их кто-то другой. Это хорошо. Но процесс настройки занимает много времени и энергии. К сожалению, на данный момент другого пути нет.

Ссылки / кредиты

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

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.