Redes neurais em trading: Previsão probabilística de séries temporais (Codificador)
Introdução
Damos continuidade ao estudo do framework K²VAE, uma arquitetura avançada desenvolvida especialmente para modelagem de séries temporais em condições de alta incerteza. Esse modelo é construído a partir da síntese de três ideias centrais: dinâmica linear no espaço latente (por meio dos operadores de Koopman), filtragem adaptativa de erros (com a ajuda do KalmanNet) e modelagem probabilística (com base no autocodificador variacional). Essa abordagem permite considerar ao mesmo tempo tanto os padrões presentes nos dados quanto o grau de confiança nesses padrões.
A principal vantagem do K²VAE não está apenas em gerar uma previsão, mas em formar uma distribuição probabilística dos estados futuros do sistema. Diferentemente dos modelos tradicionais, que se limitam a uma única versão mais provável de como os eventos irão evoluir, aqui o resultado é um intervalo de desfechos possíveis. Além disso, a amplitude desse intervalo depende do grau de confiança do modelo no estado atual. Isso torna o framework especialmente útil em áreas em que é importante considerar riscos e incertezas, por exemplo, previsão financeira, logística ou controle de sistemas técnicos.
Para entender como essa flexibilidade e essa adaptabilidade são alcançadas, vamos analisar a arquitetura geral do modelo. A estrutura do K²VAE pode ser dividida, de forma condicional, em três grandes componentes: Patching, Codificador e Decodificador, cada um com sua própria função, mas ao mesmo tempo estreitamente ligado aos demais.
- Patching prepara os dados brutos e os converte em uma representação latente.
- Codificador responde pela extração do estado oculto Z a partir das séries temporais observadas X. Diferentemente dos VAE padrão, aqui é usada uma estrutura complexa, composta por:
- KoopmanNet, um análogo treinável do operador de Koopman, que prevê a evolução das características ocultas como um sistema linear;
- Módulo de atenção, que analisa as diferenças entre os valores reconstruídos e os reais, permitindo identificar os momentos em que o modelo diverge da realidade;
- KalmanNet, uma implementação híbrida em rede neural do filtro de Kalman, que forma a matriz de covariância da incerteza com base nos sinais de controle da atenção;
- Mecanismo VAE, que realiza a amostragem de tokens futuros com base nos parâmetros definidos pelo KalmanNet e pelo KoopmanNet.
- Decodificador converte as variáveis ocultas de volta em variáveis observáveis, reconstruindo os valores previstos da série temporal. Ao mesmo tempo, para preservar a natureza probabilística do modelo, o Decodificador também foi implementado como uma estrutura treinável de rede neural com duas saídas: valor médio e variância. Isso permite modelar de forma completa a distribuição P(Y|Z) e considerar a incerteza da previsão.
A visualização autoral do framework K²VAE é apresentada abaixo.

No artigo anterior, nós nos concentramos no trabalho preparatório: implementamos a infraestrutura básica, garantimos suporte ao uso repetido de matrizes treináveis e estabelecemos a base para uma arquitetura correta e escalável. Criamos classes universais para geração de parâmetros, ajustamos o mecanismo de atualização desses parâmetros por meio dos procedimentos padrão de propagação reversa do erro, e descrevemos uma estrutura adequada para a expansão futura dos componentes. Assim, colocamos o fundamento sobre o qual agora é possível construir módulos mais complexos.
O próximo passo lógico é desenvolver o Codificador, que cumpre uma função crítica, pois transforma as séries temporais originais em uma representação latente adequada para análise linear e modelagem probabilística. Isso não é implementado por meio de um simples conjunto de camadas, mas com uma estrutura cuidadosamente elaborada.
Discussão das abordagens de construção
Antes de passar à implementação prática do Codificador com os recursos da linguagem MQL5, vamos entender em detalhes como sua arquitetura se estrutura e qual sua lógica de construção. Com isso não apenas nos orientaremos corretamente no trabalho que temos pela frente, mas também compreenderemos como cada módulo contribui para o comportamento geral do modelo.
O Codificador usado no framework K²VAE é construído com base em um princípio modular e inclui quatro componentes interligados, cada um desempenhando uma função rigorosamente definida no processo de tratamento da série temporal.
O primeiro elemento da cadeia de processamento é o KoopmanNet, uma interpretação em rede neural do operador de Koopman, cuja principal tarefa é aproximar a dinâmica linear em uma série temporal. Ele recebe como entrada o estado oculto e retorna a previsão do próximo estado, assumindo que o comportamento do sistema pode ser representado como um deslocamento linear em algum espaço latente. No entanto, como as séries temporais reais raramente seguem uma trajetória estritamente linear, a simples projeção no espaço latente do KoopmanNet não é suficiente para uma modelagem confiável. Para que o modelo consiga não apenas reproduzir a tendência, mas também avaliar sua própria precisão, os autores do framework propuseram um mecanismo bastante original.
Além de prever o próximo estado, o KoopmanNet também é treinado para reconstruir transições já ocorridas, em essência, reconstruir retrospectivamente a trajetória a partir dos valores anteriores da série temporal. Dessa forma, o modelo não se limita ao movimento para frente, mas também olha para trás, verificando o quanto seus parâmetros atuais são adequados às observações passadas. Esse raciocínio bidirecional permite ao modelo calcular a diferença entre a dinâmica presumida e os estados reais do sistema.
É justamente esse desvio, a diferença entre as transições calculadas pelo KoopmanNet e os valores históricos reais, que serve de base para avaliar a qualidade da aproximação linear. Assim, o modelo passa a ter capacidade de autodiagnóstico e pode se ajustar dinamicamente a mudanças ou instabilidades, sem perder a ligação com a estrutura linear que está em sua base.
Na implementação original do framework K²VAE módulo KoopmanNet é composto por dois pequenos modelos fully connected (MLP) separados, cada um modelando seu próprio aspecto das transições no espaço oculto: um responde pelas dependências globais, o outro, pelas locais. Essa abordagem é bastante lógica do ponto de vista teórico, mas na prática pode se mostrar rígida e limitada em excesso, especialmente em condições de dados de mercado instáveis.
Propomos reforçar o KoopmanNet com o uso de um bloco mais universal e flexível de mistura esparsa de especialistas, o CNeuronTimeMoESparseExperts. Esse é um módulo do framework TimeMoE que implementa o princípio Mixture of Experts (MoE), um mecanismo em que várias submetas especializadas (especialistas) trabalham em paralelo, enquanto um mecanismo de controle escolhe a mais adequada conforme o contexto original.
O bloco de mistura esparsa de especialistas não é apenas uma implementação técnica, mas a base lógica para separar os padrões locais e globais presentes nas séries temporais financeiras. Dentro desse bloco operam dois tipos de especialistas. O primeiro é um grupo de especialistas locais especializados, especialistas locais, cada um responsável por reconhecer transições de curto prazo e padrões dependentes do contexto. Esses especialistas, em essência, analisam o comportamento do mercado em intervalos de tempo limitados, adaptando-se à volatilidade atual e às microtendências.
O segundo é o especialista global. Ele está incorporado à estrutura CNeuronTimeMoESparseExperts e cumpre o papel de operador de Koopman linear generalizador. Sua tarefa é capturar a dinâmica estável e de longo prazo da série temporal, isolando os padrões que se mantêm ao longo de toda a janela de treinamento. Dessa forma, o modelo passa a ter uma visão dupla dos dados: uma análise local detalhada e uma perspectiva estratégica ampla.
Essa arquitetura permite ao KoopmanNet não apenas prever o próximo estado, mas também reconstruir a trajetória anterior, reconstruindo a série de transições que levaram à posição atual. A comparação entre as transições reconstruídas e os valores reais dá ao sistema a possibilidade de avaliar a precisão da sua própria aproximação.
Depois que o KoopmanNet conclui a reconstrução da dinâmica e prevê o próximo passo, entra em ação o módulo de atenção, cuja tarefa não é apenas comparar os valores reais e os modelados, mas analisar diretamente os desvios entre eles. Diferentemente do mecanismo tradicional de atenção cruzada, em que dois fluxos de dados são comparados, aqui a atenção se concentra exclusivamente no tensor de erros, identificando nele estruturas e padrões recorrentes.
Os autores do framework K²VAE propõem tratar esses desvios não como um subproduto, mas como uma fonte completa de informação. A ideia é que os próprios erros contêm um sinal: eles indicam onde e em que medida o KoopmanNet consegue descrever o sistema, e quais características locais da dinâmica permanecem fora do alcance do modelo linear. A atenção permite extrair desses erros um tensor de sinais de controle, uma espécie de instrução para ajustar as previsões na etapa seguinte.
Dessa forma, em vez de simplesmente comparar previsões com os fatos, o modelo aprende a interpretar de forma significativa as próprias limitações. E é justamente essa abordagem que está na base da alta adaptabilidade e inteligência do K²VAE, permitindo que ele trabalhe com eficiência em um ambiente instável e em constante mudança.
No contexto da implementação proposta, podemos usar um dos módulos de Self-Attention já testados na prática, anteriormente aplicados com sucesso à análise de séries temporais. Isso não apenas acelera consideravelmente o processo de desenvolvimento, mas também garante compatibilidade com os demais componentes do framework. Esse módulo é capaz de identificar padrões internos na estrutura dos erros, captando padrões recorrentes e dependências locais entre desvios em diferentes trechos temporais.
É importante destacar que, neste contexto, o Self-Attention não é aplicado ao processamento da sequência original de dados de entrada, como normalmente acontece nos transformers clássicos, mas à análise dos erros de reconstrução que surgem ao restaurar a dinâmica no KoopmanNet. Na prática, deslocamos o foco de atenção do modelo dos próprios dados para o resultado do seu próprio funcionamento, permitindo que o sistema se avalie de fora e extraia informação útil dos erros.
No entanto, ao implementar essa abordagem, surge uma nuance interessante. A profundidade do histórico analisado e o horizonte de planejamento para o qual os sinais de controle são formados podem ter tamanhos diferentes. Na versão original do framework, esse problema é resolvido por meio de uma projeção linear, uma transformação compacta que permite adaptar o vetor de controle ao horizonte de planejamento necessário.
No nosso caso, a situação é um pouco mais simples. Como o modelo está voltado à geração de apenas um próximo token, não precisamos de uma sequência de sinais de controle, mas de apenas um vetor. Claro, poderíamos usar a mesma projeção linear. Mas propomos seguir por outro caminho, focando no erro do último token no contexto de toda a cadeia anterior de desvios.
Essa abordagem abre novas possibilidades. Em vez de tratar o erro como um valor isolado, nós o analisamos em conjunto com o histórico acumulado de imprecisões. Isso permite que o mecanismo de atenção determine quais fragmentos da dinâmica passada influenciaram com mais força o erro atual. O resultado é um vetor de controle que não apenas reage à imprecisão atual, mas reflete suas causas, codificadas nos estados anteriores do sistema.
Esse método tem uma série de vantagens evidentes:
- concentração direcionada da atenção: o modelo se concentra no erro do último passo, em vez de distribuir recursos por toda a trajetória;
- redução da carga computacional: não há necessidade de formar vetores longos para cada passo futuro;
- melhor interpretabilidade: o sinal de controle se torna significativo e adequado para análise visual e diagnóstico.
No fim, obtemos um mecanismo flexível de controle interno, incorporado ao Codificador, que permite ao modelo se adaptar aos próprios pontos fracos e formar com mais confiança a distribuição para o próximo passo por meio do KalmanNet e do VAE. Essa abordagem torna a arquitetura não apenas mais robusta, como também transforma seu comportamento em algo mais transparente, o que é especialmente importante ao trabalhar em condições de incerteza de mercado.
O próximo elo na arquitetura do Codificador é o KalmanNet, um módulo que desempenha o papel de gerador adaptativo da matriz de covariância. Sua tarefa não é apenas repassar passivamente a informação adiante, mas avaliar ativamente o grau de confiança na previsão linear recebida do KoopmanNet. O sinal de controle vindo do bloco de atenção se torna uma espécie de indicador de confiabilidade do modelo anterior. Se os desvios forem pequenos e tiverem um padrão estável, o KalmanNet interpreta isso como alta confiança e, consequentemente, estreita a distribuição de covariância, assumindo que o comportamento seguinte da série temporal provavelmente seguirá o cenário previsto. Mas, se for observado um desalinhamento significativo ou mudanças estruturais nos erros, o modelo, ao contrário, amplia a matriz de covariância, abrindo espaço para resultados mais variados e probabilísticos.
Dessa forma, o KalmanNet atua, na prática, como um indicador de confiança, uma espécie de barômetro que reage de forma sensível à turbulência dentro dos dados. Ele não apenas transmite sinais, mas os interpreta, transformando oscilações invisíveis em uma forma matemática adequada para uso posterior.
O bloco de amostragem variacional (VAE) conclui o trabalho do Codificador. É justamente aqui que se forma a representação latente final do estado futuro. Ao mesmo tempo, a amostragem não é feita às cegas, ela segue a lógica estabelecida nas etapas anteriores. Os valores médios são obtidos do KoopmanNet, como base da dinâmica presumida, enquanto o grau de dispersão é definido pela matriz de covariância gerada pelo KalmanNet. Como resultado, obtemos não uma previsão pontual, mas um modelo probabilístico que considera tanto a estrutura linear da série temporal quanto a natureza estocástica do mercado.
Essa abordagem tem uma vantagem estratégica evidente. Ela permite que o modelo preserve a flexibilidade em condições de incerteza, sem ficar preso a um único cenário de evolução, ajustando sua confiança de acordo com o contexto. Isso faz com que o framework seja especialmente apropriado quando não é tão importante prever estados futuros, mas sim ter a capacidade de tomar decisões dinâmicas com base em avaliações de risco e confiança.
Para transmitir o grau de incerteza e o nível dos riscos possíveis, em nossa implementação não nos limitamos a uma única previsão. Em vez disso, amostramos uma quantidade definida de cenários possíveis para a evolução da série temporal no próximo passo. Cada um desses cenários é uma trajetória separada, gerada com base na distribuição geral formada pelo KoopmanNet e pelo KalmanNet. Essa abordagem permite não apenas obter uma estimativa média, mas abranger todo um espectro de resultados prováveis, revelando assim o quanto o modelo se sente seguro em relação ao futuro.
No conjunto, a interação entre o KoopmanNet, o bloco de atenção, o KalmanNet e o VAE forma uma estrutura complexa, mas surpreendentemente lógica, em que cada componente desempenha seu papel na construção de uma representação confiável e adaptativa do futuro.
Estrutura do objeto
Depois de uma discussão detalhada dos princípios arquiteturais e da lógica de interação entre os componentes dentro do Codificador K²VAE, é hora de passar à análise de sua implementação prática. Com o objetivo de garantir modularidade, flexibilidade e clareza do código, reunimos todos os elementos-chave dentro de um único objeto da classe CNeuronK2VAEEncoder, que herda a funcionalidade básica de CNeuronBaseOCL.
Esse objeto reúne tudo o que é necessário para o ciclo completo de funcionamento do Codificador, desde a análise da dinâmica de séries temporais com a ajuda do KoopmanNet até a construção da estrutura de covariância dos estados futuros no KalmanNet e a amostragem de previsões probabilísticas por meio do mecanismo VAE. Dentro do objeto estão representados tanto os componentes especializados da rede de Koopman, o bloco de atenção e o filtro de Kalman, quanto as camadas técnicas para transformações, operações lineares e cálculos matriciais.
Abaixo está apresentada a estrutura da classe CNeuronK2VAEEncoder, em que cada componente implementa uma função específica dentro do processo geral de codificação da sequência observada. Essa implementação torna possível a análise e a depuração em etapas em qualquer nível de profundidade do modelo, além de oferecer amplas possibilidades de escalabilidade ao trabalhar com diferentes configurações arquiteturais e profundidades de análise.
class CNeuronK2VAEEncoder : public CNeuronBaseOCL { protected: //--- Koopman CNeuronTimeMoESparseExperts cKoopman; CNeuronBaseOCL cKoopmanPred; CNeuronBaseOCL cKoopmanRest; //--- CNeuronTimeMoEAttention cAuxiliaryNet; //--- Kalman Filter CParams cB; // Control input matrix B CParams cF; // State transition matrix F CParams cH; // Observation matrix H CParams cQ; // Learnable covariance matrices Q CParams cR; // Learnable covariance matrices R CNeuronBaseOCL cP; // Covariance matrices P //--- CNeuronTransposeOCL cFT; CNeuronTransposeOCL cHT; CNeuronTransposeOCL cQT; CNeuronTransposeOCL cRT; CNeuronTransposeOCL cPT; //--- CNeuronBaseOCL cQ_QT; CNeuronBaseOCL cR_RT; CNeuronBaseOCL cXPred; CNeuronBaseOCL cF_P; CNeuronBaseOCL cPPred; //--- CNeuronBaseOCL cP_HT; CNeuronBaseOCL cH_P_HT; matrix<double> mS, mSGrad; CNeuronBaseOCL cSInv; CNeuronBaseOCL cK; CNeuronTransposeOCL cKT; CNeuronBaseOCL cYPred; CNeuronBaseOCL cDeltY; CNeuronBaseOCL cX; CNeuronBaseOCL cK_H; CNeuronBaseOCL cIdifK_H; CNeuronTransposeOCL cIdifK_HT; matrix<double> mP; matrix<double> mPGrad; matrix<double> mNoise; matrix<double> mGrad; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronK2VAEEncoder(void) {}; ~CNeuronK2VAEEncoder(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_cross, uint heads, uint layers, uint scenarios, uint experts, uint experts_dimension, uint topK, 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) const { return defNeuronK2VAEEncoder; } virtual void TrainMode(bool flag) override; virtual void SetOpenCL(COpenCLMy *obj); //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau); };
Na estrutura apresentada da nova classe, atenção especial deve ser dada aos componentes-chave que refletem a lógica de toda a arquitetura do Codificador. Como KoopmanNet, é usado o poderoso módulo CNeuronTimeMoESparseExperts, que combina a flexibilidade local da mistura esparsa de especialistas com a robustez de um preditor global. Essa solução permite modelar com eficiência tanto dinâmicas de curto prazo quanto dinâmicas estáveis em séries temporais.
No papel de módulo de atenção, é usado o CNeuronTimeMoEAttention, responsável por analisar a estrutura dos erros de reconstrução. Ele identifica padrões nos desvios, formando características de controle para avaliar a confiança nas previsões do KoopmanNet.
Um lugar especial na implementação é ocupado pelo bloco do filtro de Kalman. Aqui vemos as matrizes treináveis B, F, H, Q e R, cada uma delas apresentada na forma de uma instância separada da classe CParams. Essa abordagem permite não apenas gerenciar de forma centralizada os parâmetros do filtro, mas também garantir sua adaptação completa durante o treinamento.
Além desses componentes, a estrutura da classe também contém vários objetos auxiliares, que garantem a implementação correta da lógica passo a passo do KalmanNet e permitem gerenciar com eficiência todas as operações com tensores dentro do Codificador. A funcionalidade dos objetos auxiliares será analisada em mais detalhes à medida que os métodos da classe forem sendo implementados.
A estrutura interna da classe CNeuronK2VAEEncoder pressupõe a declaração estática de todos os componentes utilizados. Essa decisão permite simplificar o ciclo de vida do objeto: não precisamos realizar alocação dinâmica nem liberar memória manualmente. Como consequência, o construtor e o destrutor da classe podem permanecer vazios, sem sobrecarregar o código com lógica desnecessária.
Toda a configuração da arquitetura, incluindo a configuração do KoopmanNet, dos parâmetros de atenção e do filtro de Kalman, fica totalmente concentrada no método Init. É justamente aqui que o objeto recebe sua forma concreta: são configurados os parâmetros de atenção, definidos os tamanhos das características de controle, o número de especialistas, a profundidade da janela de entrada e outras características críticas do modelo.
bool CNeuronK2VAEEncoder::Init(uint numOutputs, uint myIndex, COpenCLMy * open_cl, uint window, uint window_key, uint units_cross, uint heads, uint layers, uint scenarios, uint experts, uint experts_dimension, uint topK, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * scenarios, optimization_type, batch)) return false;
Na primeira etapa, o controle é transferido para a função de mesmo nome da classe base. Isso permite aproveitar os mecanismos já implementados de validação básica dos parâmetros de entrada, além de executar a inicialização das interfaces herdadas e dos componentes comuns. Essa abordagem garante um padrão único de inicialização para todas as camadas neurais, facilitando a manutenção e a expansão da arquitetura.
Merece atenção especial o ponto relacionado à gestão da memória. Como, na nossa implementação, o codificador K²VAE retorna não um único vetor, mas um conjunto de cenários futuros possíveis, formados por amostragem, o buffer de resultados precisa ter o volume correspondente.
Em seguida, passamos à inicialização do bloco KoopmanNet, que na nossa implementação é representado pelo módulo CNeuronTimeMoESparseExperts. Inicializamos o módulo de mistura esparsa de especialistas, definindo o número de modelos e sua dimensionalidade. Esse componente responde pela extração tanto de padrões locais quanto globais da série temporal, imitando o funcionamento dos operadores de Koopman.
//--- Koopman int index = 0; if(!cKoopman.Init(0, index, OpenCL, window, 2 * window, units_cross, 1, experts, topK, optimization, iBatch)) return false; index++; if(!cKoopmanPred.Init(0, index, OpenCL, window, optimization, iBatch)) return false; index++; if(!cKoopmanRest.Init(0, index, OpenCL, window * (units_cross - 1), optimization, iBatch)) return false;
Depois disso, inicializamos dois objetos auxiliares, cKoopmanPred e cKoopmanRest. O primeiro deles será usado para armazenar os valores previstos calculados pelo KoopmanNet, o segundo, para reconstruir estados já observados da série temporal.
O passo seguinte é a inicialização do módulo de atenção, que em nossa implementação é representado pelo objeto cAuxiliaryNet.
index++; if(!cAuxiliaryNet.Init(0, index, OpenCL, window, window_key, 1, window, units_cross - 1, heads, layers, optimization, iBatch)) return false;
No entanto, a parte central da arquitetura interna da nova classe está concentrada na lógica de funcionamento do filtro de Kalman, é justamente aqui que se encontra a maior parte dos cálculos e o maior volume de lógica implementada no método Init.
Na primeira etapa, inicializamos em sequência as matrizes quadradas dos parâmetros treináveis do filtro: são as matrizes de transição de estado F, de impactos de controle B, de observação H, além das matrizes de covariância dos erros do modelo Q e das observações R.
//--- Kalman Filter index++; if(!cB.Init(0, index, OpenCL, window * window, optimization, iBatch) || !cB.Identity(window, window)) return false; index++; if(!cF.Init(0, index, OpenCL, window * window, optimization, iBatch) || !cF.Identity(window, window)) return false; index++; if(!cH.Init(0, index, OpenCL, window * window, optimization, iBatch) || !cH.Identity(window, window)) return false; index++; if(!cQ.Init(0, index, OpenCL, window * window, optimization, iBatch) || !cQ.Identity(window, window)) return false; index++; if(!cR.Init(0, index, OpenCL, window * window, optimization, iBatch) || !cR.Identity(window, window)) return false; index++; if(!cP.Init(0, index, OpenCL, window * window, optimization, iBatch) || !cP.getOutput().Fill(matrix<float>::Identity(window, window))) return false;
Durante a inicialização de cada uma dessas matrizes, suas dimensões são definidas. Em seguida, para garantir a estabilidade numérica do filtro de Kalman nas iterações iniciais do treinamento, todas essas matrizes são preenchidas com valores diagonais. Essa inicialização permite garantir a definição positiva das matrizes de covariância e evitar instabilidade nos cálculos ligados à inversão da matriz S durante o processo de correção das previsões.
A abordagem com inicialização diagonal não apenas estabiliza as etapas iniciais do treinamento, mas também garante uma sensibilidade uniforme do modelo aos diferentes componentes do estado latente, o que é criticamente importante ao trabalhar com séries temporais em que tendências dominantes podem mascarar sinais fracos, porém relevantes.
A etapa seguinte passa a ser a preparação dos objetos responsáveis pela transposição das matrizes correspondentes. Esse é um ponto importante e, à primeira vista, técnico, mas ele tem relação direta com o funcionamento correto de todo o algoritmo. O fato é que, ao longo dos cálculos do filtro de Kalman, repetidamente precisamos acessar não apenas as próprias matrizes F, H, Q, R e P, mas também suas versões transpostas. Para garantir isso, alocamos e inicializamos previamente os objetos de transposição correspondentes.
index++; if(!cFT.Init(0, index, OpenCL, window, window, optimization, iBatch)) return false; index++; if(!cHT.Init(0, index, OpenCL, window, window, optimization, iBatch)) return false; index++; if(!cQT.Init(0, index, OpenCL, window, window, optimization, iBatch)) return false; index++; if(!cRT.Init(0, index, OpenCL, window, window, optimization, iBatch)) return false; index++; if(!cPT.Init(0, index, OpenCL, window, window, optimization, iBatch)) return false;
Dessa forma, implementa-se uma espécie de bufferização das formas transpostas, o que acelera significativamente os cálculos e simplifica a estrutura do código na parte principal do algoritmo de filtragem. Além disso, isso aumenta a modularidade da arquitetura, pois permite trabalhar com cada elemento do filtro de forma independente, sem quebrar a lógica geral de execução.
Vale dar atenção especial às matrizes de covariância do ruído, Q (covariância do processo) e R (covariância das observações). Para o funcionamento correto do algoritmo do filtro de Kalman, essas matrizes precisam ter duas propriedades-chave: serem simétricas na diagonal e definidas positivas. A violação dessas condições pode levar à instabilidade da filtragem, ao surgimento de valores imaginários na inversão de matrizes ou até à degradação completa da estimativa de estado.
Para garantir essas propriedades durante o treinamento, aplicamos uma técnica consagrada e amplamente usada: representamos as matrizes Q e R como o produto das próprias matrizes por sua cópia transposta. Essa representação torna automaticamente as matrizes resultantes simétricas e definidas positivas, desde que a matriz original não seja degenerada. Isso nos livra da necessidade de controlar manualmente os valores na diagonal ou introduzir regularização.
index++; if(!cQ_QT.Init(0, index, OpenCL, window * window, optimization, iBatch)) return false; index++; if(!cR_RT.Init(0, index, OpenCL, window * window, optimization, iBatch)) return false;
A etapa seguinte da configuração do Codificador é a inicialização dos objetos destinados a armazenar os resultados intermediários dos cálculos. Esses objetos desempenham um papel central na implementação passo a passo do algoritmo do filtro de Kalman, em que cada cálculo se baseia nos resultados das operações anteriores. Além disso, também vamos usar esses valores no backward pass, com o objetivo de distribuir corretamente o gradiente do erro.
Começamos com a inicialização do objeto cXPred, que armazenará o estado previsto do sistema antes da aplicação da etapa de correção.
index++; if(!cXPred.Init(0, index, OpenCL, window, optimization, iBatch)) return false; index++; if(!cF_P.Init(0, index, OpenCL, window * window, optimization, iBatch)) return false; index++; if(!cPPred.Init(0, index, OpenCL, window * window, optimization, iBatch)) return false;
Em seguida, inicializamos os objetos cF_P e cPPred, nos quais são armazenados, respectivamente, o produto da matriz de transição pela matriz de covariância e a covariância prevista. Esses dados são necessários para as operações seguintes de avaliação da confiabilidade da previsão realizada.
Depois disso, são configurados em sequência os blocos cP_HT e cH_P_HT, responsáveis pelo cálculo dos valores intermediários no cálculo da matriz S, a covariância do erro da previsão das observações.
index++; if(!cP_HT.Init(0, index, OpenCL, window * window, optimization, iBatch)) return false; index++; if(!cH_P_HT.Init(0, index, OpenCL, window * window, optimization, iBatch)) return false; //--- mS = mSGrad = matrix<double>::Zeros(window, window);
Em paralelo, também são criadas e inicializadas as matrizes mS e mSGrad, que serão usadas para armazenar os próprios valores de covariância e os gradientes correspondentes durante o treinamento.
O objeto cSInv é destinado ao armazenamento da matriz inversa de S, necessária para o cálculo da matriz de Kalman cK, que também é acompanhada de sua própria versão transposta cKT.
index++; if(!cSInv.Init(0, index, OpenCL, window * window, optimization, iBatch)) return false; index++; if(!cK.Init(0, index, OpenCL, window * window, optimization, iBatch)) return false; index++; if(!cKT.Init(0, index, OpenCL, window, window, optimization, iBatch)) return false; //--- index++; if(!cYPred.Init(0, index, OpenCL, window, optimization, iBatch)) return false; //--- index++; if(!cDeltY.Init(0, index, OpenCL, window, optimization, iBatch)) return false;
Em seguida, vem a inicialização do bloco cYPred, previsão dos valores observáveis, e de cDeltY, divergência entre a previsão e a observação real.
Atenção especial é dada ao objeto cX, no qual é armazenado o estado final corrigido após a aplicação do filtro de Kalman.
index++; if(!cX.Init(0, index, OpenCL, window, optimization, iBatch)) return false; //--- index++; if(!cK_H.Init(0, index, OpenCL, window * window, optimization, iBatch)) return false; //--- index++; if(!cIdifK_H.Init(0, index, OpenCL, window * window, optimization, iBatch)) return false; //--- index++; if(!cIdifK_HT.Init(0, index, OpenCL, window, window, optimization, iBatch)) return false;
Na sequência, vêm cK_H, cIdifK_H e cIdifK_HT, destinados a registrar os resultados das transformações matemáticas necessárias para a correta propagação reversa dos erros e a atualização das estimativas de covariância.
Na etapa final, inicializamos as matrizes mP e mPGrad, bem como os arrays auxiliares mNoise e mGrad, que serão usados na geração de ruídos na fase de amostragem e no processo de cálculo dos gradientes durante o treinamento do modelo.
mP = mPGrad = mS; mNoise = mGrad = matrix<double>::Zeros(scenarios, window); //--- return true; }
Dessa forma, nesta etapa é criada uma infraestrutura computacional completa, que garante o funcionamento estável e correto do filtro de Kalman como parte do Codificador K²VAE. Depois da inicialização bem-sucedida de todos os componentes internos, o método de inicialização conclui sua execução, retornando ao programa chamador o resultado lógico das operações executadas.
Implementando a propagação para frente
Ao passar da etapa de inicialização dos objetos de armazenamento para a descrição do método de propagação para frente feedForward, nós de fato descemos às camadas mais profundas da lógica computacional do Codificador K²VAE. É justamente aqui que todos os componentes preparados anteriormente se unem em um único sistema. Toda a estrutura, como um mecanismo bem sincronizado, começa a trabalhar, transformando as observações originais em representações internas.
A propagação para frente começa com o bloco de Koopman, componente central do modelo, que aprende a dinâmica linear do sistema durante o treinamento. Na etapa de inferência, nas condições da propagação para frente, esse módulo gera toda uma série de reconstruções e um estado previsto com base na sequência analisada e na estrutura linear de mudanças previamente aprendida. Assim, sua saída inclui dois componentes importantes: primeiro, a previsão do estado futuro, construída exclusivamente com base na última observação e nos padrões lineares aprendidos, e segundo, a reconstrução de toda a sequência de estados anteriores, até a profundidade de análise dos dados históricos.
bool CNeuronK2VAEEncoder::feedForward(CNeuronBaseOCL * NeuronOCL) { //--- Koopman if(!cKoopman.FeedForward(NeuronOCL)) return false; //--- Pred / Rest if(!DeConcat(cKoopmanPred.getOutput(), cKoopmanRest.getPrevOutput(), cKoopman.getOutput(), cKoopman.GetWindow(), cKoopman.Neurons() - cKoopman.GetWindow(), 1)) return false; if(!Different(NeuronOCL.getOutput(), cKoopmanRest.getPrevOutput(), cKoopmanRest.getOutput(), cKoopman.GetWindow())) return false;
Essa divisão é especialmente importante: a previsão é usada no bloco do filtro de Kalman, enquanto a sequência residual, a diferença entre os dados reais e sua reconstrução, é enviada ao módulo de atenção, que processa os aspectos da dinâmica que não se encaixam no modelo linear.
//--- Rest Attention if(!cAuxiliaryNet.FeedForward(cKoopmanRest.AsObject())) return false;
Depois, se o modelo estiver operando em modo de treinamento, ocorre a ativação de todos os componentes treináveis do filtro de Kalman. São as matrizes de transição de estado, de impactos de controle, de observação, bem como as matrizes de covariância dos erros do modelo e das medições.
//--- Kalman Filter if(bTrain) { if(!cB.FeedForward()) return false; if(!cF.FeedForward()) return false; if(!cH.FeedForward()) return false; if(!cQ.FeedForward()) return false; if(!cR.FeedForward()) return false; if(!cFT.FeedForward(cF.AsObject())) return false; if(!cHT.FeedForward(cH.AsObject())) return false; if(!cQT.FeedForward(cQ.AsObject())) return false; if(!cRT.FeedForward(cR.AsObject())) return false;
Como, para o funcionamento correto do filtro, é necessária a definição positiva das matrizes Q e R, elas são previamente estabilizadas por meio da multiplicação por suas próprias cópias transpostas.
if(!MatMul(cQ.getOutput(), cQT.getOutput(), cQ_QT.getOutput(), cQT.GetCount(), cQT.GetWindow(), cQT.GetCount(), 1, true)) return false; if(!MatMul(cR.getOutput(), cRT.getOutput(), cR_RT.getOutput(), cRT.GetCount(), cRT.GetWindow(), cRT.GetCount(), 1, true)) return false; }
Depois que todos os parâmetros são preparados, começa a etapa de previsão. Primeiro, é calculada a previsão do estado no próximo passo, unindo dois fluxos de informação, os dados brutos e o módulo de atenção. Esses vetores são projetados por meio das matrizes de parâmetros treináveis F e B e, em seguida, somados. Como resultado, forma-se o estado intermediário XPred.
//--- Prediction step if(!MatMul(NeuronOCL.getOutput(), cFT.getOutput(), cXPred.getGradient(), 1, cFT.GetWindow(), cFT.GetCount(), 1, true)) return false; if(!MatMul(cAuxiliaryNet.getOutput(), cB.getOutput(), cXPred.getPrevOutput(), 1, cFT.GetWindow(), cFT.GetCount(), 1, true)) return false; if(!SumAndNormilize(cXPred.getGradient(), cXPred.getPrevOutput(), cXPred.getOutput(), cFT.GetCount(), false, 0, 0, 0, 1)) return false;
Na sequência, é calculada a previsão da covariância do erro de estado. Para isso, é realizada a multiplicação sequencial da matriz de transição e da matriz de covariância atual e, em seguida, é adicionada a matriz de ruído do modelo Q, formando assim a covariância prevista P_pred.
if(!MatMul(cF.getOutput(), cP.getOutput(), cF_P.getOutput(), cFT.GetCount(), cFT.GetWindow(), cP.Neurons() / cFT.GetWindow(), 1, true)) return false; if(!MatMul(cF_P.getOutput(), cFT.getOutput(), cPPred.getOutput(), cFT.GetCount(), cFT.GetWindow(), cFT.GetCount(), 1, true)) return false; if(!SumAndNormilize(cPPred.getOutput(), cQ_QT.getOutput(), cPPred.getOutput(), cHT.GetWindow(), false, 0, 0, 0, 1)) return false;
Nessa etapa começa a correção da previsão, justamente aquilo pelo qual o filtro de Kalman é tão valorizado. Primeiro, é calculada a covariância do erro de medição, formando-se a matriz S, que representa a soma da dispersão observada e da dispersão prevista.
//--- Update step if(!MatMul(cPPred.getOutput(), cHT.getOutput(), cP_HT.getOutput(), cPPred.Neurons() / cHT.GetWindow(), cHT.GetWindow(), cHT.GetCount(), 1, true)) return false; if(!MatMul(cH.getOutput(), cP_HT.getOutput(), cH_P_HT.getOutput(), cHT.GetCount(), cHT.GetWindow(), cHT.GetCount(), 1, true)) return false; if(!SumAndNormilize(cH_P_HT.getOutput(), cR_RT.getOutput(), cR_RT.getPrevOutput(), cRT.GetWindow(), false, 0, 0, 0, 1)) return false;
Para calcular a matriz de Kalman K, que desempenha o papel de coeficiente de peso na correção do estado, usa-se a matriz inversa S. Não fomos criar do zero um algoritmo para encontrar a matriz inversa. Em vez disso, aproveitamos a funcionalidade existente das operações matriciais do MQL5. Quando necessário, a matriz é estabilizada, diagonalizada e reconstruída para evitar degeneração.
if(cR_RT.getPrevOutput().GetData(mSGrad) <= 0) return false; mS = mSGrad.Inv(); if(mS.Rows() == 0) { mSGrad = mSGrad + mSGrad.Transpose(); vector<double> eigvals; matrix<double> eigvecs; if(!mSGrad.Eig(eigvecs, eigvals)) return false; if(eigvals.Size() > 0) { if(!eigvals.Clip(1e-6, DBL_MAX)) return false; mSGrad = matrix<double>::Zeros(eigvals.Size(), eigvals.Size()); mSGrad.Diag(eigvals); mSGrad = eigvecs.MatMul(mSGrad.MatMul(eigvecs.Transpose())); mSGrad = mSGrad + mSGrad.Transpose(); mS = mSGrad.Inv(); } if(mS.Rows() == 0) { mSGrad.Identity(); mS = mSGrad; } } cSInv.getOutput().Fill(mS); if(!MatMul(cP_HT.getOutput(), cSInv.getOutput(), cK.getOutput(), cHT.GetCount(), (int)mS.Rows(), (int)mS.Cols(), 1, true)) return false;
Na etapa de atualização da medição, o modelo corrige o estado previamente previsto, apoiando-se não em observações reais, mas em uma previsão alternativa obtida do KoopmanNet. Primeiro, calcula-se o valor esperado da observação, multiplicando o estado previsto XPred pela matriz transposta de observação H. Esse valor (YPred) reflete como a observação deveria ser se o modelo não cometesse erro na previsão.
//--- Measurement update if(!MatMul(cXPred.getOutput(), cHT.getOutput(), cYPred.getOutput(), 1, cHT.GetWindow(), cHT.GetCount(), 1, true)) return false; if(!Different(cKoopmanPred.getOutput(), cYPred.getOutput(), cDeltY.getOutput(), 1, 0, 0, 0, 1)) return false; if(!MatMul(cDeltY.getOutput(), cK.getOutput(), cX.getPrevOutput(), 1, cDeltY.Neurons(), cX.Neurons(), 1, true)) return false; if(!SumAndNormilize(cX.getPrevOutput(), cXPred.getOutput(), cX.getOutput(), 1, false, 0, 0, 0, 1)) return false;
A diferença entre as previsões dos dois modelos é interpretada como erro. Multiplicando esse erro pela matriz de Kalman K, obtemos o vetor de correção, que é adicionado à previsão original XPred, formando o estado corrigido X.
Em paralelo, é executada a atualização da matriz de covariância de estado P na forma estabilizada de Joseph, o que permite evitar o acúmulo de erros numéricos e preservar a simetria.
//--- Joseph stabilized form for P if(!MatMul(cK.getOutput(), cH.getOutput(), cK_H.getOutput(), cKT.GetCount(), cKT.GetWindow(), cHT.GetWindow(), 1, true)) return false; if(!IdentDifferent(cK_H.getOutput(), cIdifK_H.getOutput(), cHT.GetWindow(), 0, 0, 1)) return false; if(!cIdifK_HT.FeedForward(cIdifK_H.AsObject())) return false; if(!cKT.FeedForward(cK.AsObject())) return false; if(!MatMul(cIdifK_H.getOutput(), cPPred.getOutput(), cIdifK_H.getPrevOutput(), cIdifK_HT.GetCount(), cIdifK_HT.GetWindow(), cIdifK_HT.GetWindow(), 1, true)) return false; if(!MatMul(cIdifK_H.getPrevOutput(), cIdifK_HT.getOutput(), cP.getPrevOutput(), cIdifK_HT.GetCount(), cIdifK_HT.GetWindow(), cIdifK_HT.GetCount(), 1, true)) return false; if(!MatMul(cK.getOutput(), cR_RT.getOutput(), cK.getPrevOutput(), cKT.GetCount(), cRT.GetCount(), cRT.GetCount(), 1, true)) return false; if(!MatMul(cK.getPrevOutput(), cKT.getOutput(), cKT.getPrevOutput(), cKT.GetCount(), cKT.GetWindow(), cKT.GetCount(), 1, true)) return false; if(!SumAndNormilize(cP.getPrevOutput(), cKT.getPrevOutput(), cP.getOutput(), cPT.GetWindow(), false, 0, 0, 0, 1)) return false; if(!cPT.FeedForward(cP.AsObject())) return false; if(!SumAndNormilize(cP.getOutput(), cPT.getOutput(), cP.getOutput(), 1, false, 0, 0, 0, 0.5f)) return false;
Ao fim de todos os cálculos, é realizada a etapa final, a geração do vetor de saída. Para isso, usa-se a amostragem com base na matriz de covariância inversa obtida P⁻¹. O ruído aleatório é escalado por meio de P, e o resultado é adicionado ao vetor de estado X, formando a representação final.
if(!SumAndNormilize(cX.getOutput(), cAuxiliaryNet.getOutput(), cX.getOutput(), 1, false, 0, 0, 0, 1)) return false; //--- Sample Output if(!cP.getOutput().GetData(mPGrad)) return false; if(mPGrad.HasNan() > 0) { mPGrad.Identity(); if(!cP.getOutput().Fill(mPGrad)) return false; } mP = mPGrad.Inv(); if(mP.Rows() == 0) { mPGrad = mPGrad + mPGrad.Transpose(); vector<double> eigvals; matrix<double> eigvecs; if(!mPGrad.Eig(eigvecs, eigvals)) return false; if(eigvals.Size() > 0) { if(!eigvals.Clip(1e-6, DBL_MAX)) return false; mPGrad = matrix<double>::Zeros(eigvals.Size(), eigvals.Size()); mPGrad.Diag(eigvals); mPGrad = eigvecs.MatMul(mPGrad.MatMul(eigvecs.Transpose())); mPGrad = mPGrad + mPGrad.Transpose(); mP = mPGrad.Inv(); } if(mP.Rows() == 0) { mPGrad.Identity(); if(!cP.getOutput().Fill(mPGrad)) return false; mP = mPGrad.Inv(); } } mNoise.Random(-1, 1); matrix<double> temp = mNoise.MatMul(mP); if(!PrevOutput.Fill(temp)) return false; if(!SumVecMatrix(cX.getOutput(), PrevOutput, Output, (int)mNoise.Cols(), 0, 0, 0, 1)) return false; //--- return true; }
Vale destacar especialmente que a etapa final de geração do tensor de resultados não se limita à construção de um único cenário. Em vez disso, o modelo forma todo um espectro de trajetórias possíveis, cada uma delas sendo uma realização da distribuição multidimensional descrita pela matriz de covariância P. Isso não é apenas um truque matemático elegante, mas um reflexo do grau de confiança do modelo na própria previsão.
É justamente graças a essa abordagem que o modelo se torna especialmente valioso em condições de instabilidade de mercado ou ausência de informação confiável: ele pode não apenas prever uma única versão da evolução futura dos acontecimentos, mas desenhar todo um feixe de trajetórias possíveis, sustentadas pelo treinamento. Isso transforma a saída do feedForward em uma nuvem probabilística de decisões. E cada uma delas reflete diferentes facetas do possível desenrolar dos eventos.
Particularidades da distribuição do gradiente de erro
Assim que o modelo forma o espectro de trajetórias possíveis, a fase de propagação para frente se encerra e começa uma etapa não menos importante, a propagação do gradiente de erro no sentido inverso. Nós a estruturamos no método calcInputGradients, cuja principal tarefa é levar os gradientes do nível dos resultados do funcionamento do modelo até os dados brutos, distribuindo corretamente o erro por todos os componentes conectados.
O algoritmo começa com a distribuição do gradiente entre os valores médios previstos do modelo linear e a matriz de covariância P.
bool CNeuronK2VAEEncoder::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false; //--- From Output if(!SumVecMatrixGrad(cX.getGradient(), PrevOutput, Gradient, (int)mNoise.Cols(), 0, 0, 0, 1)) return false; if(!PrevOutput.GetData(mGrad)) return false; mPGrad = mNoise.Transpose().MatMul(mGrad); mP = mP.Transpose(); mP = (mP * (-1)).MatMul(mPGrad.MatMul(mP));
Além disso, também levamos o gradiente de erro pela forma estabilizada de Joseph.
//--- Joseph stabilized form for P if(!cPT.getGradient().Fill(mP)) return false; if(!cP.CalcHiddenGradients(cPT.AsObject())) return false; if(!SumAndNormilize(cP.getGradient(), cPT.getGradient(), cP.getGradient(), 1, false, 0, 0, 0, 0.5f)) return false; //--- if(!MatMulGrad(cK.getPrevOutput(), cKT.getPrevOutput(), cKT.getOutput(), cKT.getGradient(), cP.getGradient(), cKT.GetCount(), cKT.GetWindow(), cKT.GetCount(), 1, true)) return false; if(!MatMulGrad(cK.getOutput(), cK.getPrevOutput(), cR_RT.getOutput(), cR_RT.getGradient(), cKT.getPrevOutput(), cKT.GetCount(), cRT.GetCount(), cRT.GetCount(), 1, true)) return false; if(!MatMulGrad(cIdifK_H.getPrevOutput(), cIdifK_HT.getPrevOutput(), cIdifK_HT.getOutput(), cIdifK_HT.getGradient(), cP.getGradient(), cIdifK_HT.GetCount(), cIdifK_HT.GetWindow(), cIdifK_HT.GetCount(), 1, true)) return false; if(!MatMulGrad(cIdifK_H.getOutput(), cIdifK_H.getPrevOutput(), cPPred.getOutput(), cPPred.getGradient(), cIdifK_HT.getPrevOutput(), cIdifK_HT.GetCount(), cIdifK_HT.GetWindow(), cIdifK_HT.GetWindow(), 1, true)) return false; //--- if(!cK.CalcHiddenGradients(cKT.AsObject())) return false; if(!SumAndNormilize(cK.getGradient(), cK.getPrevOutput(), cK.getGradient(), 1, false, 0, 0, 0, 1)) return false;
A matriz resultante é transmitida e inicializa a cadeia de respostas de gradiente no bloco do filtro de Kalman. Primeiro, pelo módulo de atualização da medição.
//--- Measurement update if(!cIdifK_H.CalcHiddenGradients(cIdifK_HT.AsObject())) return false; if(!SumAndNormilize(cIdifK_H.getGradient(), cIdifK_H.getPrevOutput(), cIdifK_H.getGradient(), 1, false, 0, 0, 0, 1)) return false; if(!IdentDifferentGrad(cK_H.getGradient(), cIdifK_H.getGradient(), cHT.GetWindow(), 0, 0, 1)) return false; if(!MatMulGrad(cK.getOutput(), cK.getPrevOutput(), cH.getOutput(), cH.getGradient(), cK_H.getGradient(), cKT.GetCount(), cKT.GetWindow(), cHT.GetWindow(), 1, true)) return false; //--- if(!MatMulGrad(cDeltY.getOutput(), cDeltY.getGradient(), cK.getOutput(), cK.getPrevOutput(), cX.getGradient(), 1, cDeltY.Neurons(), cX.Neurons(), 1, true)) return false; if(!SumAndNormilize(cK.getGradient(), cK.getPrevOutput(), cK.getGradient(), 1, false, 0, 0, 0, 1)) return false; if(!DifferentGrad(cKoopmanPred.getGradient(), cYPred.getGradient(), cDeltY.getGradient(), 1, 0, 0, 0, 1)) return false; if(!MatMulGrad(cXPred.getOutput(), cXPred.getGradient(), cHT.getOutput(), cHT.getGradient(), cYPred.getGradient(), 1, cHT.GetWindow(), cHT.GetCount(), 1, true)) return false; if(!SumAndNormilize(cXPred.getGradient(), cX.getGradient(), cXPred.getGradient(), 1, false, 0, 0, 0, 1)) return false;
Depois disso, no bloco de correção, a propagação do gradiente passa pelas matrizes de previsões e erros. Todos esses passos organizam com cuidado o fluxo do gradiente da saída até o espaço oculto e, o que é especialmente importante, levam em conta as ligações estruturais entre as variáveis.
//--- Update step if(!MatMulGrad(cP_HT.getOutput(), cP_HT.getGradient(), cSInv.getOutput(), cSInv.getGradient(), cK.getGradient(), cHT.GetCount(), (int)mS.Rows(), (int)mS.Cols(), 1, true)) return false; if(cSInv.getGradient().GetData(mSGrad) <= 0) return false; mS = mS.Transpose(); mS = (mS * (-1)).MatMul(mSGrad.MatMul(mS)); if(cH_P_HT.getGradient().Fill(mS) <= 0) return false; if(!MatMulGrad(cH.getOutput(), cH.getPrevOutput(), cP_HT.getOutput(), cP_HT.getPrevOutput(), cH_P_HT.getGradient(), cHT.GetCount(), cHT.GetWindow(), cHT.GetCount(), 1, true)) return false; if(!SumAndNormilize(cH_P_HT.getGradient(), cR_RT.getGradient(), cR_RT.getGradient(), int(mS.Cols()), false, 0, 0, 0, 1)) return false; if(!SumAndNormilize(cH.getGradient(), cH.getPrevOutput(), cH.getPrevOutput(), 1, false, 0, 0, 0, 1)) return false; if(!SumAndNormilize(cP_HT.getGradient(), cP_HT.getPrevOutput(), cP_HT.getGradient(), 1, false, 0, 0, 0, 1)) return false; if(!MatMulGrad(cPPred.getOutput(), cPPred.getPrevOutput(), cHT.getOutput(), cHT.getPrevOutput(), cP_HT.getGradient(), cPPred.Neurons() / cHT.GetWindow(), cHT.GetWindow(), cHT.GetCount(), 1, true)) return false; if(!SumAndNormilize(cPPred.getGradient(), cPPred.getPrevOutput(), cQ_QT.getGradient(), int(cQT.GetWindow()), false, 0, 0, 0, 1)) return false; if(!SumAndNormilize(cHT.getGradient(), cHT.getPrevOutput(), cHT.getGradient(), 1, false, 0, 0, 0, 1)) return false;
Em seguida, vem o bloco de previsão, prediction step.
//--- Prediction step if(!MatMulGrad(cF_P.getOutput(), cF_P.getGradient(), cFT.getOutput(), cFT.getGradient(), cQ_QT.getGradient(), cFT.GetCount(), cFT.GetWindow(), cFT.GetCount(), 1, true)) return false; if(!MatMulGrad(cF.getOutput(), cF.getPrevOutput(), cP.getOutput(), cP.getGradient(), cF_P.getGradient(), cFT.GetCount(), cFT.GetWindow(), cP.Neurons() / cFT.GetWindow(), 1, true)) return false; if(!MatMulGrad(cAuxiliaryNet.getOutput(), cAuxiliaryNet.getGradient(), cB.getOutput(), cB.getGradient(), cXPred.getGradient(), 1, cFT.GetWindow(), cFT.GetCount(), 1, true)) return false; if(!MatMulGrad(NeuronOCL.getOutput(), NeuronOCL.getGradient(), cFT.getOutput(), cFT.getPrevOutput(), cXPred.getGradient(), 1, cFT.GetWindow(), cFT.GetCount(), 1, true)) return false; if(!SumAndNormilize(cFT.getGradient(), cFT.getPrevOutput(), cFT.getGradient(), 1, false, 0, 0, 0, 1)) return false; //--- if(!MatMulGrad(cR.getOutput(), cR.getPrevOutput(), cRT.getOutput(), cRT.getGradient(), cR_RT.getGradient(), cRT.GetCount(), cRT.GetWindow(), cRT.GetCount(), 1, false)) return false; if(!MatMulGrad(cQ.getOutput(), cQ.getPrevOutput(), cQT.getOutput(), cQT.getGradient(), cQ_QT.getGradient(), cQT.GetCount(), cQT.GetWindow(), cQT.GetCount(), 1, false)) return false; if(!cR.CalcHiddenGradients((CObject*)cRT.AsObject())) return false; if(!SumAndNormilize(cR.getGradient(), cR.getPrevOutput(), cR.getGradient(), cRT.GetWindow(), false, 0, 0, 0, 0.01f)) return false; if(!cQ.CalcHiddenGradients(cQT.AsObject())) return false; if(!SumAndNormilize(cQ.getGradient(), cQ.getPrevOutput(), cQ.getGradient(), cQT.GetWindow(), false, 0, 0, 0, 0.01f)) return false; if(!cH.CalcHiddenGradients(cHT.AsObject())) return false; if(!SumAndNormilize(cH.getGradient(), cH.getPrevOutput(), cH.getGradient(), cHT.GetWindow(), false, 0, 0, 0, 0.01f)) return false; if(!cF.CalcHiddenGradients(cFT.AsObject())) return false; if(!SumAndNormilize(cF.getGradient(), cF.getPrevOutput(), cF.getGradient(), cFT.GetWindow(), false, 0, 0, 0, 0.01f)) return false;
O bloco final diz respeito à representação no espaço de Koopman e ao módulo de atenção. É aqui que ocorre a transmissão do gradiente para as partes prevista e reconstruída (KoopmanPred, KoopmanRest).
//--- Rest Attention if(!cKoopmanRest.CalcHiddenGradients(cAuxiliaryNet.AsObject())) return false; //--- Pred / Rest if(!NeuronOCL.getPrevOutput().Fill(0)) return false; if(!DifferentGrad(NeuronOCL.getPrevOutput(), cKoopmanRest.getPrevOutput(), cKoopmanRest.getGradient(), cKoopman.GetWindow())) return false; if(!SumAndNormilize(NeuronOCL.getGradient(), NeuronOCL.getPrevOutput(), NeuronOCL.getPrevOutput(), 1, false, 0, 0, 0, 1)) return false; if(NeuronOCL.Activation() != None) { if(!DeActivation(NeuronOCL.getOutput(), NeuronOCL.getPrevOutput(), NeuronOCL.getPrevOutput(), NeuronOCL.Activation())) return false; } if(!Concat(cKoopmanPred.getGradient(), cKoopmanRest.getPrevOutput(), cKoopman.getGradient(), cKoopman.GetWindow(), cKoopman.Neurons() - cKoopman.GetWindow(), 1)) return false;
Para a última, calcula-se o gradiente pela diferença e, depois, ambas as partes são unidas em uma estrutura única de Koopman.
Ao final, o gradiente é transmitido ao nível dos dados brutos.
//--- Koopman if(!NeuronOCL.CalcHiddenGradients(cKoopman.AsObject())) return false; if(!SumAndNormilize(NeuronOCL.getGradient(), NeuronOCL.getPrevOutput(), NeuronOCL.getGradient(), 1, false, 0, 0, 0, 1)) return false; //--- return true; }
Dessa forma, o método constrói passo a passo todo o caminho da propagação reversa do erro, cobrindo todos os componentes centrais do modelo: a parte latente probabilística, a filtragem de estados, a previsão, a correção e a transformação no espaço de Koopman. Tudo isso garante o ajuste preciso dos parâmetros do modelo e permite que ele aprenda com eficiência em séries temporais.
O código-fonte completo do objeto CNeuronK2VAEEncoder e de todos os seus métodos é apresentado no anexo.
Hoje realizamos um trabalho amplo e detalhado, e o artigo já ganhou proporções consideráveis. Proponho fazer uma pequena pausa, para dar a oportunidade de assimilar o material e observá-lo com um olhar renovado. No próximo artigo, concluiremos o que foi iniciado, analisaremos em detalhes os pontos restantes e testaremos as abordagens implementadas com dados históricos reais. Isso permitirá não apenas consolidar a teoria, mas também avaliar a eficácia prática.
Considerações finais
Neste artigo, destrinchamos em detalhes a arquitetura e as principais etapas de implementação do Codificador no framework K²VAE, que reúne os recursos do KoopmanNet e do filtro de Kalman em um único sistema para análise de séries temporais. Essa abordagem permite modelar com eficiência a dinâmica complexa dos dados financeiros, combinando a previsão linear clássica com uma correção adaptativa flexível com base nas observações. O framework analisado demonstra com clareza como métodos consagrados pelo tempo podem se integrar de forma harmoniosa às tecnologias modernas de redes neurais, abrindo novas perspectivas na análise e na previsão dos mercados financeiros.
No próximo artigo, passaremos ao teste prático do modelo com dados históricos reais, para avaliar de forma objetiva sua eficácia e seu potencial em condições de trading real.
Links
- K²VAE: A Koopman-Kalman Enhanced Variational AutoEncoder for Probabilistic Time Series Forecasting
- Outros artigos da série
Programas utilizados no artigo
| # | Nome | Tipo | Descrição |
|---|---|---|---|
| 1 | Study.mq5 | Expert Advisor | EA de treinamento offline dos modelos |
| 2 | StudyOnline.mq5 | Expert Advisor | EA de treinamento online dos modelos |
| 3 | Test.mq5 | Expert Advisor | Expert Advisor para testar o 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 redes neurais |
| 6 | NeuroNet.cl | Biblioteca | Biblioteca de código de programas OpenCL |
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/18766
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.
Rede neural quântica em MQL5 (Parte I): Criando um arquivo de inclusão
Do básico ao intermediário: Recursos
Está chegando o novo MetaTrader 5 e MQL5
Símbolos personalizados em MQL5: Criando um símbolo customizado de barras 3D
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso