English Русский 中文 Español Deutsch 日本語
preview
Redes neurais de maneira fácil (Parte 27): Aprendizado Q profundo (DQN)

Redes neurais de maneira fácil (Parte 27): Aprendizado Q profundo (DQN)

MetaTrader 5Sistemas de negociação | 22 novembro 2022, 10:34
334 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Conteúdo

Introdução

No artigo anterior, começamos a explorar métodos de aprendizado por reforço e construímos o primeiro modelo treinável de entropia cruzada. Neste artigo, continuamos nosso estudo sobre métodos de aprendizado por reforço. E hoje proponho nos familiarizar com o método de aprendizado Q profundo. Foi com o aprendizado Q profundo que a equipe DeepMind conseguiu criar um modelo em 2013 que pode jogar com sucesso 7 jogos de Atari. Curiosamente, eles treinaram um mesmo modelo em todos os 7 jogos sem nenhuma mudança na arquitetura ou hiperparâmetros. Quando isso acontecia, segundo os resultados do treinamento, o modelo conseguiu melhorar os resultados alcançados anteriormente em 6 dos jogos analisados. Além disso, o modelo teve um desempenho melhor do que um humano em 3 jogos. Podemos dizer que com o lançamento desse trabalho, iniciou-se uma nova etapa no desenvolvimento do aprendizado por reforço. Vamos nos iniciar nesse método e tentar usá-lo para resolver nossos problemas.


1. Função Q - conceito

Primeiro, vamos rever um pouco o material estudado no último artigo. Quanto ao aprendizado por reforço, construímos o processo de interação entre o agente e seu ambiente. O agente analisa o estado atual do ambiente e efetua uma ação que altera o estado do ambiente. Em resposta à ação, o ambiente devolve uma recompensa ao agente. O agente não conhece a natureza da formação da recompensa. Mas seu objetivo é obter a maior recompensa total possível durante a sessão analisada.

Aqui é importante atentar para o fato de que o agente não é recompensado pela ação, mas, sim, pela transição de um estado para outro. Entretanto, tomar uma determinada ação em uma situação semelhante não garante uma transição para o mesmo estado. A execução de uma ação oferece apenas alguma probabilidade de transição para o estado esperado. As probabilidades e dependências de estados, de ações e de transições são desconhecidas para o agente. E ele deve aprendê-los a partir do processo de interação com o meio ambiente. 

Em essência, o aprendizado por reforço é baseado na suposição de que existe alguma relação entre o estado atual, a ação realizada e a recompensa. Matematicamente falando, existe uma certa função Q, que, dependendo do estado s e da ação a, retorna uma recompensa r. E é denotada por Q(s|a). Essa função é chamada de função de utilidade de ação.

Sim, o agente não conhece esta função. Mas se ela existe, então no processo de interação com o ambiente, repetindo as ações um número infinito de vezes, podemos aproximá-la.

Na vida real, certamente, não podemos repetir estados e ações um número infinito de vezes. Mas ao repeti-los um número suficiente de vezes, podemos aproximar a função com um erro aceitável. A forma de expressão da função Q pode ser diversa. No artigo anterior, após determinar a utilidade de cada ação, construímos uma tabela de dependências do estado, da ação e da recompensa média. Mas outras formas de expressar a função Q são bastante satisfatórias e podem dar resultados ainda melhores. Podem ser árvores de decisão, redes neurais, etc.

Aqui é importante notar que a função Q aproximada pelo agente não antecipa a recompensa recebida. Ela apenas retorna a recompensa esperada com base na experiência adquirida pelo agente ao interagir com o ambiente.


2. Aprendizado Q profundo

Você provavelmente já adivinhou que o aprendizado Q profundo envolve o uso de uma rede neural para aproximar a função Q. Qual é a vantagem de tal abordagem? Vamos relembrar a implementação do método tabular de entropia cruzada visto no último artigo. Lembre que eu enfatizei que a elaboração de um método de tabela pressupõe um número finito de estados e de ações possíveis. É claro que limitamos o número de estados possíveis agrupando os dados iniciais. Mas é tão bom? O agrupamento sempre nos dará melhores resultados? Ao fazer isso, o uso de rede neural não limita o número de estados possíveis diante de nós. E creio que, no que diz respeito à resolução de problemas de negociação, isto é uma grande vantagem.

E aqui parece bastante óbvio pegar e substituir a tabela do artigo anterior por uma rede neural. Mas, infelizmente, nem tudo é tão simples. Na prática, essa abordagem acaba por não ser tão boa quanto parece à primeira vista. Para implementar a abordagem, precisamos adicionar algo de heurística.

Primeiro, vamos olhar para o propósito do treinamento de nosso agente. Basicamente, seu objetivo é maximizar a recompensa total. Veja a figura 1. O agente deve passar da célula Start para a célula Finish. O agente recebe uma recompensa única quando atinge a célula Finish. Em todos os outros estados, a recompensa é zero.

Fator de desconto

A figura mostra 2 caminhos. É óbvio para nós que o caminho laranja é mais curto e preferível. Mas em termos de maximização de recompensa, eles são equivalentes.

Da mesma forma, na negociação, é preferível para nós obter um lucro imediato do que investir o dinheiro agora e obter um lucro em um futuro distante. Naturalmente, o valor do dinheiro - taxa de desconto, inflação e uma série de outros atributos - é levado em conta aqui. Fazemos o mesmo aqui. Para resolver o problema, introduzimos um fator de desconto ɣ que reduzirá o valor das recompensas futuras.

cumulativa Recompensa cumulativa

O fator de desconto ɣ é escolhido no intervalo de 0 a 1. Se o fator de desconto for 1, não ocorre desconto. E com um fator de desconto de 0, recompensas futuras não são levadas em consideração. Na prática, o fator de desconto é tomado próximo de 1.

Mas há outro problema aqui. O que parece bom no papel nem sempre funciona na prática. Podemos calcular facilmente as recompensas futuras quando temos um mapa completo de transições e de recompensas à nossa frente. Dentre elas, podemos escolher a melhor rota com a máxima recompensa no final. Mas ao resolver problemas práticos, não sabemos a que próximo estado chegaremos depois de realizar uma determinada ação nem que recompensa teremos. E isso apenas no próximo passo. E nem falar sobre todo o caminho até o final da sessão. Não podemos ver o futuro. Para receber a próxima recompensa, o agente precisa realizar uma ação. E somente após a transição para um novo estado, o ambiente retornará a recompensa. E, quando isso acontece, não há como voltar atrás. Não podemos voltar a um estado anterior e tomar uma ação diferente a fim de escolher uma melhor mais tarde.

É por isso que nos voltamos para métodos de programação dinâmica e, em particular, para o método de otimização Bellman. Ele afirma que para escolher a melhor estratégia, é preciso escolher a melhor ação em cada etapa. Ou seja, ao selecionar a ação com a recompensa máxima em cada etapa, obteremos a recompensa máxima acumulada da sessão. A fórmula matemática para atualizar a função de utilidade de uma ação é apresentada a seguir.

Otimização Belman

Observe a fórmula abaixo. Isso não lhe lembra a fórmula para atualizar os pesos de uma descida de gradiente estocástica? De fato, vemos aqui que para atualizar o valor da função de utilidade de uma ação, precisamos do valor anterior da função mais algum desvio multiplicado pelo fator de aprendizado.

Mas também na função apresentada, podemos ver que para determinar o valor da função no momento t, precisamos do valor da função de utilidade de ação na próxima etapa de tempo no momento t+1. Em outras palavras, nós, estando no estado st, realizamos a ação at e após a transição para o estado st+1 recebemos uma recompensa rt+1. Para atualizar o valor da função de utilidade de ação, precisamos adicionar o máximo da função de utilidade de ação à recompensa recebida na próxima etapa. Ou seja, a recompensa máxima esperada que podemos obter na próxima etapa. Nosso agente, certamente, não pode ver o futuro e determinar a recompensa futura. Mas ele pode usar sua função aproximável e, estando no estado st+1, calcular o valor da função para todas as ações possíveis desse estado e tomar o máximo dos valores obtidos. Sim, no processo de aprendizado, seus valores estarão longe de ser verdadeiros inicialmente. Mas é melhor do que nada. E, à medida que o agente aprende, o erro de previsão diminuirá.


2.1. Reproduzindo a experiência

A descida de gradiente estocástica é boa porque permite atualizar os valores de uma função com base nos valores de uma pequena amostra da população. Essencialmente, permite que nosso agente atualize os valores da função de utilidade de ação em cada etapa da sessão. Mas, durante o aprendizado supervisionado, usamos uma amostra de treinamento, cujos estados são independentes um do outro. Para aprimorar essa propriedade, misturamos a população a cada vez antes de escolher um novo conjunto de dados de treinamento.

No caso do aprendizado supervisionado, enquanto se movimenta ao longo do tempo em nosso ambiente, o agente entra em um novo estado cada vez que uma ação é tomada, estado esse que está intimamente relacionado com o anterior. Olhe a sua volta. Quer você esteja caminhando ou sentado a uma mesa e fazendo algo, o ambiente em seu campo de visão não muda drasticamente. A única coisa que muda é uma pequena porção da mesma que é afetada pela ação realizada por você. Da mesma forma, os estados do ambiente estudado não mudarão muito quando o agente realizar ações. Isso significa que estados sucessivos terão um relacionamento bastante amplo. Nosso agente observará a autocorrelação de tais estados.

Aqui a dificuldade está no fato de que mesmo o uso de um pequeno coeficiente de treinamento não poupa nosso agente de ajustar a função de utilidade de ação ao estado atual em detrimento da memória da experiência anterior.

No aprendizado supervisionado, o uso de estados independentes após um número bastante grande de iterações possibilita a média dos valores dos coeficientes de peso do modelo que está sendo treinado. No caso do aprendizado por reforço, quando treinamos o modelo em estados conectados e praticamente inalterados, o modelo é retreinado conforme o estado atual.

Como em qualquer série temporal, a relação de estados diminui com o aumento do tempo entre eles. Portanto, para resolver esse problema, ao treinar o modelo de nosso agente, precisamos usar estados dispersos ao longo da linha do tempo. Podemos fazer isso facilmente com dados históricos. Mas, ao passar pelo ambiente, nosso agente não possui essa memória. Ele vê apenas o estado atual e não pode pular de um estado para outro.

Mas, por que não preparamos então a memória para o agente? Olhe, precisamos do seguinte conjunto de dados para atualizar o valor da função de utilidade de ação:

Estado -> Ação -> Recompensa -> Estado

Bem, vamos fazer com que nosso agente armazene o conjunto de dados necessários em algum tipo de buffer, uma vez que ele atravessa os estados do ambiente. O tamanho do buffer é um hiperparâmetro e é determinado pelo arquiteto do modelo. Depois que o buffer estiver cheio, os dados mais antigos serão substituídos pelos novos. Neste caso, não usaremos o estado atual para treinar o modelo, mas sim os selecionados aleatoriamente a partir do buffer de memória do agente. Dessa forma, minimizamos a relação entre estados individuais e aumentamos a capacidade do modelo de generalizar os dados que estão sendo estudados.


2.2. Usando a rede alvo

Outro ponto importante ao aprender a função de utilidade de ação é seu valor máximo na próxima etapa maxQ(st+1|at+1). Em primeiro lugar, devemos entender claramente que se trata de um "valor vindo do futuro". Sim, tomamos um valor preditivo com base em nossa função de utilidade de ação aproximada. Mas estando no momento t não podemos alterar o valor do estado a partir do momento t+1. Mas cada vez que atualizamos o valor da função, atualizamos os pesos de nosso modelo e, assim, alteramos o próximo valor preditivo.

Além disso, treinamos nosso agente para obter a recompensa máxima. Ou seja, a cada iteração da atualização do modelo, maximizamos o valor esperado. E usar o valor preditivo recursivamente maximiza o valor atualizado. Desta forma, os valores da nossa função de utilidade de ação são maximizados em uma progressão. O que leva a uma superestimação dos valores de nossa função e a um aumento no erro na previsão da utilidade das ações. Como você pode imaginar, isso não é muito bom. Portanto, precisamos de um mecanismo estacionário para avaliar a utilidade da ação futura.

Poderíamos resolver esse problema criando um modelo adicional para prever a utilidade da ação futura. Mas essa abordagem exigirá custos adicionais para treinar o segundo modelo. E nós não queremos isso. Por outro lado, já estamos treinando um modelo que realiza essa funcionalidade. Só precisamos que, após alterar os coeficientes de peso, o modelo retorne os valores da função como antes da atualização. Esta contraditória tarefa é resolvida copiando o modelo. Simplesmente criamos 2 cópias do mesmo modelo de função de utilidade de ação. Treinamos uma cópia e usamos a segunda para prever a utilidade da ação futura.

Mas uma vez que o modelo da função utilidade da ação seja fixado, logo se tornará irrelevante no processo de aprendizado. E isso pode tornar a educação continuada ineficaz. Para eliminar a influência desse fator, no processo de aprendizado, precisaremos atualizar o modelo de valores preditivos. Não treinaremos a segunda cópia do modelo em paralelo. Vamos simplesmente copiar os coeficientes de peso desde a cópia treinada do modelo de função de utilidade de ação para ele com uma certa periodicidade. Assim, treinando apenas um modelo, obtemos 2 cópias bastante atualizadas do modelo da função utilidade da ação e nos livramos da superestimação recursiva dos valores preditivos. 

Então vamos resumir o que foi dito acima:

  1. Para treinar o agente, usamos uma rede neural.
  2. A rede neural é treinada para prever o valor esperado da função Q de utilidade de ação.
  3. Para minimizar a correlação entre estados vizinhos no processo de aprendizado, usamos um buffer de memória, do qual extraímos estados aleatoriamente.
  4. Para prever o valor futuro da função Q durante o treinamento, é utilizado o 2º modelo rede alvo (target net, em inglês), que é uma cópia "congelada" do modelo de treinamento.
  5. A rede alvo é atualizada através da cópia periódica das matrizes de peso do modelo de treinamento.

Em seguida, sugiro examinar a concretização da abordagem descrita usando MQL5.


3. Implementação usando MQL5

Implementaremos aprendizado Q profundo usando MQL5 no arquivo do Expert Advisor "Q-learning.mq5". Você pode encontrar o código do EA completo no anexo. Agora vamos nos concentrar apenas na elaboração do método de aprendizado Q profundo.

E antes de prosseguir com isso, precisamos decidir sobre os dados iniciais e o sistema de recompensas. E se usarmos os mesmos dados iniciais de todos os experimentos anteriores, então precisamos pensar sobre o sistema de recompensa. O problema de previsão fractal que discutimos anteriormente é bastante artificial. Podemos, certamente, ajustar o modelo conforme a definição do maior número possível de fractais. Mas nosso principal objetivo é obter o máximo lucro das operações de negociação.

Nesse contexto, faz todo o sentido usar o tamanho da próxima vela como o tamanho da recompensa. Claro, o sinal da recompensa deve corresponder à operação realizada. No modelo simplificado, temos 2 operações de negociação: compra e venda. E também podemos estar fora de posição.

Não vamos complicar o modelo agora definindo o volume da posição, adições e fechamentos parciais. Consideramos que o agente pode estar em uma posição de lote fixo ou pode feche todas as posições e estar fora do mercado.

Além disso, ao trabalhar em uma política de remuneração, devemos entender que o resultado do treinamento depende em grande parte de um sistema de remuneração adequadamente construído. A prática do aprendizado por reforço é bastante rica em exemplos onde a política de recompensa errada levou a resultados inesperados. O modelo pode aprender a tirar as conclusões erradas. Ou pode ficar preso em obter a recompensa máxima sem alcançar o resultado desejado. Por exemplo, podemos dar ao modelo uma recompensa por abrir e fechar uma posição. Mas se esta recompensa for maior que a recompensa pelo lucro acumulado vindo da transação, o modelo pode aprender a simplesmente abrir e fechar posições. Ele maximizará as recompensas e nós maximizaremos as perdas.

Por outro lado, se penalizarmos o modelo por abrir e fechar uma posição, comparável a uma taxa de transação, então o modelo pode simplesmente aprender a “permanecer fora do mercado”. Sem rendimento, mas sem perdas.

Com tudo isso em mente, decidimos criar um modelo com três possíveis ações: comprar, vender, fora do mercado.

O agente irá prever a direção do movimento esperado em cada nova vela e escolherá uma ação sem levar em conta os movimentos anteriores. Ou seja, para simplificar o modelo, não vamos alimentar o agente com informações sobre se ele está em determinada posição e em dada direção. Assim, o agente não acompanha a abertura e o fechamento de posição. Não estabelecemos recompensas pela abertura/fechamento de posição.

Para minimizar o tempo de permanência "fora do mercado" introduzimos uma penalização por ausência de posição. Claro, esta penalidade será menor do que a penalidade por uma posição perdedora.

Assim, formamos a seguinte política de remuneração do agente:

  1. Uma posição lucrativa recebe recompensas iguais ao corpo da vela (analisamos o estado do sistema a cada vela e estamos em uma posição desde a abertura da vela até seu fechamento).
  2. O fato de permanecer "fora do mercado" é penalizado conforme o tamanho do corpo da vela (o tamanho do corpo da vela com sinal negativo indica lucro perdido).
  3. Uma posição perdedora é penalizada conforme o dobro do tamanho do corpo da vela (perda + lucro perdido).

Após definir o sistema de recompensas, passamos diretamente para a implementação do método.

Como mencionado acima, o modelo que está sendo construído usará 2 redes neurais. Para fazer isso, vamos criar 2 objetos para trabalhar com redes neurais. Vamos treinar o StudyNet, e oTargetNet será usado para prever valores futuros da função Q.

CNet                StudyNet;
CNet                TargetNet;

Para que o método de Q-learning profundo funcione, precisaremos também de novas variáveis externas que definirão os hiperparâmetros de construção e aprendizado do modelo.

  • Batch - tamanho do lote de atualização de coeficientes de peso;
  • UpdateTarget - número de atualizações das matrizes de peso do modelo treinado antes de copiar em um modelo "congelado" que prevê futuros valores de função Q;
  • Iterations - número total de iterações de atualizações do modelo treinado durante o processo de treinamento;
  • DiscountFactor - fator de desconto para recompensas futuras.
input int                  Batch =  100;
input int                  UpdateTarget = 20;
input int                  Iterations = 1000;
input double               DiscountFactor =   0.9;

Deixaremos a criação real do modelo de rede neural fora do escopo deste EA. Para criá-lo, usaremos a ferramenta dos artigos sobre transferência de aprendizado. Essa abordagem nos permitirá realizar experimentos usando modelos de várias arquiteturas sem fazer alterações no Expert Advisor. Portanto, no método de inicialização do Expert Advisor, fazemos apenas o carregamento de um modelo criado anteriormente.

//---
   float temp1, temp2;
   if(!StudyNet.Load(FileName + ".nnw", dError, temp1, temp2, dtStudied, false) ||
      !TargetNet.Load(FileName + ".nnw", dError, temp1, temp2, dtStudied, false))
      return INIT_FAILED;

Observe que, como estamos usando 2 cópias do mesmo modelo, estamos carregando os dois modelos desde o mesmo arquivo.

A liberdade de usar diferentes arquiteturas de modelos implica não apenas a capacidade de usar diferentes arquiteturas de camadas ocultas e tamanhos, mas também a capacidade de ajustar a profundidade do histórico que está sendo analisado. E se antes criávamos um modelo no código EA, e a profundidade do histórico era definida por um parâmetro externo, agora podemos definir a profundidade do histórico analisado pelo tamanho da camada de dados de entrada. E o EA a determinará analiticamente, com base no tamanho da camada de dados de entrada. Apenas o número de neurônios por vela no histórico analisado e o tamanho da camada de resultados permanecem inalterados. Uma vez que esses parâmetros estão estruturalmente relacionados aos indicadores utilizados e ao número de ações previsíveis.

   if(!StudyNet.GetLayerOutput(0, TempData))
      return INIT_FAILED;
   HistoryBars = TempData.Total() / 12;
   StudyNet.getResults(TempData);
   if(TempData.Total() != Actions)
      return INIT_PARAMETERS_INCORRECT;

É verdade, não discutimos anteriormente o tamanho da camada de entrada no modelo de aprendizado Q profundo. Como mencionado acima, a função Q retorna a recompensa esperada dependendo do estado e da ação realizada. E para determinar a ação mais útil, precisamos calcular o valor da função para todas as ações possíveis no estado atual. O uso de uma rede neural nos permite criar uma camada de resultados com o número de neurônios igual ao número de todas as ações possíveis. Nesse caso, cada neurônio da camada de resultados será responsável por prever a utilidade de uma determinada ação. Isto nos dará o valor da utilidade de todas as ações em uma só passagem da rede neural. E tudo o que temos que fazer é escolher o valor máximo.

No restante, a função de inicialização do EA permanece inalterada. E seu código completo pode ser encontrado no anexo.

Configuramos o processo de treinamento do modelo na funçãoTrain. No início do corpo desta função, determinamos o tamanho da sessão de treinamento e carregamos os dados históricos. Assim como já fizemos antes com aprendizado supervisionado e não supervisionado.

void Train(void)
  {
//---
   MqlDateTime start_time;
   TimeCurrent(start_time);
   start_time.year -= StudyPeriod;
   if(start_time.year <= 0)
      start_time.year = 1900;
   datetime st_time = StructToTime(start_time);
//---
   int bars = CopyRates(Symb.Name(), TimeFrame, st_time, TimeCurrent(), Rates);
   if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
     {
      ExpertRemove();
      return;
     }
   if(!ArraySetAsSeries(Rates, true))
     {
      ExpertRemove();
      return;
     }
//---
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();

É importante dizer que por conta do uso de dados históricos para treinamento do modelo, não podemos criar um buffer de memória. Afinal, podemos usar todos os dados históricos carregados como um único buffer de memória. No caso de treinamento de modelos em tempo real, por outro lado, devemos adicionar um buffer de memória e realizar seu processo de manutenção.

Em seguida, vamos preparar as variáveis auxiliares:

  • total - tamanho da amostra de treinamento;
  • use_target - sinalizador que indica o uso de rede alvo para prever recompensas futuras.

   int total = bars - (int)HistoryBars - 240;
   bool use_target = false;

O uso do sinalizador use_target se deve à necessidade de desabilitar a previsão de recompensas futuras antes da primeira atualização do modelo de rede alvo. Na verdade, é um ponto bastante delicado. Afinal de contas, na etapa inicial nosso modelo é iniciado com fatores de peso aleatórios. Isso significa que os valores previstos por ele serão completamente aleatórios. E, muito provavelmente, estarão muito longe dos verdadeiros valores. O uso de tais valores aleatórios só pode distorcer o processo de aprendizado do modelo. Nesse caso, o modelo aproximará não os valores reais das recompensas, mas, sim, os valores aleatórios embutidos no próprio modelo. Portanto, antes da primeira iteração da atualização do modelo rede alvo, a exclusão desse ruído nos trará mais benefícios.

Em seguida, preparamos um sistema de ciclos para o treinamento do agente. O loop externo contará o número total de iterações de atualização da matriz de pesos do nosso agente.

   for(int iter = 0; (iter < Iterations && !IsStopped()); iter += UpdateTarget)
     {
      int i = 0;

Em um loop aninhado, contaremos o tamanho do lote de atualização de pesos e o número de atualizações antes de atualizar a rede alvo. É importante notar aqui que em nosso modelo, os coeficientes de peso são atualizados a cada iteração da retropropagação. Portanto, usar o pacote de atualização provavelmente não parece muito correto. Já que para o nosso modelo é sempre igual a "1". No entanto, para equilibrar o número de estados processados entreas atualizações da rede alvo, sua frequência será igual ao produto do tamanho do lote e o número de atualizações entre as atualizações.

No corpo do loop, determinamos aleatoriamente o estado do sistema para a iteração atual do treinamento do modelo. Aqui limpamos os buffers para gravar 2 estados subsequentes. O primeiro estado será usado para propagação do modelo treinado. E o segundo é para os valores preditivos da função Q na rede alvo.

      for(int batch = 0; batch < Batch * UpdateTarget; batch++)
        {
         i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (total));
         State1.Clear();
         State2.Clear();
         int r = i + (int)HistoryBars;
         if(r > bars)
            continue;

Em seguida, em um loop aninhado, preenchemos os buffers preparados com dados históricos. Para evitar operações desnecessárias, antes de preencher o segundo buffer de estado, verificamos o sinalizador de uso de rede alvo. O buffer é preenchido somente quando necessário.

         for(int b = 0; b < (int)HistoryBars; b++)
           {
            int bar_t = r - b;
            float open = (float)Rates[bar_t].open;
            TimeToStruct(Rates[bar_t].time, sTime);
            float rsi = (float)RSI.Main(bar_t);
            float cci = (float)CCI.Main(bar_t);
            float atr = (float)ATR.Main(bar_t);
            float macd = (float)MACD.Main(bar_t);
            float sign = (float)MACD.Signal(bar_t);
            if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
               continue;
            //---
            if(!State1.Add((float)Rates[bar_t].close - open) || !State1.Add((float)Rates[bar_t].high - open) ||
               !State1.Add((float)Rates[bar_t].low - open) || !State1.Add((float)Rates[bar_t].tick_volume / 1000.0f) ||
               !State1.Add(sTime.hour) || !State1.Add(sTime.day_of_week) || !State1.Add(sTime.mon) ||
               !State1.Add(rsi) || !State1.Add(cci) || !State1.Add(atr) || !State1.Add(macd) || !State1.Add(sign))
               break;
            if(!use_target)
               continue;
            //---
            bar_t --;
            open = (float)Rates[bar_t].open;
            TimeToStruct(Rates[bar_t].time, sTime);
            rsi = (float)RSI.Main(bar_t);
            cci = (float)CCI.Main(bar_t);
            atr = (float)ATR.Main(bar_t);
            macd = (float)MACD.Main(bar_t);
            sign = (float)MACD.Signal(bar_t);
            if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
               continue;
            //---
            if(!State2.Add((float)Rates[bar_t].close - open) || !State2.Add((float)Rates[bar_t].high - open) ||
               !State2.Add((float)Rates[bar_t].low - open) || !State2.Add((float)Rates[bar_t].tick_volume / 1000.0f) ||
               !State2.Add(sTime.hour) || !State2.Add(sTime.day_of_week) || !State2.Add(sTime.mon) ||
               !State2.Add(rsi) || !State2.Add(cci) || !State2.Add(atr) || !State2.Add(macd) || !State2.Add(sign))
               break;
           }

Depois de preencher com sucesso os buffers com dados históricos, verificamos seu tamanho e realizamos uma propagação de ambos os modelos. Ao fazer isso, não nos esquecemos de verificar o resultado das operações de inicialização do buffer.

         if(IsStopped())
           {
            ExpertRemove();
            return;
           }
         if(State1.Total() < (int)HistoryBars * 12 ||
            (use_target && State2.Total() < (int)HistoryBars * 12))
            continue;
         if(!StudyNet.feedForward(GetPointer(State1), 12, true))
            return;
         if(use_target)
           {
            if(!TargetNet.feedForward(GetPointer(State2), 12, true))
               return;
            TargetNet.getResults(TempData);
           }

Após uma propagação bem-sucedida, recebemos uma recompensa do ambiente e preparamos um buffer de alvos para a retropropagação de acordo com a política de recompensa definida acima.

Aqui você deve prestar atenção a 2 situações. Primeiro, verificamos o sinalizador de uso de rede alvo. E adicionamos o valor preditivo apenas com um resultado positivo. Se o sinalizador estiver definido como false, igualamos os valores preditivos da função Q a "0".

A segunda situação é o desvio em relação à equação de Bellman. Como se lembrarão, a equação de Bellman toma o valor máximo da recompensa futura. Assim, o modelo é treinado para obter o rendimento máximo. Essa abordagem, é claro, leva à máxima lucratividade. Mas no caso de negociações, quando os gráficos de preços são preenchidos com muito ruído, isso provoca um aumento do volume de negócios. E a presença de ruído reduz a qualidade das previsões. Pode ser comparado à tentativa de prever cada nova vela. O que potencialmente leva à abertura e fechamento de uma posição em quase todas as novas velas. Em vez de identificar uma tendência e abrir uma posição em sua direção.

Para eliminar a influência do fator acima, decidi fazer desvios em relação à equação de Bellman e atualizar o modelo da função Q, usei valores unidirecionais. Usei apenas o máximo para a ação "Fora do mercado".

         Rewards.Clear();
         double reward = Rates[i - 1 + 240].close - Rates[i - 1 + 240].open;
         if(reward >= 0)
           {
            if(!Rewards.Add((float)(reward + (use_target ? DiscountFactor * TempData.At(0) : 0))) ||
               !Rewards.Add((float)(-2 * (use_target ? reward + DiscountFactor * TempData.At(1) : 0)))
               ||
               !Rewards.Add((float)(-reward + (use_target ? DiscountFactor * TempData.At(TempData.Maximum(0, 3)) : 0))))
               return;
           }
         else
            if(!Rewards.Add((float)(2 * reward + (use_target ? DiscountFactor * TempData.At(0) : 0))) ||
               !Rewards.Add((float)(-reward + (use_target ? DiscountFactor * TempData.At(1) : 0))) ||
               !Rewards.Add((float)(reward + (use_target ? DiscountFactor * TempData.At(TempData.Maximum(0, 3)) : 0))))
               return;

Após preparar o buffer de recompensa, realizamos a retropropagação do modelo treinado. E novamente verificamos o resultado da operação.

         if(!StudyNet.backProp(GetPointer(Rewards)))
            return;
        }

Desta forma, completa-se o ciclo aninhado para contar as iterações de aprendizado de nosso agente. Após sua conclusão, atualizamos o modelo rede alvo. Nossos modelos não possuem métodos de troca de coeficientes de peso. E aqui eu não me dei à tarefa de inventar algo novo e grandioso. Em vez disso, decidi usar o mecanismo existente para salvar e carregar o modelo. De fato, neste caso, obtemos uma cópia exata do modelo, com todo o seu conteúdo.

Nós simplesmente salvamos o modelo de treinamento em um arquivo. E então carregamos o modelo salvo a partir do arquivo para TargetNet. É claro, não se esqueça de verificar o resultado das operações.

      if(!StudyNet.Save(FileName + ".nnw", StudyNet.getRecentAverageError(), 0, 0, Rates[i].time, false))
         return;
      float temp1, temp2;
      if(!TargetNet.Load(FileName + ".nnw", dError, temp1, temp2, dtStudied, false))
         return;
      use_target = true;
      PrintFormat("Iteration %d, loss %.5f", iter, StudyNet.getRecentAverageError());
     }

Uma vez que o modelo TargetNet tenha sido atualizado com sucesso, mudaremos seu sinalizador de uso, exibiremos uma mensagem informativa no log e passaremos para a próxima iteração do loop externo.

Após a conclusão do processo de treinamento, limpamos os comentários e iniciamos o fechamento do EA de treinamento do modelo.

   Comment("");
//---
   ExpertRemove();
  }

Você pode encontrar o código do EA completo no anexo.


4. Teste

O método foi testado no instrumento EURUSD e no timeframe H1 nos últimos 2 anos. No entanto, como todos os experimentos anteriores, os parâmetros dos indicadores foram usados conforme definido no Expert Advisor por padrão.

Para teste, foi criado um modelo convolucional da seguinte arquitetura:

  1. Camada de dados de entrada, 240 elementos (20 velas, 12 neurônios para descrição de uma vela).
  2. Camada convolucional, janela de dados de entrada 24 (2 velas), passo 12 (1 vela), na saída de 6 filtros.
  3. Camada convolucional, janela de dados de entrada 2, passo 1, 2 filtros.
  4. Camada convolucional, janela de dados de entrada 3, passo 1, 2 filtros.
  5. Camada convolucional, janela de dados de entrada 3, passo 1, 2 filtros.
  6. Camada neural totalmente conectada com 1000 elementos.
  7. Camada neural totalmente conectada com 1000 elementos.
  8. Camada totalmente conectada de 3 elementos (camada de resultados para 3 ações).

Da 2ª à 7ª camada foram ativados pelo sigmóide. Para a camada de resultados, a tangente hiperbólica foi usada como função de ativação.

O gráfico da dinâmica do erro no processo de treinamento do modelo é apresentado abaixo. Como você pode ver no gráfico, durante o processo de aprendizado, o erro na previsão da recompensa esperada diminuiu rapidamente. E depois de 500 iterações ficou próximo de "0". O processo de treinamento de um modelo de 1000 iterações terminou com um erro de 0,00105.

Gráfico de teste para o modelo DQN


Considerações finais

Neste artigo, continuamos a nos iniciar nos métodos de aprendizado por reforço. Analisamos o método aprendizado Q profundo, que foi introduzido pela equipe DeepMind em 2013. Com o advento deste método, podemos dizer que começou uma nova fase no desenvolvimento de algoritmos de aprendizado por reforço. É o uso deste método que tem mostrado que é possível treinar modelos para construir estratégias. O uso de um único modelo torna possível treiná-lo para resolver diferentes problemas sem fazer mudanças estruturais em sua arquitetura ou hiperparâmetros. E estas foram as primeiras experiências em que um algoritmo treinado superou o desempenho de um profissional especializado.

Analisamos a implementação do método usando MQL5. E os resultados obtidos ao testar o modelo demonstram a possibilidade de usar o método para construir modelos de negociação funcionais.


Referências

  1. Playing Atari with Deep Reinforcement Learning
  2. Redes neurais de maneira fácil (Parte 25): Exercícios práticos de transferência de aprendizado
  3. Redes neurais de maneira fácil (Parte 26): aprendizado por reforço

Programas utilizados no artigo

# Nome Tipo Descrição
1 Q-learning.mq5 EA EA para treinamento de modelos 
2 NeuroNet.mqh Biblioteca de classes Biblioteca para organizar modelos de redes neurais
3 NeuroNet.cl Biblioteca
Biblioteca de código de programa OpenCL paraorganizarmodelos de redes neurais


Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/11369

Arquivos anexados |
MQL5.zip (66.7 KB)
Como desenvolver um sistema de negociação baseado no indicador Bull's Power Como desenvolver um sistema de negociação baseado no indicador Bull's Power
Bem-vindo a um novo artigo em nossa série sobre como desenvolver um sistema de negociação com base nos indicadores técnicos mais populares, aqui está um novo artigo sobre como aprender a desenvolver um sistema de negociação pelo indicador técnico Bull's Power.
Redes neurais de maneira fácil (Parte 26): aprendizado por reforço Redes neurais de maneira fácil (Parte 26): aprendizado por reforço
Continuamos a estudar métodos de aprendizado de máquina. Com este artigo, começamos outro grande tópico chamado aprendizado por reforço. Essa abordagem permite que os modelos estabeleçam certas estratégias para resolver as tarefas. E esperamos que essa propriedade inerente ao aprendizado de reforço abra novos horizontes para a construção de estratégias de negociação.
DoEasy. Controles (Parte 16): Objeto WinForms TabControl - múltiplas fileiras de cabeçalhos de guias, modo esticamento de cabeçalhos consoante o tamanho do contêiner DoEasy. Controles (Parte 16): Objeto WinForms TabControl - múltiplas fileiras de cabeçalhos de guias, modo esticamento de cabeçalhos consoante o tamanho do contêiner
Neste artigo vamos continuar o desenvolvimento do controle TabControl, e trataremos da localização dos cabeçalhos das guias nos quatro lados do controle para todos os modos de tamanho de cabeçalho: "Normal", "Fixed" e "Fill To Right".
Redes neurais de maneira fácil (Parte 25): Exercícios práticos de transferência de aprendizado Redes neurais de maneira fácil (Parte 25): Exercícios práticos de transferência de aprendizado
Nos dois últimos artigos, criamos uma ferramenta que permite criar e editar modelos de redes neurais. E agora é hora de avaliar o uso potencial da transferência de aprendizado (transfer learning, em inglês) usando exemplos práticos.