Напоминание

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

Кстати, в следующей статье приводится полный код эксперимента, так что проверьте его.

Напомним, что для проверки концепции мы использовали следующие синтетические данные:

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

Однако на этот раз мы хотим усложнить эту простую задачу, увеличив распространение присвоения кредита:

Фаза синуса увеличена вдвое.

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

Кроме того, напомним, что мы использовали такую ​​архитектуру нейронной сети:

Что было добавлено и почему

LSTM

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

Обратите внимание, что я тоже немного улучшил описание. Единственное отличие от старой сети - это первый скрытый слой LSTM.

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

Мы используем эту схему с LSTM:

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

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

Презентаций

Формирование вознаграждения на основе потенциала, PBRS, является мощным инструментом для повышения скорости, стабильности и сохранения оптимальности процесса поиска политики для решения проблем среды. Я рекомендую прочитать хотя бы эту основополагающую статью: https://people.eecs.berkeley.edu/~russell/papers/ml99-shaping.ps

Потенциал определяет, насколько хорошо мы находимся в состоянии w.r.t. состояние цели, в которое мы хотим войти. Симлистическое представление о том, как это работает:

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

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

Вознаграждение в форме потенциала принимает следующую форму (уравнение 1):

r ’= r + гамма * F (s’) - F (s)

где F обозначает потенциальное состояние, а r - исходное вознаграждение.

С этими мыслями мы переходим к кодированию.

Реализация в R

Вот код нейронной сети на основе Keras API:

# configure critic NN — — — — — —
library('keras')
library('R6')
state_names_length <- 12 # just for example
lstm_seq_length <- 4
learning_rate <- 1e-3
a_CustomLayer <- R6::R6Class(
 “CustomLayer”
 , inherit = KerasLayer
 , public = list(
 
 call = function(x, mask = NULL) {
 x — k_mean(x, axis = 2, keepdims = T)
 }
 
 )
)
a_normalize_layer <- function(object) {
 create_layer(a_CustomLayer, object, list(name = ‘a_normalize_layer’))
}
v_CustomLayer <- R6::R6Class(
 “CustomLayer”
 , inherit = KerasLayer
 , public = list(
 
 call = function(x, mask = NULL) {
 k_concatenate(list(x, x, x), axis = 2)
 }
 
 , compute_output_shape = function(input_shape) {
 
 output_shape = input_shape
 output_shape[[2]] <- input_shape[[2]] * 3L
 
 output_shape
 }
 )
)
v_normalize_layer <- function(object) {
 create_layer(v_CustomLayer, object, list(name = ‘v_normalize_layer’))
}
noise_CustomLayer <- R6::R6Class(
 “CustomLayer”
 , inherit = KerasLayer
 , lock_objects = FALSE
 , public = list(
 
 initialize = function(output_dim) {
 self$output_dim <- output_dim
 }
 
 , build = function(input_shape) {
 
 self$input_dim <- input_shape[[2]]
 
 sqr_inputs <- self$input_dim ** (1/2)
 
 self$sigma_initializer <- initializer_constant(.5 / sqr_inputs)
 
 self$mu_initializer <- initializer_random_uniform(minval = (-1 / sqr_inputs), maxval = (1 / sqr_inputs))
 
 self$mu_weight <- self$add_weight(
 name = ‘mu_weight’, 
 shape = list(self$input_dim, self$output_dim),
 initializer = self$mu_initializer,
 trainable = TRUE
 )
 
 self$sigma_weight <- self$add_weight(
 name = ‘sigma_weight’, 
 shape = list(self$input_dim, self$output_dim),
 initializer = self$sigma_initializer,
 trainable = TRUE
 )
 
 self$mu_bias <- self$add_weight(
 name = ‘mu_bias’, 
 shape = list(self$output_dim),
 initializer = self$mu_initializer,
 trainable = TRUE
 )
 
 self$sigma_bias <- self$add_weight(
 name = ‘sigma_bias’, 
 shape = list(self$output_dim),
 initializer = self$sigma_initializer,
 trainable = TRUE
 )
 
 }
 
 , call = function(x, mask = NULL) {
 
 #sample from noise distribution
 
 e_i = k_random_normal(shape = list(self$input_dim, self$output_dim))
 e_j = k_random_normal(shape = list(self$output_dim))
 
 
 #We use the factorized Gaussian noise variant from Section 3 of Fortunato et al.
 
 eW = k_sign(e_i) * (k_sqrt(k_abs(e_i))) * k_sign(e_j) * (k_sqrt(k_abs(e_j)))
 eB = k_sign(e_j) * (k_abs(e_j) ** (1/2))
 
 
 #See section 3 of Fortunato et al.
 
 noise_injected_weights = k_dot(x, self$mu_weight + (self$sigma_weight * eW))
 noise_injected_bias = self$mu_bias + (self$sigma_bias * eB)
 output = k_bias_add(noise_injected_weights, noise_injected_bias)
 
 output
 
 }
 
 , compute_output_shape = function(input_shape) {
 
 output_shape <- input_shape
 output_shape[[2]] <- self$output_dim
 
 output_shape
 
 }
 )
)
noise_add_layer <- function(object, output_dim) {
 create_layer(
 noise_CustomLayer
 , object
 , list(
 name = ‘noise_add_layer’
 , output_dim = as.integer(output_dim)
 , trainable = T
 )
 )
}
critic_input <- layer_input(
 shape = list(NULL, as.integer(state_names_length))
 , name = ‘critic_input’
)
common_lstm_layer <- layer_lstm(
 units = 20
 , activation = “tanh”
 , recurrent_activation = “hard_sigmoid”
 , use_bias = T
 , return_sequences = F
 , stateful = F
 , name = ‘lstm1’
)
critic_layer_dense_v_1 <- layer_dense(
 units = 10
 , activation = “tanh”
)
critic_layer_dense_v_2 <- layer_dense(
 units = 5
 , activation = “tanh”
)
critic_layer_dense_v_3 <- layer_dense(
 units = 1
 , name = ‘critic_layer_dense_v_3’
)
critic_layer_dense_a_1 <- layer_dense(
 units = 10
 , activation = “tanh”
)
# critic_layer_dense_a_2 <- layer_dense(
# units = 5
# , activation = “tanh”
# )
critic_layer_dense_a_3 <- layer_dense(
 units = length(actions)
 , name = ‘critic_layer_dense_a_3’
)
critic_model_v <-
 critic_input %>%
 common_lstm_layer %>%
 critic_layer_dense_v_1 %>%
 critic_layer_dense_v_2 %>%
 critic_layer_dense_v_3 %>%
 v_normalize_layer
critic_model_a <-
 critic_input %>%
 common_lstm_layer %>%
 critic_layer_dense_a_1 %>%
 #critic_layer_dense_a_2 %>%
 noise_add_layer(output_dim = 5) %>%
 critic_layer_dense_a_3 %>%
 a_normalize_layer
critic_output <-
 layer_add(
 list(
 critic_model_v
 , critic_model_a
 )
 , name = ‘critic_output’
 )
critic_model_1 <- keras_model(
 inputs = critic_input
 , outputs = critic_output
)
critic_optimizer = optimizer_adam(lr = learning_rate)
keras::compile(
 critic_model_1
 , optimizer = critic_optimizer
 , loss = ‘mse’
 , metrics = ‘mse’
)
train.x <- array_reshape(rnorm(10 * lstm_seq_length * state_names_length)
 , dim = c(10, lstm_seq_length, state_names_length)
 , order = ‘C’)
predict(critic_model_1, train.x)
layer_name <- ‘noise_add_layer’
intermediate_layer_model <- keras_model(inputs = critic_model_1$input, outputs = get_layer(critic_model_1, layer_name)$output)
predict(intermediate_layer_model, train.x)[1,]
critic_model_2 <- critic_model_1

Тщательно отлаживайте свое решение…

Результаты и сравнение

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

Сравнение включает:

  • предыдущая версия без LSTM и презентаций
  • простой 2-элементный LSTM
  • 4-элементный LSTM
  • 4-элементный LSTM с PBRS

Что ж, здесь это довольно внутриглазная травма, поскольку агент в форме PBRS сходится так быстро и стабильно по сравнению с предыдущими попытками. Скорость примерно в 4–5 раз выше, чем без презентаций. Стабильность замечательная.

Когда дело доходит до использования LSTM, 4 ячейки показали лучшие результаты, чем 2 ячейки. Двухэлементный LSTM показал лучший результат, чем версия без LSTM.

Заключительные слова

Мы стали свидетелями того, что повторяемость и формирование вознаграждения на основе потенциала помогают. Мне особенно понравилось, как PBRS показал себя так высоко.

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

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