English Русский 中文 Español Deutsch 日本語
preview
Redes neurais em trading: Usando modelos de linguagem para previsão de séries temporais

Redes neurais em trading: Usando modelos de linguagem para previsão de séries temporais

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

Introdução

Nesta série de artigos, exploramos diversas soluções arquitetônicas para modelar séries temporais. Muitas delas alcançam resultados satisfatórios. No entanto, é perceptível que não utilizam plenamente as vantagens dos padrões complexos em séries temporais, como a sazonalidade e as tendências. Esses componentes são fatores distintivos fundamentais das séries temporais. Consequentemente, pesquisas recentes mostram que arquiteturas baseadas em aprendizado profundo podem não ser tão robustas quanto se acreditava anteriormente, e até mesmo redes neurais rasas ou modelos lineares podem superá-las em alguns benchmarks.

Enquanto isso, o surgimento de modelos básicos no processamento de linguagem natural (NLP) e visão computacional (CV) representou marcos importantes no aprendizado eficiente de representações. O pré-treinamento de modelos básicos para séries temporais com grandes volumes de dados melhora o desempenho em tarefas subsequentes. Além disso, grandes modelos de linguagem oferecem uma maneira de utilizar representações já existentes adquiridas durante o pré-treinamento, sem a necessidade de aprendizado do zero. No entanto, as estruturas básicas e os métodos existentes em modelos de linguagem não capturam completamente a evolução dos padrões temporais, que são fundamentais para o modelamento de séries temporais.

Os autores do artigo "TEMPO: Prompt-based Generative Pre-trained Transformer for Time Series Forecasting" abordam os desafios de adaptar grandes modelos previamente treinados para tarefas de previsão de séries temporais e propõem um modelo abrangente baseado no GPT, denominado TEMPO. Ele é composto por dois componentes analíticos essenciais para o aprendizado eficiente de representações de séries temporais. Um componente foca na modelagem de padrões específicos das séries temporais, como tendências e sazonalidade. O outro concentra-se em obter insights mais gerais e transferíveis das propriedades internas dos dados, por meio de uma abordagem baseada em "prompts". Especificamente, o TEMPO primeiro decompõe os dados brutos multimodais de séries temporais em três componentes: tendência, sazonalidade e resíduos. Em seguida, cada um desses componentes é mapeado para um espaço latente correspondente, para que se construa a incorporação inicial da série temporal ao GPT.

Os autores do método realizam uma análise formal, conectando o domínio de séries temporais ao domínio de frequência, para destacar a necessidade de decompor esses componentes na análise de séries temporais. Além disso, eles demonstram teoricamente que o mecanismo de atenção tem dificuldade em realizar essa decomposição automaticamente.

No TEMPO, são utilizadas instruções que codificam conhecimentos temporais sobre tendências e sazonalidade. Isso permite ajustar eficientemente o GPT para resolver tarefas de previsão. Além disso, as tendências, a sazonalidade e os resíduos são utilizados para fornecer uma estrutura interpretável que ajuda a compreender as interações entre os componentes originais.


1. Algoritmo TEMPO

No trabalho, os autores do método TEMPO aplicam uma abordagem híbrida que combina a confiabilidade da análise estatística de séries temporais com a adaptabilidade de métodos baseados em dados. Eles propõem uma nova integração da decomposição de tendências e sazonalidade em modelos de linguagem previamente treinados, com base na arquitetura Transformer. Essa estratégia permite aproveitar as vantagens únicas tanto dos métodos estatísticos quanto dos métodos de aprendizado de máquina, aumentando a capacidade do modelo de processar eficientemente dados de séries temporais.

Além disso, é introduzida uma abordagem semi-flexível com prompts, que melhora a adaptabilidade de modelos previamente treinados para lidar com dados de séries temporais. Essa abordagem inovadora permite que os modelos combinem seus vastos conhecimentos adquiridos durante o pré-treinamento com os requisitos específicos do estudo de séries temporais.

No caso de utilização de dados multimodais de séries temporais, a representação de dados complexos por meio de sua decomposição em componentes significativos, como tendências e sazonalidade, pode ajudar a extrair informações de maneira otimizada.

O componente de tendência XT captura padrões de longo prazo nos dados. O componente sazonal XS encapsula ciclos curtos e repetitivos, que podem ser avaliados após a remoção da tendência. O componente residual XR representa a parte remanescente dos dados originais após a extração da tendência e da sazonalidade.

Na prática, recomenda-se utilizar a maior quantidade possível de informações para alcançar uma decomposição mais precisa. No entanto, considerando a eficiência computacional, os autores do método preferem não realizar a decomposição no intervalo de dados mais amplo possível em cada instância. Em vez disso, eles realizam uma decomposição local em cada instância, utilizando um tamanho de janela fixo, e adicionam parâmetros treináveis para estimar os diferentes componentes da decomposição local. Esse princípio também é aplicado a outros componentes do modelo.

Os experimentos realizados pelos autores do método mostram que a decomposição simplifica significativamente o processo de previsão.

A decomposição dos dados brutos proposta nesse trabalho desempenha um papel crítico nos métodos modernos baseados na arquitetura Transformer, pois os mecanismos de atenção, teoricamente, não conseguem desmembrar automaticamente os sinais unidimensionais de tendência e sazonalidade. Se os componentes de tendência e sazonalidade de uma série temporal não forem ortogonais, não será possível separá-los e desmembrá-los completamente por meio de qualquer conjunto de bases ortogonais. A camada Self-Attention é transformada em uma transformação ortogonal, de maneira semelhante ao método de componentes principais. Portanto, o uso direto de atenção em séries temporais não processadas seria ineficaz para separar os componentes de tendência e sazonalidade que não são ortogonais.

Primeiramente, os autores do método TEMPO aplicam uma normalização reversível dos dados para cada componente global, facilitando a transferência de informações e minimizando perdas causadas por mudanças na distribuição.

Além disso, é implementada uma função de perda de reconstrução baseada no erro quadrático médio (MSE). Isso garante que a decomposição local em componentes esteja de acordo com a decomposição global observada no conjunto de treinamento.

Em seguida, os dados brutos de séries temporais são segmentados, com a adição de codificação posicional para capturar a semântica local e a agregação de passos de tempo adjacentes em tokens. Isso aumenta significativamente o horizonte histórico e reduz a redundância.

Os tokens de séries temporais obtidos por meio desse processo são enviados para a camada de incorporação. As incorporações treinadas da representação da série temporal permitem que a arquitetura do modelo de linguagem transfira eficientemente suas capacidades para a nova modalidade sequencial de séries temporais.

Os métodos baseados em prompts demonstraram uma eficácia notável em uma ampla gama de aplicações, aproveitando o potencial do conhecimento prévio sobre tarefas específicas, codificado em prompts cuidadosamente elaborados. Esse sucesso pode ser explicado pela capacidade dos prompts de fornecer uma estrutura que alinha os resultados do modelo com os objetivos desejados, resultando em melhorias na precisão, consistência e qualidade geral do conteúdo gerado. Buscando utilizar a rica informação semântica contida nos diferentes componentes das séries temporais, os autores do método introduziram uma estratégia de prompts suavizada. Essa abordagem envolve a geração de prompts individuais correspondentes a cada componente principal da série temporal: tendência, sazonalidade e resíduos. Os prompts são combinados com os respectivos componentes dos dados brutos, permitindo uma abordagem mais avançada de modelagem sequencial que considera a natureza multifacetada dos dados das séries temporais.

Essa estrutura possibilita associar cada instância dos dados brutos a prompts específicos, como um viés indutivo, codificando conjuntamente informações críticas relacionadas à tarefa de previsão. É importante destacar que a construção operacional proposta pelos autores é altamente adaptável, garantindo compatibilidade com uma ampla gama de análises de séries temporais. Essa adaptabilidade ressalta o potencial da estratégia baseada em prompts, que pode evoluir conforme as complexidades apresentadas por diferentes conjuntos de dados de séries temporais.

No trabalho, os autores do método TEMPO utilizam o GPT baseado em decodificador como o modelo base para construir a fundação das representações de séries temporais. Para o uso eficiente da informação semântica decomposta, os prompts e os diversos componentes são combinados e enviados para o bloco GPT.

Como alternativa, é possível criar blocos GPT separados para lidar com diferentes tipos de componentes das séries temporais.

O resultado geral da previsão deve ser uma combinação das previsões de cada componente. Após passar pelo bloco GPT, cada componente é enviado para uma camada totalmente conectada, a fim de gerar valores previstos. As previsões obtidas são projetadas de volta ao subespaço dos dados brutos pela adição dos respectivos indicadores estatísticos extraídos na etapa de normalização. A soma das previsões de cada componente permite reconstruir a trajetória completa da série temporal.

Abaixo, é apresentada a visualização do método TEMPO pelos autores.

2. Implementação em MQL5

Após considerarmos os aspectos teóricos do método TEMPO proposto, partimos para a parte prática do artigo, na qual implementamos nossa visão dos conceitos abordados utilizando MQL5.

Desde já, é importante ressaltar que, infelizmente, não dispomos de um modelo de linguagem previamente treinado. Portanto, não podemos avaliar plenamente a eficácia da transferência de conhecimento dos modelos de linguagem para a área de previsão de séries temporais. No entanto, podemos recriar uma estrutura semelhante à solução arquitetônica proposta e avaliar sua eficácia na resolução de tarefas de previsão de movimentos de preços com base em dados históricos reais.

Antes de passar à análise do código do programa, porém, é necessário dedicar um pouco de atenção às soluções arquitetônicas utilizadas.

Os dados brutos que chegam à entrada do modelo são divididos em três componentes: tendência, sazonalidade e resíduos. Para extrair a tendência, os autores do método utilizam o cálculo da média dos dados brutos usando uma janela deslizante, o que lembra o indicador padrão de Média Móvel. Em minha implementação, preferi usar o método PLR, anteriormente analisado. Na minha opinião, esse método é mais informativo, pois é capaz de identificar tendências de diferentes comprimentos. Contudo, os resultados desse método não podem ser subtraídos diretamente da série original. Aqui, é necessário aprimorar o algoritmo, sobre o qual falaremos em mais detalhes durante a implementação.

O segundo ponto é a extração da sazonalidade. Considero o uso do espectro de frequência uma solução óbvia. Contudo, como você sabe, a Transformada Discreta de Fourier (DFT) é capaz de representar completamente a série temporal original no domínio da frequência. A transformada inversa (iDFT), por sua vez, retornará a série temporal original sem distorções. Para separar o componente sazonal da série temporal original do ruído e dos valores atípicos, é necessário eliminar algumas frequências do espectro. Surge, então, a questão sobre o volume e a lista de frequências a serem zeradas. Não há uma resposta definitiva para isso. Já discutimos questões semelhantes ao prever séries temporais no domínio da frequência. Porém, desta vez, abordei a questão de maneira um pouco diferente. Na análise de dados, utilizamos uma série temporal multimodal referente a um único instrumento financeiro. É razoável esperar que os ciclos dos componentes individuais estejam alinhados entre si. Então, por que não utilizar o mecanismo de Self-Attention para identificar frequências alinhadas nos espectros das séries temporais unificadas? Esperamos que as frequências alinhadas no espectro destaquem o componente sazonal.

Dessa forma, podemos dividir os dados brutos em componentes separados, conforme previsto pelo método TEMPO. Com isso, a operação do modelo em construção se torna parcialmente mais clara. Já temos uma solução pronta para dividir modelos unitários em segmentos individuais e incorporá-los. O mesmo se aplica às soluções arquitetônicas baseadas no Transformer. No entanto, há a questão dos "prompts". Os autores do método sugerem o uso de "prompts" que orientem o modelo GPT a gerar sequências dentro do contexto esperado. Nesta implementação, decidi usar os resultados do método PLR como "prompts".

E talvez a última questão global a ser discutida seja a quantidade de modelos de atenção a serem utilizados: um único modelo geral ou um modelo específico para cada componente. Optei por usar um modelo geral, pois isso permite organizar todo o processo de processamento de dados simultaneamente em fluxos paralelos. O uso de modelos separados para cada componente resultaria em um processamento sequencial. Isso aumentaria tanto o tempo necessário para o treinamento dos modelos quanto para a tomada de decisões subsequentes.

Com os principais pontos do modelo discutidos, podemos avançar para a parte prática.

#2.1 Complemento do programa OpenCL

Iniciaremos o trabalho criando novos kernels no programa OpenCL. Como mencionado anteriormente, para identificar as tendências principais em séries temporais multimodais dos dados brutos, utilizaremos o método de Representação Linear por Segmentos (PLR), que representa cada segmento com três valores: o ângulo de inclinação da linha, o deslocamento e o tamanho do segmento. É evidente que subtrair as tendências dos dados originais é um desafio com essa representação da série temporal. No entanto, isso é viável. Para implementar essa funcionalidade, criaremos o kernel CutTrendAndOther. Nos parâmetros, este kernel receberá quatro ponteiros para buffers de dados. Dois deles conterão os dados brutos na forma de um tensor da série temporal original (inputs) e um tensor da representação linear por segmentos dos dados (plr). Os resultados das operações serão armazenados em outros dois buffers:

  • trend: tendências representadas como uma série temporal regular;
  • other: a diferença entre os valores dos dados brutos e a linha de tendência.

__kernel void CutTrendAndOther(__global const float *inputs,
                               __global const float *plr,
                               __global float *trend,
                               __global float *other
                              )
  {
   const size_t i = get_global_id(0);
   const size_t lenth = get_global_size(0);
   const size_t v = get_global_id(1);
   const size_t variables = get_global_size(1);

Planejamos chamar esse kernel em um espaço de tarefas bidimensional. A primeira dimensão representará o tamanho da sequência dos dados brutos, e a segunda o número de variáveis analisadas (sequências unitárias). No corpo do kernel, identificaremos o fluxo atual em todas as dimensões do espaço de tarefas.

Em seguida, definiremos as constantes necessárias.

//--- constants
   const int shift_in = i * variables + v;
   const int step_in = variables;
   const int shift_plr = v;
   const int step_plr = 3 * step_in;

O próximo passo é localizar o segmento da representação linear por segmentos ao qual o elemento atual pertence. Para isso, faremos uma iteração pelos segmentos utilizando um laço.

//--- calc position
   int pos = -1;
   int prev_in = 0;
   int dist = 0;
   do
     {
      pos++;
      prev_in += dist;
      dist = (int)fmax(plr[shift_plr + pos * step_plr + 2 * step_in] * lenth, 1);
     }
   while(!(prev_in <= i && (prev_in + dist) > i));

Com base nos parâmetros do segmento encontrado, determinaremos o valor da linha de tendência no ponto atual e sua diferença em relação ao valor da série temporal original.

//--- calc trend
   float sloat = plr[shift_plr + pos * step_plr];
   float intercept = plr[shift_plr + pos * step_plr + step_in];
   pos = i - prev_in;
   float trend_i = sloat * pos + intercept;
   float other_i = inputs[shift_in] - trend_i;

Por fim, restará apenas salvar os valores obtidos nos elementos correspondentes dos buffers globais de resultados.

//--- save result
   trend[shift_in] = trend_i;
   other[shift_in] = other_i;
  }

De maneira semelhante, construímos o kernel CutTrendAndOtherGradient para distribuir os gradientes de erro através das operações mencionadas anteriormente, destinado à propagação reversa. É fácil perceber que esse kernel recebe, em seus parâmetros, ponteiros para buffers de dados semelhantes contendo os gradientes de erro.

__kernel void CutTrendAndOtherGradient(__global float *inputs_gr,
                                       __global const float *plr,
                                       __global float *plr_gr,
                                       __global const float *trend_gr,
                                       __global const float *other_gr
                                      )
  {
   const size_t i = get_global_id(0);
   const size_t lenth = get_global_size(0);
   const size_t v = get_global_id(1);
   const size_t variables = get_global_size(1);

Aqui, utilizamos o mesmo espaço de tarefas bidimensional, no qual identificamos o fluxo atual. Em seguida, definimos os valores das constantes.

//--- constants
   const int shift_in = i * variables + v;
   const int step_in = variables;
   const int shift_plr = v;
   const int step_plr = 3 * step_in;

Depois, repetimos o algoritmo para localizar o segmento necessário.

//--- calc position
   int pos = -1;
   int prev_in = 0;
   int dist = 0;
   do
     {
      pos++;
      prev_in += dist;
      dist = (int)fmax(plr[shift_plr + pos * step_plr + 2 * step_in] * lenth, 1);
     }
   while(!(prev_in <= i && (prev_in + dist) > i));

Desta vez, no entanto, calculamos os gradientes de erro dos parâmetros do segmento.

//--- get gradient
   float other_i_gr = other_gr[shift_in];
   float trend_i_gr = trend_gr[shift_in] - other_i_gr;
//--- calc plr gradient
   pos = i - prev_in;
   float sloat_gr = trend_i_gr * pos;
   float intercept_gr = trend_i_gr;

Por fim, salvamos os resultados nos buffers de dados.

//--- save result
   plr_gr[shift_plr + pos * step_plr] += sloat_gr;
   plr_gr[shift_plr + pos * step_plr + step_in] += intercept_gr;
   inputs_gr[shift_in] = other_i_gr;
  }

Observe que não sobrescrevemos, mas adicionamos o gradiente de erro aos dados existentes no buffer de gradientes da representação linear por segmentos. Isso se deve ao fato de que pretendemos utilizar os resultados da representação linear por segmentos das séries temporais em duas direções:

  • Para a identificação de tendências, como implementado no kernel apresentado anteriormente;
  • Como "prompts" para o modelo de atenção, conforme mencionado anteriormente.

Consequentemente, precisamos compilar o gradiente de erro proveniente das duas direções. Para evitar o uso de um buffer adicional e a operação desnecessária de somar valores de dois buffers, implementamos a soma diretamente neste kernel.

Além disso, criamos os kernels CutOneFromAnother e CutOneFromAnotherGradient para separar o componente sazonal dos outros dados. O algoritmo desses kernels é extremamente simples, consistindo literalmente de duas a três linhas de código. Creio que você não terá dificuldade em compreendê-los. O código completo de todas as aplicações usadas na preparação deste artigo está disponível no anexo.

Com isso, concluímos o trabalho com o programa OpenCL e partimos para o desenvolvimento de nossa biblioteca principal.

2.2 Criação da classe do método TEMPO

No que se refere ao programa principal, precisaremos construir um algoritmo bastante complexo e abrangente do método TEMPO discutido. Como você já deve ter notado, a abordagem proposta possui uma estrutura de fluxo de dados complexa e ramificada. Nesse caso, implementar toda a abordagem dentro de uma única classe aumentará significativamente a eficiência de utilização dos métodos propostos.

Para implementar essas abordagens, criaremos a classe CNeuronTEMPOOCL, que herdará a funcionalidade principal da classe base de camada totalmente conectada CNeuronBaseOCL. Abaixo, está representada a estrutura robusta da nova classe. Ela conterá elementos familiares de trabalhos anteriores, bem como componentes novos. Detalharemos a funcionalidade de cada elemento da estrutura da nova classe à medida que implementarmos seus métodos.

class CNeuronTEMPOOCL   :  public CNeuronBaseOCL
  {
protected:
   //--- constants
   uint              iVariables;
   uint              iSequence;
   uint              iForecast;
   uint              iFFT;
   //--- Trend
   CNeuronPLROCL     cPLR;
   CNeuronBaseOCL    cTrend;
   //--- Seasons
   CNeuronBaseOCL    cInputSeasons;
   CNeuronTransposeOCL cTranspose[2];
   CBufferFloat      cInputFreqRe;
   CBufferFloat      cInputFreqIm;
   CNeuronBaseOCL    cInputFreqComplex;
   CNeuronBaseOCL    cNormFreqComplex;
   CBufferFloat      cMeans;
   CBufferFloat      cVariances;
   CNeuronComplexMLMHAttention cFreqAtteention;
   CNeuronBaseOCL    cUnNormFreqComplex;
   CBufferFloat      cOutputFreqRe;
   CBufferFloat      cOutputFreqIm;
   CNeuronBaseOCL    cOutputTimeSeriasRe;
   CBufferFloat      cOutputTimeSeriasIm;
   CBufferFloat      cZero;
   //--- Noise
   CNeuronBaseOCL    cResidual;
   //--- Forecast
   CNeuronBaseOCL    cConcatInput;
   CNeuronBatchNormOCL cNormalize;
   CNeuronPatching   cPatching;
   CNeuronBatchNormOCL cNormalizePLR;
   CNeuronPatching   cPatchingPLR;
   CNeuronPositionEncoder acPE[2];
   CNeuronMLCrossAttentionMLKV cAttention;
   CNeuronTransposeOCL  cTransposeAtt;
   CNeuronConvOCL    acForecast[2];
   CNeuronTransposeOCL  cTransposeFrc;
   CNeuronRevINDenormOCL cRevIn;
   CNeuronConvOCL    cSum;
   //--- Complex functions
   virtual bool      FFT(CBufferFloat *inp_re, CBufferFloat *inp_im, 
                         CBufferFloat *out_re, CBufferFloat *out_im, bool reverse = false);
   virtual bool      ComplexNormalize(void);
   virtual bool      ComplexUnNormalize(void);
   virtual bool      ComplexNormalizeGradient(void);
   virtual bool      ComplexUnNormalizeGradient(void);
   //---
   bool              CutTrendAndOther(CBufferFloat *inputs);
   bool              CutTrendAndOtherGradient(CBufferFloat *inputs_gr);
   bool              CutOneFromAnother(void);
   bool              CutOneFromAnotherGradient(void);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   //---
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   //---

public:
                     CNeuronTEMPOOCL(void)   {};
                    ~CNeuronTEMPOOCL(void)   {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                          uint sequence, uint variables, uint forecast, uint heads, uint layers, 
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   const   {  return defNeuronTEMPOOCL;   }
   //---
   virtual bool      Save(int const file_handle);
   virtual bool      Load(int const file_handle);
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau);
   virtual void      SetOpenCL(COpenCLMy *obj);
   //---
   virtual CBufferFloat   *getWeights(void)  override;
  };

Observe que, apesar da grande variedade de objetos incorporados, todos eles são declarados estaticamente. Isso nos permite deixar o construtor e o destrutor da classe vazios, transferindo para o sistema toda a responsabilidade pela liberação de memória após a exclusão de um objeto da classe.

A inicialização direta das variáveis e objetos incorporados da classe é realizada no método Init. Como de costume, os parâmetros principais que definem de forma inequívoca a arquitetura da camada a ser criada são recebidos como argumentos desse método. Aqui, encontramos parâmetros que já nos são familiares:

  • sequence: o tamanho da sequência analisada da série temporal multimodal;
  • variables: o número de variáveis analisadas (sequências unitárias);
  • forecast: a profundidade de planejamento dos valores previstos;
  • heads: o número de cabeças de atenção nos mecanismos de Self-Attention utilizados;
  • layers: o número de camadas nos blocos de atenção.

bool CNeuronTEMPOOCL::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                           uint sequence, uint variables, uint forecast, uint heads, uint layers, 
                           ENUM_OPTIMIZATION optimization_type, uint batch)
  {
//--- base
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, forecast * variables, 
                                                          optimization_type, batch))
      return false;

No corpo do método, para inicializar os objetos herdados, chamamos o método homônimo da classe pai, como de costume. Além disso, dentro do método da classe pai, é realizado um controle mínimo necessário sobre os parâmetros recebidos.

Após a execução bem-sucedida das operações do método da classe pai, salvamos os parâmetros recebidos nas variáveis incorporadas.

//--- constants
   iVariables = variables;
   iForecast = forecast;
   iSequence = MathMax(sequence, 1);

Imediatamente em seguida, definimos o tamanho dos buffers de dados para as operações de decomposição frequencial do sinal.

//--- Calculate FFTsize
   uint size = iSequence;
   int power = int(MathLog(size) / M_LN2);
   if(MathPow(2, power) < size)
      power++;
   iFFT = uint(MathPow(2, power));

Para identificar as tendências na sequência analisada dos dados brutos, inicializamos o objeto de Representação Linear por Segmentos.

//--- trend
   if(!cPLR.Init(0, 0, OpenCL, iVariables, iSequence, true, optimization, iBatch))
      return false;

Também inicializamos o objeto responsável por registrar as tendências identificadas como uma série temporal regular.

   if(!cTrend.Init(0, 1, OpenCL, iSequence * iVariables, optimization, iBatch))
      return false;

A diferença entre a série temporal de tendências e os valores originais será registrada em um objeto separado, que servirá como os dados brutos para o bloco de extração de flutuações sazonais.

//--- seasons
   if(!cInputSeasons.Init(0, 2, OpenCL, iSequence * iVariables, optimization, iBatch))
      return false;

É importante notar que os dados brutos obtidos são uma sequência de dados multimodais que descrevem etapas temporais individuais. Para extrair o espectro de frequência das séries temporais unitárias, é necessário transpor o tensor dos dados brutos. Em seguida, na saída do bloco, realizaremos a operação inversa. Para essa funcionalidade, inicializamos duas camadas de transposição de dados.

   if(!cTranspose[0].Init(0, 3, OpenCL, iSequence, iVariables, optimization, iBatch))
      return false;
   if(!cTranspose[1].Init(0, 4, OpenCL, iVariables, iSequence, optimization, iBatch))
      return false;

Os resultados da decomposição frequencial do sinal serão salvos em dois buffers de dados: um para a parte real e outro para a parte imaginária do sinal.

   if(!cInputFreqRe.BufferInit(iFFT * iVariables, 0) || !cInputFreqRe.BufferCreate(OpenCL))
      return false;
   if(!cInputFreqIm.BufferInit(iFFT * iVariables, 0) || !cInputFreqIm.BufferCreate(OpenCL))
      return false;

Para o bloco de atenção no domínio da frequência, será necessário concatenar os dois buffers de dados em um único objeto.

   if(!cInputFreqComplex.Init(0, 5, OpenCL, iFFT * iVariables * 2, optimization, batch))
      return false;

Por fim, é importante lembrar que os modelos apresentam resultados mais estáveis quando se trabalha com dados normalizados. Assim, criamos objetos para registrar os dados normalizados e os parâmetros extraídos da distribuição original.

   if(!cNormFreqComplex.Init(0, 6, OpenCL, iFFT * iVariables * 2, optimization, batch))
      return false;
   if(!cMeans.BufferInit(iVariables, 0) || !cMeans.BufferCreate(OpenCL))
      return false;
   if(!cVariances.BufferInit(iVariables, 0) || !cVariances.BufferCreate(OpenCL))
      return false;

Agora, iniciaremos o objeto de atenção no domínio da frequência. Vale lembrar que, de acordo com nossa lógica, sua função é destacar as características frequenciais presentes nos dados multimodais, ajudando-nos a identificar flutuações sazonais nos dados brutos.

   if(!cFreqAtteention.Init(0, 7, OpenCL, iFFT, 32, heads, iVariables, layers, optimization, batch))
      return false;

Nesse caso, utilizamos o número de cabeças de atenção e o número de camadas no bloco de atenção conforme os valores dos parâmetros externos.

Após destacar as principais características frequenciais, realizamos as operações inversas. Primeiro, retornamos as frequências ao formato respectivo no domínio original.

   if(!cUnNormFreqComplex.Init(0, 8, OpenCL, iFFT * iVariables * 2, optimization, batch))
      return false;

Em seguida, separamos a parte real e imaginária do sinal em buffers de dados distintos.

   if(!cOutputFreqRe.BufferInit(iFFT * iVariables, 0) || !cOutputFreqRe.BufferCreate(OpenCL))
      return false;
   if(!cOutputFreqIm.BufferInit(iFFT * iVariables, 0) || !cOutputFreqIm.BufferCreate(OpenCL))
      return false;

Depois, transformamos esses sinais de volta para o domínio temporal.

   if(!cOutputTimeSeriasRe.Init(0, 9, OpenCL, iFFT * iVariables, optimization, iBatch))
      return false;
   if(!cOutputTimeSeriasIm.BufferInit(iFFT * iVariables, 0) || 
      !cOutputTimeSeriasIm.BufferCreate(OpenCL))
      return false;

Criamos também um buffer auxiliar preenchido com valores nulos, que será utilizado para preencher dados ausentes (valores vazios).

   if(!cZero.BufferInit(iFFT * iVariables, 0) || !cZero.BufferCreate(OpenCL))
      return false;

Com isso, finalizamos o trabalho com o bloco de extração do componente sazonal. A diferença entre os sinais será armazenada em um objeto separado, que representa as três componentes do sinal.

//--- Noise
   if(!cResidual.Init(0, 10, OpenCL, iSequence * iVariables, optimization, iBatch))
      return false;

Após dividir o sinal dos dados brutos em três componentes, avançamos para a próxima etapa do algoritmo TEMPO: a previsão de valores subsequentes. Inicialmente, concatenamos os dados das três componentes em um único tensor.

//--- Forecast
   if(!cConcatInput.Init(0, 11, OpenCL, 3 * iSequence * iVariables, optimization, iBatch))
      return false;

Depois, adaptamos os dados para um formato comparável.

   if(!cNormalize.Init(0, 12, OpenCL, 3 * iSequence * iVariables, iBatch, optimization))
      return false;

Agora, precisamos segmentar as sequências unitárias, cujo número triplicou devido à decomposição de cada sequência em três componentes.

   int window = MathMin(5, (int)iSequence - 1);
   int patches = (int)iSequence - window + 1;
   if(!cPatching.Init(0, 13, OpenCL, window, 1, 8, patches, 3 * iVariables, optimization, iBatch))
      return false;
   if(!acPE[0].Init(0, 14, OpenCL, patches, 3 * 8 * iVariables, optimization, iBatch))
      return false;

Adicionamos, então, a codificação posicional aos segmentos obtidos.

Executamos operações semelhantes para a representação linear por segmentos da série temporal original.

   int plr = cPLR.Neurons();
   if(!cNormalizePLR.Init(0, 15, OpenCL, plr, iBatch, optimization))
      return false;
   plr = MathMax(plr/(3 * (int)iVariables),1);
   if(!cPatchingPLR.Init(0, 16, OpenCL, 3, 3, 8, plr, iVariables, optimization, iBatch))
      return false;
   if(!acPE[1].Init(0, 17, OpenCL, plr, 8 * iVariables, optimization, iBatch))
      return false;

Em seguida, inicializamos a camada de atenção cruzada, que analisará o sinal dividido em três componentes no contexto da representação linear por segmentos da série temporal original.

   if(!cAttention.Init(0, 18, OpenCL, 3 * 8 * iVariables, 3 * iVariables, MathMax(heads, 1), 
                       8 * iVariables, MathMax(heads / 2, 1), patches, plr, MathMax(layers, 1), 
                       2, optimization, iBatch))
      return false;

Após o processamento, passamos para a previsão dos dados subsequentes. Neste ponto, entendemos que, assim como no caso da decomposição frequencial, é necessário prever os dados das sequências unitárias. Para isso, começamos transpondo os dados.

   if(!cTransposeAtt.Init(0, 19, OpenCL, patches, 3 * 8 * iVariables, optimization, iBatch))
      return false;

Depois, utilizamos um bloco composto por duas camadas convolucionais sequenciais, que serão responsáveis por prever os dados das sequências unitárias individuais. A primeira camada fará a previsão das sequências unitárias para cada elemento da incorporação.

   if(!acForecast[0].Init(0, 20, OpenCL, patches, patches, iForecast, 3 * 8 * iVariables, 
                                                                      optimization, iBatch))
      return false;
   acForecast[0].SetActivationFunction(LReLU);

A segunda camada ajustará as sequências de incorporações para formar as séries unitárias das componentes analisadas nos dados brutos.

   if(!acForecast[1].Init(0, 21, OpenCL, 8 * iForecast, 8 * iForecast, iForecast, 3 * iVariables, 
                                                                            optimization, iBatch))
      return false;
   acForecast[1].SetActivationFunction(TANH);

Finalmente, retornamos o tensor de valores previstos à dimensão esperada nos resultados.

   if(!cTransposeFrc.Init(0, 22, OpenCL, 3 * iVariables, iForecast, optimization, iBatch))
      return false;

Por fim, projetamos os valores obtidos de volta ao domínio original das componentes analisadas. Para isso, adicionamos os parâmetros estatísticos extraídos durante a normalização dos dados.

   if(!cRevIn.Init(0, 23, OpenCL, 3 * iVariables * iForecast, 11, GetPointer(cNormalize)))
      return false;

Para obter os valores previstos das variáveis-alvo, é necessário somar os valores previstos de suas componentes individuais. Decidi substituir a operação de soma simples por uma soma ponderada com parâmetros treináveis, implementada dentro de uma camada convolucional.

   if(!cSum.Init(0, 24, OpenCL, 3, 3, 1, iVariables, iForecast, optimization, iBatch))
      return false;
   cSum.SetActivationFunction(None);

Para evitar cópias excessivas de dados, substituiremos os ponteiros para os buffers correspondentes.

   SetActivationFunction(None);
   SetOutput(cSum.getOutput(), true);
   SetGradient(cSum.getGradient(), true);
//---
   return true;
  }

Com isso, concluímos a descrição do método de inicialização da nova classe. Não devemos esquecer de monitorar o processo de execução das operações em cada etapa. Ao final do método, retornamos um valor lógico que indica o sucesso das operações para o programa chamador.

Após a inicialização do objeto, passamos à próxima etapa: a construção do algoritmo de propagação para frente. Devo dizer que, para implementar a propagação para frente, foram criados vários métodos de enfileiramento para a execução dos kernels descritos anteriormente. O algoritmo desses métodos já lhe é familiar, e os novos métodos não introduzem particularidades construtivas. Por isso, deixamos esses métodos para estudo independente. Como você deve se lembrar, o código completo desta classe e de todos os seus métodos está anexado. Agora proponho observar a implementação do algoritmo principal de propagação para frente no método CNeuronTEMPOOCL::feedForward.

bool CNeuronTEMPOOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
//--- trend
   if(!cPLR.FeedForward(NeuronOCL))
      return false;

Nos parâmetros do método, recebemos um ponteiro para o objeto da camada anterior, que nos transmite os dados brutos. Em seguida, encaminhamos o ponteiro recebido para o método de propagação para frente da camada incorporada de extração de tendências, com base no método de Representação Linear por Segmentos.

Observe que, nesta etapa, não verificamos a validade do ponteiro recebido. Isso ocorre porque essa verificação já é realizada no método do objeto incorporado chamado, e organizar outro ponto de controle seria redundante.

Após determinar as tendências, subtraímos sua influência dos dados brutos.

   if(!CutTrendAndOther(NeuronOCL.getOutput()))
      return false;

O próximo passo é a extração do componente sazonal. Primeiro, transpomos os dados obtidos após a subtração das tendências.

   if(!cTranspose[0].FeedForward(cInputSeasons.AsObject()))
      return false;

Em seguida, utilizamos a Transformada Rápida de Fourier para obter o espectro das características frequenciais do sinal analisado.

   if(!FFT(cTranspose[0].getOutput(), NULL,GetPointer(cInputFreqRe),GetPointer(cInputFreqIm),false))
      return false;

Concatenamos as partes real e imaginária das características frequenciais em um único tensor.

   if(!Concat(GetPointer(cInputFreqRe), GetPointer(cInputFreqIm), cInputFreqComplex.getOutput(),
                                                                           1, 1, iFFT * iVariables))
      return false;

Normalizamos os valores obtidos.

   if(!ComplexNormalize())
      return false;

No bloco de atenção, destacamos a parte significativa do espectro das características frequenciais.

   if(!cFreqAtteention.FeedForward(cNormFreqComplex.AsObject()))
      return false;

Por meio da execução das operações inversas, obtemos o componente sazonal na forma de uma série temporal.

   if(!ComplexUnNormalize())
      return false;
   if(!DeConcat(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), 
                cUnNormFreqComplex.getOutput(), 1, 1, iFFT * iVariables))
      return false;
   if(!FFT(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), 
           GetPointer(cInputFreqRe), GetPointer(cOutputTimeSeriasIm), true))
      return false;
   if(!DeConcat(cOutputTimeSeriasRe.getOutput(), cOutputTimeSeriasRe.getGradient(), 
                GetPointer(cInputFreqRe), iSequence, iFFT - iSequence, iVariables))
      return false;
   if(!cTranspose[1].FeedForward(cOutputTimeSeriasRe.AsObject()))
      return false;

Depois, extraímos os valores das três componentes.

//--- Noise
   if(!CutOneFromAnother())
      return false;

Com as três componentes extraídas da série temporal, concatenamos todas elas em um único tensor.

//--- Forecast
   if(!Concat(cTrend.getOutput(), cTranspose[1].getOutput(), cResidual.getOutput(),
              cConcatInput.getOutput(), 1, 1, 1, 3 * iSequence * iVariables))
      return false;

Observe que, ao concatenar os dados, selecionamos sequencialmente um elemento de cada componente individual. Isso nos permite posicionar, lado a lado, elementos de diferentes componentes que correspondem ao mesmo passo temporal de uma única sequência unitária. Essa disposição dos dados permite utilizar uma camada convolucional para realizar a soma ponderada dos valores previstos de componentes individuais e obter a sequência de previsão na saída da camada.

Em seguida, normalizamos os valores do tensor das componentes concatenadas, o que nos permite uniformizar os indicadores de componentes individuais e das variáveis analisadas.

   if(!cNormalize.FeedForward(cConcatInput.AsObject()))
      return false;

Os dados normalizados são divididos em segmentos e, a eles, associamos incorporações (embeddings).

   if(!cPatching.FeedForward(cNormalize.AsObject()))
      return false;

Posteriormente, adicionamos codificação posicional para identificar inequivocamente a posição de cada elemento no tensor.

   if(!acPE[0].FeedForward(cPatching.AsObject()))
      return false;

Preparamos os dados da Representação Linear por Segmentos de forma similar. Primeiro, normalizamos esses dados.

   if(!cNormalizePLR.FeedForward(cPLR.AsObject()))
      return false;

Depois, dividimos em segmentos e adicionamos a codificação posicional.

   if(!cPatchingPLR.FeedForward(cPatchingPLR.AsObject()))
      return false;
   if(!acPE[1].FeedForward(cPatchingPLR.AsObject()))
      return false;

Com as representações das componentes e as "prompts" preparadas, podemos usar o bloco de atenção, que destacará as principais características da representação da série temporal analisada.

   if(!cAttention.FeedForward(acPE[0].AsObject(), acPE[1].getOutput()))
      return false;

Depois, transpondo os dados,

   if(!cTransposeAtt.FeedForward(cAttention.AsObject()))
      return false;

avançamos para prever os valores subsequentes com um MLP de duas camadas, cujas funções são implementadas por duas camadas convolucionais.

   if(!acForecast[0].FeedForward(cTransposeAtt.AsObject()))
      return false;
   if(!acForecast[1].FeedForward(acForecast[0].AsObject()))
      return false;

O uso de camadas convolucionais nos permite organizar a previsão independente de sequências no contexto de cada sequência unitária.

Retornamos os dados previstos ao formato original.

   if(!cTransposeFrc.FeedForward(acForecast[1].AsObject()))
      return false;

E adicionamos os parâmetros estatísticos da distribuição dos dados originais, extraídos durante a normalização do tensor concatenado das componentes.

   if(!cRevIn.FeedForward(cTransposeFrc.AsObject()))
      return false;

No final do método, somamos os valores previstos das componentes individuais para obter a série desejada de valores subsequentes.

   if(!cSum.FeedForward(cRevIn.AsObject()))
      return false;
//---
   return true;
  }

Lembro que, graças à substituição de ponteiros nos buffers de resultados e gradientes de erro, eliminamos a operação desnecessária de copiar dados do buffer de resultados da camada de soma das componentes para o buffer de resultados da nossa camada. Além disso, evitamos também a operação inversa: a cópia de gradientes de erro durante a construção dos métodos de propagação reversa.

Como você sabe, em nossa implementação, a propagação reversa geralmente é construída com dois métodos:

  • calcInputGradients: distribuição do gradiente de erro para todos os elementos, de acordo com sua influência no resultado geral;
  • updateInputWeights: ajuste dos parâmetros do modelo com o objetivo de minimizar o erro.

Primeiro, realizamos as operações de distribuição do gradiente de erro para determinar a influência de cada parâmetro do modelo no resultado geral. Essas operações seguem exatamente o fluxo de dados da propagação para frente, mas no sentido inverso.

bool CNeuronTEMPOOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;
//--- Devide to Trend, Seasons and Noise
   if(!cRevIn.calcHiddenGradients(cSum.AsObject()))
      return false;

Iniciamos distribuindo o gradiente de erro obtido entre as componentes individuais e ajustando-o com base nos parâmetros de normalização dos dados.

//--- Forecast gradient
   if(!cTransposeFrc.calcHiddenGradients(cRevIn.AsObject()))
      return false;

Em seguida, passamos o gradiente de erro pelo MLP de previsão de sequência.

   if(!acForecast[1].calcHiddenGradients(cTransposeFrc.AsObject()))
      return false;
   if(acForecast[1].Activation() != None &&
      !DeActivation(acForecast[1].getOutput(), acForecast[1].getGradient(),
                    acForecast[1].getGradient(), acForecast[1].Activation())
     )
      return false;
   if(!acForecast[0].calcHiddenGradients(acForecast[1].AsObject()))
      return false;

E pelo bloco de atenção cruzada.

//--- Attention gradient
   if(!cTransposeAtt.calcHiddenGradients(acForecast[0].AsObject()))
      return false;
   if(!cAttention.calcHiddenGradients(cTransposeAtt.AsObject()))
      return false;
   if(!acPE[0].calcHiddenGradients(cAttention.AsObject(), acPE[1].getOutput(), 
                                   acPE[1].getGradient(), (ENUM_ACTIVATION)acPE[1].Activation()))
      return false;

No caso da propagação para frente, o bloco de atenção cruzada recebe dados de dois fluxos de informação:

  • Componentes concatenadas;
  • Representação linear por segmentos (PLR) dos dados brutos.

Distribuímos o gradiente de erro sequencialmente por ambos os fluxos. Primeiro, pelo fluxo PLR.

//--- Gradient to PLR
   if(!cPatchingPLR.calcHiddenGradients(acPE[1].AsObject()))
      return false;
   if(!cNormalizePLR.calcHiddenGradients(cPatchingPLR.AsObject()))
      return false;
   if(!cPLR.calcHiddenGradients(cNormalizePLR.AsObject()))
      return false;

Depois, pelo tensor concatenado das componentes.

//--- Gradient to Concatenate buffer of Trend, Season and Noise
   if(!cPatching.calcHiddenGradients(acPE[0].AsObject()))
      return false;
   if(!cNormalize.calcHiddenGradients(cPatching.AsObject()))
      return false;
   if(!cConcatInput.calcHiddenGradients(cNormalize.AsObject()))
      return false;

Em seguida, distribuímos o gradiente de erro nos buffers das componentes individuais.

//--- DeConcatenate
   if(!DeConcat(cTrend.getGradient(), cOutputTimeSeriasRe.getGradient(), cResidual.getGradient(),
                cConcatInput.getGradient(), 1, 1, 1, 3 * iSequence * iVariables))
      return false;

Aqui, é importante entender que, ao dividir o tensor concatenado em partes separadas, cada componente recebe sua parcela do gradiente de erro. Contudo, há outro fluxo de informação. Durante a determinação do componente residual de ruído, subtraímos o componente sazonal do valor total. Consequentemente, o componente sazonal influencia os valores do ruído e, portanto, deve receber o gradiente de erro do ruído. Ajustamos os valores dos gradientes.

//--- Seasons
   if(!CutOneFromAnotherGradient())
      return false;
   if(!SumAndNormilize(cOutputTimeSeriasRe.getGradient(), cTranspose[1].getGradient(), 
                       cTranspose[1].getGradient(), 1, false, 0, 0, 0, 1))
      return false;

Após isso, preparamos o gradiente de erro para a série temporal do componente sazonal. Vale lembrar que, ao formar o componente sazonal a partir do espectro frequencial utilizando a Transformada Inversa de Fourier, obtemos as partes real e imaginária da série temporal. O gradiente de erro da parte real é determinado com base no valor recebido do ruído e do tensor concatenado das componentes. Os elementos ausentes são preenchidos com valores nulos.

   if(!cOutputTimeSeriasRe.calcHiddenGradients(cTranspose[1].AsObject()))
      return false;
   if(!Concat(cOutputTimeSeriasRe.getGradient(), GetPointer(cZero), GetPointer(cInputFreqRe), 
                                                 iSequence, iFFT - iSequence, iVariables))
      return false;

Para a parte imaginária, esperamos valores nulos. Portanto, atribuiremos ao gradiente de erro os próprios valores da parte imaginária, mas com sinal invertido.

   if(!SumAndNormilize(GetPointer(cOutputTimeSeriasIm), GetPointer(cOutputTimeSeriasIm),

                       GetPointer(cOutputTimeSeriasIm), 1, false, 0, 0, 0, -0.5f))
      return false;

Os gradientes de erro obtidos são então transformados para o domínio da frequência.

   if(!FFT(GetPointer(cInputFreqRe), GetPointer(cOutputTimeSeriasIm), GetPointer(cOutputFreqRe),

           GetPointer(cOutputFreqIm), false))
      return false;

Passamos esses gradientes pelo bloco de atenção no domínio da frequência até os dados brutos.

   if(!Concat(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), 
              cUnNormFreqComplex.getGradient(), 1, 1, iFFT * iVariables))
      return false;
   if(!ComplexUnNormalizeGradient())
      return false;
   if(!cNormFreqComplex.calcHiddenGradients(cFreqAtteention.AsObject()))
      return false;
   if(!ComplexNormalizeGradient())
      return false;
   if(!DeConcat(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), 
                cInputFreqComplex.getGradient(), 1, 1, iFFT * iVariables))
      return false;
   if(!FFT(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), 
           GetPointer(cInputFreqRe), GetPointer(cInputFreqIm), true))
      return false;
   if(!DeConcat(cTranspose[0].getGradient(), GetPointer(cInputFreqIm), 
                GetPointer(cInputFreqRe), iSequence, iFFT - iSequence, iVariables))
      return false;
   if(!cInputSeasons.calcHiddenGradients(cTranspose[0].AsObject()))
      return false;

E adicionamos o gradiente de erro do ruído ao gradiente dos dados brutos obtido.

   if(!SumAndNormilize(cInputSeasons.getGradient(), cResidual.getGradient(),
                       cInputSeasons.getGradient(), 1, 1, false, 0, 0, 1))
      return false;

Finalmente, conduzimos o gradiente de erro pelo bloco PLR e o enviamos para o nível da camada anterior.

//--- trend
   if(!CutTrendAndOtherGradient(NeuronOCL.getGradient()))
      return false;
//---  input gradient
   if(!NeuronOCL.calcHiddenGradients(cPLR.AsObject()))
      return false;
   if(!SumAndNormilize(NeuronOCL.getGradient(), cInputSeasons.getGradient(), 
                       NeuronOCL.getGradient(), 1, false, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

O algoritmo do método de atualização dos parâmetros do modelo não tem particularidades. Ele apenas chama, de maneira sequencial, os métodos homônimos dos objetos incorporados que contêm os parâmetros treináveis. Por isso, não entraremos em detalhes sobre ele, deixando-o para estudo independente. O mesmo se aplica aos métodos auxiliares que dão suporte à nova classe. Lembro que o código completo da classe e de todos os seus métodos está incluído no anexo.


Considerações finais

Neste artigo, exploramos o novo método abrangente de previsão de séries temporais, o TEMPO, proposto por seus autores, que sugeriram o uso de modelos de linguagem previamente treinados para a previsão de séries temporais. Além disso, eles sugeriram uma nova abordagem para a decomposição de séries temporais, aumentando a eficiência do aprendizado da representação dos dados brutos.

Na parte prática deste artigo, implementamos nossa visão das abordagens propostas utilizando MQL5. Foi realizado um trabalho bastante extenso. Infelizmente, o formato do artigo não permite incluir todo o conteúdo abordado. Portanto, os resultados do modelo com base em dados históricos reais serão apresentados no próximo artigo.


Referências

Programas utilizados no artigo

# Nome Tipo Descrição
1 Research.mq5 Expert Advisor EA para coleta de exemplos
2 ResearchRealORL.mq5
Expert Advisor
EA para coleta de exemplos pelo método Real-ORL
3 Study.mq5 Expert Advisor EA para treinamento de Modelos
4 StudyEncoder.mq5 Expert Advisor
EA para treinamento do Codificador
5 Test.mq5 Expert Advisor EA para testar o modelo
6 Trajectory.mqh Biblioteca de classe Estrutura de descrição do estado do sistema
7 NeuroNet.mqh Biblioteca de classe Biblioteca de classes para criação de redes neurais
8 NeuroNet.cl Biblioteca Biblioteca de código do programa OpenCL

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

Arquivos anexados |
MQL5.zip (2433.12 KB)
Aprendendo MQL5 do iniciante ao profissional (Parte IV): Sobre Arrays, Funções e Variáveis Globais do Terminal Aprendendo MQL5 do iniciante ao profissional (Parte IV): Sobre Arrays, Funções e Variáveis Globais do Terminal
Este artigo é uma continuação do ciclo para iniciantes. Ele descreve em detalhes arrays de dados, a interação entre dados e funções, bem como variáveis globais do terminal que permitem a troca de dados entre diferentes programas MQL5.
Redes neurais em trading: Modelos "leves" para previsão de séries temporais Redes neurais em trading: Modelos "leves" para previsão de séries temporais
Os modelos leves para previsão de séries temporais oferecem alto desempenho utilizando uma quantidade mínima de parâmetros. Isso reduz o consumo de recursos computacionais e acelera a tomada de decisões. Ao mesmo tempo, eles alcançam qualidade de previsão comparável à de modelos mais complexos.
Do básico ao intermediário: Template e Typename (III) Do básico ao intermediário: Template e Typename (III)
Neste artigo iremos ver a primeira parte de algo que para iniciantes é muito confuso de entender. Mas para que fique devidamente explicado e assim o tema não se torne confuso, além do necessário. Irei dividir a coisa em etapas. A primeira etapa é a que estará sendo mostrada neste artigo. No entanto, apesar de no final parecer que ficamos em um beco sem saída. Não será bem isto que estará ocorrendo. Já que o próximo passo nos levará a uma outra situação em que será melhor entendida no próximo artigo.
Algoritmo de comportamento social adaptativo — Adaptive Social Behavior Optimization (ASBO): Evolução em duas fases Algoritmo de comportamento social adaptativo — Adaptive Social Behavior Optimization (ASBO): Evolução em duas fases
Este artigo dá continuidade ao tema do comportamento social dos organismos vivos e ao seu impacto no desenvolvimento de um novo modelo matemático, o ASBO (Adaptive Social Behavior Optimization). Exploraremos a evolução em duas fases, realizaremos testes no algoritmo e apresentaremos as conclusões. Assim como na natureza, onde grupos de organismos vivos se unem para sobreviver, o ASBO utiliza princípios de comportamento coletivo para resolver problemas complexos de otimização.