Джомайкл Альварес

Модели глубокого обучения становятся все более популярными в области машинного обучения. Однако настройка этих моделей может оказаться непростой задачей. В этой статье мы рассмотрим, как легко настроить модели глубокого обучения с помощью Scikit-Optimize. К концу этой статьи вы будете лучше понимать, как настраивать модели глубокого обучения и как использовать код, чтобы упростить и ускорить процесс с помощью байесовской оптимизации.

На этом этапе я предполагаю, что вы хорошо понимаете три вещи:

  1. Очистка данных
  2. Разработка функций для машинного обучения
  3. Как реализовать нейронные сети с помощью Tensorflow

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

Внедрение и библиотеки

!pip install scikit-optimize tensorflow

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

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split, KFold
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from skopt import BayesSearchCV
import tensorflow as tf
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense, Dropout, BatchNormalization
from tensorflow.keras.optimizers import Adam, RMSProp, Nadam
from keras.wrappers.scikit_learn import KerasRegressor

#quality of life
tf.random.set_seed(42)

Прежде чем мы продолжим, позвольте мне представить некоторые библиотеки, которые я загрузил. Я включил стандартный массив для загрузки, очистки и преобразования данных, а также необходимые для нашей работы тензорные библиотеки. Кроме того, я включил KFold и tf.random.set_seed(42), чтобы сделать наши эксперименты максимально повторяемыми. Наконец, я включил BayesSearchCV, который является предметом нашего обсуждения.

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

Переменные и обратные вызовы

Отсюда мы собираемся создать некоторые переменные и функции обратного вызова для использования в нашем пространстве поиска, моделях и функции поиска.

val_cb_reducelr = tf.keras.callbacks.ReduceLROnPlateau(
    monitor='val_loss',
    mode='min',
    factor=0.01,
    patience=10,
    verbose=1,
    min_lr=0.00001
)
val_cb_earlystop = tf.keras.callbacks.EarlyStopping(
    monitor='val_loss',
    mode='min',
    min_delta=0.001,
    patience=15,
    verbose=1
)
val_cb_checkpoint = tf.keras.callbacks.ModelCheckpoint(
    filepath='checkpoints/model-{epoch:02d}-{val_loss:.2f}.hdf5',
    monitor='val_loss',
    mode='min',
    save_best_only=True,
    verbose=1
)
cb_csvlogger = tf.keras.callbacks.CSVLogger(
    filename='training_log.csv',
    separator=',',
    append=False
)

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

cb_reducelr = tf.keras.callbacks.ReduceLROnPlateau(
    monitor='loss',
    mode='min',
    factor=0.01,
    patience=10,
    verbose=1,
    min_lr=0.00001
)
cb_earlystop = tf.keras.callbacks.EarlyStopping(
    monitor='loss',
    mode='min',
    min_delta=0.001,
    patience=15,
    verbose=1
)

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

Теперь, когда мы закончили с обратными вызовами, можно начать с нескольких переменных.

#constants
scoring = 'neg_mean_squared_error'
kfold = KFold(n_splits=5, random_state=42, shuffle=True)
n_iter=32
epochs=100

#things to change
max_layers = 5
output_layer = Dense(1)
input_shape = X_train.shape[1],

Вы должны настроить эти переменные в зависимости от потребностей вашей модели.

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

Функция для параметров

Вот наша первая функция, которую нужно создать.

#function to build search parameters
def build_search_space():
    
    #simple to do params section
    search_space = {'lr': np.arange(.001, .0501, .0001),
                    'layers': np.arange(1, max_layers+1),
                    'batch_size': [16, 32, 64, 128],
                    'optimizers': [RMSprop, Adam, Nadam]
                    #SGD causes the exploding gradient problem in regression
                   }

    
    #adds to search_space with the number of layers desired
    for i in range(1, max_layers+1):
        search_space.update({'n_units'+str(i): np.arange(64, 513, 64)
                            })
        search_space.update({'kernel'+str(i): ('glorot_normal', 'he_normal', 
                                               'he_uniform', 'glorot_uniform')
                            })
        search_space.update({'active'+str(i): ('relu', 'tanh', 'sigmoid')
                            })
        search_space.update({'dropout'+str(i): np.arange(0.0, 0.8, 0.1)
                            })
        search_space.update({'normalizer'+str(i): [0, 1]
                            })
        
    return search_space

Как видите, я просматриваю не так уж много ядер, как и не так много функций активации, которые я просматриваю. Это специально.

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

Функция для создания модели

Наш второй большой фрагмент кода — это наша модель.

#function to model
def build_nn_model(lr,
                   layers,
                   batch_size,
                   optimizers,
                   active1,active2,active3,active4,active5,
                   n_units1,n_units2,n_units3,n_units4,n_units5,
                   kernel1,kernel2,kernel3,kernel4,kernel5,
                   dropout1,dropout2,dropout3,dropout4,dropout5,
                   normalizer1,normalizer2,normalizer3,normalizer4,normalizer5,
                  ):
    
    model = Sequential()
    
    #layers as a search param allows us to alternate all the sizes of our model
    for i in range(1, layers):
        #input layer
        if i == 1:
            model.add(Dense(eval(f"n_units{i}"), activation=eval(f"active{i}"),
                            kernel_initializer=eval(f"kernel{i}"),
                            input_shape=input_shape,
                            name = 'dense_layer'+str(i)))

        #hidden layers
        else:
            model.add(Dense(eval(f"n_units{i}"), activation=eval(f"active{i}"),
                            kernel_initializer=eval(f"kernel{i}"),
                            name = 'dense_layer'+str(i)))

        #regularization
        model.add(Dropout(eval(f"dropout{i}")))
        
        if eval(f"normalizer{i}") == 1:
          model.add(BatchNormalization())

    #add output layer
    model.add(output_layer)

    model.compile(optimizer=optimizers(learning_rate=lr), loss='mse')
    
    return model

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

Создание и поиск

Наш последний шаг настройки — выполнить наши функции, обернуть нашу модель и выполнить BayesSearchCV.

#build search params
search_space=build_search_space()

#build model inside wrapper
model = KerasRegressor(build_fn=build_nn_model, verbose=0, epochs=epochs,
                       callbacks=[cb_reducelr, cb_earlystop]
                      )

#create search through wrapper with bayes
bayes = BayesSearchCV(estimator=model, search_spaces=search_space,
                      cv=kfold, n_iter=n_iter, scoring=scoring, verbose=1,
                       random_state=42, n_jobs=-1
)

#fit to bayes via cv
bayes.fit(X_train, y_train) #change the fit

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

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

Окончательная настроенная модель

В качестве бонуса за то, что вы дошли до конца, вот быстрый способ просмотреть вашу новую настроенную модель.

#create tuned model
tuned_model = Sequential()

#for loop using best model size and params
for i in range(1, bayes.best_params_['layers']):
    #input layer
    if i==1:
        tuned_model.add(Dense(bayes.best_params_[f"n_units{i}"],
                              activation=bayes.best_params_[f"active{i}"], 
                              kernel_initializer=bayes.best_params_[f"kernel{i}"],
                              input_shape=input_shape))
        
    #hidden layer(s)
    else:
        tuned_model.add(Dense(bayes.best_params_[f"n_units{i}"],
                              activation=bayes.best_params_[f"active{i}"], 
                              kernel_initializer=bayes.best_params_[f"kernel{i}"]))
    
    #regularization
    tuned_model.add(Dropout(bayes.best_params_[f"dropout{i}"]))
    
    if bayes.best_params_[f"normalizer{i}"] == 1:
      tuned_model.add(BatchNormalization())

#output layer
tuned_model.add(output_layer)
    
#compile tuned model with best optimizer and lr
tuned_model.compile(loss='mse', metrics=['mae', RSquare()],
                   optimizer=bayes.best_params_['optimizers'](
                      learning_rate=bayes.best_params_['lr']))

#fit using callbacks to ensure best possible results and logging
tuned_model.fit(X_train_ccs_nout_mm_scaled, y_train_ccs_nout_mm_scaled,
                epochs=200, batch_size=bayes.best_params_['batch_size'],
                     callbacks=[val_cb_checkpoint, val_cb_reducelr,
                                val_cb_earlystop, cb_csvlogger],
                     validation_data=(X_test, y_test))

Я решил увеличить наши эпохи до 200 и сохранить нашу самую эффективную модель. На этот раз мы поменяли наши обратные вызовы на разделение проверки, так как мы пытаемся проверить нашу настроенную модель.

Заключение

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

Примечание для читателя. Альтернативой байесовской оптимизации является гиперполосная оптимизация. На самом деле гиперполосная оптимизация часто является лучшим выбором между ними, но для знакомого лица и надежного решения байесовская оптимизация все еще имеет свое место. Еще одним интересным решением является новое возможно лучшее решение BOHB (Bayesion Optimization Hyper Band). Для получения дополнительной информации об этой новой возможной функции поиска я рекомендую вам посмотреть это видео https://www.youtube.com/watch?v=IqQT8se9ofQ.