Melhorando o agente Q-Learning baseado em ações, adicionando recorrências e recompensas

Lembrete


Olá Habr! Trago à sua atenção outra tradução do meu novo artigo da mídia .

Da última vez ( primeiro artigo ) ( Habr ), criamos um agente usando a tecnologia Q-Learning, que realiza transações em séries temporais simuladas e reais de troca e tentamos verificar se essa área de tarefas é adequada para o aprendizado reforçado.

Desta vez, adicionaremos uma camada LSTM para levar em conta as dependências de tempo da trajetória e recompensar a engenharia de modelagem com base nas apresentações.

imagem

Deixe-me lembrá-lo que, para verificar o conceito, usamos os seguintes dados sintéticos:

imagem

Dados sintéticos: seno com ruído branco.

A função seno foi o primeiro ponto de partida. Duas curvas simulam o preço de compra e venda de um ativo, em que o spread é o custo mínimo da transação.

No entanto, desta vez queremos complicar essa tarefa simples estendendo o caminho da atribuição de crédito:

imagem

Dados sintéticos: seno com ruído branco.

A fase sinusal foi duplicada.

Isso significa que as recompensas esparsas que usamos devem se espalhar por trajetórias mais longas. Além disso, reduzimos significativamente a probabilidade de receber uma recompensa positiva, pois o agente teve que executar uma sequência de ações corretas duas vezes mais para superar os custos de transação. Ambos os fatores complicam bastante a tarefa da RL, mesmo em condições simples como uma onda senoidal.

Além disso, lembramos que usamos essa arquitetura de rede neural:

imagem

O que foi adicionado e por quê


Lstm


Antes de tudo, queríamos dar ao agente mais entendimento da dinâmica das mudanças dentro da trajetória. Simplificando, o agente deve entender melhor seu próprio comportamento: o que ele fez agora e por algum tempo no passado e como a distribuição das ações do Estado, bem como as recompensas recebidas, se desenvolveram. O uso de uma camada de recorrência pode resolver exatamente esse problema. Bem-vindo à nova arquitetura usada para lançar um novo conjunto de experiências:

imagem

Por favor, note que eu melhorei ligeiramente a descrição. A única diferença do NN antigo é a primeira camada LSTM oculta, em vez de uma camada totalmente ligada.

Observe que, com o LSTM em funcionamento, precisamos alterar a seleção de exemplos da reprodução da experiência para treinamento: agora precisamos de sequências de transição em vez de exemplos separados. Veja como funciona (este é um dos algoritmos). Usamos amostragem pontual antes:

imagem

O esquema fictício do buffer de reprodução.

Usamos esse esquema com LSTM:

imagem

Agora as seqüências são selecionadas (cujo comprimento especificamos empiricamente).

Como antes, e agora a amostra é regulada por um algoritmo de prioridade baseado em erros de aprendizado temporal-temporal.

O nível de recorrência LSTM permite a disseminação direta de informações de séries temporais para interceptar um sinal adicional oculto em atrasos anteriores. Nossa série temporal é um tensor bidimensional com tamanho: o comprimento da sequência na representação de nossa ação estatal.

Apresentações


A engenharia premiada, PBRS (Potential Based Reward Shaping), com base no potencial, é uma ferramenta poderosa que permite aumentar a velocidade, a estabilidade e não violar a otimização do processo de pesquisa de políticas para resolver nosso ambiente. Eu recomendo a leitura de pelo menos este documento original sobre o tópico:

people.eecs.berkeley.edu/~russell/papers/ml99-shaping.ps

Potencial determina quão bem nosso estado atual é relativo ao estado de destino que queremos inserir. Uma visão esquemática de como isso funciona:

imagem

Existem opções e dificuldades que você pode entender após tentativa e erro e omitimos esses detalhes, deixando você com sua lição de casa.

Vale mencionar mais uma coisa: a PBRS pode ser justificada usando apresentações, que são uma forma de conhecimento especializado (ou simulado) sobre o comportamento quase ideal do agente no ambiente. Existe uma maneira de encontrar essas apresentações para nossa tarefa usando esquemas de otimização. Omitimos os detalhes da pesquisa.

A recompensa potencial assume a seguinte forma (equação 1):

r '= r + gama * F (s') - F (s)

onde F é o potencial do estado er é a recompensa inicial, gama é o fator de desconto (0: 1).

Com esses pensamentos, passamos à codificação.

Implementação em R
Aqui está o código de rede neural baseado na API Keras:

Código
# 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 


Depurando sua decisão em sua consciência ...

Resultados e Comparação


Vamos mergulhar nos resultados finais. Nota: todos os resultados são estimativas pontuais e podem diferir em várias execuções com diferentes sementes aleatórias.

A comparação inclui:

  • versão anterior sem LSTM e apresentações
  • LSTM simples de 2 elementos
  • LSTM de 4 elementos
  • LSTM de 4 células com recompensas PBRS geradas

imagem

O retorno médio por episódio atingiu em média mais de 1000 episódios.

imagem

Total de episódios retornados.

Gráficos para o agente mais bem-sucedido:

imagem

imagem

Desempenho do agente.

Bem, é óbvio que o agente na forma de PBRS converge tão rapidamente e de forma estável em comparação com tentativas anteriores que pode ser aceito como um resultado significativo. A velocidade é cerca de 4-5 vezes maior do que sem apresentações. A estabilidade é maravilhosa.

Quando se trata de usar LSTM, 4 células tiveram desempenho melhor que 2 células. Um LSTM de 2 células teve um desempenho melhor que uma versão não LSTM (no entanto, talvez isso seja uma ilusão de um único experimento).

Palavras finais


Vimos que a recorrência e as recompensas de capacitação ajudam. Eu gostei especialmente de como o PBRS se saiu tão bem.

Não acredite em ninguém que me faça dizer que é fácil criar um agente de RL que converge bem, pois isso é mentira. Cada novo componente adicionado ao sistema o torna potencialmente menos estável e requer muita configuração e depuração.

No entanto, há evidências claras de que a solução do problema pode ser melhorada simplesmente melhorando os métodos utilizados (os dados permaneceram intactos). É fato que, para qualquer tarefa, um determinado intervalo de parâmetros funciona melhor que outros. Com isso em mente, você está embarcando em um caminho de aprendizado bem-sucedido.

Obrigada

Source: https://habr.com/ru/post/pt436628/


All Articles