Redes neurais em trading: Desvendando os componentes estruturais (Final)
Introdução
Neste artigo, passamos à etapa final da implementação, por meio do MQL5, da nossa própria visão das abordagens propostas pelos autores do framework SCNN. SCNN (Seasonal Convolutional Neural Network) é uma arquitetura especializada, desenvolvida para a análise de séries temporais com estrutura bem definida. Seu objetivo principal é separar os dados originais em vários componentes-chave: tendência de longo prazo, componente sazonal, componente de curto prazo, além de considerar a inter-relação espacial entre as variáveis. Essa decomposição permite não apenas melhorar a qualidade da previsão, mas também garantir a interpretabilidade do modelo, uma qualidade rara, porém extremamente valiosa em tarefas de trading algorítmico.
SCNN se apoia em princípios clássicos de análise, incluindo a normalização por períodos e o uso da transformação sazonal, mas os combina com métodos modernos de processamento de informações: mecanismo de atenção, projeção paramétrica de atributos, além da agregação convolucional dos resultados. A visualização do framework SCNN proposta pelos autores é apresentada abaixo.

Nos artigos anteriores, analisamos, passo a passo, a estrutura e a finalidade de todos os componentes internos do modelo, implementando-os por meio de classes especializadas e estruturando cuidadosamente cada elemento da futura arquitetura. Esse trabalho de implementação não foi simples, mas o esforço compensou, pois agora temos diante de nós um conjunto de módulos completos, cada um cumprindo uma função estritamente definida no sistema.
O próximo passo lógico é uni-los em uma estrutura única e coerente. É justamente nessa etapa que se forma a estrutura arquitetônica da SCNN, que define como será estruturado o fluxo de informações dentro do modelo, desde os dados originais até a previsão final. Passamos da etapa de preparação para a etapa de funcionamento: os componentes entram em operação, interagem e, finalmente, formam um único mecanismo analítico.
Concluída a montagem técnica, passaremos à parte mais aguardada: os testes do modelo em dados históricos. Isso permitirá avaliar não apenas a correção da implementação, mas também a robustez prática da abordagem em diferentes regimes de mercado. SCNN não é apenas mais uma arquitetura de redes neurais. É uma tentativa de unir precisão computacional e transparência na tomada de decisões.
codificador SCNN
No artigo anterior, concluímos a análise da arquitetura do codificador SCNN, criado no objeto CNeuronSCNNEncoder, e examinamos em detalhes o procedimento de inicialização de todos os seus componentes internos. Cada módulo, seja a normalização, a transposição ou a adaptação espacial, foi preparado para operar, receber e processar os dados originais. A estrutura do objeto é apresentada abaixo.
class CNeuronSCNNEncoder : public CNeuronTransposeOCL { protected: CNeuronPeriodNorm cLongNorm; CNeuronTransposeVRCOCL cSeasonTransp; CNeuronPeriodNorm cSeasonNorm; CNeuronTransposeVRCOCL cUnSeasonTransp; CNeuronPeriodNorm cShortNorm; CNeuronAdaptSpatialNorm cAdaptSpatNorm; CNeuronBaseOCL cConcatenated; CNeuronSwiGLUOCL cProjection; CNeuronTransposeOCL cTranspose; CNeuronConvOCL caFusion[2]; CNeuronBaseOCL cFusionOut; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronSCNNEncoder(void) {}; ~CNeuronSCNNEncoder(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units_count, uint variables, uint forecast, uint season_period, uint short_period, 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 defNeuronSCNNEncoder; } virtual void TrainMode(bool flag) override; virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void SetActivationFunction(ENUM_ACTIVATION value) override { } };
Hoje, continuamos o que iniciamos e passamos à construção do elemento-chave: o algoritmo de propagação para frente. É ele que determina como os dados percorrem sequencialmente todas as camadas do modelo, são transformados, agregados e, por fim, levam à geração da representação de saída.
A implementação da propagação para frente no método feedForward demonstra o funcionamento coordenado de todo o sistema.
bool CNeuronSCNNEncoder::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cLongNorm.FeedForward(NeuronOCL)) return false;
Primeiro, os dados originais passam pelo bloco de normalização de longo prazo, onde são eliminados o deslocamento e as diferenças de escala acumulados ao longo de um período extenso. Isso cria uma base alinhada para a análise posterior.
Em seguida, os dados processados são transmitidos em cadeia para o bloco de extração do componente sazonal. Aqui, como discutimos no artigo anterior, é realizada a transposição com um passo definido, correspondente ao período de sazonalidade. Com isso, os elementos da série temporal são agrupados em sequências por fases do ciclo, permitindo identificar com mais eficiência padrões sazonais estáveis. À estrutura obtida é aplicada a normalização por períodos, o que intensifica a expressividade das flutuações sazonais. Já a transposição inversa prepara os dados para as etapas seguintes de processamento.
if(!cSeasonTransp.FeedForward(cLongNorm.AsObject())) return false; if(!cSeasonNorm.FeedForward(cSeasonTransp.AsObject())) return false; if(!cUnSeasonTransp.FeedForward(cSeasonNorm.AsObject())) return false;
Após o processamento sazonal, vem a etapa de extração do componente de curto prazo. Nesse passo, extraímos, na prática, oscilações locais e rápidas, que podem conter informações importantes sobre mudanças recentes na dinâmica do mercado.
if(!cShortNorm.FeedForward(cUnSeasonTransp.AsObject())) return false;
O toque final na fase de pré-processamento é a aplicação da normalização espacial. Essa etapa é bem importante para alinhar as informações extraídas dos diferentes componentes: de longo prazo, sazonal e de curto prazo. A normalização espacial permite adaptar a escala e a influência mútua das variáveis de entrada, eliminando distorções decorrentes das diferenças de dinâmica entre os atributos. A particularidade desse passo é que ele foi implementado levando em conta as dependências espaciais e utiliza o mecanismo de atenção, o que torna a representação resultante especialmente expressiva e informativa.
if(!cAdaptSpatNorm.FeedForward(cShortNorm.AsObject())) return false;
Antes de passar à etapa seguinte, é necessário unir os resultados produzidos por todos os módulos, incluindo os dados normalizados e os parâmetros estatísticos calculados, em um único tensor coerente. Esse é um passo crítico, que garante a integridade posterior do fluxo de informações no modelo.
Vale destacar especialmente: embora a dimensionalidade das próprias sequências temporais seja preservada após a normalização, optamos conscientemente por não estender os parâmetros estatísticos (médias e desvios-padrão) até o comprimento total da sequência. Essa decisão foi tomada em favor da economia de memória, afinal, repetir o mesmo valor ao longo de todo o comprimento não acrescenta informação. No entanto, essa otimização leva a uma diferença de dimensionalidade entre as séries temporais e as estatísticas correspondentes.
Apesar disso, preservamos uma grade comum quanto ao número de sequências unitárias analisadas, e este é o ponto-chave. É justamente nele que nos apoiamos durante a concatenação. Todas as operações são executadas sequencialmente no âmbito desses segmentos unitários, o que garante a correção da montagem final e permite evitar distorções na estrutura dos dados.
Logo no início da etapa de concatenação, formamos o primeiro bloco concatenado, que inclui os dados originais brutos e a saída do módulo de normalização de longo prazo. Isso permite preservar o contexto da série inicial e, ao mesmo tempo, complementá-lo com informações sobre as tendências de longo prazo identificadas no decorrer da normalização. Além disso, acrescentamos a esses dados os parâmetros estatísticos calculados (valores médios e desvios-padrão), que atuam como uma espécie de marcadores de escala e variabilidade.
uint windows[3] = {NeuronOCL.Neurons() / iWindow, cLongNorm.GetPeriod()*cLongNorm.GetUnits(), 2 * cLongNorm.GetUnits() }; if(!Concat(NeuronOCL.getOutput(), cLongNorm.getOutput(), cLongNorm.GetMeanSTDevs().getOutput(), cConcatenated.getOutput(), windows[0], windows[1], windows[2], iWindow)) return false;
No passo seguinte, expandimos nosso tensor com as informações extraídas do componente sazonal.
windows[0] = windows[0] + windows[1] + windows[2]; windows[1] = cSeasonNorm.GetPeriod() * cSeasonNorm.GetUnits(); windows[2] = 2 * cSeasonNorm.GetUnits(); if(!cConcatenated.SwapOutputs() || !Concat(cConcatenated.getPrevOutput(), cSeasonNorm.getOutput(), cSeasonNorm.GetMeanSTDevs().getOutput(), cConcatenated.getOutput(), windows[0], windows[1], windows[2], iWindow)) return false;
Depois da informação sazonal, os dados dos componentes de curto prazo e acoplado (espacial) são adicionados ao tensor, de forma sequencial. Nessa etapa, continuamos seguindo a lógica de combinação por sequências unitárias, expandindo o tensor pela incorporação sequencial de novos atributos.
windows[0] = windows[0] + windows[1] + windows[2]; windows[1] = cShortNorm.GetPeriod() * cShortNorm.GetUnits(); windows[2] = 2 * cShortNorm.GetUnits(); if(!cConcatenated.SwapOutputs() || !Concat(cConcatenated.getPrevOutput(), cShortNorm.getOutput(), cShortNorm.GetMeanSTDevs().getOutput(), cConcatenated.getOutput(), windows[0], windows[1], windows[2], iWindow)) return false; windows[0] = windows[0] + windows[1] + windows[2]; windows[1] = cAdaptSpatNorm.GetUnits(); windows[2] = 2 * cAdaptSpatNorm.GetUnits(); if(!cConcatenated.SwapOutputs() || !Concat(cConcatenated.getPrevOutput(), cAdaptSpatNorm.getOutput(), cAdaptSpatNorm.GetMeanSTDevs().getOutput(), cConcatenated.getOutput(), windows[0], windows[1], windows[2], iWindow)) return false;
O componente de curto prazo fornece ao modelo um contexto recente: oscilações locais, picos e micropadrões, que são especialmente importantes para a alta sensibilidade da previsão. A normalização espacial, por sua vez, conclui a fase de preparação, garantindo estabilidade adicional e suavização dos dados originais considerando todas as variáveis.
Como resultado, formamos uma representação significativa e equilibrada da série temporal, na qual cada componente é apresentado de forma coerente. Esse tensor combinado é passado para a camada de projeção, cuja tarefa consiste em formar uma representação ponderada dos dados originais considerando o horizonte de previsão. Nessa etapa, os dados são ajustados à dimensionalidade de saída necessária, adaptando-se às necessidades da análise posterior. É justamente aqui que se estabelece a base para a geração de uma previsão coerente, capaz de considerar tanto a profundidade histórica quanto a estrutura das mudanças esperadas.
if(!cProjection.FeedForward(cConcatenated.AsObject())) return false;
As representações das sequências unitárias individuais, obtidas nas etapas anteriores, são alinhadas em uma estrutura de série temporal multimodal no módulo Fusion. Aqui ocorre a transição da representação por sequências unitárias para os passos temporais: o tensor é transposto, e o processamento posterior passa a ser estruturado ao longo do eixo temporal.
Nessa etapa, são usadas duas camadas convolucionais sequenciais. O primeiro deles utiliza a função de ativação TANH, permitindo identificar uma representação rica e não linear de cada atributo, que, em essência, corresponde a uma forma normalizada do sinal dos atributos. A segunda camada, que utiliza SIGMOID, atua como um mecanismo de gate: ela destaca o grau de importância de cada atributo no contexto temporal, ponderando suavemente os valores obtidos.
O resultado final é obtido pela multiplicação elemento a elemento das saídas das duas camadas, o que permite filtrar o ruído de forma eficiente e destacar as características mais relevantes da série temporal, já em um espaço alinhado à tarefa de previsão.
if(!cTranspose.FeedForward(cProjection.AsObject())) return false; for(uint i = 0; i < caFusion.Size(); i++) if(!caFusion[i].FeedForward(cTranspose.AsObject())) return false; if(!ElementMult(caFusion[0].getOutput(), caFusion[1].getOutput(), cFusionOut.getOutput())) return false; //--- return CNeuronTransposeOCL::feedForward(cFusionOut.AsObject()); }
Na etapa final da propagação para frente, os dados passam pela transposição inversa, retornando da representação temporal para a estrutura original das sequências unitárias. Isso permite preservar a coerência dos formatos na entrada e na saída do codificador SCNN.
Assim, o método feedForward representa uma arquitetura cuidadosamente estruturada e bem concebida, na qual cada componente cumpre uma função estritamente definida. Da normalização e decomposição às projeções e à agregação multimodal, todo o fluxo é voltado à construção de uma descrição rica, estruturada e informativa da série temporal.
Concluída a análise do algoritmo de propagação para frente, o passo lógico é avançar para uma etapa igualmente importante: a retropropagação do erro. É nesse momento que começa o treinamento do modelo: os valores dos gradientes, calculados na saída, são gradualmente propagados de volta por toda a cadeia de operações, permitindo ajustar os parâmetros internos de cada módulo. O método calcInputGradients implementa essa etapa, garantindo uma sequência operacional rigorosa e coerente com a arquitetura da propagação para frente.
bool CNeuronSCNNEncoder::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false; //--- if(!CNeuronTransposeOCL::calcInputGradients(cFusionOut.AsObject())) return false; if(!ElementMultGrad(caFusion[0].getOutput(), caFusion[0].getGradient(), caFusion[1].getOutput(), caFusion[1].getGradient(), cFusionOut.getGradient(), caFusion[0].Activation(), caFusion[1].Activation())) return false; if(!cTranspose.CalcHiddenGradients(caFusion[0].AsObject())) return false;
A propagação reversa começa no nível superior do modelo, onde são usados como entrada os gradientes recebidos da próxima camada do modelo (ou da função de perda, quando se trata da própria saída). Os primeiros a serem processados são os blocos convolucionais empregados no módulo Fusion. Aqui, é importante entender que, na etapa de propagação para frente, usamos dois canais convolucionais paralelos: um com a função de ativação TANH para gerar uma representação não linear do atributo, e outro com SIGMOID, que atua como mecanismo de gate, determinando o grau de importância do atributo. Na saída, esses canais eram multiplicados elemento a elemento, formando a representação final.
Na propagação reversa, essa operação de multiplicação exige um tratamento especial. O método ElementMultGrad distribui corretamente os gradientes de volta entre as duas ramificações, considerando as particularidades de cada função de ativação. Isso é criticamente importante para a precisão dos cálculos, pois é justamente aqui que se decide como ajustar os pesos dentro das convoluções.
Em seguida, passamos a repassar os gradientes ao módulo Transpose, que, vale lembrar, na propagação para frente era responsável pela reorientação dos eixos do tensor: da representação das sequências unitárias para os passos temporais. Era exatamente nessa configuração que os dados chegavam à entrada das duas camadas convolucionais do módulo Fusion, portanto, agora a tarefa da propagação reversa é agregar corretamente os gradientes recebidos das duas ramificações.
No primeiro passo, retropropagamos o gradiente do erro a partir de uma das camadas convolucionais. Em vez de copiar as informações de todo o tensor, usamos a troca temporária do ponteiro para o buffer de dados, o que permite alternar de forma eficiente a área de memória e preservar os valores obtidos sem gasto adicional de recursos. Em seguida, com o ponteiro temporariamente trocado, iniciamos a propagação reversa da segunda camada convolucional.
CBufferFloat* temp = cTranspose.getGradient(); if(!cTranspose.SetGradient(cTranspose.getPrevOutput(), false) || !cTranspose.CalcHiddenGradients(caFusion[1].AsObject()) || !SumAndNormilize(temp, cTranspose.getGradient(), temp, cTranspose.GetCount(), false, 0, 0, 0, 1) || !cTranspose.SetGradient(temp, false)) return false;
Depois disso, os dois fluxos de gradientes são acumulados por soma elemento a elemento, refletindo a influência combinada das duas ramificações sobre o erro final. Por fim, os ponteiros dos buffers retornam ao estado original, garantindo a correção da estrutura dos dados para a continuação da retropropagação dos gradientes. Essa abordagem mantém a coerência do modelo e otimiza a gestão da memória durante o treinamento.
Em seguida, o erro é repassado ao bloco Projection, responsável pela ponderação das informações antes dos módulos de convolução. O gradiente vindo dele é repassado ao bloco Concatenated, onde começa uma das etapas tecnicamente mais complexas: a deconcatenação.
if(!cProjection.CalcHiddenGradients(cTranspose.AsObject())) return false; if(!cConcatenated.CalcHiddenGradients(cProjection.AsObject())) return false;
Vale lembrar que, na propagação para frente, concatenamos os dados por etapas, vindos de diferentes normalizadores: de longo prazo, sazonal, de curto prazo e espacial. Além disso, junto com os dados normalizados, também foram incluídos no tensor combinado os parâmetros estatísticos (valores médios e desvios-padrão).
Agora, a tarefa é inversa: desdobrar o tensor combinado de volta em suas partes constituintes. Para isso, é usado o procedimento sequencial DeConcat, no qual os tamanhos das janelas de cada componente são calculados seguindo rigorosamente a composição anterior. Essa etapa exige cuidado especial com a precisão, pois o menor erro no tamanho da janela pode levar a deslocamento ou perda de dados.
uint windows[3] = {0}; windows[1] = cAdaptSpatNorm.GetUnits(); windows[2] = 2 * cAdaptSpatNorm.GetUnits(); windows[0] = cConcatenated.Neurons() / iWindow - windows[1] - windows[2]; if(!DeConcat(cConcatenated.getPrevOutput(), cAdaptSpatNorm.getGradient(), cAdaptSpatNorm.GetMeanSTDevs().getGradient(), cConcatenated.getGradient(), windows[0], windows[1], windows[2], iWindow)) return false; windows[1] = cShortNorm.GetPeriod() * cShortNorm.GetUnits(); windows[2] = 2 * cShortNorm.GetUnits(); windows[0] = windows[0] - windows[1] - windows[2]; if(!DeConcat(cConcatenated.getGradient(), cShortNorm.getPrevOutput(), cShortNorm.GetMeanSTDevs().getGradient(), cConcatenated.getPrevOutput(), windows[0], windows[1], windows[2], iWindow)) return false; windows[1] = cSeasonNorm.GetPeriod() * cSeasonNorm.GetUnits(); windows[2] = 2 * cSeasonNorm.GetUnits(); windows[0] = windows[0] - windows[1] - windows[2]; if(!DeConcat(cConcatenated.getPrevOutput(), cSeasonNorm.getPrevOutput(), cSeasonNorm.GetMeanSTDevs().getGradient(), cConcatenated.getGradient(), windows[0], windows[1], windows[2], iWindow)) return false; windows[1] = cLongNorm.GetPeriod() * cLongNorm.GetUnits(); windows[2] = 2 * cLongNorm.GetUnits(); windows[0] = windows[0] - windows[1] - windows[2]; if(!DeConcat(NeuronOCL.getPrevOutput(), cLongNorm.getPrevOutput(), cLongNorm.GetMeanSTDevs().getGradient(), cConcatenated.getPrevOutput(), windows[0], windows[1], windows[2], iWindow)) return false;
Nesta etapa, vale destacar separadamente um ponto técnico importante. Todos os módulos de normalização, com exceção do espacial, não constituíam o ponto final da cadeia de preparação dos dados. Suas saídas eram usadas como dados de entrada para as etapas seguintes de processamento. Isso significa que, no momento da propagação reversa, cada um desses módulos não recebe o gradiente do erro de uma única fonte, e sim de várias. Para considerar corretamente a contribuição de todas as operações posteriores, não podemos simplesmente sobrescrever o gradiente, como é comum em arquiteturas mais simples. Em vez disso, é usado um buffer auxiliar: cada módulo armazena nesse buffer seu gradiente atual antes de receber um novo.
Assim, a propagação reversa pela cadeia de normalizações é implementada com acumulação de gradientes em buffers especiais. Isso garante a reconstrução precisa da influência de cada componente do modelo e evita perdas de informação.
Após a deconcatenação bem-sucedida, cada um dos módulos de normalização, começando pelo cShortNorm de curto prazo, depois pelo cSeasonNorm sazonal e, por fim, pelo cLongNorm de longo prazo, recebe a parcela correspondente do gradiente do erro, que reflete a influência da cadeia de processamento dos dados na propagação para frente. Mas, como já observamos, cada um desses normalizadores não participou apenas da preparação dos dados. Portanto, simplesmente repassar o gradiente de volta por um único caminho seria insuficiente.
Na prática, isso é implementado da seguinte forma. Depois que o normalizador recebe o gradiente do erro vindo do objeto que utiliza seus dados, nós o somamos aos valores já existentes, obtidos na etapa de deconcatenação. Assim, em cada etapa, acumulamos cuidadosamente as informações vindas de várias fontes. Só depois desse procedimento de agregação o gradiente resultante é repassado pela cadeia em direção à camada anterior.
if(!cShortNorm.CalcHiddenGradients(cAdaptSpatNorm.AsObject()) || !SumAndNormilize(cShortNorm.getGradient(), cShortNorm.getPrevOutput(), cShortNorm.getGradient(), cShortNorm.GetPeriod(), false, 0, 0, 0, 1)) return false; if(!cUnSeasonTransp.CalcHiddenGradients(cShortNorm.AsObject())) return false; if(!cSeasonNorm.CalcHiddenGradients(cUnSeasonTransp.AsObject()) || !SumAndNormilize(cSeasonNorm.getGradient(), cSeasonNorm.getPrevOutput(), cSeasonNorm.getGradient(), cSeasonNorm.GetPeriod(), false, 0, 0, 0, 1)) return false; if(!cSeasonTransp.CalcHiddenGradients(cSeasonNorm.AsObject())) return false; if(!cLongNorm.CalcHiddenGradients(cSeasonTransp.AsObject()) || !SumAndNormilize(cLongNorm.getGradient(), cLongNorm.getPrevOutput(), cLongNorm.getGradient(), cLongNorm.GetPeriod(), false, 0, 0, 0, 1)) return false;
Essa abordagem garante que nenhum vínculo informacional estabelecido na propagação para frente seja perdida ou distorcida na etapa de retropropagação do erro. É justamente esse rigor e cuidado na propagação dos gradientes que constitui um dos fatores de estabilidade e alta interpretabilidade do framework SCNN em condições reais de mercado.
A propagação reversa se encerra com o cálculo dos gradientes na própria entrada, no objeto NeuronOCL. Essa etapa é criticamente importante, pois é justamente aqui que convergem todos os fluxos de informação que passaram por uma cadeia complexa de transformações e normalizações.
if(!NeuronOCL.CalcHiddenGradients(cLongNorm.AsObject()) || !SumAndNormilize(NeuronOCL.getGradient(), NeuronOCL.getPrevOutput(), NeuronOCL.getGradient(), cLongNorm.GetPeriod(), false, 0, 0, 0, 1)) return false; //--- return true; }
É importante observar que, nesse ponto, dois fluxos-chave se encontram: um vindo do complexo sistema de normalizações e outro vindo da camada de projeção, responsável pela construção do espaço de atributos para a previsão. A agregação correta desses fluxos garante a continuidade do fluxo de gradientes e a atualização correta dos pesos do modelo durante o treinamento.
Assim, o método calcInputGradients implementa não apenas um retorno técnico pela cadeia, mas um procedimento preciso, modular e estruturado de retropropagação do erro. Cada ação nele se relaciona claramente com as etapas anteriores da propagação para frente, tornando a arquitetura do codificador SCNN íntegra, simétrica e facilmente interpretável.
A otimização dos parâmetros treináveis do codificador CNeuronSCNNEncoder é implementada por meio da delegação de responsabilidades aos módulos internos correspondentes, cada um com seus próprios pesos e sua própria lógica de atualização. O método updateInputWeights transfere sequencialmente o controle para esses módulos, garantindo um controle centralizado, porém modular, do treinamento.
bool CNeuronSCNNEncoder::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(!cAdaptSpatNorm.UpdateInputWeights(cShortNorm.AsObject())) return false; if(!cProjection.UpdateInputWeights(cConcatenated.AsObject())) return false; for(uint i = 0; i < caFusion.Size(); i++) if(!caFusion[i].UpdateInputWeights(cTranspose.AsObject())) return false; //--- return true; }
Graças à estrutura modular, o algoritmo de atualização dos pesos permanece simples e lógico, sem sobrecarregar o código com dependências desnecessárias.
O código-fonte completo da classe CNeuronSCNNEncoder, incluindo a implementação de todos os métodos-chave, é apresentado no anexo. Graças à estrutura transparente e à decomposição detalhada em módulos, esse código pode servir de base para experimentos e modificações posteriores.
Módulo de nível superior
Os autores do framework SCNN propõem usar uma arquitetura hierárquica, na qual vários codificadores são empilhados em uma única pilha. Essa solução é voltada para aumentar a expressividade do modelo e, como consequência, melhorar a qualidade da previsão. Cada codificador seguinte nessa cadeia não opera de forma isolada, mas utiliza os resultados do anterior como dados de entrada. O uso de conexões residuais garante contexto adicional para a análise. Com isso, o modelo é capaz de considerar um contexto mais amplo, aprofundando a análise e capturando tanto dependências locais quanto de longo prazo na série temporal.
No entanto, essa flexibilidade também tem seu preço. Na transição de uma camada para outra, surge um problema bastante concreto: a diferença no tamanho do tensor na entrada e na saída. A questão é que, na saída de cada codificador SCNN, obtemos sequências unitárias em formato comparável, mas ampliadas pelo horizonte de previsão definido.
É aqui que surge uma tarefa não trivial. Por um lado, é necessário fornecer dados à próxima camada sem incluir os valores previstos nesses dados, caso contrário, estaríamos vazando informação futura, o que é inadmissível. Por outro lado, não podemos simplesmente descartar esses valores previstos, pois eles são necessários para a soma final das saídas de todas as camadas. Em termos simples, precisamos considerar simultaneamente tanto aquilo que o modelo já viu quanto aquilo que ele previu, preservando a estrutura temporal dos dados.
Para resolver essa tarefa, criaremos um objeto de nível superior, CNeuronSCNN. Trata-se de um elemento de controle generalizado, que encapsula toda a estrutura em pilha dos codificadores SCNN. Sua função não é apenas chamar os Encoders em sequência, mas garantir o funcionamento correto de toda a arquitetura: repassar dados entre as camadas, alinhar seus tamanhos, acumular previsões, gerenciar o treinamento e a retropropagação do erro. É ele que se torna o elo entre a lógica computacional e a coerência arquitetônica do modelo.
A estrutura do novo objeto é apresentada abaixo.
class CNeuronSCNN : public CNeuronBaseOCL { protected: CLayer cLayers; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronSCNN(void) {}; ~CNeuronSCNN(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units_count, uint variables, uint forecast, uint season_period, uint short_period, uint layers, 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 defNeuronSCNN; } virtual void TrainMode(bool flag) override; virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void SetActivationFunction(ENUM_ACTIVATION value) override { } };
Dentro de CNeuronSCNN, há um objeto do tipo CLayer, que, na prática, funciona como um contêiner para todos os codificadores SCNN aninhados. O único objeto interno da classe é declarado estaticamente, o que nos permite manter vazios o construtor e o destrutor da classe, delegando a gestão da memória ao sistema.
Para colocar toda a arquitetura em operação, é necessário montar corretamente a arquitetura a partir de suas partes constituintes. Essa tarefa é resolvida no método Init, onde toda a estrutura dinâmica do objeto CNeuronSCNN é criada e configurada.
bool CNeuronSCNN::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units_count, uint variables, uint forecast, uint season_period, uint short_period, uint layers, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, (units_count + forecast)*variables, optimization_type, batch)) return false; SetActivationFunction(None);
Aqui, tudo começa com a inicialização básica por meio do método de mesmo nome da classe base, no qual são criadas as interfaces básicas da camada neural. Em seguida, a função de ativação é desativada, pois ela não é necessária nesse nível, uma vez que os codificadores SCNN já contêm toda a não linearidade e todo o pré-processamento de dados necessários.
Depois disso, o contêiner cLayers é limpo e preparado para receber os objetos necessários.
cLayers.Clear(); cLayers.SetOpenCL(OpenCL); CNeuronSCNNEncoder* encoder = NULL; CNeuronBaseOCL* residual = NULL; for(uint l = 0; l < layers; l++) { encoder = new CNeuronSCNNEncoder(); if(!encoder) return false; if(!encoder.Init(0, l, OpenCL, units_count, variables, forecast, season_period, short_period, optimization, iBatch) || !cLayers.Add(encoder)) return false; encoder.SetActivationFunction(None); if((l + 1) == layers) break;
O laço pelo número de camadas (parâmetro layers) começa a criar, um a um, os codificadores CNeuronSCNNEncoder. Para cada camada, é criado um objeto-codificador, inicializado com os parâmetros: dimensionalidade das entradas, número de variáveis, comprimento da previsão e períodos de sazonalidade. Se a inicialização for bem-sucedida, o objeto é adicionado ao contêiner de camadas.
Mas a lógica não termina aqui. Para preservar as conexões residuais entre as camadas, são inseridas camadas adicionais entre os codificadores, atuando como uma espécie de buffers de compatibilização. Esses blocos intermediários recebem as saídas dos codificadores SCNN anteriores e descartam os valores previstos, garantindo a continuidade do fluxo de dados entre os níveis. Aqui, a função de ativação também é desativada, pois esses blocos cumprem uma função mais auxiliar do que computacional.
residual = new CNeuronBaseOCL(); if(!residual) return false; if(!residual.Init(0, l, OpenCL, units_count * variables, optimization, iBatch) || !cLayers.Add(residual)) return false; residual.SetActivationFunction(None); } if(!SetGradient(encoder.getGradient(), true)) return false; //--- return true; }
É importante observar que o codificador SCNN final da pilha não recebe uma camada auxiliar subsequente. Seu gradiente é usado como gradiente principal de toda a camada superior, o que permite eliminar uma operação desnecessária de cópia de dados.
Assim, o método Init deixa de ser apenas um inicializador e se transforma em uma espécie de construtor do objeto, montando dinamicamente toda a estrutura em pilha dos codificadores SCNN, ajustando cuidadosamente cada detalhe e levando em conta o alinhamento dos tamanhos, os horizontes de previsão e o roteamento do fluxo de gradientes. Graças a essa arquitetura bem concebida, torna-se possível não apenas treinar o modelo de forma eficiente, mas também escalá-lo para tarefas reais, seja uma previsão de preços de curto prazo ou uma análise de longo prazo das tendências sazonais.
Após a conclusão da inicialização, quando a estrutura do modelo SCNN multicamadas já está montada e pronta para operar, chega o momento-chave: a execução da propagação para frente. É justamente aqui que o modelo começa sua função principal: processar os dados originais, gerar previsões e acumular informações em cada nível.
bool CNeuronSCNN::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false; if(!Output.Fill(0)) return false; CNeuronBaseOCL* inputs = NeuronOCL; CNeuronSCNNEncoder* current = NULL; CNeuronBaseOCL* residual = NULL; int layers = cLayers.Total();
Tudo começa pela verificação da validade do ponteiro recebido para os dados originais. Se os dados estiverem ausentes, a execução é interrompida. Depois disso, o buffer de resultados é limpo, para evitar o acúmulo de artefatos de iterações anteriores. Em seguida, a variável inputs é inicializada com o ponteiro para a fonte externa de dados, que é passado à entrada do codificador SCNN. Então começa o percurso por todas as camadas.
A arquitetura do modelo é estruturada de modo que as camadas se alternem: nas posições pares ficam os codificadores SCNN, nas ímpares, os buffers das conexões residuais, implementados como objetos neurais básicos. É por isso que, no laço, o passo é igual a dois: uma iteração abrange tanto o codificador SCNN quanto o bloco residual correspondente.
for(int l = 0; l < layers; l += 2) { current = cLayers[l]; if(!current || !current.FeedForward(inputs) || !SumAndNormilize(Output, current.getOutput(), Output, current.GetCount(), false, 0, 0, 0, 1)) return false; if((l + 1) == layers) break;
A cada iteração do laço, extraímos o codificador SCNN atual, chamamos seu método FeedForward e, em seguida, adicionamos a saída do módulo à previsão resultante. Assim, já durante a propagação para frente, começa a se formar o resultado acumulado de todas as previsões por camada.
Mas, se ainda não for a última camada, resta executar uma etapa adicional. Extraímos o próximo objeto, de posição ímpar: o buffer da conexão residual. Ele tem a tarefa de ajustar a dimensionalidade dos dados originais para o próximo codificador SCNN, removendo a redundância surgida pela inclusão dos valores previstos. Essa tarefa é resolvida pelo método DeConcat, que separa a parte de previsão do histórico limpo, devolvendo os dados à forma original. Depois disso, as novas entradas são somadas às anteriores, o que garante o efeito das conexões residuais, e então são normalizadas. Cada camada opera não de forma isolada, mas considerando os resultados das anteriores, recebendo uma representação mais rica da série temporal.
uint variables = current.GetWindow(); uint dimension = inputs.Neurons() / variables; uint forecast = current.GetCount() - dimension; residual = cLayers[l + 1]; if(!residual) return false; if(!DeConcat(residual.getOutput(), current.getPrevOutput(), current.getOutput(), dimension, forecast, variables) || !SumAndNormilize(residual.getOutput(), inputs.getOutput(), residual.getOutput(), dimension, true, 0, 0, 0, 1)) return false; inputs = residual; } //--- return true; }
Os dados obtidos após a deconcatenação e a normalização são repassados ao próximo codificador SCNN, e isso continua até a conclusão da passagem por todos os níveis. Na saída, é formada a previsão agregada, resultado da operação conjunta de todos os codificadores SCNN do bloco.
Assim, o método feedForward implementa uma sequência cuidadosamente estruturada, na qual cada passo é voltado para enriquecer a representação, eliminar distorções e preparar uma previsão equilibrada. Ele coloca em prática toda a filosofia da arquitetura em pilha: o aprofundamento sequencial da análise sem perda do contexto histórico e com consideração dos aportes de cada nível.
Após a conclusão da propagação para frente, passamos a uma etapa igualmente importante: a retropropagação do erro. Aqui ocorre a propagação dos gradientes da camada de saída de volta aos dados originais, permitindo otimizar os parâmetros do modelo. Essa lógica está implementada no método calcInputGradients.
bool CNeuronSCNN::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false; if(!PrevOutput.Fill(0)) return false; //--- CNeuronBaseOCL* inputs = NULL; CNeuronSCNNEncoder* current = cLayers[-1]; CNeuronBaseOCL* residual = NULL; int layers = cLayers.Total() - 2;
O procedimento começa com verificações básicas: a presença de um ponteiro para o objeto externo NeuronOCL e o zeramento do buffer auxiliar PrevOutput. Em seguida, é determinado o número de camadas que participam do treinamento. Aqui, é importante entender que a contagem é feita em ordem inversa, começando pelo último codificador SCNN, pois a propagação reversa exige o movimento da saída para a entrada do modelo.
O laço com passo igual a "-1" percorre todas as camadas, incluindo os objetos das conexões residuais, e, para cada uma, executa a lógica correspondente conforme o tipo da camada.
for(int l = layers; l >= 0; l--) switch(cLayers[l].Type()) { case defNeuronBaseOCL: inputs = cLayers[l]; if(!inputs || !inputs.CalcHiddenGradients(current)) return false; if(!!residual) if(!SumAndNormilize(inputs.getGradient(), residual.getGradient(), inputs.getGradient(), current.GetWindow(), false, 0, 0, 0, 1)) return false; residual = inputs; break;
Se o objeto atual for um CNeuronBaseOCL básico, executamos duas ações. Primeiro, calculamos os gradientes da camada oculta com base no codificador SCNN atual. Depois, havendo um bloco residual anterior (variável residual), somamos seus gradientes aos atuais, ajustando o gradiente repassado. Isso garante a continuidade da informação entre as camadas e permite evitar perdas causadas pela estrutura residual da arquitetura.
Se, por sua vez, a camada atual for um codificador SCNN, a lógica se torna mais complexa. Primeiro, verificamos se o objeto residual já foi definido. Se não estiver definido, simplesmente avançamos. Caso esteja, extraímos o objeto localizado duas camadas à frente, isto é, aquele que alimentava a entrada do codificador SCNN atual durante a propagação para frente. Isso permite reconstruir com precisão a estrutura dos dados.
case defNeuronSCNNEncoder: current = cLayers[l]; if(!residual) break; inputs = cLayers[l + 2]; if(!inputs) return false; if(!Concat(residual.getGradient(), PrevOutput, current.getGradient(), residual.Neurons() / current.GetWindow(), current.GetCount() - residual.Neurons() / current.GetWindow(), current.GetWindow()) || !SumAndNormilize(current.getGradient(), inputs.getGradient(), current.getGradient(), current.GetWindow(), false, 0, 0, 0, 1)) return false; break; default: return false; break; }
Em seguida, com a função Concat, restauramos a forma do gradiente: adicionamos ao gradiente atual da camada residual valores zero correspondentes ao seu PrevOutput, correspondentes à parte prevista. Isso é necessário porque, na propagação para frente, os dados foram divididos em histórico e previsão, e agora essa estrutura precisa ser reproduzida com precisão no gradiente do erro. Depois ocorre a soma com o gradiente vindo do codificador SCNN subsequente. Esse é um ponto importante, pois a informação pode seguir por dois fluxos de informação, e ambos precisam ser reunidos em uma representação única.
Após a conclusão do percurso por todas as camadas, executamos a etapa final: repassar o gradiente para o nível mais externo. Aqui, NeuronOCL recebe o gradiente vindo do codificador SCNN de nível inferior e, havendo uma camada residual, também ocorre a soma final dos gradientes.
if(!NeuronOCL.CalcHiddenGradients(current)) return false; if(!!residual) if(!SumAndNormilize(NeuronOCL.getGradient(), residual.getGradient(), NeuronOCL.getGradient(), current.GetWindow(), false, 0, 0, 0, 1)) return false; //--- return true; }
Assim, o método calcInputGradients incorpora a ideia central da arquitetura em pilha: cada camada pode ajustar seus parâmetros não apenas com base em seu próprio erro, mas também em sua influência sobre o modelo como um todo. Isso garante um ajuste fino e alta sensibilidade às particularidades das séries temporais.
Após a conclusão do cálculo dos gradientes em todos os níveis da arquitetura em pilha, chega a etapa final do treinamento: a atualização dos pesos. É justamente aqui que ocorre a implementação prática de todas as etapas realizadas anteriormente: transformamos as informações acumuladas sobre o erro em ajustes dos parâmetros treináveis do modelo.
O método updateInputWeights é responsável pela atualização sequencial dos pesos de todos os blocos treináveis dentro do modelo. Tudo começa com a inicialização da variável inputs com o ponteiro para a fonte externa de dados, o objeto NeuronOCL.
bool CNeuronSCNN::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { CNeuronBaseOCL* inputs = NeuronOCL; CNeuronBaseOCL* current = NULL; //--- for(int l = 0; l < cLayers.Total(); l++) { current = cLayers[l]; if(!current) return false; if(current.Type() == defNeuronSCNNEncoder) if(!current.UpdateInputWeights(inputs)) return false; inputs = current; } //--- return true; }
Em seguida, inicia-se o percurso por todas as camadas do modelo. A cada passo do laço, verificamos o tipo da camada. O que nos interessa são apenas os codificadores SCNN, pois são eles que contêm os parâmetros treináveis. Se a camada atual corresponder a esse tipo, chamamos o método UpdateInputWeights, ao qual são passados os dados originais. Isso permite reajustar corretamente os pesos de acordo com o gradiente obtido. Caso a atualização seja bem-sucedida, substituímos o ponteiro na variável inputs, para que ela passe a servir de entrada para a próxima camada. Dessa forma, preserva-se a coerência lógica característica da propagação para frente e da retropropagação do erro.
O método updateInputWeights conclui o ciclo de treinamento, permitindo que cada Encoder se adapte aos erros do modelo e contribua para a otimização geral. Tudo é implementado de forma contida e objetiva, sem complexidade desnecessária, mas com total respeito à lógica da arquitetura.
O código completo dessa classe CNeuronSCNN e de todos os seus métodos é apresentado no anexo.
Arquitetura do modelo
Após concluir a descrição da lógica dos objetos que constroem o framework SCNN, o passo natural é mergulhar na arquitetura do próprio modelo, pois é ela que determina com que precisão e estabilidade o sistema é capaz de extrair padrões dos dados. Aqui começa a montagem da configuração do pipeline de rede neural, realizada no método CreateDescriptions.
Esse método é responsável por criar e preencher as descrições das camadas, os blocos estruturais a partir dos quais o futuro modelo computacional será formado. Em termos simples, é aqui que, tijolo por tijolo, são definidos os níveis da futura rede neural: da camada de dados originais aos Encoders e às ramificações de previsão. No início, vemos a criação e a inicialização de seis contêineres: para o Encoder principal do estado do ambiente, três variantes de modelos preditivos, além das ramificações Actor e Critic, usadas em tarefas de aprendizado por reforço.
O próprio Encoder do estado do ambiente começa com uma camada básica totalmente conectada, que recebe o vetor de dados originais formado com base nas barras históricas.
bool CreateDescriptions(CArrayObj *&encoder, CArrayObj *&forecast1, CArrayObj *&forecast2, CArrayObj *&forecast3, CArrayObj *&actor, CArrayObj *&critic ) { //--- CLayerDescription *descr; //--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) return false; } if(!forecast1) { forecast1 = new CArrayObj(); if(!forecast1) return false; } if(!forecast2) { forecast2 = new CArrayObj(); if(!forecast2) return false; } if(!forecast3) { forecast3 = new CArrayObj(); if(!forecast3) return false; } if(!actor) { actor = new CArrayObj(); if(!actor) return false; } if(!critic) { critic = new CArrayObj(); if(!critic) return false; } //--- Encoder encoder.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; uint prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
Em seguida, é adicionada uma camada de normalização em lote com adição de ruído na etapa de treinamento, que atua como regularizador e aumenta a robustez do modelo a ruídos nos dados.
//--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormWithNoise; descr.count = prev_count; descr.batch = BatchSize; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConcatDiff; prev_count = descr.count = HistoryBars; descr.layers = BarDescr; descr.step = 1; descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
Depois vem a camada CNeuronConcatDiff de adição de atributos de primeira diferença, que cria atributos derivados, ajudando a rede a capturar melhor as mudanças locais.
A camada CMamba4CastEmbeding merece atenção especial, pois representa um elemento arquitetônico moderno. Ela extrai atributos ocultos levando em conta várias janelas temporais (neste caso, diária e mensal), criando assim embeddings levando em conta as harmônicas temporais. É justamente aqui que a rede neural passa a considerar, pela primeira vez, a sazonalidade e as tendências de longo prazo.
//--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defMamba4CastEmbeding; prev_count = descr.count = HistoryBars; descr.window = 2 * BarDescr; uint prev_out = descr.window_out = NSkills; { uint temp[] = {PeriodSeconds(PERIOD_D1), PeriodSeconds(PERIOD_MN1)}; if(ArrayCopy(descr.windows, temp) < (int)temp.Size()) return false; } descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronTransposeOCL; descr.count = prev_count; prev_count = descr.window = prev_out; descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } prev_out = descr.count;
Em seguida, os embeddings são transpostos para representações de sequências unitárias.
O fechamento de toda a estrutura do Encoder do estado do ambiente é a integração do módulo descrito anteriormente: a pilha de codificadores SCNN. É justamente esse bloco, estruturado com quatro níveis de aninhamento e cuidadosamente ajustado para captar padrões sazonais e padrões de curto prazo, que forma o núcleo do processamento inteligente dos dados.
//--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronSCNN; descr.variables = prev_count; { uint temp[]={prev_out,NForecast,SeasonPeriod,ShortPeriod}; if(ArrayCopy(descr.windows,temp)<(int)temp.Size()) return false; } descr.count=descr.windows[0]+descr.windows[1]; descr.layers=4; descr.batch = BatchSize; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; } uint variables=descr.variables; uint count=descr.count;
Aqui acontece algo além de uma simples transformação dos dados. Esse módulo assume a carga analítica central: ele não apenas extrai atributos, mas também os estrutura na forma de representações ocultas multicamadas, cada uma delas generalizando os padrões observados em sua própria escala. Na saída, obtemos vetores de previsão já enriquecidos por conexões residuais e pela memória dos níveis anteriores.
Em essência, é justamente nesse elemento que os dados originais brutos se transformam em uma descrição de alto nível do estado do mercado. Aqui, padrões abstratos de comportamento são convertidos em estruturas numéricas, prontas para serem interpretadas tanto pelos módulos de previsão quanto pelas ramificações de controle do modelo. E é a partir desse momento que a arquitetura se completa, passando da preparação e normalização para a análise, a interpretação e a tomada de decisões.
Aqui vale destacar especialmente um ponto técnico importante. Na saída da pilha de codificadores SCNN, obtemos um conjunto de sequências unitárias. Esse formato é conveniente e lógico para os modelos de previsão subsequentes. É ideal para gerar previsões em um horizonte definido. No entanto, esse formato não é totalmente adequado no contexto da análise que será realizada pelos módulos Actor e Critic.
O ponto é que, para tomar decisões, o agente precisa não apenas considerar cada previsão separadamente, mas entendê-las como uma estrutura temporal coerente, uma dinâmica multimodal que abrange tanto os aspectos de curto prazo quanto os de longo prazo do comportamento do sistema. Para isso, é necessária outra disposição dos dados: o ordenamento sequencial dos passos temporais com preservação de suas inter-relações.
É justamente por isso que adicionamos mais uma camada de transposição. Esse passo transforma nosso tensor de um conjunto de previsões unitárias em um formato correspondente aos passos temporais de uma sequência multimodal. Cada ponto temporal passa a ter acesso a todas as variáveis da previsão, reunidas a partir de diferentes canais. Graças a isso, Actor e Critic conseguem perceber a dinâmica do ambiente ao longo do tempo como um quadro coeso, e não como fragmentos isolados.
//--- layer 6 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronTransposeOCL; descr.count = variables; prev_count = descr.window = count; descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
Assim, essa camada final de transformação conclui a arquitetura do Encoder e desempenha um papel essencial em assegurar uma interface correta entre o mecanismo de previsão e os módulos de tomada de decisões.
A arquitetura dos modelos de previsão e tomada de decisões foi reaproveitada de nossas implementações anteriores praticamente sem alterações. Esses componentes já comprovaram sua eficácia, por isso, neste artigo, não vamos detalhá-los. Para quem tiver interesse em compreender mais a fundo as soluções arquitetônicas, recomendamos consultar os materiais incluídos no anexo.
Além disso, no anexo estão os códigos-fonte dos programas que viabilizam o treinamento e os testes dos modelos. Esses materiais permitirão não apenas acompanhar a lógica interna da montagem do sistema, mas também reproduzir o ciclo completo do experimento.
Testes
O treinamento do modelo foi estruturado em duas etapas sequenciais. Na primeira, a etapa offline, o treinamento foi realizado em dados históricos do par de moedas EURUSD, timeframe H1, durante todo o ano de 2024. Esse período abrangeu uma ampla variedade de cenários de mercado. A diversidade dos dados permitiu que o modelo assimilasse tanto situações típicas quanto situações raras de mercado.
Após a conclusão do treinamento offline, passamos à segunda etapa: o ajuste fino online, executado em condições o mais próximas possível do mercado real. O treinamento ocorreu no Testador de Estratégias do MetaTrader 5, onde o modelo analisava o fluxo de dados por barras, passo a passo. Isso permitiu não apenas verificar a robustez do modelo a ruídos e distorções de mercado, mas também assegurar sua capacidade de adaptação a condições em mudança. Essa abordagem aumentou significativamente a robustez operacional do modelo, minimizou o sobreajuste e melhorou sua capacidade de generalização.
A etapa final foi a validação do modelo em dados completamente novos: cotações no período de janeiro a março de 2025. Todos os parâmetros e configurações obtidos durante o treinamento foram mantidos sem alterações. Assim, os resultados obtidos fornecem uma avaliação objetiva tanto da precisão quanto da confiabilidade prática do método proposto. Os resultados dos testes são apresentados abaixo.


Durante os três meses de teste, nosso modelo demonstrou crescimento do capital de $100 para aproximadamente $430, enquanto o lucro médio por operação ($13,17) superou o prejuízo médio ($11,71). O percentual de operações vencedoras ficou próximo de 48%, e o fator de lucro de cerca de 1.04 indica que o sistema opera com uma pequena vantagem a favor do resultado positivo.
Ao mesmo tempo, o rebaixamento máximo superou 82%, e a queda mais profunda ocorreu na segunda quinzena do mês de março. Esse foi o período de maior afastamento em relação à amostra de treinamento. Isso indica que o modelo enfrenta dificuldades ao se deparar com novas condições de mercado e ainda não é suficientemente robusto a picos inesperados de volatilidade.
De modo geral, o teste mostrou que a arquitetura SCNN é capaz de gerar lucro e equilibrar risco e retorno, mas, para aplicação prática, são necessários mecanismos adicionais de gestão de risco e a ampliação do período de treinamento. Isso permitirá ao modelo não apenas operar de forma estável em um mercado conhecido, mas também se adaptar com mais confiança a condições novas e imprevisíveis.
Conclusão
Neste artigo, concluímos o desenvolvimento e a validação prática do modelo SCNN para previsão de séries temporais. Percorremos um amplo conjunto de etapas: desde a análise da ideia teórica de decomposição de séries temporais até uma pilha completa de SCNN Encoders treinados e à implementação das abordagens propostas na arquitetura Actor-Critic.
O teste com dados do par de moedas EURUSD de janeiro a março de 2025 confirmou a capacidade do modelo de gerar lucro e formar previsões equilibradas, mas também revelou sua vulnerabilidade a mudanças bruscas de mercado fora da amostra de treinamento. Os rebaixamentos profundos em março reforçam a necessidade de prosseguir com a otimização do modelo.
Referências
- Disentangling Structured Components: Towards Adaptive, Interpretable and Scalable Time Series Forecasting
- Outros artigos da série
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 redes neurais |
| 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/19022
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.
Caminhe em novos trilhos: Personalize indicadores no MQL5
Componentes View e Controller para tabelas no paradigma MVC em MQL5: dimensões ajustáveis dos elementos
Está chegando o novo MetaTrader 5 e MQL5
Rede neural quântica em MQL5 (Parte III): Processador quântico virtual com qubits
- 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