English Русский 中文 Español Deutsch 日本語
preview
Redes neurais de maneira fácil (Parte 69): restrição de política comportamental com base na densidade de dados off-line (SPOT)

Redes neurais de maneira fácil (Parte 69): restrição de política comportamental com base na densidade de dados off-line (SPOT)

MetaTrader 5Sistemas de negociação |
168 2
Dmitriy Gizlyk
Dmitriy Gizlyk

Introdução

O aprendizado por reforço off-line nos permite treinar modelos com base em dados coletados durante a interação com o ambiente. Isso nos permite reduzir significativamente o processo de interação com o mesmo. Além disso, quando a modelagem do ambiente é complexa, podemos coletar dados em tempo real de vários agentes de pesquisa, e, posteriormente, treinar o modelo com essa informação.

Mas, o uso de um conjunto de treinamento estático reduz significativamente as informações disponíveis sobre o ambiente. A limitação de recursos não nos permite manter no conjunto de treinamento toda a diversidade do ambiente.

Porém, no processo de treinamento da política ótima do Agente, há uma alta probabilidade de que suas ações excedam a distribuição do conjunto de treinamento. É evidente que, devido à ausência de feedback do ambiente, não podemos obter uma avaliação real dessas ações. E devido à falta de dados no conjunto de treinamento, nosso Crítico também não pode fornecer uma avaliação adequada. Nesse caso, podemos obter expectativas tanto superestimadas quanto subestimadas.

É preciso dizer que expectativas superestimadas são muito mais perigosas do que subestimadas. Com avaliações subestimadas, o modelo pode desistir de realizar essas ações, levando ao treinamento de uma política subótima do Agente. No caso de avaliações superestimadas, o modelo tenderá a repetir essas ações, o que pode levar a perdas significativas durante a utilização prática. Por isso, manter a política do Agente dentro do conjunto de treinamento se torna um aspecto importante para garantir a confiabilidade do aprendizado off-line.

Diversos métodos de aprendizado por reforço off-line para resolver essa tarefa utilizam parametrização ou regularização, que limitam a política do Agente a realizar ações dentro do conjunto de suporte do conjunto de treinamento. Construções detalhadas normalmente interferem nos modelos dos Agentes, o que pode levar a custos adicionais durante a utilização prática e não permite utilizar completamente os métodos estabelecidos de aprendizado por reforço online. Os métodos de regularização reduzem a divergência entre a política aprendida e o conjunto de treinamento, o que pode não corresponder à definição de suporte com base na densidade e, assim, evitar ineficazmente ações fora da distribuição.

Nesse contexto, destaca-se positivamente o método Supported Policy Optimization (SPOT), que foi apresentado no artigo "Supported Policy Optimization for Offline Reinforcement Learning". Seus métodos derivam diretamente da formalização teórica da restrição de política com base na densidade da distribuição do conjunto de treinamento. SPOT utiliza um estimador de densidade baseado em autocodificador variacional (VAE). Ele representa um elemento de regularização simples, mas eficaz. E pode ser incorporado em algoritmos de aprendizado por reforço existentes. O SPOT atinge o melhor desempenho da categoria em benchmarks padrão para RL off-line. Além disso, devido ao design flexível, os modelos pré-treinados no modo off-line usando SPOT também podem ser ajustados no modo online.


1. Algoritmo Supported Policy OpTimization (SPOT)

Restrições de suporte são um método típico para mitigar erros em aprendizado por reforço off-line. Por sua vez, a restrição de suporte pode ser formalizada com base na densidade da estratégia comportamental. Os autores do método Supported Policy OpTimization propõem um algoritmo de regularização com uma perspectiva de avaliação explícita da densidade. O SPOT inclui um termo de regularização que é derivado diretamente da formalização teórica da restrição de suporte pela densidade de distribuição. Como elemento de regularização, é utilizado um autocodificador variacional estendido (CVAE), que aprende a densidade do conjunto de treinamento.

Assim como a estratégia ideal pode ser extraída da função Q ideal, a estratégia ideal suportada também pode ser obtida usando uma escolha gulosa:

No caso de aproximação de função, isso vira um problema de otimização da estratégia com restrição.

Ao invés de só ajustar a política do Agente ou usar penalidades por desvios, como em outros métodos, os autores do SPOT sugerem usar a densidade dos dados diretamente como uma restrição:

aqui ϵ' = log ϵ facilita a notação.

A restrição usando a densidade comportamental é simples e direta no contexto da restrição de suporte. Os autores do método propõem usar a função de verossimilhança logarítmica em vez da probabilística devido à sua conveniência matemática.

Por sua vez, isso impõe uma restrição adicional de que a densidade da estratégia comportamental seja limitada inferiormente em cada ponto do espaço de estados. E praticamente é impossível resolver tal problema devido ao grande, até mesmo infinito, número de restrições. Em vez disso, os autores do SPOT usam uma abordagem heurística que considera a densidade comportamental média:

Transformamos o problema de otimização com restrições em um problema sem restrições. Para isso, consideramos o termo da restrição como uma penalidade. Assim, obtemos a função objetivo de aprendizado da estratégia:

aqui λ é o multiplicador de Lagrange.

O termo de regularização na função de perda apresentada acima requer acesso à política comportamental usada na coleta do conjunto de treinamento. Mas só temos dados off-line, gerados por essa política. E podemos avaliar explicitamente a densidade em qualquer ponto usando vários métodos de estimativa de densidade. O autocodificador variacional (VAE) é um dos melhores modelos de estimativa de densidade usando redes neurais. E os autores do método decidiram usar o autocodificador variacional estendido como seu estimador de densidade. Após treinar o VAE, podemos simplesmente usá-lo como um limite inferior.

A estrutura geral apresentada acima pode ser construída com base em vários algoritmos de aprendizado por reforço após modificações mínimas. Em seu artigo, os autores do método usam TD3 como algoritmo base.


2. Implementação com MQL5

Depois de considerar os aspectos teóricos do método Supported Policy Optimization, passamos à sua implementação prática com MQL5. Vamos implementar nosso modelo com base nos EAs do artigo sobre o método Real-ORL. Lembro que o modelo base utilizado é construído com base no método Soft Actor-Critic, que é similar ao TD3 utilizado pelos autores do SPOT. Além disso, nosso modelo é complementado por vários métodos discutidos em artigos anteriores.

Primeiro, é importante notarmos que o método SPOT adiciona a regularização da política do Agente, com base na densidade dos dados no conjunto de treinamento. Essa regularização é aplicada durante a etapa de treinamento off-line da política do Agente. Isso não interfere de forma alguma com a interação do modelo com o ambiente. Por isso, os EAs usados para coletar dados de treinamento e testes foram mantidos inalterados, e você pode analisá-los por conta própria no anexo.

Assim, é claro que agora estamos prontos para passar para o treinamento do modelo. No entanto, antes de começar a treinar a política, precisamos treinar o autocodificador da função de densidade do conjunto de treinamento. Sendo assim, dividimos o processo de treinamento em 2 etapas. E o treinamento do autocodificador será realizado em um EA separado "\SPOT\StudyCVAE.mq5".

2.1 Treinamento do modelo de densidade

Antes de começar a construir o EA de treinamento do modelo de densidade, vamos primeiro discutir o que e como iremos treinar. Os autores do método SPOT propuseram usar um autocodificador estendido para aprender a densidade do conjunto de treinamento. O que isso significa na prática?

Já discutimos as propriedades do autocodificador de compressão e restauração de dados. Também mencionamos que as redes neurais só podem funcionar de maneira estável em um ambiente semelhante ao do conjunto de treinamento. Então, quando a entrada do modelo recebe dados de entrada distantes da distribuição do conjunto de treinamento, os resultados serão próximos a valores aleatórios. E o erro de decodificação desses dados aumentará significativamente. Vamos explorar essa combinação de propriedades do modelo de autocodificador.

Treinamos o autocodificador na distribuição de ações do Agente no conjunto de treinamento. E, durante o treinamento do Agente, forneceremos ao autocodificador as ações propostas pela política atualizada do Agente. O erro de decodificação dos dados vai nos mostrar indiretamente a distância das ações previstas em relação à distribuição do conjunto de treinamento.

Com essa análise, entendemos a função do autocodificador. Mas será que só saber se a ação do Agente faz parte do conjunto de treinamento é o suficiente? Sabemos muito bem que a mesma ação em diferentes estados do ambiente pode levar a resultados completamente distintos. Portanto, precisamos treinar o autocodificador para distinguir as distribuições das ações em diferentes estados do ambiente. Chegamos à conclusão de que precisamos fornecer ao autocodificador o par "Estado-Ação" como entrada. Esperamos que a saída do autocodificador seja a Ação do Agente que foi fornecida na entrada.

Devemos observar que, ao fornecer o par "Estado-Ação" como entrada para o autocodificador, esperamos que sua representação latente contenha informações compactadas sobre o Estado e a Ação. No entanto, se treinarmos o autocodificador apenas para decodificar a ação, é altamente provável que ele aprenda a ignorar as informações sobre o Estado do ambiente. E use todo o tamanho da representação latente para transmitir a Ação desejada. Isso, no final, nos leva de volta à situação de codificação e decodificação da Ação sem considerar o estado. E isso é altamente indesejável. Portanto, é importante destacar para o Autocodificador ambas as componentes dos dados de entrada "Estado-Ação". Para alcançar esse resultado, os autores do método usam um autocodificador estendido, cuja arquitetura prevê a entrada de uma Chave para decodificação dos dados. Essa Chave, juntamente com a representação latente, é fornecida na entrada do decodificador. No nosso caso, usaremos o estado do ambiente como Chave.

Portanto, precisamos construir um modelo de autocodificador, cuja entrada da propagação deve receber 3 tensores:

  • Estado do ambiente (na entrada do Codificador)
  • Ação do Agente (na entrada do Codificador)
  • Estado do ambiente (Chave na entrada do Decodificador)

Anteriormente, construímos modelos apenas com dados de entrada de 2 tensores. E agora enfrentamos a questão de lidar com dados de entrada de 3 tensores. Claro, essa tarefa pode ser resolvida de várias maneiras.

Primeiro, podemos combinar o par "Estado-Ação" em um único tensor. Então, a Chave será o segundo tensor de dados de entrada e isso se encaixa no modelo de 2 tensores de dados de entrada que usamos anteriormente. No entanto, combinar dados incompatíveis, como o estado do ambiente e as ações do Agente, pode afetar negativamente a qualidade do modelo e limitar nossas possibilidades de pré-processamento dos dados brutos do estado do ambiente.

A segunda opção é adicionar um método para o modelo trabalhar com 3 tensores de dados de entrada. Esse é um processo trabalhoso, que pode levar à criação infinita de métodos para cada tarefa específica. O que tornaria nossa biblioteca volumosa e complicada de entender e manter.

Nesta série de artigos, escolhi a terceira opção, que considero a mais simples: criar modelos separados para o Codificador e o Decodificador. Cada um trabalha com 2 tensores de dados de entrada. E sua implementação se encaixa totalmente nos métodos que desenvolvemos anteriormente.

Teoricamente, estamos prontos. Agora, vamos descrever a arquitetura dos modelos do nosso Autocodificador. Realizaremos esse trabalho no método CreateCVAEDescriptions. Na entrada do método, fornecemos ponteiros para 2 arrays dinâmicos, nos quais coletaremos a arquitetura de 2 modelos: Codificador e Decodificador. No corpo do método, verificamos os ponteiros recebidos e, se necessário, criamos novas instâncias de objetos de arrays dinâmicos.

bool CreateCVAEDescriptions(CArrayObj *encoder, CArrayObj *decoder)
  {
//---
   CLayerDescription *descr;
//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }
   if(!decoder)
     {
      decoder = new CArrayObj();
      if(!decoder)
         return false;
     }

Primeiro, descrevemos a arquitetura do Codificador. Alimentamos o modelo com dados históricos de movimentos de preços e indicadores analisados. Os dados são fornecidos na entrada do modelo de forma bruta e não processada. Realizamos o pré-processamento numa camada de normalização em lote.

//--- Encoder
   encoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (HistoryBars * BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1000;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Depois, comprimimos os dados e extraímos padrões consolidados com o bloco de camadas convolucionais.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = HistoryBars;
   descr.window = BarDescr;
   descr.step = BarDescr;
   int prev_wout = descr.window_out = BarDescr / 2;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = prev_count;
   descr.step = prev_wout;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = prev_count;
   descr.window = prev_wout;
   descr.step = prev_wout;
   prev_wout = descr.window_out = 8;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = prev_count;
   descr.step = prev_wout;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

A incorporação do estado do ambiente obtida dessa forma é combinada com o vetor de ações do Agente.

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = prev_count;
   descr.step = NActions;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Depois, usamos 2 camadas totalmente conectadas para comprimir os dados.

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 2 * EmbeddingSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

E na saída do Codificador, criamos uma representação latente estocástica usando a camada interna do autocodificador variacional.

//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronVAEOCL;
   descr.count = EmbeddingSize;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

A seguir, descrevemos a arquitetura do Decodificador. Na entrada do modelo, fornecemos a representação latente gerada pelo Codificador.

//--- Decoder
   decoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }

Imediatamente concatenamos o tensor obtido com o vetor do estado do ambiente.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = EmbeddingSize;
   descr.window = prev_count;
   descr.step = (HistoryBars * BarDescr);
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }

Note que, no Codificador, os dados do estado do ambiente entram brutos e são pré-processados numa camada de normalização. No Decodificador, não temos a possibilidade de realizar essa normalização. Optei por não normalizar duas vezes os dados. Simplesmente usamos, durante o treinamento e uso, os dados normalizados vindos do Codificador. Isso simplifica o Decodificador e reduz o tempo de processamento dos dados.

Depois, usamos camadas totalmente conectadas para restaurar o vetor de ações a partir dos dados de entrada.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = NActions;
   descr.optimization = ADAM;
   descr.activation = SIGMOID;
   if(!decoder.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

Após descrevermos a arquitetura do nosso Autocodificador, passamos à construção do EA para seu treinamento. Como já mencionado, treinaremos 2 modelos: Codificador e Decodificador.

CNet                 Encoder;
CNet                 Decoder;

No método de inicialização do programa OnInit, carregamos primeiro o conjunto de treinamento. E não nos esquecemos de verificar o resultado da operação. Pois, se houver erro no carregamento, não teremos dados para treinar os modelos.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   ResetLastError();
   if(!LoadTotalBase())
     {
      PrintFormat("Error of load study data: %d", GetLastError());
      return INIT_FAILED;
     }

Em seguida, tentamos carregar modelos previamente treinados e, se necessário, geramos novos modelos inicializados com parâmetros aleatórios.

//--- load models
   float temp;
   if(!Encoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true) ||
      !Decoder.Load(FileName + "Dec.nnw", temp, temp, temp, dtStudied, true))
     {
      Print("Init new CVAE");
      CArrayObj *encoder = new CArrayObj();
      CArrayObj *decoder = new CArrayObj();
      if(!CreateCVAEDescriptions(encoder,decoder))
        {
         delete encoder;
         delete decoder;
         return INIT_FAILED;
        }
      if(!Encoder.Create(encoder) || !Decoder.Create(decoder))
        {
         delete encoder;
         delete decoder;
         return INIT_FAILED;
        }
         delete encoder;
         delete decoder;
     }

Depois, transferimos ambos os modelos para um contexto OpenCL único, facilitando a troca de dados entre eles sem recarregar na memória principal.

   OpenCL = Encoder.GetOpenCL();
   Decoder.SetOpenCL(OpenCL);

Também realizamos o controle mínimo necessário da arquitetura dos modelos carregados (ou criados). Com controle obrigatório dos resultados das operações.

   Encoder.GetLayerOutput(0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Encoder doesn't match state description (%d <> %d)", Result.Total(), 
                                                                                          (HistoryBars * BarDescr));
      return INIT_FAILED;
     }
//---
   Encoder.getResults(Result);
   int latent_state = Result.Total();
   Decoder.GetLayerOutput(0, Result);
   if(Result.Total() != latent_state)
     {
      PrintFormat("Input size of Decoder doesn't match result of Encoder (%d <> %d)", Result.Total(), latent_state);
      return INIT_FAILED;
     }

E iniciamos a criação do evento de início do processo de treinamento do modelo. Depois, concluímos o método de inicialização do programa com o resultado INIT_SUCCEEDED.

   if(!EventChartCustom(ChartID(), 1, 0, 0, "Init"))
     {
      PrintFormat("Error of create study event: %d", GetLastError());
      return INIT_FAILED;
     }
//---
   return(INIT_SUCCEEDED);
  }

No método de desinicialização do programa OnDeinit, salvamos os modelos treinados e limpamos a memória dos objetos criados no programa.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   Encoder.Save(FileName + "Enc.nnw", 0, 0, 0, TimeCurrent(), true);
   Decoder.Save(FileName + "Dec.nnw", Decoder.getRecentAverageError(), 0, 0, TimeCurrent(), true);
   delete Result;
   delete OpenCL;
  }

Observe que todos os modelos são salvos no diretório geral dos terminais. Isso os torna acessíveis tanto ao usar programas no terminal quanto no testador de estratégias.

O treinamento dos modelos é tradicionalmente realizado no método Train. No corpo do método, primeiro criamos as variáveis locais necessárias.

//+------------------------------------------------------------------+
//| Train function                                                   |
//+------------------------------------------------------------------+
void Train(void)
  {
   int total_tr = ArraySize(Buffer);
   uint ticks = GetTickCount();
   int bar = (HistoryBars - 1) * BarDescr;

Depois, geramos o ciclo de treinamento.

   for(int iter = 0; (iter < Iterations && !IsStopped()); iter ++)
     {
      int tr = int((MathRand() * MathRand() / MathPow(32767, 2)) * (total_tr));
      int i = int((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2));
      if(i < 0)
         continue;

Observe que, ao contrário de nossos trabalhos recentes, aqui não usamos priorização de trajetórias. Isso é uma escolha consciente e intencional. Na verdade, neste estágio, buscamos aprender a densidade verdadeira dos dados no conjunto de treinamento. E o uso da priorização de trajetórias pode distorcer as informações em favor de trajetórias com maior prioridade. Então, usamos amostragem uniforme de trajetórias e estados nelas.

Após amostrar a trajetória e o estado, preenchemos os buffers de descrição do Estado do ambiente e Ações do Agente a partir do conjunto de treinamento.

      State.AssignArray(Buffer[tr].States[i].state);
      Actions.AssignArray(Buffer[tr].States[i].action);
      if(Actions.GetIndex() >= 0)
         Actions.BufferWrite();

Lembro que normalmente, o conceito de "descrição do ambiente" incluiria o estado da conta e posições abertas. Aqui, não enfatizei o estado da conta, pois a direção das posições abertas ou mantidas é determinada pelo estado do mercado. A análise do estado da conta é realizada para gerenciamento de riscos e determinação do tamanho da posição. Neste estágio, decidi me limitar ao estudo da densidade das ações em situações de mercado específicas e não enfatizar o modelo de gerenciamento de riscos.

Após preparar os buffers de dados de entrada, realizamos a propagação do autocodificador. Como discutido anteriormente, fornecemos o apontador do Codificador duas vezes na entrada do Decodificador. Usamos a saída do modelo como o fluxo principal de dados de entrada. E para o fluxo adicional de dados de entrada, usamos os resultados da camada de normalização em lote do Codificador. Monitoramos o processo de execução das operações.

      if(!Encoder.feedForward(GetPointer(State), 1,false, GetPointer(Actions)) ||
         !Decoder.feedForward(GetPointer(Encoder), -1, GetPointer(Encoder),1))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         break;
        }

No processo de treinamento do Autocodificador, não precisamos analisar ou processar seu desempenho. Basta indicarmos os valores-alvo, que são o vetor de ações do Agente. É o mesmo vetor que fornecemos na entrada do Codificador. Em outras palavras, o buffer de resultados já está preparado, e chamamos os métodos de retropropagação de ambos os modelos do autocodificador.

      if(!Decoder.backProp(GetPointer(Actions), GetPointer(Encoder), 1) ||
         !Encoder.backPropGradient(GetPointer(Actions), GetPointer(Actions)))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         break;
        }

Observe que o Codificador ajusta seus parâmetros com base no gradiente de erro obtido do Decodificador. Não precisamos gerar um buffer separado de valores-alvo para o Codificador.

Com isso, concluímos uma iteração de treinamento do autocodificador. Resta-nos apenas informar o usuário sobre o progresso e passar para a próxima iteração do ciclo de treinamento dos modelos.

      if(GetTickCount() - ticks > 500)
        {
         string str = StringFormat("%-15s %5.2f%% -> Error %15.8f\n", "Decoder", iter * 100.0 / (double)(Iterations), 
                                                                                    Decoder.getRecentAverageError());
         Comment(str);
         ticks = GetTickCount();
        }
     }

Aqui, mostramos informações apenas sobre o erro do Decodificador, pois o erro do Codificador não é calculado.

Após concluir com sucesso todas as iterações do ciclo de treinamento do autocodificador, limpamos o campo de comentários do gráfico e começamos o encerramento do EA.

   Comment("");
//---
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Decoder", Decoder.getRecentAverageError());
   ExpertRemove();
//---
  }

O código completo do EA de treinamento do modelo de densidade pode ser encontrado no anexo. Lá também estão todos os programas utilizados no artigo.

2.2 Treinamento da política do Agente

Após treinar o modelo de densidade, passamos ao EA de treinamento da política do Agente "\SPOT\Study.mq5". É importante dizer que o processo de treinamento do Agente foi praticamente transferido sem alterações. Ele foi apenas ligeiramente complementado na parte de regularização de sua política comportamental. A arquitetura de todos os modelos treinados também foi copiada sem alterações. Sendo assim, analisaremos de maneira pontual os métodos do EA "...\SPOT\Study.mq5". O código completo, como sempre, pode ser consultado no anexo.

Embora as mudanças no algoritmo de treinamento da política do Agente sejam pequenas, elas envolvem os modelos de autocodificador treinados anteriormente. Precisamos adicioná-los ao programa.

STrajectory          Buffer[];
CNet                 Actor;
CNet                 Critic1;
CNet                 Critic2;
CNet                 TargetCritic1;
CNet                 TargetCritic2;
CNet                 Convolution;
CNet                 Encoder;
CNet                 Decoder;

No método de inicialização do programa OnInit, como de costume, carregamos o conjunto de treinamento, verificando sempre a execução das operações.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   ResetLastError();
   if(!LoadTotalBase())
     {
      PrintFormat("Error of load study data: %d", GetLastError());
      return INIT_FAILED;
     }

Depois, antes de carregar os modelos treináveis, carregamos o Autocodificador. Se não conseguirmos carregar os modelos, informamos o usuário e encerramos o método de inicialização com o resultado INIT_FAILED.

//--- load models
   float temp;
   if(!Encoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true) ||
      !Decoder.Load(FileName + "Dec.nnw", temp, temp, temp, dtStudied, true))
     {
      Print("Cann't load CVAE");
      return INIT_FAILED;
     }

É importante notar que, na ausência de modelos previamente treinados, não criamos novos com parâmetros aleatórios. Os modelos não treinados apenas distorceriam o processo de treinamento. E os resultados desse treinamento seriam imprevisíveis.

Por outro lado, poderíamos adicionar uma flag para, na ausência de modelos treinados do Autocodificador, realizar o treinamento da política do Agente sem a regularização de suas ações, como feito anteriormente. Em um projeto real, provavelmente faria isso. Mas, neste caso, queremos avaliar especificamente o trabalho da regularização. Por isso, interromper o programa serve como um ponto adicional de controle do "fator humano".

Em seguida, carregamos os modelos treináveis e, se necessário, criamos novos, inicializados com parâmetros aleatórios.

   if(!Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) ||
      !Critic1.Load(FileName + "Crt1.nnw", temp, temp, temp, dtStudied, true) ||
      !Critic2.Load(FileName + "Crt2.nnw", temp, temp, temp, dtStudied, true) ||
      !TargetCritic1.Load(FileName + "Crt1.nnw", temp, temp, temp, dtStudied, true) ||
      !TargetCritic2.Load(FileName + "Crt2.nnw", temp, temp, temp, dtStudied, true))
     {
      Print("Init new models");
      CArrayObj *actor = new CArrayObj();
      CArrayObj *critic = new CArrayObj();
      CArrayObj *convolution = new CArrayObj();
      if(!CreateDescriptions(actor, critic, convolution))
        {
         delete actor;
         delete critic;
         delete convolution;
         return INIT_FAILED;
        }
      if(!Actor.Create(actor) || !Critic1.Create(critic) || !Critic2.Create(critic) ||
         !Convolution.Create(convolution))
        {
         delete actor;
         delete critic;
         delete convolution;
         return INIT_FAILED;
        }
      if(!TargetCritic1.Create(critic) || !TargetCritic2.Create(critic))
        {
         delete actor;
         delete critic;
         delete convolution;
         return INIT_FAILED;
        }
      delete actor;
      delete critic;
      delete convolution;
      //---
      TargetCritic1.WeightsUpdate(GetPointer(Critic1), 1.0f);
      TargetCritic2.WeightsUpdate(GetPointer(Critic2), 1.0f);
      StartTargetIter = StartTargetIteration;
     }
   else
      StartTargetIter = 0;
   if(!Convolution.Load(FileName + "CNN.nnw", temp, temp, temp, dtStudied, true))
     {
      Print("Init new Encoder model");
      CArrayObj *actor = new CArrayObj();
      CArrayObj *critic = new CArrayObj();
      CArrayObj *convolution = new CArrayObj();
      if(!CreateDescriptions(actor, critic, convolution))
        {
         delete actor;
         delete critic;
         delete convolution;
         return INIT_FAILED;
        }
      if(!Convolution.Create(convolution))
        {
         delete actor;
         delete critic;
         delete convolution;
         return INIT_FAILED;
        }
      delete actor;
      delete critic;
      delete convolution;
     }

Após carregar e/ou inicializar com sucesso novos modelos, transferimos todos para um contexto OpenCL único. Nos modelos de treinamento, desativamos o modo de atualização dos parâmetros. Ou seja, não treinaremos o Autocodificador nesta etapa.

   OpenCL = Actor.GetOpenCL();
   Critic1.SetOpenCL(OpenCL);
   Critic2.SetOpenCL(OpenCL);
   TargetCritic1.SetOpenCL(OpenCL);
   TargetCritic2.SetOpenCL(OpenCL);
   Convolution.SetOpenCL(OpenCL);
   Encoder.SetOpenCL(OpenCL);
   Decoder.SetOpenCL(OpenCL);
   Encoder.TrainMode(false);
   Decoder.TrainMode(false);

Aqui, vale destacar que, embora o codificador aleatório também não seja treinado, não alteramos sua flag de modo de treinamento. Não havia necessidade disso. O método de alteração do modo de treinamento não remove buffers não utilizados. Logo, não limpa a memória. Apenas altera a flag que regula o algoritmo de retropropagação. Não chamamos o método de retropropagação do codificador no programa. Então o efeito de alterar a flag de treinamento do codificador aleatório é próximo de "0". No caso do autocodificador, a situação é um pouco diferente. E vamos abordar isso no método de treinamento dos modelos Train. Mas, por agora, vamos voltar ao método de inicialização do EA.

Após criar os modelos e transferi-los para um contexto OpenCL único, realizamos o controle mínimo para saber se suas arquiteturas estão em conformidade com as constantes usadas no programa.

Primeiro, verificamos o tamanho da camada de resultados do Ator com o tamanho do vetor de ações do Agente.

   Actor.getResults(Result);
   if(Result.Total() != NActions)
     {
      PrintFormat("The scope of the actor does not match the actions count (%d <> %d)", NActions, Result.Total());
      return INIT_FAILED;
     }

O tamanho dos dados de entrada do Ator deve corresponder ao tamanho do vetor de descrição do estado do ambiente.

   Actor.GetLayerOutput(0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Actor doesn't match state description (%d <> %d)", Result.Total(), (HistoryBars * BarDescr));
      return INIT_FAILED;
     }

Verificamos o tamanho da camada latente do Ator e do buffer de dados de entrada do Crítico.

   Actor.GetLayerOutput(LatentLayer, Result);
   int latent_state = Result.Total();
   Critic1.GetLayerOutput(0, Result);
   if(Result.Total() != latent_state)
     {
      PrintFormat("Input size of Critic doesn't match latent state Actor (%d <> %d)", Result.Total(), latent_state);
      return INIT_FAILED;
     }

Fazemos verificações semelhantes para os modelos do Codificador e Decodificador do autocodificador.

   Decoder.getResults(Result);
   if(Result.Total() != NActions)
     {
      PrintFormat("The scope of the Decoder does not match the actions count (%d <> %d)", NActions, Result.Total());
      return INIT_FAILED;
     }
   Encoder.GetLayerOutput(0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Encoder doesn't match state description (%d <> %d)", Result.Total(), 
                                                                                          (HistoryBars * BarDescr));
      return INIT_FAILED;
     }
   Encoder.getResults(Result);
   latent_state = Result.Total();
   Decoder.GetLayerOutput(0, Result);
   if(Result.Total() != latent_state)
     {
      PrintFormat("Input size of Decoder doesn't match result of Encoder (%d <> %d)", Result.Total(), latent_state);
      return INIT_FAILED;
     }

Com isso, concluímos a preparação dos modelos. Inicializamos o buffer auxiliar e geramos o evento de início do processo de treinamento.

   Gradient.BufferInit(AccountDescr, 0);
//---
   if(!EventChartCustom(ChartID(), 1, 0, 0, "Init"))
     {
      PrintFormat("Error of create study event: %d", GetLastError());
      return INIT_FAILED;
     }
//---
   return(INIT_SUCCEEDED);
  }

Depois, finalizamos o método de inicialização do EA com um resultado positivo.

Como não vamos alterar os parâmetros dos modelos do autocodificador durante o treinamento, não precisamos salvá-los após o encerramento do programa. Por isso, o método de desinicialização OnDeinit permanece inalterado. Você pode se familiarizar com ele no anexo. Vamos agora ao processo de treinamento dos modelos e ao método Train.

O algoritmo do método de treinamento da política do Ator é mais complexo e elaborado em comparação ao método de treinamento do modelo de densidade abordado anteriormente. Vamos nos concentrar nele com mais detalhes.

No início do método, preparamos várias variáveis locais e matrizes, que serão usadas no treinamento dos modelos. 

void Train(void)
  {
   int total_tr = ArraySize(Buffer);
   uint ticks = GetTickCount();
//---
   int total_states = Buffer[0].Total;
   for(int i = 1; i < total_tr; i++)
      total_states += Buffer[i].Total;
   vector<float> temp, next;
   Convolution.getResults(temp);
   matrix<float> state_embedding = matrix<float>::Zeros(total_states, temp.Size());
   matrix<float> rewards = matrix<float>::Zeros(total_states, NRewards);
   matrix<float> actions = matrix<float>::Zeros(total_states, NActions);

Depois, definimos um sistema de ciclos para gerar incorporações de todos os estados a partir do buffer de reprodução da experiência. O ciclo externo de nosso sistema percorrerá as trajetórias no conjunto de treinamento. E o ciclo aninhado percorrerá os estados do ambiente que o Agente visitou ao longo da trajetória.

   int state = 0;
   for(int tr = 0; tr < total_tr; tr++)
     {
      for(int st = 0; st < Buffer[tr].Total; st++)
        {
         State.AssignArray(Buffer[tr].States[st].state);

No corpo do sistema de ciclos, carregamos do conjunto de treinamento o vetor de descrição de um estado específico do ambiente. Complementamos com a descrição do estado da conta e das posições abertas.

         float PrevBalance = Buffer[tr].States[MathMax(st - 1, 0)].account[0];
         float PrevEquity = Buffer[tr].States[MathMax(st - 1, 0)].account[1];
         State.Add((Buffer[tr].States[st].account[0] - PrevBalance) / PrevBalance);
         State.Add(Buffer[tr].States[st].account[1] / PrevBalance);
         State.Add((Buffer[tr].States[st].account[1] - PrevEquity) / PrevEquity);
         State.Add(Buffer[tr].States[st].account[2]);
         State.Add(Buffer[tr].States[st].account[3]);
         State.Add(Buffer[tr].States[st].account[4] / PrevBalance);
         State.Add(Buffer[tr].States[st].account[5] / PrevBalance);
         State.Add(Buffer[tr].States[st].account[6] / PrevBalance);

 Também adicionamos harmônicos da marca temporal ao buffer.

         double x = (double)Buffer[tr].States[st].account[7] / (double)(D'2024.01.01' - D'2023.01.01');
         State.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         x = (double)Buffer[tr].States[st].account[7] / (double)PeriodSeconds(PERIOD_MN1);
         State.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0));
         x = (double)Buffer[tr].States[st].account[7] / (double)PeriodSeconds(PERIOD_W1);
         State.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         x = (double)Buffer[tr].States[st].account[7] / (double)PeriodSeconds(PERIOD_D1);
         State.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         State.AddArray(vector<float>::Zeros(NActions));

Para avaliar os estados independentemente das ações tomadas pelo Agente, preenchemos o restante do buffer com valores nulos.

Após preencher o buffer de dados de entrada com sucesso, chamamos o método de propagação do codificador aleatório.

         if(!Convolution.feedForward((CBufferFloat *)GetPointer(State), 1, false, (CBufferFloat *)NULL))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            ExpertRemove();
            return;
           }

Salvamos os resultados em uma matriz de incorporações.

         Convolution.getResults(temp);
         if(!state_embedding.Row(temp, state))
            continue;

Paralelamente, salvamos as ações realizadas e as recompensas obtidas devido às transições subsequentes.

         if(!temp.Assign(Buffer[tr].States[st].action) ||
            !actions.Row(temp, state))
            continue;
         if(!temp.Assign(Buffer[tr].States[st].rewards) ||
            !next.Assign(Buffer[tr].States[st + 1].rewards) ||
            !rewards.Row(temp - next * DiscFactor, state))
            continue;

Após adicionar com sucesso todas as entidades às matrizes locais, aumentamos o contador de estados processados. Informamos o usuário sobre o progresso do processo de incorporação dos estados e passamos para a próxima iteração do sistema de ciclos.

         state++;
         if(GetTickCount() - ticks > 500)
           {
            string str = StringFormat("%-15s %6.2f%%", "Embedding ", state * 100.0 / (double)(total_states));
            Comment(str);
            ticks = GetTickCount();
           }
        }
     }

Após concluir todas as iterações do sistema de ciclos com sucesso, ajustamos, se necessário, o tamanho das matrizes locais ao tamanho real dos dados utilizados.

   if(state != total_states)
     {
      rewards.Resize(state, NRewards);
      actions.Resize(state, NActions);
      state_embedding.Reshape(state, state_embedding.Cols());
      total_states = state;
     }

Passamos para a próxima etapa do trabalho preparatório, onde preparamos mais algumas variáveis locais e determinamos a prioridade de amostragem das trajetórias do conjunto de treinamento no processo de treinamento dos modelos.

   vector<float> rewards1, rewards2, target_reward;
   STarget target;
   int bar = (HistoryBars - 1) * BarDescr;
//---
   vector<float> probability = GetProbTrajectories(Buffer, 0.9);

Com isso, o trabalho preparatório é concluído, e passamos ao treinamento dos modelos. Para isso, definimos um ciclo de treinamento com o número de iterações especificado nos parâmetros externos do EA.

No corpo do ciclo, amostramos uma trajetória considerando as prioridades e escolhemos aleatoriamente um estado nela.

   for(int iter = 0; (iter < Iterations && !IsStopped()); iter ++)
     {
      int tr = SampleTrajectory(probability);
      int i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2));
      if(i < 0)
        {
         iter--;
         continue;
        }

De acordo com o método SAC, calculamos a recompensa esperada até o final do episódio. Para isso, usamos os modelos alvo dos Críticos. No entanto, realizamos essas operações apenas com modelos previamente treinados. Antes de iniciar as operações, verificamos se o número mínimo necessário de iterações de pré-treinamento foi concluído com sucesso.

      target_reward = vector<float>::Zeros(NRewards);
      //--- Target
      if(iter >= StartTargetIter)
        {
         State.AssignArray(Buffer[tr].States[i + 1].state);

Após passar no controle com sucesso, preenchemos o buffer de dados de entrada com a descrição do estado subsequente do ambiente.

Também preenchemos separadamente o buffer de descrição do estado da conta e das posições abertas.

         float PrevBalance = Buffer[tr].States[i].account[0];
         float PrevEquity = Buffer[tr].States[i].account[1];
         Account.Clear();
         Account.Add((Buffer[tr].States[i + 1].account[0] - PrevBalance) / PrevBalance);
         Account.Add(Buffer[tr].States[i + 1].account[1] / PrevBalance);
         Account.Add((Buffer[tr].States[i + 1].account[1] - PrevEquity) / PrevEquity);
         Account.Add(Buffer[tr].States[i + 1].account[2]);
         Account.Add(Buffer[tr].States[i + 1].account[3]);
         Account.Add(Buffer[tr].States[i + 1].account[4] / PrevBalance);
         Account.Add(Buffer[tr].States[i + 1].account[5] / PrevBalance);
         Account.Add(Buffer[tr].States[i + 1].account[6] / PrevBalance);

Nesse buffer, também adicionamos os harmônicos da marca temporal.

         double x = (double)Buffer[tr].States[i + 1].account[7] / (double)(D'2024.01.01' - D'2023.01.01');
         Account.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         x = (double)Buffer[tr].States[i + 1].account[7] / (double)PeriodSeconds(PERIOD_MN1);
         Account.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0));
         x = (double)Buffer[tr].States[i + 1].account[7] / (double)PeriodSeconds(PERIOD_W1);
         Account.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         x = (double)Buffer[tr].States[i + 1].account[7] / (double)PeriodSeconds(PERIOD_D1);
         Account.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
         //---
         if(Account.GetIndex() >= 0)
            Account.BufferWrite();

Os dados coletados são suficientes para a propagação do Ator.

         if(!Actor.feedForward(GetPointer(State), 1, false, GetPointer(Account)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            break;
           }

Observe que chamamos o método de propagação para o modelo treinável do Ator com o estado subsequente do ambiente. Isso gera a ação do Ator de acordo com a política atualizada. Assim, os Críticos alvo avaliam a recompensa esperada da política atualizada até o final do episódio.

         if(!TargetCritic1.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actor)) ||
            !TargetCritic2.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actor)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            break;
           }

Como valor esperado, usamos a avaliação mínima obtida dos 2 Críticos alvo nas operações subsequentes.

         TargetCritic1.getResults(rewards1);
         TargetCritic2.getResults(rewards2);
         target_reward.Assign(Buffer[tr].States[i + 1].rewards);
         if(rewards1.Sum() <= rewards2.Sum())
            target_reward = rewards1 - target_reward;
         else
            target_reward = rewards2 - target_reward;
         target_reward *= DiscFactor;
         target_reward[NRewards - 1] = EntropyLatentState(Actor);
        }

O próximo passo é treinar nossos Críticos. Para a precisão de suas avaliações, o treinamento é baseado na correspondência entre as ações e recompensas reais do conjunto de treinamento. Mas aqui vale lembrar que usamos o Ator para pré-processar o estado do ambiente em nosso modelo. Portanto, como antes, preenchemos os buffers de dados de entrada com a descrição do estado amostrado do ambiente.

      //--- Q-function study
      State.AssignArray(Buffer[tr].States[i].state);

Preenchemos o buffer de descrição do estado da conta e das posições abertas.

      float PrevBalance = Buffer[tr].States[MathMax(i - 1, 0)].account[0];
      float PrevEquity = Buffer[tr].States[MathMax(i - 1, 0)].account[1];
      Account.Clear();
      Account.Add((Buffer[tr].States[i].account[0] - PrevBalance) / PrevBalance);
      Account.Add(Buffer[tr].States[i].account[1] / PrevBalance);
      Account.Add((Buffer[tr].States[i].account[1] - PrevEquity) / PrevEquity);
      Account.Add(Buffer[tr].States[i].account[2]);
      Account.Add(Buffer[tr].States[i].account[3]);
      Account.Add(Buffer[tr].States[i].account[4] / PrevBalance);
      Account.Add(Buffer[tr].States[i].account[5] / PrevBalance);
      Account.Add(Buffer[tr].States[i].account[6] / PrevBalance);

Adicionamos os harmônicos da marca temporal.

      double x = (double)Buffer[tr].States[i].account[7] / (double)(D'2024.01.01' - D'2023.01.01');
      Account.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
      x = (double)Buffer[tr].States[i].account[7] / (double)PeriodSeconds(PERIOD_MN1);
      Account.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0));
      x = (double)Buffer[tr].States[i].account[7] / (double)PeriodSeconds(PERIOD_W1);
      Account.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
      x = (double)Buffer[tr].States[i].account[7] / (double)PeriodSeconds(PERIOD_D1);
      Account.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
      if(Account.GetIndex() >= 0)
         Account.BufferWrite();

E realizamos a propagação do Ator.

      if(!Actor.feedForward(GetPointer(State), 1, false, GetPointer(Account)))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         break;
        }

Observe que neste ponto temos todos os dados necessários para a propagação do Autocodificador. Não adiamos o que pode ser feito agora. E chamamos os métodos de propagação do Codificador e do Decodificador.

      if(!Encoder.feedForward((CBufferFloat *)GetPointer(State), 1, false, (CNet *)GetPointer(Actor)) ||
         !Decoder.feedForward(GetPointer(Encoder), -1, GetPointer(Encoder), 1))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         break;
        }

Como mencionado anteriormente, o treinamento dos Críticos é realizado nas ações reais do Ator do conjunto de treinamento. Portanto, carregamos essas ações no buffer de dados e chamamos os métodos de propagação de ambos os Críticos.

      Actions.AssignArray(Buffer[tr].States[i].action);
      if(Actions.GetIndex() >= 0)
         Actions.BufferWrite();
      //---
      if(!Critic1.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actions)) ||
         !Critic2.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actions)))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         break;
        }

Em seguida, complementamos o buffer de descrição do estado atual com os dados necessários e realizamos a incorporação do estado analisado usando o codificador aleatório.

      if(!State.AddArray(GetPointer(Account)) || !State.AddArray(vector<float>::Zeros(NActions)) ||
         !Convolution.feedForward((CBufferFloat *)GetPointer(State), 1, false, (CBufferFloat *)NULL))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         break;
        }

Com base nos resultados da incorporação, formamos os valores-alvo do Ator e dos Críticos.

      Convolution.getResults(temp);
      target = GetTargets(Quant, temp, state_embedding, rewards, actions);

Depois, atualizamos os parâmetros dos nossos Críticos. Lembrando que o método CAGrad é usado para ajustar o vetor de gradientes para melhorar a convergência do modelo.

      Critic1.getResults(rewards1);
      Result.AssignArray(CAGrad(target.rewards + target_reward - rewards1) + rewards1);
      if(!Critic1.backProp(Result, GetPointer(Actions), GetPointer(Gradient)) ||
         !Actor.backPropGradient(GetPointer(Account), GetPointer(Gradient), LatentLayer))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         break;
        }
      Critic2.getResults(rewards2);
      Result.AssignArray(CAGrad(target.rewards + target_reward - rewards2) + rewards2);
      if(!Critic2.backProp(Result, GetPointer(Actions), GetPointer(Gradient)) ||
         !Actor.backPropGradient(GetPointer(Account), GetPointer(Gradient), LatentLayer))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         break;
        }

Após atualizar com sucesso os modelos dos Críticos, passamos à otimização da política do Ator. Este processo pode ser dividido em três blocos. No primeiro bloco, ajustamos a política do Agente para repetir uma ação específica, coletada das ações no conjunto de treinamento realizadas em estados semelhantes e ponderadas pela recompensa recebida.

      //--- Policy study
      Actor.getResults(rewards1);
      Result.AssignArray(CAGrad(target.actions - rewards1) + rewards1);
      if(!Actor.backProp(Result, GetPointer(Account), GetPointer(Gradient)))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         break;
        }

Na segunda etapa, usamos os resultados do Autocodificador e verificamos a divergência das ações geradas pelo Agente em relação ao conjunto de treinamento. Se a margem de erro de decodificação da Ação exceder o limite, tentamos retornar a política do Ator para a distribuição do conjunto de treinamento. Para isso, realizamos a retropropagação do Autocodificador, e o erro de codificação é transmitido diretamente para o Ator na forma de gradiente de erro. De maneira semelhante à transmissão do gradiente de erro dos Críticos. Para realizar essa operação com segurança, desativamos o modo de treinamento no Codificador e no Decodificador na etapa de inicialização do programa.

      Decoder.getResults(rewards2);
      if(rewards2.Loss(rewards1, LOSS_MSE) > MeanCVAEError)
        {
         Actions.AssignArray(rewards1);
         if(!Decoder.backProp(GetPointer(Actions), GetPointer(Encoder), 1) ||
            !Encoder.backPropGradient((CNet*)GetPointer(Actor)) ||
            !Actor.backPropGradient(GetPointer(Account), GetPointer(Gradient)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            break;
           }
        }

Na próxima etapa do treinamento da política do Ator, verificamos a precisão das previsões dos nossos Críticos. E, se houver confiança suficiente em suas previsões, ajustamos a política do Ator na direção da recompensa máxima mais provável. Nesta etapa, também desativamos o modo de atualização dos parâmetros dos Críticos para evitar o efeito de adaptação mútua dos modelos. 

      CNet *critic = NULL;
      if(Critic1.getRecentAverageError() <= Critic2.getRecentAverageError())
         critic = GetPointer(Critic1);
      else
         critic = GetPointer(Critic2);
      if(MathAbs(critic.getRecentAverageError()) <= MaxErrorActorStudy)
        {
         if(!critic.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actor)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            break;
           }
         critic.getResults(rewards1);
         Result.AssignArray(CAGrad(target.rewards + target_reward - rewards1) + rewards1);
         critic.TrainMode(false);
         if(!critic.backProp(Result, GetPointer(Actor)) ||
            !Actor.backPropGradient(GetPointer(Account), GetPointer(Gradient)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            critic.TrainMode(true);
            break;
           }
         critic.TrainMode(true);
        }

Em seguida, atualizamos os modelos alvo dos Críticos.

      //--- Update Target Nets
      if(iter >= StartTargetIter)
        {
         TargetCritic1.WeightsUpdate(GetPointer(Critic1), Tau);
         TargetCritic2.WeightsUpdate(GetPointer(Critic2), Tau);
        }
      else
        {
         TargetCritic1.WeightsUpdate(GetPointer(Critic1), 1);
         TargetCritic2.WeightsUpdate(GetPointer(Critic2), 1);
        }

E informamos o usuário sobre o progresso do treinamento.

      if(GetTickCount() - ticks > 500)
        {
         string str = StringFormat("%-15s %5.2f%% -> Error %15.8f\n", "Critic1", iter * 100.0 / (double)(Iterations),
                                                                                    Critic1.getRecentAverageError());
         str += StringFormat("%-15s %5.2f%% -> Error %15.8f\n", "Critic2", iter * 100.0 / (double)(Iterations), 
                                                                                    Critic2.getRecentAverageError());
         str += StringFormat("%-14s %5.2f%% -> Error %15.8f\n", "Actor", iter * 100.0 / (double)(Iterations), 
                                                                                      Actor.getRecentAverageError());
         Comment(str);
         ticks = GetTickCount();
        }
     }

Após concluir todas as iterações do ciclo de treinamento dos modelos, limpamos o campo de comentários no gráfico. Registramos no log as informações sobre os resultados do treinamento dos modelos e iniciamos o processo de encerramento do EA.

   Comment("");
//---
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Critic1", Critic1.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Critic2", Critic2.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Actor", Actor.getRecentAverageError());
   ExpertRemove();
//---
  }

Com isso, concluímos a implementação do método Supported Policy Optimization com MQL5. O código completo de todos os programas usados no artigo pode ser consultado no anexo. Agora passamos para a próxima parte do nosso artigo. Na qual verificaremos os resultados do nosso trabalho em um caso prático.


3. Testes

Acima, realizamos a implementação do método Supported Policy OpTimization (SPOT) com MQL5 e chegou a hora de verificar na prática os resultados do nosso trabalho. Como sempre, quero chamar sua atenção para o fato de que esta obra apresenta minha própria visão dos métodos propostos pelos autores. Além disso, eles foram sobrepostos aos trabalhos anteriores, que utilizam outros métodos. Como resultado, construímos um modelo a partir de um conglomerado de várias ideias, compiladas pela minha visão do processo. Consequentemente, quaisquer deficiências observadas não podem ser totalmente atribuídas a nenhum dos métodos utilizados.

Como sempre, o treinamento e os testes dos modelos são realizados com dados históricos do par EURUSD no timeframe H1. Todos os parâmetros dos indicadores são usados com seus valores padrão. O treinamento do modelo é realizado com dados dos primeiros 7 meses de 2023. O teste dos modelos treinados é realizado com dados de agosto de 2023.

Como mencionado anteriormente, os modelos de interação com o ambiente foram transferidos sem alterações. Portanto, para a primeira etapa de treinamento, podemos usar o conjunto de treinamento coletado durante o trabalho no artigo Real-ORL, que serviu de doador para os modelos. Apenas fiz uma cópia do conjunto de treinamento com o nome "SPOT.bd".

Na primeira etapa, realizamos o treinamento do Autocodificador. O conjunto de treinamento contém 500 trajetórias com 3591 estados do ambiente em cada uma. Isso totaliza quase 1,8 milhão de conjuntos "Estado-Ação-Recompensa". Nesta etapa, realizei 5 ciclos de treinamento do Autocodificador com 0,5 milhão de iterações em cada, o que é 40% superior ao volume do conjunto de treinamento.

Após o treinamento inicial do Autocodificador, iniciamos o processo de treinamento dos modelos no EA "... \SPOT\Study.mq5". Desde já, digo que a duração do processo de treinamento dos modelos é significativamente maior que o tempo de treinamento do Autocodificador.

Também é importante notar que manter a política do Agente dentro do conjunto de treinamento não deixa esperança de obter resultados que superem as passagens do conjunto de treinamento. Portanto, para obter políticas mais otimizadas, precisamos de uma atualização iterativa do buffer de replay e dos modelos, incluindo o Autocodificador.

Portanto, paralelamente ao processo de treinamento dos modelos, lanço no testador de estratégias a otimização do EA "ResearchExORL.mq5" para explorar estratégias fora do conjunto de treinamento.

Após concluir o ciclo de treinamento dos modelos, iniciamos a otimização do EA "Research.mq5" no testador de estratégias com 200 passagens, que explora o ambiente em torno da política aprendida pelo Ator.

Com base no conjunto de treinamento atualizado, repetimos o treinamento do Autocodificador com 0,5 milhão de iterações. E continuamos o treinamento da política do Ator.

Após vários ciclos de treinamento, consegui treinar a política do Ator para gerar lucro no período histórico de treinamento e teste. Os resultados do modelo para agosto de 2023 são apresentados abaixo.

Resultados dos testes

Resultados dos testes

Como pode ser visto nos dados apresentados, durante um mês de teste da estratégia, o modelo realizou 124 operações (92 curtas e 32 longas). Destas, quase 47% foram fechadas com lucro. É interessante notar que a proporção de posições lucrativas nas operações longas e curtas é próxima (50% e 46%, respectivamente). Além disso, a operação lucrativa média supera a operação com perda média em 25%. E a operação mais lucrativa é quase o dobro da maior perda. No geral, com base nos resultados de negociação, o fator de lucro foi de 1,15.


Considerações finais

Neste artigo, exploramos o método Supported Policy OpTimization (SPOT), que é uma solução bem-sucedida para o problema de aprendizado off-line com um conjunto de treinamento limitado. Sua capacidade de regular a política considerando a densidade estimada da estratégia comportamental demonstra resultados excelentes em cenários de teste padrão. O SPOT integra-se facilmente aos algoritmos de RL off-line existentes, proporcionando flexibilidade de aplicação em vários contextos. Sua estrutura modular permite usá-lo em conjunto com diferentes abordagens de aprendizado.

A característica única do SPOT é o uso de regularização baseada na avaliação explícita da densidade dos dados do conjunto de treinamento. Isso proporciona um controle preciso das ações permitidas pela política e previne eficazmente a extrapolação além do conjunto de treinamento.

Na parte prática, implementamos nossa visão dos métodos propostos com MQL5. Os resultados dos testes realizados permitem concluir a eficácia deste método. Durante o treinamento, pode-se observar a estabilidade do processo. E, ao final do treinamento, conseguimos encontrar uma estratégia lucrativa para o comportamento do Ator.

No entanto, vale notar que manter a política do Ator dentro do conjunto de treinamento limita a exploração além dele. Por um lado, isso torna o processo de treinamento mais estável. Por outro, limita a exploração de subespaços inexplorados do ambiente. Portanto, podemos concluir que a aplicação deste método é mais eficaz na presença de passagens subótimas no conjunto de treinamento.

Ao mesmo tempo, para estimular a exploração do ambiente, podemos tentar "inverter" o método e estimular a investigação de ações fora do conjunto de treinamento. Mas isso é tema para pesquisas futuras.


Referências

  • Supported Policy Optimization for Offline Reinforcement Learning
  • Redes neurais de maneira fácil (Parte 67): aprendendo com experiências passadas para resolver novos problemas

  • Programas utilizados no artigo

    # Nome Tipo Descrição
    1 Research.mq5 EA EA de coleta de exemplos
    2 ResearchRealORL.mq5
    EA
    EA para coleta de exemplos pelo método Real-ORL
    3 ResearchExORL.mq5 EA EA para coleta de exemplos pelo método ExORL
    4 Study.mq5  EA EA para treinamento do agente
    5 StudyCVAE.mq5 EA
    EA de treinamento do autocodificador
    6 Test.mq5 EA Expert Advisor para testar o modelo
    7 Trajectory.mqh Biblioteca de classe Estrutura de descrição do estado do sistema
    8 NeuroNet.mqh Biblioteca de classe Biblioteca das classes para criar uma rede neural
    9 NeuroNet.cl Biblioteca Biblioteca de código OpenCL


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

    Arquivos anexados |
    MQL5.zip (653.77 KB)
    Últimos Comentários | Ir para discussão (2)
    Tabata Voegele
    Tabata Voegele | 24 dez. 2023 em 10:44

    É intencional o fato de não haver anexos neste artigo?

    Dmitriy Gizlyk
    Dmitriy Gizlyk | 24 dez. 2023 em 11:15
    Tabata Voegele #:

    É intencional o fato de não haver anexos neste artigo?

    Esse é um erro infeliz e uma versão funcional do artigo foi publicada. Corrigido.

    Redes neurais de maneira fácil (Parte 70): melhorando a política usando operadores de forma fechada (CFPI) Redes neurais de maneira fácil (Parte 70): melhorando a política usando operadores de forma fechada (CFPI)
    Neste artigo, propomos explorar um algoritmo que utiliza operadores de melhoria de política de forma fechada para otimizar as ações do Agente em um ambiente off-line.
    Desenvolvendo um sistema de Replay (Parte 52): Complicando as coisas (IV) Desenvolvendo um sistema de Replay (Parte 52): Complicando as coisas (IV)
    Neste artigo vamos fazer uma mudança no indicador de mouse a fim de poder efetuar a interação com o indicador de controle, já que a interação está sendo feita de forma errática.
    Algoritmos de otimização populacionais: algoritmo híbrido de otimização de forrageamento bacteriano com algoritmo genético (Bacterial Foraging Optimization - Genetic Algorithm, BFO-GA) Algoritmos de otimização populacionais: algoritmo híbrido de otimização de forrageamento bacteriano com algoritmo genético (Bacterial Foraging Optimization - Genetic Algorithm, BFO-GA)
    Este artigo apresenta uma nova abordagem para resolver problemas de otimização, combinando as ideias dos algoritmos de otimização de forrageamento bacteriano (BFO) com as técnicas usadas no algoritmo genético (GA), resultando no algoritmo híbrido BFO-GA. Ele utiliza o comportamento de enxameamento das bactérias para a busca global da solução ótima e operadores genéticos para refinar os ótimos locais. Ao contrário do BFO original, as bactérias agora podem mutar e herdar genes.
    Algoritmos de otimização populacional: algoritmos de estratégias evolutivas (Evolution Strategies, (μ,λ)-ES e (μ+λ)-ES) Algoritmos de otimização populacional: algoritmos de estratégias evolutivas (Evolution Strategies, (μ,λ)-ES e (μ+λ)-ES)
    Neste artigo, vamos falar sobre um grupo de algoritmos de otimização conhecidos como "Estratégias Evolutivas" (Evolution Strategies ou ES). Eles são alguns dos primeiros algoritmos que usam princípios de evolução para encontrar soluções ótimas. Vamos mostrar as mudanças feitas nas versões clássicas das ES, além de revisar a função de teste e a metodologia de avaliação dos algoritmos.