English Русский 中文 Español Deutsch 日本語
preview
Redes neurais em trading: Representação adaptativa de grafos (NAFS)

Redes neurais em trading: Representação adaptativa de grafos (NAFS)

MetaTrader 5Sistemas de negociação |
101 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Introdução

Nos últimos anos, o aprendizado de representação de grafos tem sido amplamente aplicado em diferentes cenários, como a clusterização de nós, a previsão de conexões, a classificação de nós e a classificação de grafos. O objetivo do aprendizado de representação de grafos é codificar as informações dos grafos por meio da incorporação dos nós. Os métodos tradicionais de aprendizado de representação de grafos focavam na preservação da estrutura do grafo. No entanto, esses métodos enfrentam duas limitações principais:

  1. Arquitetura superficial. Embora redes neurais convolucionais em grafos (GCN) apliquem múltiplas camadas para capturar informações estruturais profundas, o aumento do número de camadas frequentemente leva a um excesso de suavização e incorporações indistinguíveis.
  2. Baixa escalabilidade. Métodos baseados em GNN para aprendizado de representação de grafos não escalam bem para grafos grandes devido ao alto custo computacional e ao uso intensivo de memória.

Esses problemas foram abordados pelos autores do trabalho "NAFS: A Simple yet Tough-to-beat Baseline for Graph Representation Learning", no qual foi proposto um novo método de representação de grafos por meio da simples suavização de características dos nós, seguida de uma combinação adaptativa. O método de suavização adaptativa das características dos nós (Node-Adaptive Feature SmoothingNAFS) gera incorporações de nós mais eficazes, integrando informações estruturais do grafo e características dos nós. Com base na observação de que diferentes nós possuem diferentes "velocidades de suavização", NAFS suaviza cada característica de nó de forma adaptativa, utilizando informações de vizinhanças de ordens inferiores e superiores. Além disso, um conjunto de características também é utilizado para combinar as características suavizadas, extraídas por diferentes operadores de suavização. Como o NAFS não requer aprendizado, ele reduz significativamente os custos de treinamento e oferece melhor escalabilidade para grafos de grande porte.

1. Algoritmo NAFS

Diversos pesquisadores sugeriram separar a suavização e a transformação das características em cada camada da GCN para tornar a classificação de nós mais escalável. Especificamente, eles realizaram previamente as operações de suavização das características, que então alimentavam uma MLP simples para gerar os rótulos finais previstos dos nós.

Essas GNN desacopladas são compostas por duas partes: a suavização das características e o treinamento da MLP. A suavização das características visa combinar a informação estrutural do grafo e as propriedades dos nós em características melhores para a posterior MLP. Durante o treinamento, a MLP aprende apenas com as características suavizadas.

Existe ainda outro ramo das GNN que também separa a suavização e a transformação das características. Nesse caso, uma MLP para gerar incorporações intermediárias é inicialmente alimentada com as características brutas dos nós. Em seguida, são realizadas operações de propagação personalizadas sobre essas incorporações para obter os resultados finais da previsão. No entanto, esse tipo de GNN ainda precisa executar recursivamente as operações de propagação em cada época de treinamento, o que torna inviável seu uso em grafos de grande escala.

A maneira mais simples de capturar informações estruturais detalhadas do grafo é empilhar várias camadas de GNN. No entanto, um grande número de operações de suavização de características em uma GNN leva a incorporações de nós indistinguíveis, ou seja, ao problema do excesso de suavização.

Uma análise quantitativa mostra empiricamente que o grau de cada nó desempenha um papel importante no passo de suavização ideal. Intuitivamente, nós com graus mais altos devem ter passos de suavização relativamente menores do que nós com graus mais baixos.

Embora o uso de operações de suavização de características dentro de GNN desacopladas seja escalável para o aprendizado de representação de grafos grandes, isso resulta em representações de nós subótimas. Não é eficiente suavizar as características de todos os nós de maneira uniforme, já que nós com diferentes propriedades estruturais possuem velocidades de suavização distintas. Portanto, deve-se utilizar uma suavização de características adaptada a cada nó, o que atende às diversas necessidades de suavização específicas de cada nó.

Ao aplicar sucessivamente 𝐗l=Â𝐗l−1, a matriz de incorporações 𝐗l−1 dos nós suavizados acumula informações estruturais mais profundas do grafo à medida que l aumenta. As matrizes de incorporações multiescalares dos nós {𝐗0, 𝐗1, …, 𝐋K} (onde K é o passo máximo de suavização) são então combinadas em uma única matriz , que reúne informações locais e globais sobre os nós vizinhos.

A análise realizada pelos autores do método NAFS demonstra que a velocidade com que cada nó atinge o estado estacionário varia significativamente. Isso indica a necessidade de uma abordagem individualizada para análise dos nós. Com esse objetivo, os autores do NAFS introduzem o conceito de "Peso de Suavização", baseado na distância entre os vetores de características locais e suavizados de cada nó. Isso permite adaptar o processo de suavização a cada nó de forma específica.

Uma alternativa mais eficaz é substituir a matriz de suavização  pela similaridade cosseno. Uma maior similaridade cosseno entre os vetores de características local e suavizado indica que o nó vi está mais distante do estado estacionário e [Âk𝐗]i intuitivamente contém informações mais relevantes sobre o nó. Portanto, para o nó vi, a característica suavizada com maior similaridade cosseno deve contribuir mais para a incorporação final do nó.

Diferentes operadores de suavização funcionam, na prática, como diferentes extratores de conhecimento. Isso permite capturar estruturas do grafo e conhecimentos em diferentes escalas e dimensões. Para obter esse mesmo efeito, a operação de ensemble de características utiliza diversos extratores de conhecimento. Esses extratores são usados dentro da operação de suavização de características para criar diferentes versões suavizadas das características.

NAFS gera incorporações de nós sem necessidade de aprendizado, o que o torna extremamente eficiente e escalável. Além disso, a estratégia de suavização adaptativa das características dos nós permite capturar informações estruturais profundas.

A visualização do método NAFS feita pelos autores é apresentada abaixo.


2. Implementação em MQL5

Após explorarmos os aspectos teóricos do framework NAFS, passamos à implementação prática das abordagens propostas usando MQL5. E antes de iniciarmos a implementação do framework, vamos delinear claramente suas etapas principais.

  1. Construção da matriz de representações multiescalares dos nós.
  2. Definição dos pesos de suavização com base na similaridade cosseno entre o vetor de características do nó e suas representações suavizadas.
  3. Cálculo dos valores médios ponderados para a incorporação final.

Vale destacar que algumas dessas operações podem ser cobertas com funcionalidades já presentes na nossa biblioteca. Por exemplo, as operações de cálculo da similaridade cosseno e dos valores médios ponderados podem ser facilmente implementadas com multiplicação de 2 matrizes. E para o cálculo dos coeficientes de suavização, podemos contar com a camada SoftMax.

Resta em aberto a questão da construção da matriz de representações multiescalares dos nós.

2.1 Matriz de Representação Multiescalar dos Nós

Para construir a matriz de representações multiescalares dos nós, utilizaremos uma simples média dos valores das características individuais dos nós com os parâmetros correspondentes de seus vizinhos mais próximos. A multiescalaridade é obtida ao se usar janelas de média com diferentes tamanhos.

Lembrando que o processo principal de cálculos foi transferido para o contexto OpenCL. Sendo assim, o processo de construção da matriz também será levado para a área de computação paralela. Para isso, criaremos um novo kernel no programa OpenCL chamado FeatureSmoothing.

__kernel void FeatureSmoothing(__global const float *feature,
                               __global float *outputs,
                               const int smoothing
                              )
  {
   const size_t pos = get_global_id(0);
   const size_t d = get_global_id(1);
   const size_t total = get_global_size(0);
   const size_t dimension = get_global_size(1);

Nos parâmetros desse kernel, recebemos ponteiros para 2 buffers de dados (dados brutos e resultados) e uma constante representando a quantidade de escalas de suavização. Neste caso, não definimos o passo da escala de suavização, pois consideramos ele igual a "1". Com isso, a janela de média é aumentada em 2 elementos, já que ampliamos igualmente antes e depois do elemento sendo analisado.

É importante destacar também que o número de escalas de suavização não pode ser um valor negativo. E, no caso de valor zero, simplesmente transferimos os dados brutos.

A execução desse kernel será feita em um espaço bidimensional de tarefas completamente independentes, sem a criação de grupos de trabalho locais. A primeira dimensão corresponde ao tamanho da sequência de entrada analisada, e a segunda, ao número de características no vetor de descrição de cada elemento da sequência.

No corpo do kernel, identificamos imediatamente a thread atual em todas as dimensões do espaço de tarefas, assim como definimos suas dimensões.

Com base nesses dados obtidos, determinamos o deslocamento nos buffers de dados.

   const int shift_input = pos * dimension + d;
   const int shift_output = dimension * pos * smoothing + d;

Com isso, a etapa de preparação é concluída e passamos diretamente à construção das representações em diferentes escalas. E, em primeiro lugar, transferimos os dados brutos, que representam a versão sem nenhuma média aplicada (suavização zero).

   float value = feature[shift_input];
   if(isinf(value) || isnan(value))
      value = 0;
   outputs[shift_output] = value;

Em seguida, organizamos um laço para o cálculo das médias das características individuais dentro da janela de média. Como você pode imaginar, aqui será necessário somar todos os valores da janela e depois dividir essa soma pelo número total de elementos envolvidos.

Observe que as janelas de média de todas as escalas são formadas ao redor do mesmo elemento sendo analisado. Portanto, cada nova escala usará todos os elementos da escala anterior. Aproveitaremos essa característica e, para minimizar os acessos à custosa memória global, em cada iteração apenas adicionaremos os novos valores à soma acumulada previamente, e em seguida dividiremos a soma atual pelo número de elementos da janela de média analisada.

   for(int s = 1; s <= smoothing; s++)
     {
      if((pos - s) >= 0)
        {
         float temp = feature[shift_input - s * dimension];
         if(isnan(temp) || isinf(temp))
            temp = 0;
         value += temp;
        }
      if((pos + s) < total)
        {
         float temp = feature[shift_input + s * dimension];
         if(isnan(temp) || isinf(temp))
            temp = 0;
         value += temp;
        }
      float factor = 1.0f / (min((int)total, (int)(pos + s)) - max((int)(pos - s), 0) + 1);
      if(isinf(value) || isnan(value))
         value = 0;
      float out = value * factor;
      if(isinf(out) || isnan(out))
         out = 0;
      outputs[shift_output + s * dimension] = out;
     }
  }

Também vale destacar, por mais estranho que possa parecer, que nem todas as janelas de média de uma mesma escala possuem o mesmo tamanho. Isso ocorre porque existem elementos extremos da sequência em que a janela de média ultrapassa os limites da sequência para um dos lados. Por isso, em cada iteração, determinamos a quantidade real de elementos da média.

De forma semelhante, construímos o algoritmo de propagação do gradiente do erro por meio das operações descritas acima no kernel FeatureSmoothingGradient, com o qual recomendo que você se familiarize por conta própria. O código completo do programa OpenCL está disponível no anexo.

2.2 Construção da classe NAFS

Após realizarmos as modificações necessárias no programa OpenCL, passamos para a parte principal do programa, onde criaremos a nova classe de formação adaptativa de incorporações de nós, chamada CNeuronNAFS. A estrutura da nova classe é apresentada abaixo.

class CNeuronNAFS :  public CNeuronBaseOCL
  {
protected:
   uint                 iDimension;
   uint                 iSmoothing;
   uint                 iUnits;
   //---
   CNeuronBaseOCL       cFeatureSmoothing;
   CNeuronTransposeOCL  cTranspose;
   CNeuronBaseOCL       cDistance;
   CNeuronSoftMaxOCL    cAdaptation;
   //---
   virtual bool      FeatureSmoothing(const CNeuronBaseOCL *neuron, const CNeuronBaseOCL *smoothing);
   virtual bool      FeatureSmoothingGradient(const CNeuronBaseOCL *neuron, const CNeuronBaseOCL *smoothing);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return true; }

public:
                     CNeuronNAFS(void) {};
                    ~CNeuronNAFS(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint step, uint units_count,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override   const   {  return defNeuronNAFS; }
   //---
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      SetOpenCL(COpenCLMy *obj) override;
  };

Como se pode notar, na estrutura da nova classe declaramos 3 variáveis e 4 camadas internas. Veremos como cada uma funciona durante a implementação dos métodos virtuais sobrescritos.

Também é importante mencionar que há 2 métodos-wrapper para os kernels de mesmo nome da aplicação OpenCL descritos anteriormente. Eles foram construídos com base no algoritmo padrão de chamada de kernels, e recomendo que você os explore por conta própria.

Todos os objetos internos da nova classe são declarados de forma estática, o que nos permite deixar os métodos construtor e destrutor da classe vazios. A inicialização de todos os objetos declarados e herdados é realizada no método Init.

bool CNeuronNAFS::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                       uint dimension, uint smoothing, uint units_count,
                       ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, dimension * units_count,
                            optimization_type, batch))
      return false;

Nos parâmetros desse método, recebemos constantes principais que definem de forma inequívoca a arquitetura do objeto a ser criado. Neste caso, são elas:

  • dimension — o tamanho do vetor de descrição de um elemento da sequência;
  • smoothing — o número de escalas de média (se o valor for zero, apenas os dados brutos são copiados);
  • units_count — o tamanho da sequência sendo analisada.

Observe que todos os parâmetros são do tipo inteiro sem sinal. Esse cuidado elimina a possibilidade de se obter valores negativos nos parâmetros. 

No corpo do método, como de costume, primeiro chamamos o método de mesmo nome da classe pai, onde, como você já sabe, está implementado o controle dos parâmetros recebidos e a inicialização dos objetos herdados. O tamanho do tensor de resultados é assumido como igual ao tensor de entrada, sendo definido como o produto entre o número de elementos na sequência analisada e o tamanho do vetor de descrição de um elemento.

Após a execução bem-sucedida das operações do método da classe pai, armazenamos os parâmetros recebidos do programa externo nas variáveis internas com nomes correspondentes.

   iDimension = dimension;
   iSmoothing = smoothing;
   iUnits = units_count;

E então passamos à inicialização dos objetos recém-declarados. O primeiro deles é a camada interna destinada ao armazenamento da matriz de representações multiescalares dos nós. Seu tamanho deve ser suficiente para conter a matriz completa. Portanto, ele é (iSmoothing + 1) vezes maior do que o tamanho dos dados brutos.

   if(!cFeatureSmoothing.Init(0, 0, OpenCL, (iSmoothing + 1) * iUnits * iDimension, optimization, iBatch))
      return false;
   cFeatureSmoothing.SetActivationFunction(None);

Após definirmos as representações multiescalares dos nós (no nosso caso, representações de padrões de candle em diferentes escalas), precisamos calcular a similaridade cosseno entre essas representações obtidas e o vetor de características do candle analisado. Para isso, multiplicamos o tensor de dados brutos pelo tensor das representações multiescalares dos nós. No entanto, antes de executar essa operação, é necessário transpor o tensor das representações multiescalares.

   if(!cTranspose.Init(0, 1, OpenCL, (iSmoothing + 1)*iUnits, iDimension, optimization, iBatch))
      return false;
   cTranspose.SetActivationFunction(None);

A operação de multiplicação de matrizes já está implementada em nossa classe base de camadas neurais e foi herdada da classe pai. E para armazenar os resultados dessa operação, inicializamos o objeto interno cDistance.

   if(!cDistance.Init(0, 2, OpenCL, (iSmoothing + 1)*iUnits, optimization, iBatch))
      return false;
   cDistance.SetActivationFunction(None);

Aqui vale lembrar que, ao multiplicar vetores na mesma direção, obtemos valores positivos, enquanto vetores em direções opostas resultam em valores negativos. Obviamente, se o candle analisado estiver alinhado com a tendência geral, o resultado da multiplicação entre o vetor de representação do candle e os valores suavizados será positivo. Caso contrário, o valor será negativo. No caso de lateralização (flat), o vetor de valores suavizados tende a ficar próximo de "0". Assim, o produto resultante também se aproxima de zero. Para normalizar os valores obtidos e determinar os coeficientes de influência adaptativa das diferentes escalas, utilizamos a função SoftMax.

   if(!cAdaptation.Init(0, 3, OpenCL, cDistance.Neurons(), optimization, iBatch))
      return false;
   cAdaptation.SetActivationFunction(None);
   cAdaptation.SetHeads(iUnits);

Agora, para determinar a incorporação final do nó (candle) analisado, basta multiplicar o vetor de coeficientes adaptativos de cada nó pela matriz correspondente de representações multiescalares. O resultado dessa operação será armazenado no buffer de interface de troca de dados com a próxima camada, herdado da classe pai. Portanto, não criamos um novo objeto interno. Apenas desativamos a função de ativação e encerramos o método de inicialização, retornando o resultado lógico da execução das operações ao programa chamador.

   SetActivationFunction(None);
//---
   return true;
  }

Após concluirmos a inicialização do novo objeto, passamos à construção do método de propagação para frente feedForward. Nos parâmetros do método, como de costume, recebemos o ponteiro para os dados brutos.

bool CNeuronNAFS::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!FeatureSmoothing(NeuronOCL, cFeatureSmoothing.AsObject()))
      return false;

Com esses dados, inicialmente construímos o tensor de representações multiescalares por meio da chamada do método-wrapper do kernel FeatureSmoothing apresentado anteriormente.

   if(!FeatureSmoothing(NeuronOCL, cFeatureSmoothing.AsObject()))
      return false;

Como foi mencionado na descrição do algoritmo do método de inicialização, realizamos a transposição da matriz de representações multiescalares dos nós obtida.

   if(!cTranspose.FeedForward(cFeatureSmoothing.AsObject()))
      return false;

Em seguida, multiplicamos essa matriz pelo tensor dos dados brutos para obter os coeficientes de similaridade cosseno.

   if(!MatMul(NeuronOCL.getOutput(), cTranspose.getOutput(), cDistance.getOutput(), 1, iDimension,
                                                                           iSmoothing + 1, iUnits))
      return false;

E normalizamos os valores obtidos utilizando a função SoftMax.

   if(!cAdaptation.FeedForward(cDistance.AsObject()))
      return false;

Agora, basta multiplicar o tensor de coeficientes adaptativos obtido pela matriz de representações multiescalares formada anteriormente.

   if(!MatMul(cAdaptation.getOutput(), cFeatureSmoothing.getOutput(), Output, 1, iSmoothing + 1, 
                                                                             iDimension, iUnits))
      return false;
//---
   return true;
  }

Como resultado dessa operação, obtemos as incorporações finais dos nós, que são armazenadas no buffer da interface de interação entre as camadas neurais do modelo. Depois disso, encerramos o método, retornando à aplicação chamadora o resultado lógico da execução das operações.

A próxima etapa do nosso trabalho é a implementação dos algoritmos de propagação reversa para a nova classe do framework NAFS. E aqui temos duas particularidades. Em primeiro lugar, nosso novo objeto não possui parâmetros treináveis, como foi explicado na parte teórica deste artigo. Sendo assim, o método de atualização de parâmetros updateInputWeights será sobrescrito com um método de preenchimento ("stub") que sempre retorna um resultado positivo.

   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return true; }

Já o método de propagação do gradiente de erro calcInputGradients merece atenção especial. Apesar da simplicidade do método de propagação para frente, ele faz uso duplo tanto dos dados brutos quanto da matriz de representações multiescalares. Por isso, para repassar corretamente o gradiente de erro ao nível dos dados brutos, será necessário conduzi-lo cuidadosamente por todos os caminhos informacionais do algoritmo estruturado.

bool CNeuronNAFS::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;

Nos parâmetros do método, recebemos um ponteiro para o objeto da camada anterior, para o qual devemos repassar o gradiente de erro conforme a influência dos dados brutos sobre o resultado do modelo. No corpo do método, verificamos de imediato se o ponteiro recebido é válido, pois, do contrário, todas as operações subsequentes perdem o sentido.

Primeiro, precisamos distribuir o gradiente de erro, vindo da camada seguinte, entre os coeficientes adaptativos e a matriz de representações multiescalares. No entanto, também planejamos encaminhar à matriz de representações multiescalares o gradiente de erro proveniente do fluxo de informação dos coeficientes adaptativos. Portanto, neste estágio, salvamos o gradiente de erro do tensor de representações multiescalares em um buffer temporário.

   if(!MatMulGrad(cAdaptation.getOutput(), cAdaptation.getGradient(),
                  cFeatureSmoothing.getOutput(), cFeatureSmoothing.getPrevOutput(),
                  Gradient, 1, iSmoothing + 1, iDimension, iUnits))
      return false;

Em seguida, trabalhamos com o fluxo informacional dos coeficientes adaptativos. Aqui, repassamos primeiramente o gradiente de erro até o nível do tensor de similaridade cosseno, por meio da chamada do método de propagação de gradiente do objeto correspondente.

   if(!cDistance.calcHiddenGradients(cAdaptation.AsObject()))
      return false;

Na etapa seguinte, distribuímos o gradiente de erro entre os dados brutos e o tensor transposto das representações multiescalares. Também prevemos aqui a obtenção posterior do gradiente de erro no nível dos dados brutos por meio de um segundo fluxo informacional. Portanto, neste momento, salvamos o gradiente de erro correspondente em um buffer temporário.

   if(!MatMulGrad(NeuronOCL.getOutput(), PrevOutput,
                  cTranspose.getOutput(), cTranspose.getGradient(),
                  cDistance.getGradient(), 1, iDimension, iSmoothing + 1, iUnits))
      return false;

Em seguida, transpomos o gradiente de erro da matriz de representações multiescalares e o somamos aos dados anteriormente armazenados.

   if(!cFeatureSmoothing.calcHiddenGradients(cTranspose.AsObject()) ||
      !SumAndNormilize(cFeatureSmoothing.getGradient(), cFeatureSmoothing.getPrevOutput(),
                       cFeatureSmoothing.getGradient(), iDimension, false, 0, 0, 0, 1)
     )
      return false;

Agora, nos resta apenas repassar o gradiente de erro para o nível dos dados brutos. Primeiro, transmitimos o gradiente de erro da matriz de representações multiescalares.

   if(!FeatureSmoothingGradient(NeuronOCL, cFeatureSmoothing.AsObject()) ||
      !SumAndNormilize(NeuronOCL.getGradient(), cFeatureSmoothing.getPrevOutput(),
                       NeuronOCL.getGradient(), iDimension, false, 0, 0, 0, 1) ||
      !DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(), NeuronOCL.getGradient(),
                    (ENUM_ACTIVATION)NeuronOCL.Activation())
     )
      return false;
//---
   return true;
  }

Depois, adicionamos os dados anteriormente salvos e ajustamos o gradiente de erro com base na derivada da função de ativação da camada de dados brutos. Encerramos então a execução do método, retornando o resultado lógico da operação ao programa chamador.

Com isso, concluímos a análise dos princípios de construção dos métodos da classe CNeuronNAFS. O código dessa classe e de todos os seus métodos está disponível no anexo.

2.3 Arquitetura dos modelos

Algumas observações devem ser feitas sobre a arquitetura dos modelos treináveis. O novo objeto de suavização adaptativa de características foi incorporado ao modelo do codificador do estado do ambiente. Esse modelo foi reutilizado a partir do artigo anterior, dedicado ao framework AMCT. Assim, nosso novo modelo utiliza abordagens dos dois frameworks. A arquitetura do modelo está descrita no método CreateEncoderDescriptions.

Mantivemos os princípios gerais de construção de modelos e, inicialmente, criamos uma camada totalmente conectada para transmitir os dados brutos ao modelo.

bool CreateEncoderDescriptions(CArrayObj *&encoder)
  {
//---
   CLayerDescription *descr;
//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }
//--- 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;
     }

Vale ressaltar que o algoritmo NAFS permite aplicar a suavização adaptativa diretamente sobre os dados brutos. No entanto, lembramos que nosso modelo recebe dados crus e não processados, fornecidos diretamente pelo terminal. Como resultado, os atributos analisados podem apresentar diferentes distribuições de valores. Para minimizar os efeitos negativos desse fator, sempre utilizamos uma camada de normalização. E neste caso, aplicamos a mesma abordagem.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Depois disso, aplicamos a camada de suavização adaptativa das características. Essa ordem é a recomendada para seus experimentos, pois grandes diferenças nas distribuições dos atributos individuais podem levar à predominância de uma característica com a maior amplitude de valores durante a formação dos coeficientes de atenção adaptativa às escalas de suavização.

A maioria dos parâmetros do novo objeto se encaixa no formato já familiar da descrição de uma camada neural.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronNAFS;
   descr.count = HistoryBars;
   descr.window = BarDescr;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;

Utilizamos 5 escalas de média, o que corresponde à formação de janelas {1, 3, 5, 7, 9, 11}.

   descr.window_out = 5;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

A arquitetura restante do Codificador permaneceu inalterada e ainda inclui a camada AMCT.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronAMCT;
   descr.window = BarDescr;                           // Window (Indicators to bar)
     {
      int temp[] = {HistoryBars, 50};                // Bars, Properties
      if(ArrayCopy(descr.units, temp) < (int)temp.Size())
         return false;
     }
   descr.window_out = EmbeddingSize / 2;              // Key Dimension
   descr.layers = 5;                                  // Layers
   descr.step = 4;                                    // Heads
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Em seguida, vem uma camada totalmente conectada para redução de dimensionalidade.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

A arquitetura dos modelos do Ator e do Crítico também não sofreu alterações. Juntamente com eles, reutilizamos da pesquisa anterior os programas de interação com o ambiente e de treinamento dos modelos. O código completo dessas implementações pode ser consultado no anexo, onde está incluído o código de todas as aplicações e classes utilizadas na elaboração deste artigo.


3. Testes

Até aqui, realizamos um trabalho substancial na implementação das abordagens propostas pelos autores do framework NAFS, usando os recursos do MQL5. Agora é hora de testar sua eficácia na resolução de nossos desafios. Para isso, vamos treinar os modelos com os métodos propostos utilizando dados reais do ativo EURUSD ao longo de todo o ano de 2023. Durante o treinamento, utilizamos dados históricos do timeframe H1.

Como antes, seguimos com o treinamento offline dos modelos, realizando atualizações periódicas no conjunto de dados de treinamento para mantê-lo relevante em relação à política atual do Ator.

Como mencionado, o novo modelo do Codificador do estado do ambiente foi construído com base no Transformer contrastivo de padrões. Para uma comparação clara dos resultados, testamos o novo modelo mantendo todos os parâmetros do teste do modelo base. A seguir, são apresentados os resultados dos testes realizados nos três primeiros meses de 2024.

De imediato, vale comentar que a comparação entre os resultados do modelo atual e do modelo base gera sentimentos ambíguos. Por um lado, observamos uma redução do fator de lucro de 1.4 para 1.29. Por outro lado, graças ao aumento de 2,5 vezes no número de operações de trading, tivemos um crescimento proporcional no lucro total durante o mesmo período de testes.

Além disso, ao contrário do modelo base, observamos uma tendência constante de crescimento no saldo ao longo de todo o período testado. No entanto, apenas posições vendidas foram abertas. Isso pode estar relacionado ao forte foco nas tendências globais dos valores suavizados. Com isso, durante o processo de filtragem de ruído, certas tendências locais acabam sendo ignoradas.

Ainda assim, ao observarmos o gráfico de rentabilidade mês a mês, notamos uma queda gradual. Essa constatação reforça a suposição feita no artigo anterior de que a representatividade do conjunto de dados de treinamento diminui com o aumento da duração do período de testes.


Considerações finais

Neste artigo, vimos o método NAFS (Node-Adaptive Feature Smoothing), que representa uma abordagem simples e eficaz, não paramétrica, para a construção de representações de nós em grafos, sem a necessidade de treinamento de parâmetros. Ele combina as características suavizadas dos vizinhos dos nós e o uso de ensembles com diferentes estratégias de suavização fortalece as representações finais, tornando-as mais robustas e informativas.

Na parte prática do artigo, implementamos nossa visão das abordagens propostas utilizando os recursos do MQL5. Treinamos os modelos construídos com dados históricos reais. E conduzimos testes fora do conjunto de dados de treinamento. A partir dos resultados de nossos experimentos, podemos concluir que há um potencial real nas abordagens propostas. Podemos combinar esses métodos com outros frameworks. Além disso, a incorporação dessas abordagens permite aumentar a eficiência dos modelos base.


Referências


Programas utilizados no artigo

#NomeTipoDescrição
1Research.mq5Expert AdvisorEA de coleta de exemplos
2ResearchRealORL.mq5
Expert Advisor
EA de coleta de exemplos com o método Real-ORL
3Study.mq5Expert AdvisorEA para treinamento dos modelos
4Test.mq5Expert AdvisorEA para testes do modelo
5Trajectory.mqhBiblioteca de classeEstrutura de descrição do estado do sistema
6NeuroNet.mqhBiblioteca de classeBiblioteca de classes para construção de redes neurais
7NeuroNet.clBibliotecaBiblioteca de código do programa OpenCL

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

Arquivos anexados |
MQL5.zip (2051.38 KB)
Criando um Painel de Administrador de Negociação em MQL5 (Parte I): Construindo uma Interface de Mensagens Criando um Painel de Administrador de Negociação em MQL5 (Parte I): Construindo uma Interface de Mensagens
Este artigo discute a criação de uma Interface de Mensagens para o MetaTrader 5, voltada para Administradores de Sistema, para facilitar a comunicação com outros traders diretamente dentro da plataforma. Integrações recentes de plataformas sociais com o MQL5 permitem a transmissão rápida de sinais através de diferentes canais. Imagine ser capaz de validar sinais enviados com apenas um clique—"SIM" ou "NÃO". Continue lendo para saber mais.
Redes neurais em trading: Transformer contrativo de padrões (Conclusão) Redes neurais em trading: Transformer contrativo de padrões (Conclusão)
No último artigo da série, analisamos o framework Atom-Motif Contrastive Transformer (AMCT), que utiliza aprendizado contrastivo para identificar padrões-chave em todos os níveis, desde os elementos básicos até estruturas complexas. Neste artigo, continuamos a implementar as abordagens do AMCT com recursos do MQL5.
Automatizando Estratégias de Negociação com a Estratégia Parabolic SAR em MQL5: Criando um Expert Advisor Eficaz Automatizando Estratégias de Negociação com a Estratégia Parabolic SAR em MQL5: Criando um Expert Advisor Eficaz
Neste artigo, vamos automatizar as estratégias de negociação com a Estratégia Parabolic SAR em MQL5: Criando um Expert Advisor Eficaz. O EA realizará negociações com base nas tendências identificadas pelo indicador Parabolic SAR.
Simulação de mercado (Parte 16): Sockets (X) Simulação de mercado (Parte 16): Sockets (X)
Estamos a um passo de concluir este desafio. Porém, quero que você, caro leitor, procure entender primeiro estes dois artigos. Tanto este como o anterior. Isto para que consiga de fato entender o próximo onde abordarei exclusivamente a parte referente a programação em MQL5. Apesar de que ali a coisa será igualmente voltada a ser fácil de entender. Se você não compreender estes dois últimos artigos. Com toda a certeza terá grandes problemas em entender o próximo. O motivo disto é simples: As coisas vão se acumulando. Quando mais coisas é preciso fazer, mais coisas é preciso criar e entender para poder atingir o objetivo.