Руководство для новичков по использованию байесовской оптимизации с помощью Scikit-Optimize

В парадигме машинного обучения и глубокого обучения параметры модели и гиперпараметры - это два часто используемых термина, где параметры определяют переменные конфигурации, которые являются внутренними для модели и значения которых могут быть оценены на основе данных обучения, а гиперпараметры определяют переменные конфигурации, которые являются внешними по отношению к модели и значения которых нельзя оценить по обучающим данным (В чем разница между параметром и гиперпараметром?). Таким образом, практикующему специалисту необходимо вручную присвоить значения гиперпараметров.

Каждая модель машинного обучения и глубокого обучения, которую мы создаем, имеет другой набор значений гиперпараметров, которые необходимо настроить, чтобы получить удовлетворительный результат. По сравнению с моделями машинного обучения модели глубокого обучения, как правило, имеют большее количество гиперпараметров, которые необходимо оптимизировать для получения желаемых прогнозов из-за своей архитектурной сложности по сравнению с типичными моделями машинного обучения.

Неоднократные эксперименты с различными комбинациями значений вручную для получения оптимальных значений гиперпараметров для каждого из этих гиперпараметров могут быть очень трудоемкой и утомительной задачей, требующей хорошей интуиции, большого опыта и глубокого понимания модели. Более того, для некоторых значений гиперпараметров могут потребоваться непрерывные значения, которые будут иметь неопределенное количество возможностей, и даже если гиперпараметры требуют дискретного значения, количество возможностей огромно, поэтому вручную выполнить эту задачу довольно сложно. Сказав все это, оптимизация гиперпараметров может показаться сложной задачей, но благодаря нескольким библиотекам, которые легко доступны в киберпространстве, эта задача стала более простой. Эти библиотеки помогают с меньшими усилиями реализовать различные алгоритмы оптимизации гиперпараметров. Несколько таких библиотек: Scikit-Optimize, Scikit-Learn и Hyperopt.

Существует несколько алгоритмов оптимизации гиперпараметров, которые часто использовались на протяжении многих лет, это поиск по сетке, случайный поиск и автоматизированные методы оптимизации гиперпараметров. Как Grid Search, так и Random Search создают сетку гиперпараметров, но в Grid Search каждая отдельная комбинация значений будет исчерпывающе исследована, чтобы найти комбинацию значений гиперпараметров, которая дает наилучшие значения точности, что делает этот метод очень неэффективным. С другой стороны, случайный поиск будет многократно выбирать случайные комбинации из сетки до тех пор, пока не будет достигнуто указанное количество итераций, и будет доказано, что он дает лучшие результаты по сравнению с поиском по сетке. Однако, даже несмотря на то, что ему удается дать хорошую комбинацию гиперпараметров, мы не можем быть уверены, что это действительно лучшая комбинация. Автоматическая оптимизация гиперпараметров использует различные методы, такие как байесовская оптимизация, которая выполняет управляемый поиск лучших гиперпараметров (Настройка гиперпараметров с использованием сетки и случайного поиска). Исследования показали, что байесовская оптимизация может дать лучшие комбинации гиперпараметров, чем случайный поиск (Байесовская оптимизация для настройки гиперпараметров).

В этой статье мы предоставим пошаговое руководство по выполнению задачи оптимизации гиперпараметров в модели глубокого обучения с использованием байесовской оптимизации, которая использует гауссовский процесс. Для выполнения этой задачи мы использовали пакет gp_minimize из библиотеки Scikit-Optimize (skopt). Оптимизацию гиперпараметров мы будем выполнять на простой модели прогнозирования цены закрытия акций, разработанной с помощью TensorFlow.

Scikit-Optimize (скопт)

Scikit-Optimize - это библиотека, которая относительно проста в использовании, чем другие библиотеки оптимизации гиперпараметров, а также имеет лучшую поддержку сообщества и документацию. Эта библиотека реализует несколько методов последовательной оптимизации на основе моделей за счет сокращения дорогостоящих и шумных функций черного ящика. Для получения дополнительной информации вы можете обратиться к статье neptune.ai, где они провели всесторонний анализ возможностей и использования skopt.

Байесовская оптимизация с использованием гауссовского процесса

Байесовская оптимизация - одна из многих функций, которые предлагает skopt. Байесовская оптимизация находит апостериорное распределение как функцию, которая должна быть оптимизирована во время оптимизации параметров, затем использует функцию сбора данных (например, ожидаемое улучшение-EI, другую функцию и т. Д.) Для выборки из этого апостериорного распределения, чтобы найти следующий набор параметров, который необходимо изучить. Поскольку байесовская оптимизация определяет следующую точку на основе более систематического подхода с учетом имеющихся данных, ожидается, что она приведет к достижению лучших конфигураций быстрее по сравнению с методами оптимизации с исчерпывающими параметрами, такими как поиск по сетке и случайный поиск. Вы можете прочитать больше о байесовском оптимизаторе в скопте здесь.

Предупреждение кода!

Итак, хватит теории, приступим к делу!

Этот пример кода выполнен с использованием Python и TensorFlow. Кроме того, цель этой задачи оптимизации гиперпараметров - получить набор значений гиперпараметров, которые могут дать минимально возможную среднеквадратичную ошибку (RMSE) для нашей модели глубокого обучения. Мы надеемся, что это будет очень просто для любого новичка.

Во-первых, давайте установим Scikit-Optimize. Вы можете установить его с помощью pip, выполнив эту команду.

pip install scikit-optimize

Обратите внимание, что вам придется внести некоторые изменения в существующий код модели глубокого обучения, чтобы он работал с оптимизацией.

Во-первых, давайте сделаем необходимый импорт.

import skopt
from skopt import gp_minimize
from skopt.space import Real, Integer
from skopt.utils import use_named_args
import tensorflow as tf
import numpy as np
import pandas as pd
from math import sqrt
import atexit
from time import time, strftime, localtime
from datetime import timedelta
from sklearn.metrics import mean_squared_error
from skopt.plots import plot_convergence

Теперь мы установим начальные значения TensorFlow и Numpy, поскольку мы хотим получить воспроизводимые результаты.

randomState = 46
np.random.seed(randomState)
tf.set_random_seed(randomState)

Ниже показаны некоторые важные глобальные переменные Python, которые мы объявили. Среди переменных мы также объявили гиперпараметры, которые мы надеемся оптимизировать (2-й набор переменных).

input_size=1
features = 2
column_min_max = [[0, 2000],[0,500000000]]
columns = ['Close', 'Volume']

num_steps = None
lstm_size = None
batch_size = None
init_learning_rate = None
learning_rate_decay = None
init_epoch = None
max_epoch = None
dropout_rate = None

«Input_size» отображает часть формы прогноза. «Функции» обозначают количество функций в наборе данных, а список «столбцов» содержит имена заголовков этих двух функций. Переменная «column_min_max» содержит верхнюю и нижнюю границы масштабирования обеих функций (это было получено при проверке проверочных и обучающих разбиений).

После объявления всех этих переменных, наконец, пришло время объявить пространство поиска для каждого гиперпараметра, который мы надеемся оптимизировать.

lstm_num_steps = Integer(low=2, high=14, name='lstm_num_steps')
size = Integer(low=8, high=200, name='size')
lstm_learning_rate_decay = Real(low=0.7, high=0.99, prior='uniform', name='lstm_learning_rate_decay')
lstm_max_epoch = Integer(low=20, high=200, name='lstm_max_epoch')
lstm_init_epoch = Integer(low=2, high=50, name='lstm_init_epoch')
lstm_batch_size = Integer(low=5, high=100, name='lstm_batch_size')
lstm_dropout_rate = Real(low=0.1, high=0.9, prior='uniform', name='lstm_dropout_rate')
lstm_init_learning_rate = Real(low=1e-4, high=1e-1, prior='log-uniform', name='lstm_init_learning_rate')

Если вы присмотритесь, то увидите, что мы объявили lstm_init_learning_rate до log-uniform, а не просто указали униформу. Это означает, что, если вы указали до однородности, оптимизатору придется искать от 1e-4 (0,0001) до 1e-1 (0,1) в однородном распределении. Но когда он объявлен как log-uniform, оптимизатор будет искать от -4 до -1, что делает процесс намного более эффективным. Это было рекомендовано при назначении области поиска для скорости обучения библиотекой skopt.

Есть несколько типов данных, с помощью которых вы можете определить пространство поиска. Это категориальные, вещественные и целые числа. При определении пространства поиска, которое включает значения с плавающей запятой, вы должны выбрать «Real», а если оно включает целые числа, выберите «Integer». Если ваше пространство поиска включает категориальные значения, такие как различные функции активации, вам следует выбрать тип «Категориальный».

Теперь мы собираемся записать параметры, которые мы собираемся оптимизировать, в список «измерений». Этот список будет передан в функцию «gp_minimize» позже. Как видите, мы также объявили параметры default_parameters. Это значения параметров по умолчанию, которые мы присвоили каждому гиперпараметру. Не забудьте ввести значения по умолчанию в том же порядке, в каком вы указали гиперпараметры в списке «размеры».

dimensions = [lstm_num_steps, size, lstm_init_epoch, lstm_max_epoch,
lstm_learning_rate_decay, lstm_batch_size, lstm_dropout_rate, lstm_init_learning_rate]

default_parameters = [2,128,3,30,0.99,64,0.2,0.001]

Самое важное, что нужно помнить, - это то, что гиперпараметры в списке default_parameters будут отправной точкой вашей задачи оптимизации. Байесовский оптимизатор будет использовать параметры по умолчанию, которые вы объявили в первой итерации, и в зависимости от результата функция сбора данных определит, какую точку она хочет исследовать следующей.

Можно сказать, что если вы ранее запускали модель несколько раз и нашли приличный набор значений гиперпараметров, вы можете установить их в качестве значений гиперпараметров по умолчанию и начать исследование оттуда. Это может помочь алгоритму быстрее найти наименьшее значение RMSE (меньше итераций). Однако имейте в виду, что это не всегда может быть правдой. Кроме того, не забудьте назначить значение в пределах области поиска, которую вы определили при назначении значений по умолчанию.

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

Однако, чтобы дать вам немного больше контекста, мы разделили наш набор данных на три части для обучения, проверки и тестирования. Обучающий набор использовался для обучения модели, а набор проверки - для выполнения задачи оптимизации гиперпараметров. Как упоминалось ранее, мы используем среднеквадратическую ошибку (RMSE), чтобы оценить модель и выполнить оптимизацию (минимизировать RMSE).

Точность, оцененная с помощью разделения проверки, не может использоваться для оценки модели, поскольку выбранные гиперпараметры, минимизирующие RMSE с разделением проверки, могут быть переоборудованы для набора проверки во время процесса оптимизации гиперпараметров. Таким образом, стандартная процедура заключается в использовании тестового разбиения, которое не использовалось в какой-либо точке конвейера, для измерения точности окончательной модели.

Ниже показана реализация нашей модели глубокого обучения:

def setupRNN(inputs, model_dropout_rate):

  cell = tf.contrib.rnn.LSTMCell(lstm_size, state_is_tuple=True, activation=tf.nn.tanh,use_peepholes=True)

  val1, _ = tf.nn.dynamic_rnn(cell, inputs, dtype=tf.float32)

  val = tf.transpose(val1, [1, 0, 2])

  last = tf.gather(val, int(val.get_shape()[0]) -1, name="last_lstm_output")

  dropout = tf.layers.dropout(last, rate=model_dropout_rate, training=True,seed=46)

  weight = tf.Variable(tf.truncated_normal([lstm_size, input_size]))
  bias = tf.Variable(tf.constant(0.1, shape=[input_size]))

  prediction = tf.matmul(dropout, weight) +bias

  return prediction

Функция «setupRNN» содержит нашу модель глубокого обучения. Тем не менее, вы можете не захотеть понимать эти детали, поскольку байесовская оптимизация рассматривает эту функцию как черный ящик, который принимает определенные гиперпараметры в качестве входных данных, а затем выводит прогноз. Поэтому, если вам не интересно понимать, что у нас внутри этой функции, вы можете пропустить следующий абзац.

Наша модель глубокого обучения содержит слой LSTM, слой исключения и выходной уровень. Необходимая информация, необходимая для работы модели, должна быть отправлена ​​в эту функцию (в нашем случае это были входные данные и коэффициент отсева). Затем вы можете продолжить реализацию своей модели глубокого обучения внутри этой функции. В нашем случае мы использовали уровень LSTM для определения временных зависимостей нашего набора данных о запасах.

Затем мы отправили последний вывод LSTM в слой отсева для целей регуляризации и получили прогноз через выходной слой. Наконец, не забудьте вернуть этот прогноз (в задаче классификации это может быть ваш логит) в функцию, которая будет передана в байесовскую оптимизацию («setupRNN» будет вызываться этой функцией).

Если вы выполняете оптимизацию гиперпараметров для алгоритма машинного обучения (с использованием библиотеки, такой как Scikit-Learn), вам не понадобится отдельная функция для реализации вашей модели, поскольку сама модель уже предоставлена ​​библиотекой, и вы будете писать только код для тренируйтесь и получайте прогнозы. Следовательно, этот код может войти в функцию, которая будет возвращена в байесовскую оптимизацию.

Теперь мы подошли к самому важному разделу задачи оптимизации гиперпараметров - функции «приспособленность».

@use_named_args(dimensions=dimensions)
def fitness(lstm_num_steps, size, lstm_init_epoch, lstm_max_epoch,
            lstm_learning_rate_decay, lstm_batch_size, lstm_dropout_rate, lstm_init_learning_rate):

    global iteration, num_steps, lstm_size, init_epoch, max_epoch, learning_rate_decay, dropout_rate, init_learning_rate, batch_size

    num_steps = np.int32(lstm_num_steps)
    lstm_size = np.int32(size)
    batch_size = np.int32(lstm_batch_size)
    learning_rate_decay = np.float32(lstm_learning_rate_decay)
    init_epoch = np.int32(lstm_init_epoch)
    max_epoch = np.int32(lstm_max_epoch)
    dropout_rate = np.float32(lstm_dropout_rate)
    init_learning_rate = np.float32(lstm_init_learning_rate)

    tf.reset_default_graph()
    tf.set_random_seed(randomState)
    sess = tf.Session()

    train_X, train_y, val_X, val_y, nonescaled_val_y = pre_process()

    inputs = tf.placeholder(tf.float32, [None, num_steps, features], name="inputs")
    targets = tf.placeholder(tf.float32, [None, input_size], name="targets")
    model_learning_rate = tf.placeholder(tf.float32, None, name="learning_rate")
    model_dropout_rate = tf.placeholder_with_default(0.0, shape=())
    global_step = tf.Variable(0, trainable=False)

    prediction = setupRNN(inputs,model_dropout_rate)

    model_learning_rate = tf.train.exponential_decay(learning_rate=model_learning_rate, global_step=global_step, decay_rate=learning_rate_decay,
                                                decay_steps=init_epoch, staircase=False)

    with tf.name_scope('loss'):
        model_loss = tf.losses.mean_squared_error(targets, prediction)

    with tf.name_scope('adam_optimizer'):
        train_step = tf.train.AdamOptimizer(model_learning_rate).minimize(model_loss,global_step=global_step)

    sess.run(tf.global_variables_initializer())

    for epoch_step in range(max_epoch):

        for batch_X, batch_y in generate_batches(train_X, train_y, batch_size):
            train_data_feed = {
                inputs: batch_X,
                targets: batch_y,
                model_learning_rate: init_learning_rate,
                model_dropout_rate: dropout_rate
            }
            sess.run(train_step, train_data_feed)

    val_data_feed = {
        inputs: val_X,
    }
    vali_pred = sess.run(prediction, val_data_feed)

    vali_pred_vals = rescle(vali_pred)

    vali_pred_vals = np.array(vali_pred_vals)

    vali_pred_vals = vali_pred_vals.flatten()

    vali_pred_vals = vali_pred_vals.tolist()

    vali_nonescaled_y = nonescaled_val_y.flatten()

    vali_nonescaled_y = vali_nonescaled_y.tolist()

    val_error = sqrt(mean_squared_error(vali_nonescaled_y, vali_pred_vals))

    return val_error

Как показано выше, мы передаем значения гиперпараметров функции с именем «fitness». Функция «пригодности» будет передана процессу оптимизации байесовского гиперпараметра (gp_minimize). Обратите внимание, что на первой итерации значения, переданные в эту функцию, будут значениями по умолчанию, которые вы определили, и далее байесовская оптимизация будет выбирать значения гиперпараметров самостоятельно. Затем мы присваиваем выбранные значения глобальным переменным Python, которые мы объявили в начале, чтобы мы могли использовать последние выбранные значения гиперпараметров вне функции пригодности.

Затем мы подходим к довольно важному моменту в нашей задаче оптимизации. Если вы использовали TensorFlow до этой статьи, вы бы знали, что TensorFlow работает путем создания вычислительного графа для любого типа модели глубокого обучения, которую вы создаете.

В процессе оптимизации гиперпараметров на каждой итерации мы будем сбрасывать существующий график и строить новый. Этот процесс выполняется для минимизации памяти, занимаемой графом, и предотвращения наложения графов друг на друга. Сразу после сброса графика вам нужно будет установить случайное начальное число TensorFlow, чтобы получить воспроизводимые результаты. После описанного выше процесса мы наконец можем объявить сеанс TensorFlow.

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

Здесь важно помнить, что нужно вернуть окончательное значение метрики (в данном случае значение RMSE), полученное для группы проверки. Это значение будет возвращено процессу байесовской оптимизации и будет использоваться при выборе следующего набора гиперпараметров, который он хочет изучить.

Примечание: если вы имеете дело с проблемой классификации, вы можете указать свою точность как отрицательное значение (например, -96), потому что, даже если чем выше точность, тем лучше модель, байесовская функция будет продолжать попытки уменьшить значение, так как он предназначен для поиска значений гиперпараметров для возвращаемого ему наименьшего значения.

Давайте теперь обозначим точку выполнения для всего этого процесса, «главную» функцию. Внутри основной функции мы объявили функцию «gp_minimize». Затем мы передаем в эту функцию несколько важных параметров.

if __name__ == '__main__':

    start = time()

    search_result = gp_minimize(func=fitness,
                                dimensions=dimensions,
                                acq_func='EI', # Expected Improvement.
                                n_calls=11,
                                x0=default_parameters,
                                random_state=46)

print(search_result.x)
print(search_result.fun)
plot = plot_convergence(search_result,yscale="log")

atexit.register(endlog)
logger("Start Program")

Параметр «func» - это функция, которую вы хотели бы смоделировать с помощью байесовского оптимизатора. Параметр «размеры» - это набор гиперпараметров, которые вы надеетесь оптимизировать, а «acq_func» обозначает функцию сбора данных и является функцией, которая помогает определить следующий набор значений гиперпараметров, которые следует использовать. gp_minimize поддерживает 4 типа функций сбора данных. Они есть:

  • LCB: нижняя граница достоверности
  • EI: ожидаемое улучшение
  • PI: вероятность улучшения
  • gp_hedge: вероятностно выбрать одну из трех вышеуказанных функций сбора данных на каждой итерации.

Приведенная выше информация была взята из документации. У каждого из них есть свои преимущества, но если вы новичок в байесовской оптимизации, попробуйте использовать «EI» или «gp_hedge», поскольку «EI» является наиболее широко используемой функцией сбора данных, а «gp_hedge» выберет одну из вышеуказанных функций сбора данных. функционирует вероятностно, поэтому вам не придется слишком об этом беспокоиться.

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

Возвращаясь к объяснению остальных параметров, параметр «n_calls» - это количество раз, которое вы хотите запустить фитнес-функцию. Задача оптимизации начнется с использования значений гиперпараметров, определенных как «x0», значений гиперпараметров по умолчанию. Наконец, мы устанавливаем случайное состояние оптимизатора гиперпараметров, поскольку нам нужны воспроизводимые результаты.

Теперь, когда вы запустите функцию gp_optimize, поток событий будет следующим:

Фитнес-функция будет использоваться с параметрами, переданными в x0. LSTM будет обучаться с указанными эпохами, и будет запущен ввод проверки, чтобы получить значение RMSE для его прогнозов. Затем в зависимости от этого значения байесовский оптимизатор решит, какой следующий набор значений гиперпараметров он хочет исследовать с помощью функции сбора данных.

На второй итерации функция приспособленности будет работать со значениями гиперпараметров, полученными при байесовской оптимизации, и тот же процесс будет повторяться до тех пор, пока не будет повторяться «n_call» раз. Когда весь процесс подходит к концу, объект Scikit-Optimize будет назначен переменной search _result.

Мы можем использовать этот объект для получения полезной информации, как указано в документации.

  • x [список]: расположение минимума.
  • fun [float]: минимальное значение функции.
  • модели: суррогатные модели, используемые для каждой итерации.
  • x_iters [список списков]: расположение оценки функции для каждой итерации.
  • func_vals [массив]: значение функции для каждой итерации.
  • space [Space]: пространство оптимизации.
  • specs [dict] `: спецификации вызова.
  • rng [экземпляр RandomState]: состояние случайного состояния в конце минимизации.

«Search_result.x» дает нам оптимальные значения гиперпараметров, а с помощью «search_result.fun» мы можем получить значение RMSE набора проверки, соответствующее полученным значениям гиперпараметров (наименьшее значение RMSE, полученное для набора проверки).

Ниже показаны оптимальные значения гиперпараметров, которые мы получили для нашей модели, и самое низкое значение RMSE из набора для проверки. Если вам трудно определить порядок, в котором значения гиперпараметров перечисляются при использовании «search_result.x», он находится в том же порядке, в котором вы указали свои гиперпараметры в списке «измерений».

Значения гиперпараметров:

  • lstm_num_steps: 6
  • lstm_size: 171
  • lstm_init_epoch: 3
  • lstm_max_epoch: 58
  • lstm_learning_rate_decay: 0.7518394019565194
  • lstm_batch_size: 24
  • lstm_dropout_rate: 0.21830825193089087
  • lstm_init_learning_rate: 0.0006401363567813549

Наименьшее среднеквадратичное значение 2,73755355221523

График сходимости

Гиперпараметры, которые дали самую низкую точку байесовской оптимизации на этом графике, - это то, что мы получаем как оптимальный набор значений гиперпараметров.

На графике показано сравнение самых низких значений RMSE, записанных для каждой итерации (50 итераций) в байесовской оптимизации и случайном поиске. Мы видим, что байесовская оптимизация смогла сойтись лучше, чем случайный поиск. Однако вначале мы видим, что случайный поиск нашел лучший минимум быстрее, чем байесовский оптимизатор. Это может быть связано с тем, что случайная выборка является природой случайного поиска.

Мы наконец подошли к концу этой статьи, поэтому в заключение мы надеемся, что эта статья упростила задачу построения модели глубокого обучения, показав лучший способ поиска оптимального набора гиперпараметров. Больше не нужно беспокоиться об оптимизации гиперпараметров. Удачного кодирования, друзья-гики!

Полезные материалы:

  • Полный код можно найти по этой ссылке.
  • Для получения дополнительной информации о байесовской оптимизации вы можете обратиться к этой статье.
  • Для получения дополнительной информации о функциях сбора см. Этот документ.

Первоначально опубликовано на https://dzone.com.