Русский
preview
Redes neurais em trading: decomposição em vez de escalonamento: construção dos módulos

Redes neurais em trading: decomposição em vez de escalonamento: construção dos módulos

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

Introdução

No artigo anterior, começamos a implementar nossa própria visão das abordagens propostas pelos autores do framework SSCNN. Neste artigo, vamos avançar para a implementação prática dos principais componentes do framework. No entanto, antes de avançar para o código e os algoritmos, é importante lembrar ao leitor quais são os pontos fortes da SSCNN e quais princípios orientam seu funcionamento. Compreender esses fundamentos permite não apenas reproduzir a implementação, mas adaptá-la conscientemente a tarefas reais de trading.

O framework SSCNN (Segmental Structured Convolutional Neural Network) foi criado como uma ferramenta especializada para a análise de séries temporais em condições de instabilidade do mercado e fragmentação dos sinais. Sua ideia central não é processar valores isolados, mas segmentos completos de dados, permitindo que o modelo considere tanto características locais quanto dependências contextuais mais amplas. Em vez de uma visão linear da série temporal, a SSCNN propõe uma abordagem estratificada: cada intervalo temporal é analisado no contexto que o envolve, o que torna possível extrair atributos relevantes com precisão mesmo com alto nível de ruído.

Na primeira etapa, os dados passam por uma decomposição estrutural, na qual o sinal é dividido em componentes estáveis: tendência de longo prazo, oscilações sazonais, flutuações de curto prazo e ruído residual. Essa etapa é especialmente importante ao trabalhar com séries financeiras, em que a tendência e os desvios de curto prazo têm papéis analíticos distintos. Para cada componente, são criados modelos de processamento separados, o que permite melhorar a qualidade da análise e a interpretabilidade dos resultados. A previsão final é obtida pela montagem coordenada das saídas desses modelos.

A SSCNN também dedica atenção especial ao mecanismo de normalização baseada em atenção (AttnNorm). Esse componente não apenas estabiliza as representações internas do modelo, mas também permite que ele se concentre de forma adaptativa nos trechos mais informativos dos dados. Em condições de turbulência do mercado, quando o valor do sinal pode ser fortemente distorcido por picos de curto prazo, essa seletividade proporciona maior estabilidade e precisão à previsão. A normalização é implementada considerando o contexto dos segmentos e inclui o treinamento dos parâmetros de atenção, o que torna a retropropagação do erro mais significativa e direcionada.

A visualização do framework SSCNN elaborada pelo autor é apresentada a seguir.


Módulo de separação de componentes

Na parte prática do artigo anterior, concentramo-nos na implementação do kernel de normalização com pesos de atenção no ambiente OpenCL. Essa etapa nos permitiu estabelecer a base para a execução eficiente e paralela das principais operações: o cálculo das médias e dos desvios-padrão por segmentos de dados, considerando os pesos de atenção, bem como a normalização direta do sinal original. Tudo isso é executado no contexto da GPU, o que garante alto desempenho e escalabilidade da solução.

Agora, damos continuidade ao que foi iniciado anteriormente e passamos a focar a interação do programa principal com esses kernels OpenCL. Precisaremos implementar as rotinas de acionamento desses kernels, incluindo a preparação dos buffers, a configuração dos parâmetros de chamada, a sincronização e o controle da integridade dos dados trocados entre CPU e GPU. Essa é uma etapa importante, pois a eficiência do framework SSCNN depende, em boa parte, não apenas da precisão matemática do kernel, mas também da boa estruturação e da consistência da troca de dados entre o processador central e o acelerador gráfico.

O elemento central dessa arquitetura é o objeto CNeuronAttentNorm. É ele que coordena a interação entre a lógica de alto nível da camada de normalização baseada em atenção e a computação de baixo nível na GPU. Esse objeto herda da classe base CNeuronBaseOCL, o que lhe fornece uma interface universal e permite sua integração à pilha geral do modelo sem perda de flexibilidade. A estrutura do novo objeto é apresentada a seguir.

class CNeuronAttentNorm :  public CNeuronBaseOCL
  {
protected:
   uint                    iPeriod;
   uint                    iVariables;
   uint                    iCount;
   //---
   CParams                 cAttention;
   CNeuronSoftMaxOCL       cSoftMax;
   CNeuronBaseOCL          cMeans;
   CNeuronBaseOCL          cSTDevs;
   //---
   virtual bool      AttentNorm(CNeuronBaseOCL *NeuronOCL);
   virtual bool      AttentNormGrad(CNeuronBaseOCL *NeuronOCL);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronAttentNorm(void) :  iPeriod(0), iVariables(0), iCount(0) {};
                    ~CNeuronAttentNorm(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units_count, uint period, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      Save(const int file_handle) override;
   virtual bool      Load(const int file_handle) override;
   //---
   virtual int       Type(void) override const  {  return defNeuronAttentNorm; }
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   CNeuronBaseOCL*   GetMeans(void) { return cMeans.AsObject(); }
   CNeuronBaseOCL*   GetSTDevs(void) { return cSTDevs.AsObject(); }
   virtual uint      GetPeriod(void) const { return iPeriod; }
   virtual uint      GetVariables(void) const { return iVariables; }
   virtual uint      GetUnits(void) const { return iCount; }
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override {  activation = None; }
  };

Na estrutura da classe, vemos que os principais parâmetros internos são iPeriod, iVariables e iCount. Eles definem, respectivamente, o tamanho do segmento de dados, o número de variáveis e a quantidade de segmentos em uma única sequência. Esses valores determinam a estrutura do array analisado.

Vale dedicar atenção especial ao objeto cAttention, que armazena os parâmetros associados ao mecanismo de atenção. Também merece destaque o cSoftMax, componente que transforma os pesos de atenção para uma forma normalizada. Esses dois módulos operam de forma integrada: os parâmetros treináveis de atenção passam pela transformação SoftMax e só depois são usados no cálculo das médias e dos desvios-padrão. Para armazenar essas estatísticas, são usados os objetos cMeans e cSTDevs.

Todos os objetos internos da classe CNeuronAttentNorm são declarados de forma estática, o que torna seu tempo de vida previsível. Com essa abordagem, tanto o construtor quanto o destrutor podem permanecer vazios. A alocação e a liberação de recursos são tratadas explicitamente ao longo do ciclo de vida do modelo. A inicialização de todos os componentes internos e herdados ocorre de forma centralizada no método Init, que recebe os parâmetros do ambiente de execução e da configuração da camada.

bool CNeuronAttentNorm::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                             uint units_count, uint period, uint variables,
                             ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, units_count * period * variables,
                                                                 optimization_type, batch))
      return false;
   CNeuronBaseOCL::SetActivationFunction(None);

Primeiro, transferimos o controle para o método de mesmo nome da classe pai, em que a inicialização de todas as interfaces herdadas já está implementada. Em seguida, definimos explicitamente o tipo da função de ativação como None, pois essa camada, por si só, não executa transformações não lineares; sua função se concentra na normalização com base nos pesos de atenção.

Em seguida, armazenamos os parâmetros de configuração da camada: a quantidade de blocos (unidades), o número de variáveis e o período de segmentação. Esses valores serão usados posteriormente para controlar a lógica interna da normalização.

   iCount = units_count;
   iVariables = variables;
   iPeriod = period;

Depois, passamos à inicialização dos objetos que acabamos de declarar. O primeiro é o objeto cAttention, responsável pela geração dos pesos de atenção. Após a inicialização bem-sucedida, desativamos a função de ativação, mantendo o resultado em forma linear.

   if(!cAttention.Init(0, 0, OpenCL, iPeriod * iVariables, optimization, iBatch))
      return false;
   cAttention.SetActivationFunction(None);
   if(!cSoftMax.Init(0, 1, OpenCL, cAttention.Neurons(), optimization, iBatch))
      return false;
   cSoftMax.SetHeads(iVariables);

Em seguida, é configurado o componente cSoftMax, que transforma os valores de atenção em uma distribuição probabilística. Sua dimensionalidade corresponde ao número de neurônios em cAttention. O método SetHeads define a quantidade de cabeças de atenção. Neste caso, ela é definida como igual ao número de variáveis, o que permite processar cada atributo de forma independente.

Depois, é criado o objeto cMeans, que será usado para armazenar as médias por segmento. A dimensionalidade é definida pelo número de blocos multiplicado pelo número de variáveis. A função de ativação é novamente desativada, pois estamos tratando de uma estatística.

//---
   if(!cMeans.Init(0, 2, OpenCL, iCount * iVariables, optimization, iBatch))
      return false;
   cMeans.SetActivationFunction(None);
   if(!cSTDevs.Init(0, 3, OpenCL, iCount * iVariables, optimization, iBatch))
      return false;
   cSTDevs.SetActivationFunction(None);
//---
   return true;
  }

De modo análogo, é configurado o bloco cSTDevs, responsável pela dispersão ou pelo desvio-padrão necessário para a normalização. Ele replica integralmente a configuração de cMeans.

Assim, o método Init cria uma base arquitetural sólida para a camada CNeuronAttentNorm, conectando os componentes lógicos e computacionais em um sistema integrado e facilmente escalável. Cada objeto responde por uma parte bem definida do processamento, enquanto a inicialização centralizada oferece controle completo sobre os parâmetros da camada e seu comportamento no contexto OpenCL.

Após inicializarmos todos os componentes internos, a etapa-chave passa a ser a implementação da propagação para frente pela camada. É nesse ponto que o modelo começa a usar as estruturas inicializadas anteriormente, como pesos de atenção, transformação SoftMax e objetos de características estatísticas, para executar sua tarefa principal: normalizar os dados originais considerando a importância de cada elemento. O algoritmo é implementado no método feedForward.

bool CNeuronAttentNorm::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(bTrain)
     {
      if(!cAttention.FeedForward())
         return false;
      if(!cSoftMax.FeedForward(cAttention.AsObject()))
         return false;
     }
//---
   return AttentNorm(NeuronOCL);
  }

O método recebe como argumento um ponteiro para o objeto NeuronOCL, que representa a camada neural anterior e contém os dados a serem analisados.

Se o modelo estiver em modo de treinamento (bTrain == true), a primeira operação executada é a propagação para frente pelo componente cAttention, no qual são gerados os valores brutos de atenção. Esses valores não dependem diretamente do estado atual dos dados originais e são aprendidos durante o treinamento do modelo. Portanto, durante a operação, os resultados gerados pelo bloco serão estáticos, e não precisamos executar operações desnecessárias a cada propagação.

Depois disso, os pesos de atenção gerados são passados para o bloco cSoftMax, que os transforma em uma distribuição probabilística normalizada. Essa etapa é crítica, pois garante que a soma dos pesos de atenção no segmento seja igual a um e, assim, preserva a correção dos cálculos posteriores.

Independentemente do modo atual, treinamento ou inferência, a ação final do método feedForward é a chamada da função AttentNorm. Essa função atua como um encapsulador em torno do kernel OpenCL de mesmo nome e cuida de toda a preparação de baixo nível: da definição dos parâmetros ao enfileiramento da tarefa para execução. A estrutura do método segue rigorosamente o esquema padrão adotado para todas as chamadas desse tipo, e essa padronização se torna especialmente evidente graças às macros implementadas anteriormente e aos templates universais. Por isso, não vamos analisar em detalhes a implementação interna de AttentNorm, limitando-nos a reconhecê-la como um ponto de integração típico e tecnicamente correto entre a lógica do modelo e o contexto OpenCL.

Após concluída a propagação para frente, vem uma etapa igualmente importante: o cálculo dos gradientes de erro necessários para a atualização correta dos pesos do modelo. Essa etapa é implementada no método calcInputGradients, que segue uma lógica clara de retropropagação do erro.

bool CNeuronAttentNorm::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!AttentNormGrad(NeuronOCL))
      return false;
   if(NeuronOCL.Activation() != None)
      if(!DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(),
                       NeuronOCL.getGradient(), NeuronOCL.Activation()))
         return false;

O método começa com a chamada de AttentNormGrad, que, por analogia com AttentNorm, atua como interface com o kernel OpenCL de mesmo nome. Sua tarefa é agregar os gradientes do sinal de saída normalizado, considerando as particularidades da atenção e os parâmetros do segmento de dados atual. Esse é um passo essencial, pois é a partir dele que, na etapa reversa, a influência dos dados originais sobre as estatísticas finais é levada em conta, como a média e a variância.

Em seguida, é feita uma verificação: se a função de ativação da camada de origem tiver sido definida, é necessário diferenciar os gradientes obtidos considerando a não linearidade especificada. Isso é implementado pela chamada da função DeActivation, que corrige os gradientes, garantindo, assim, plena conformidade com o esquema clássico da propagação reversa.

O passo final do método é propagar os gradientes de volta pela cadeia de atenção. Para isso, é usado o método CalcHiddenGradients do objeto cAttention, que recebe como entrada o gradiente de saída do módulo SoftMax. Essa sequência de ações garante a continuidade do fluxo de informação dos gradientes, desde a camada de saída até os parâmetros ocultos do modelo e, por fim, até suas entradas.

   if(!cAttention.CalcHiddenGradients(cSoftMax.AsObject()))
      return false;
//---
   return true;
  }

A atualização dos parâmetros no módulo de normalização com pesos de atenção é concluída pelo método updateInputWeights, responsável por ajustar os pesos internos com base nos gradientes acumulados. Neste caso, a implementação do método é extremamente concisa: a execução é delegada à função homônima do objeto cAttention, responsável pela geração dos pesos de atenção. Isso está totalmente alinhado à arquitetura geral do framework SSCNN, na qual cada componente do modelo responde pela sua parte dos cálculos.

bool CNeuronAttentNorm::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
   return cAttention.UpdateInputWeights();
  }

Essa abordagem garante alta modularidade e flexibilidade ao sistema: o mecanismo de atenção fica isolado em seu próprio objeto, o que simplifica tanto a depuração quanto a adaptação futura do modelo a novas condições. Além disso, a delegação direta da execução deixa o código mais limpo e fácil de ler, além de favorecer a reutilização dos componentes sem a necessidade de reescrevê-los.

Com isso, concluímos a análise dos métodos da camada de normalização com pesos de atenção. Percorremos toda a cadeia lógica, desde a inicialização dos objetos internos até as etapas finais das propagações para frente e reversa, incluindo a propagação dos gradientes e a atualização dos pesos. Vale destacar que todos os principais cálculos e operações de controle são executados por meio de uma interface única e consistente.

O código-fonte completo da classe CNeuronAttentNorm e de todos os seus métodos está disponível no anexo.


Separação do componente espacial

Ao passarmos para o próximo bloco lógico da arquitetura, vamos analisar o objeto destinado à extração da componente espacial durante a normalização baseada em atenção. Trata-se da classe CNeuronSAttentNorm, que estende a funcionalidade de CNeuronTransposeOCL e, em contraste com a implementação anterior, é orientada ao processamento de dados transpostos, oferecendo flexibilidade adicional ao trabalhar com atributos espaciais e contextualmente distribuídos.

class CNeuronSAttentNorm :   public CNeuronTransposeOCL
  {
protected:
   CNeuronTransposeOCL     cTranspose;
   CNeuronBaseOCL          cAttention;
   CNeuronSoftMaxOCL       cSoftMax;
   CNeuronBaseOCL          cMeans;
   CNeuronBaseOCL          cSTDevs;
   CNeuronBaseOCL          cNorm;
   //---
   virtual bool      AttentNorm(CNeuronBaseOCL *NeuronOCL);
   virtual bool      AttentNormGrad(CNeuronBaseOCL *NeuronOCL);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronSAttentNorm(void) {};
                    ~CNeuronSAttentNorm(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units_count, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      Load(const int file_handle) override;
   //---
   virtual int       Type(void) override const  {  return defNeuronSAttentNorm; }
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   CNeuronBaseOCL*   GetMeans(void) { return cMeans.AsObject(); }
   CNeuronBaseOCL*   GetSTDevs(void) { return cSTDevs.AsObject(); }
   virtual uint      GetVariables(void) const { return iWindow; }
   virtual uint      GetUnits(void) const { return iCount; }
   //---
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override {  activation = None; }
  };

A estrutura do objeto, em certo sentido, espelha a camada CNeuronAttentNorm analisada anteriormente, mas com uma diferença essencial na formação dos pesos de atenção. Já a normalização propriamente dita é realizada ao longo do eixo dos atributos dentro da janela.

Como antes, a inicialização do objeto CNeuronSAttentNorm está concentrada no respectivo método Init, responsável por configurar corretamente todos os componentes internos da camada e prepará-los para as tarefas de propagação para frente e propagação reversa.

bool CNeuronSAttentNorm::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                              uint units_count, uint variables,
                              ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronTransposeOCL::Init(numOutputs, myIndex, open_cl, units_count, variables,
                                                            optimization_type, batch))
      return false;
   activation = None;

Primeiro, é chamado o método Init da classe pai CNeuronTransposeOCL, que realiza a configuração básica dos parâmetros da camada atual, incluindo as interfaces herdadas. Depois disso, a função de ativação da camada é explicitamente definida como None, pois nesta etapa nenhuma ativação é aplicada.

Em seguida, começa a inicialização sequencial de todos os objetos internos, cada um com uma função específica no fluxo computacional. O objeto cTranspose cuida da transposição do tensor original, transformando a fatia temporal em um vetor de atributos que será processado posteriormente pelo módulo de atenção.

   if(!cTranspose.Init(0, 0, OpenCL, iWindow, iCount, optimization, iBatch))
      return false;

Na sequência, é criado e configurado o objeto cAttention, que, neste caso, atua apenas como buffer para armazenar os coeficientes de atenção.

   if(!cAttention.Init(0, 1, OpenCL, iWindow * iWindow, optimization, iBatch))
      return false;
   cAttention.SetActivationFunction(None);
   if(!cSoftMax.Init(0, 2, OpenCL, cAttention.Neurons(), optimization, iBatch))
      return false;
   cSoftMax.SetHeads(iWindow);

Depois dele, vem o bloco associado cSoftMax, que executa a normalização dos pesos ao longo da dimensão especificada. Neste caso, trata-se da dimensão dos atributos.

Em seguida, são inicializados os objetos de médias cMeans e de desvios-padrão cSTDevs, necessários para posteriormente escalonar e alinhar os dados analisados.

   if(!cMeans.Init(0, 3, OpenCL, iCount, optimization, iBatch))
      return false;
   cMeans.SetActivationFunction(None);
   if(!cSTDevs.Init(0, 4, OpenCL, iCount, optimization, iBatch))
      return false;
   cSTDevs.SetActivationFunction(None);

A configuração se encerra com o objeto cNorm, que representa o buffer no qual os dados normalizados são armazenados antes da transposição reversa para a representação de séries temporais.

   if(!cNorm.Init(0, 5, OpenCL, Neurons(), optimization, iBatch))
      return false;
   cNorm.SetActivationFunction(None);
//---
   return true;
  }

Concluída a inicialização, o objeto CNeuronSAttentNorm se torna totalmente funcional, e podemos passar à análise da etapa-chave da propagação para frente. Esse método desempenha um papel central no processamento dos dados analisados, transformando-os passo a passo com base nos blocos computacionais internos da camada.

bool CNeuronSAttentNorm::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cTranspose.FeedForward(NeuronOCL))
      return false;

A propagação para frente começa com a chamada do método FeedForward do objeto cTranspose, responsável pela transformação inicial do tensor de entrada. Nesta etapa, os dados são reorganizados para que a janela temporal seja desdobrada em uma representação espacial. Isso é necessário para o uso posterior da matriz de atenção, na qual cada atributo será comparado com todos os demais.

O passo seguinte é a chamada da função MatMul, que executa a multiplicação matricial dos dados originais por sua cópia transposta. Nesse ponto, é criada a matriz de atenção, na qual cada linha contém uma estimativa da importância dos demais elementos dos dados analisados em relação ao atributo atual. O resultado dessa operação é armazenado no objeto cAttention, que representa um mapa de atenção bruto, ainda não normalizado.

   if(!MatMul(NeuronOCL.getOutput(), cTranspose.getOutput(), cAttention.getOutput(),
              iWindow, iCount, iWindow, 1, false))
      return false;
   if(!cSoftMax.FeedForward(cAttention.AsObject()))
      return false;

Depois de obtida a matriz de atenção, é preciso normalizá-la. Para isso, é chamado o método FeedForward do objeto cSoftMax, que transforma os valores numéricos em pesos probabilísticos, garantindo, assim, a interpretabilidade e a estabilidade numérica dos cálculos posteriores.

Em seguida, os pesos de atenção obtidos são passados para o método AttentNorm, que implementa a ideia principal da camada: o escalonamento e a normalização dos dados originais considerando os pesos de atenção calculados. Essa etapa combina as estatísticas da janela, como a média e o desvio-padrão, levando em conta a importância de cada atributo, o que permite ao modelo identificar padrões estruturais nos dados mesmo na presença de ruídos significativos ou desvios.

   if(!AttentNorm(cTranspose.AsObject()))
      return false;
//---
   return CNeuronTransposeOCL::feedForward(cNorm.AsObject());
  }

A ação final é delegar a execução ao método feedForward da classe base CNeuronTransposeOCL, que processa o tensor de dados normalizados cNorm. Com isso, os resultados obtidos são devolvidos à representação dos dados originais.

Concluída a análise da propagação para frente, é natural avançar para uma etapa igualmente importante: a retropropagação do erro. O método calcInputGradients calcula os gradientes necessários para a posterior correção dos pesos e parâmetros do modelo durante o treinamento. Aqui, uma cadeia de operações em etapas faz o sinal de erro retornar da camada de saída do modelo até sua entrada, considerando todas as transformações internas, incluindo o mecanismo de atenção e a normalização.

bool CNeuronSAttentNorm::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;
//---
   if(!CNeuronTransposeOCL::calcInputGradients(cNorm.AsObject()))
      return false;

O procedimento começa com uma verificação básica da validade do ponteiro para o objeto de dados de origem NeuronOCL, para o qual precisaremos passar o gradiente de erro. Em seguida, a chamada é repassada ao método calcInputGradients da classe pai, mas já usando o objeto cNorm, que representa a saída normalizada do módulo de atenção. Isso permite propagar corretamente o gradiente pela última etapa da propagação para frente.

A próxima etapa-chave é a chamada do método AttentNormGrad, que implementa as rotinas de acionamento do kernel homônimo do programa OpenCL. Aqui, é importante levar em conta a influência de todas as transformações estatísticas aplicadas anteriormente, bem como dos pesos de atenção, para garantir uma propagação coerente do erro.

   if(!AttentNormGrad(cTranspose.AsObject()))
      return false;

Depois disso, é chamado o método CalcHiddenGradients do objeto cAttention, o que permite propagar o gradiente do erro do nível dos pesos probabilísticos da função SoftMax para os pesos de atenção. Isso é importante porque os pesos de atenção, por serem função de várias variáveis de entrada, exigem tratamento especial no cálculo do gradiente.

   if(!cAttention.CalcHiddenGradients(cSoftMax.AsObject()))
      return false;
   if(!MatMulGrad(NeuronOCL.getOutput(), PrevOutput,
                  cTranspose.getOutput(), cTranspose.getPrevOutput(),
                  cAttention.getGradient(),
                  iWindow, iCount, iWindow, 1, false))
      return false;

Em seguida, é chamada a função MatMulGrad, que executa a operação reversa correspondente à multiplicação matricial aplicada na propagação para frente. Nessa etapa, são considerados os dados originais e sua cópia transposta. Isso permite reconstruir os gradientes levando em conta as relações internas entre os elementos da janela analisada.

No entanto, é preciso considerar que, no nível dos dados transpostos, já recebemos o gradiente de erro pelo caminho principal do fluxo de normalização dos dados. Além disso, ainda precisamos transferir os dados do objeto de transposição para o nível dos dados originais. Por isso, nesta etapa, armazenamos os gradientes de erro obtidos em buffers auxiliares.

Na etapa seguinte, somamos os gradientes de erro recebidos pelos dois fluxos de informação no nível do objeto de transposição.

   if(!SumAndNormilize(cTranspose.getGradient(), cTranspose.getPrevOutput(), cTranspose.getGradient(),
                       iWindow, false, 0, 0, 0, 1))
      return false;

Os valores obtidos são passados para o nível dos dados originais, onde também somamos os valores dos dois fluxos de informação.

   if(!NeuronOCL.CalcHiddenGradients(cTranspose.AsObject()))
      return false;
   if(!SumAndNormilize(NeuronOCL.getGradient(), getPrevOutput(), NeuronOCL.getGradient(),
                       iCount, false, 0, 0, 0, 1))
      return false;
//---
   if(NeuronOCL.Activation() != None)
      if(!DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(),
                       NeuronOCL.getGradient(), NeuronOCL.Activation()))
         return false;
//---
   return true;
  }

Por fim, se uma função de ativação tiver sido aplicada aos dados originais, aplica-se sua derivada por meio do método DeActivation. Isso fecha a cadeia de propagação do erro, garantindo que o sinal de erro percorra, no sentido reverso, todas as etapas de transformação.

Aqui, vale destacar especialmente que todo o algoritmo implementado para a extração da componente espacial, com atenção e normalização, não contém parâmetros treináveis no sentido habitual, isto é, não há pesos, vieses nem parâmetros internos que precisem ser otimizados por descida de gradiente. Todos os cálculos se baseiam exclusivamente nos valores atuais dos dados originais, nas estatísticas da amostra e na função SoftMax, que gera a distribuição de atenção.

É exatamente por isso que o método updateInputWeights, responsável pela atualização dos parâmetros do modelo após cada propagação, neste caso, não é sobrescrito. Em vez disso, mantém-se a implementação vazia herdada da classe pai. Essa solução está alinhada à arquitetura geral do projeto: não aumenta a complexidade do modelo, não exige cálculos adicionais e reforça o caráter auxiliar e estrutural dessa camada no contexto do sistema de redes neurais como um todo.

O código completo desta classe e de todos os seus métodos pode ser consultado no anexo. Sua estrutura evidencia o cuidado no desenho arquitetural, bem como o alinhamento rigoroso com os componentes já implementados, o que torna o módulo fácil de escalar e reutilizar em esquemas de redes neurais mais complexos.


Módulo de regressão polinomial

O próximo componente que precisamos implementar é o módulo de regressão polinomial. Os autores do framework SSCNN ampliaram substancialmente a arquitetura do componente ao adicionar interações multiplicativas às conexões aditivas. Essa extensão permitiu modelar formas mais complexas e expressivas de interdependência entre os componentes de entrada do modelo.

Por que isso é necessário? As séries temporais financeiras raramente seguem padrões estritamente lineares ou puramente aditivos. Na prática, lidamos com um entrelaçamento de tendências, ciclos, sazonalidade e ruídos, entre os quais frequentemente surgem dependências complexas e não lineares. Mesmo com um grande número de graus de liberdade, o modelo clássico de regressão polinomial muitas vezes não é flexível o bastante para descrever bem esses efeitos.

Para modelar adequadamente interações de segunda ordem e até superiores, o sinal de entrada é processado em paralelo por dois caminhos:

  • o caminho aditivo, implementado por meio de uma camada convolucional, responsável por captar dependências lineares e fracamente não lineares entre atributos considerados de forma independente;
  • o caminho multiplicativo, representado por um bloco de camadas convolucionais. É nele que se implementa a ideia de modulação dos atributos dependente do contexto, o que permite considerar combinações não lineares e reforços ou enfraquecimentos mútuos das componentes.

Em nossa implementação, fomos ainda além, adicionando ao caminho multiplicativo a ativação SwiGLU, uma das não linearidades modernas e eficientes que ajudam a aumentar a capacidade de aprendizado do modelo sem crescimento significativo do número de parâmetros.

O objeto CNeuronPolynomialRegression implementa uma camada estendida de regressão polinomial e faz parte da pilha computacional do modelo neural voltado à previsão de séries temporais no ambiente OpenCL. Ele herda as principais interfaces da classe base CNeuronBaseOCL, o que garante compatibilidade com os demais componentes do framework sem necessidade de adaptação. A arquitetura dessa camada se baseia em princípios de composição: cada bloco interno executa uma tarefa bem definida, e a interação entre eles forma um sistema complexo, porém flexível, de processamento do sinal de entrada.

class CNeuronPolynomialRegression   :  public CNeuronBaseOCL
  {
protected:
   CNeuronSwiGLUOCL     cProjection;
   CNeuronConvOCL       cConvolution;
   CNeuronConvOCL       cResidual;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronPolynomialRegression(void) {};
                    ~CNeuronPolynomialRegression(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units_count, uint window, uint window_out,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      Save(const int file_handle) override;
   virtual bool      Load(const int file_handle) override;
   //---
   virtual int       Type(void) override const  {  return defNeuronPolynomialRegression; }
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   virtual uint      GetWindowIn(void) const { return cProjection.GetWindow(); }
   virtual uint      GetWindowOut(void) const { return cResidual.GetFilters(); }
   virtual uint      GetUnits(void) const { return cProjection.GetUnits(); }
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override { }
  };

A classe contém três componentes principais: o bloco de projeção cProjection, baseado no mecanismo SwiGLU, atua como o primeiro bloco de transformação dos dados. Ele expande os atributos, preparando-os para o processamento posterior. Além disso, são usadas duas camadas convolucionais, destinadas a capturar dependências residuais e multiplicativas. Essa separação permite combinar relações aditivas e polinomiais de segunda ordem ou superiores entre os atributos, ampliando a capacidade expressiva do modelo sem tornar a estrutura excessivamente complexa.

A estrutura interna da classe declara todos os componentes constituintes como membros estáticos. Isso significa que eles são criados uma única vez durante a inicialização e não exigem alocação dinâmica de memória nem controle manual do ciclo de vida. Essa abordagem simplifica a estrutura, reduz a sobrecarga e elimina a necessidade de implementar explicitamente o construtor e o destrutor da classe. Esses métodos permanecem vazios, reforçando a simplicidade e a autonomia da estrutura.

Toda a arquitetura interna é configurada no método Init, que, na prática, inicializa o grafo computacional da camada.

bool CNeuronPolynomialRegression::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                       uint units_count, uint window, uint window_out,
                                       ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, units_count * window_out,
                                                         optimization_type, batch))
      return false;
   activation = None;

Na primeira etapa, é chamado o método de inicialização da classe base. Essa chamada cria a base para a alocação de memória e a vinculação dos buffers computacionais. Depois disso, a função de ativação da camada é explicitamente definida como None, pois sua implementação está incorporada à estrutura dos blocos subordinados.

Em seguida, cada componente da camada é inicializado em sequência. O módulo cProjection é configurado como a primeira etapa de processamento do sinal: ele recebe os dados originais e expande sua representação conforme o número definido de unidades e o tamanho da janela. Sua configuração determina a vazão de processamento da camada e estabelece a base para a regressão subsequente.

   if(!cProjection.Init(0, 0, OpenCL, window, window, window_out, units_count, 1,
                                                            optimization, iBatch))
      return false;
   if(!cConvolution.Init(0, 1, OpenCL, window_out, window_out, window_out, units_count,
                                                              1, optimization, iBatch))
      return false;
   cConvolution.SetActivationFunction(None);

Na sequência, é inicializado cConvolution, responsável pelas dependências polinomiais. Ele recebe a saída do bloco de projeção, e suas dimensões correspondem à janela de resultados. A função de ativação desse componente é desativada, pois é usada uma convolução pura, sem distorcer o sinal.

A etapa final é o objeto cResidual. Ele também processa os dados originais, mas executa uma convolução convencional, sem multiplicação. Assim, a parte linear é modelada.

   if(!cResidual.Init(0, 2, OpenCL, window, window, window_out, units_count, 1, optimization, iBatch))
      return false;
   cResidual.SetActivationFunction(None);

Após a inicialização bem-sucedida dos três componentes, ocorre a sincronização dos buffers de gradientes entre as camadas. O gradiente de cConvolution é definido como principal para a camada atual e para cResidual. Isso garante uma direção única para o ajuste do modelo, sem cópias desnecessárias de dados.

   if(!SetGradient(cConvolution.getGradient(), true))
      return false;
   if(!cResidual.SetGradient(getGradient(), true))
      return false;
//---
   return true;
  }

Se todas as etapas de inicialização forem concluídas sem erros, o método retorna true, sinalizando que a camada está pronta para uso. Assim, toda a configuração fica estritamente centralizada, o que torna o comportamento do componente previsível, gerenciável e robusto contra erros em outras partes do sistema.

Concluída a inicialização, passamos ao coração dos cálculos: a propagação para frente implementada no método feedForward.

bool CNeuronPolynomialRegression::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cProjection.FeedForward(NeuronOCL))
      return false;
   if(!cConvolution.FeedForward(cProjection.AsObject()))
      return false;

Primeiro, delegamos o processamento dos dados originais ao módulo de projeção cProjection, passando a ele um ponteiro para o objeto da camada neural anterior. Se algo der errado nessa etapa, a função encerra imediatamente sua execução com o resultado false.

Se tudo ocorrer bem, o resultado da transformação SwiGLU segue para o bloco convolucional cConvolution, no qual é criada a contribuição quadrática. Aqui, novamente, verifica-se o sucesso da operação e, em caso de erro, o método interrompe a execução das etapas seguintes.

Em seguida, os mesmos dados originais são passados para o bloco linear cResidual, responsável por preservar a parte linear pura do sinal.

   if(!cResidual.FeedForward(NeuronOCL))
      return false;
   if(!SumAndNormilize(cConvolution.getOutput(), cResidual.getOutput(),
                       Output, GetWindowOut(), true, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

Quando os três componentes concluem sua execução com sucesso, suas saídas são passadas para a função SumAndNormilize, que soma os ramos elemento a elemento e normaliza o resultado pelo tamanho da janela. Os valores obtidos são armazenados no buffer de resultados da interface externa. Assim, sem pausas desnecessárias, cada etapa se encadeia à próxima, garantindo o funcionamento coeso e confiável da camada polinomial.

Após a propagação para frente bem-sucedida, começa a retropropagação do erro no método calcInputGradients.

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

Primeiro, é verificada a validade do ponteiro para o objeto dos dados originais. Se ele estiver vazio, a função apenas retorna false, sem executar nenhuma ação adicional.

Em seguida, o gradiente atual do bloco convolucional é passado para o módulo de projeção.

   if(!cProjection.CalcHiddenGradients(cConvolution.AsObject()))
      return false;
//---
   if(!NeuronOCL.CalcHiddenGradients(cProjection.AsObject()))
      return false;

Depois, o método acessa diretamente o próprio objeto dos dados originais, chamando o método CalcHiddenGradients, o que permite propagar os gradientes até o nível correspondente.

Assim que essa parte é concluída, passamos à propagação do gradiente de erro pelo segundo fluxo de informação. Antes disso, porém, armazenamos o ponteiro para o buffer atual em uma variável temporária temp, para não perder os valores acumulados. Para processar a componente residual (linear), substituímos o ponteiro para o buffer de gradientes no objeto dos dados originais por um buffer livre e, só então, chamamos novamente o método CalcHiddenGradients, propagando a parte linear do gradiente de erro.

   CBufferFloat* temp = NeuronOCL.getGradient();
   if(!NeuronOCL.SetGradient(NeuronOCL.getPrevOutput(), false) ||
      !NeuronOCL.CalcHiddenGradients(cResidual.AsObject()) ||
      !SumAndNormilize(temp, NeuronOCL.getGradient(), temp,
                       GetWindowIn(), false, 0, 0, 0, 1) ||
      !NeuronOCL.SetGradient(temp, false))
      return false;
//---
   return true;
  }

Depois disso, somamos os dois fluxos de gradientes. Por fim, o gradiente combinado é novamente definido no neurônio por meio do método SetGradient. Se nenhuma das operações falhar, o método termina retornando true, e o conjunto de fluxos de gradientes percorre com sucesso o caminho reverso, garantindo o ajuste correto dos pesos na camada polinomial.

Agora que os gradientes foram reunidos e já podem ser aplicados, avançamos de forma natural para a etapa de atualização dos pesos no método updateInputWeights. Aqui, cada componente volta a operar em sequência: primeiro, o módulo de projeção cProjection ajusta seus parâmetros internos; depois, a execução passa para o bloco cConvolution.

bool CNeuronPolynomialRegression::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cProjection.UpdateInputWeights(NeuronOCL))
      return false;
   if(!cConvolution.UpdateInputWeights(cProjection.AsObject()))
      return false;
   if(!cResidual.UpdateInputWeights(NeuronOCL))
      return false;
//---
   return true;
  }

Por fim, o bloco linear cResidual encerra a cadeia, atualizando seus pesos com base nos dados originais e nos gradientes de erro obtidos anteriormente. Se ocorrer uma falha em qualquer uma dessas três etapas, o método retorna false imediatamente. Mas, quando cada ajuste é executado sem erros, updateInputWeights é concluído sem erros, retornando true e garantindo que todos os componentes de CNeuronPolynomialRegression estejam sincronizados e prontos para o próximo ciclo de treinamento.

O código completo da classe e de todos os seus métodos é apresentado no anexo.

Hoje, demos um passo importante: três componentes principais do framework SSCNN foram criados com sucesso. Agora é hora de fazer uma breve pausa, para que os novos conhecimentos e observações se consolidem com calma. Depois desse descanso, passaremos à próxima etapa com mais clareza e energia renovada.

No próximo artigo, conectaremos todos os módulos em uma única cadeia computacional e os submeteremos a um verdadeiro teste prático: executaremos o algoritmo com dados históricos de mercado para avaliar sua estabilidade e precisão. Nesse cenário mais próximo da realidade, verificaremos até que ponto nossas soluções são elegantes e eficazes. E, é claro, tiraremos conclusões que ajudarão a tornar a SSCNN ainda mais confiável e eficiente.


Conclusão

A transição da descrição conceitual do framework SSCNN para sua implementação demonstrou o quanto a estruturação e a consistência são importantes em todos os níveis da construção de modelos voltados à análise de séries temporais de mercado. Os autores do framework abriram mão conscientemente do escalonamento como abordagem universal, em favor da decomposição e da modularidade: um princípio que permite preservar a interpretabilidade, a adaptabilidade e a estabilidade do modelo mesmo com alto ruído de mercado.

A estrutura CNeuronAttentNorm tornou-se a síntese da nossa abordagem: ela combina mecanismos de atenção e normalização semanticamente ricos, implementados na GPU e ajustados ao contexto do segmento, e as particularidades dos sinais financeiros. A abordagem de inicialização, transferência e sincronização dos dados entre CPU e GPU foi projetada para evitar os gargalos típicos associados à computação paralela em OpenCL.

Dedicamos atenção especial ao treinamento dos parâmetros de atenção, permitindo que o modelo se concentre seletivamente nos trechos mais relevantes do sinal analisado. Isso não apenas aumenta a precisão da previsão, mas também representa um passo em direção a uma generalização mais interpretável, na qual cada componente do modelo, da decomposição estrutural à normalização, atua em sintonia com o contexto.

No próximo artigo, integraremos todos os componentes em um único algoritmo e realizaremos um teste de campo com dados financeiros históricos. Isso permitirá avaliar a estabilidade e a precisão da SSCNN em condições reais.

Links


Programas usados no artigo

# Nome Tipo Descrição
1 Study.mq5 Expert Advisor EA de treinamento offline de modelos
2 StudyOnline.mq5 Expert Advisor EA de treinamento online de modelos
3 Test.mq5 Expert Advisor EA para teste do modelo
4 Trajectory.mqh Biblioteca de classe Estrutura de descrição do estado do sistema e da arquitetura dos modelos
5 NeuroNet.mqh Biblioteca de classe Biblioteca de classes para criação de uma rede neural
6 NeuroNet.cl Biblioteca Biblioteca de código do programa OpenCL

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

Arquivos anexados |
MQL5.zip (2963.02 KB)
Aprendendo MQL5 do iniciante ao profissional (Parte VII): Princípios de depuração de aplicativos MQL Aprendendo MQL5 do iniciante ao profissional (Parte VII): Princípios de depuração de aplicativos MQL
A correção de erros é uma parte indispensável do ciclo de programação. Neste artigo, veremos técnicas comuns de depuração, que é o processo de correção de erros, em qualquer aplicativo executado no ambiente MetaTrader 5.
Implementação do circuito quântico de Quantum Reservoir Computing (QRC) Implementação do circuito quântico de Quantum Reservoir Computing (QRC)
Trata-se de uma abordagem revolucionária do aprendizado de máquina aplicado ao trading por meio da computação quântica. O artigo descreve a aplicação prática de um sistema QRC adaptativo com ajuste contínuo incremental para prever movimentos do mercado em tempo real.
Está chegando o novo MetaTrader 5 e MQL5 Está chegando o novo MetaTrader 5 e MQL5
Esta é apenas uma breve resenha do MetaTrader 5. Eu não posso descrever todos os novos recursos do sistema por um período tão curto de tempo - os testes começaram em 09.09.2009. Esta é uma data simbólica, e tenho certeza que será um número de sorte. Alguns dias passaram-se desde que eu obtive a versão beta do terminal MetaTrader 5 e MQL5. Eu ainda não consegui testar todos os seus recursos, mas já estou impressionado.
Robô de trading baseado em redes neurais com arquitetura Mamba e SSM seletivo Robô de trading baseado em redes neurais com arquitetura Mamba e SSM seletivo
Este artigo analisa a revolucionária arquitetura de rede neural Mamba/SSM para a previsão de séries temporais financeiras. Ele apresenta uma implementação completa em MQL5 de uma alternativa moderna ao Transformer, que possui complexidade linear O(N) em vez de quadrática O(N²). Além disso, o texto examina detalhadamente os modelos de espaço de estado seletivos, as otimizações orientadas ao hardware, as técnicas de patching e os métodos avançados de treinamento com AdamW. O artigo inclui resultados práticos de testes que mostram um aumento da precisão de 62% para 71% e uma redução do tempo de treinamento de 45 para 8 minutos. Também é apresentado um Expert Advisor pronto para uso, com treinamento automático e gestão de risco adaptativa para MetaTrader 5.