Redes neurais em trading: Aprendizado multitarefa baseado no modelo ResNeXt
Introdução
O rápido avanço da inteligência artificial levou à adoção ativa de métodos de aprendizado profundo na análise de dados, incluindo o setor financeiro. Os dados financeiros apresentam alta dimensionalidade, heterogeneidade e estrutura temporal, o que dificulta a aplicação de métodos tradicionais de processamento. Ao mesmo tempo, o aprendizado profundo demonstra alta eficácia na análise de dados complexos e não estruturados.
Entre as arquiteturas modernas de modelos convolucionais, destaca-se o ResNeXt, apresentado no trabalho "Aggregated Residual Transformations for Deep Neural Networks". ResNeXt mostra capacidade de identificar dependências locais e globais, além de trabalhar eficientemente com dados multidimensionais, reduzindo a complexidade computacional por meio da convolução em grupo.
Um dos principais focos da análise financeira com base em aprendizado profundo é o aprendizado multitarefa (Multi-Task Learning, MTL). Essa abordagem permite resolver várias tarefas relacionadas simultaneamente, melhorando a precisão dos modelos e sua capacidade de generalização. Ao contrário do método clássico, em que cada modelo resolve uma tarefa isolada, o aprendizado multitarefa utiliza representações compartilhadas dos dados, tornando o modelo mais resiliente às mudanças do mercado e melhorando o processo de aprendizado. Essa abordagem é especialmente útil para a previsão de tendências de mercado, avaliação de riscos e determinação de preços de ativos, já que os mercados financeiros são dinâmicos e influenciados por diversos fatores.
No artigo "Collaborative Optimization in Financial Data Mining Through Deep Learning and ResNeXt", foi apresentado um framework para integrar a arquitetura ResNeXt em modelos multitarefa. A solução apresentada abre novas possibilidades na manipulação de séries temporais, identificação de padrões espaço-temporais e formação de previsões precisas. A convolução em grupo e os blocos residuais ResNeXt aumentam a velocidade de treinamento e reduzem a probabilidade de perda de características importantes, tornando esse método especialmente relevante para a análise financeira.
Outro benefício importante da abordagem proposta é a automatização da extração de características relevantes dos dados brutos. Nos métodos tradicionais de análise de informações financeiras, é necessária uma engenharia de características complexa, enquanto modelos de redes neurais profundas conseguem destacar, por conta própria, os principais padrões. Isso é especialmente relevante na análise de dados financeiros multimodais, onde é necessário considerar várias fontes de informação ao mesmo tempo, incluindo indicadores de mercado, relatórios macroeconômicos e publicações de notícias. A flexibilidade do aprendizado multitarefa permite alterar dinamicamente os pesos das tarefas e as funções de perda, o que aumenta a adaptabilidade do modelo às mudanças no ambiente de mercado e melhora a precisão das previsões.
Arquitetura ResNeXt
A arquitetura ResNeXt adota o princípio modular de construção e convoluções em grupo. Ela é baseada em blocos convolucionais com conexões residuais, obedecendo a duas regras principais:
- Se os mapas de características de saída têm o mesmo tamanho, os blocos utilizam os mesmos hiperparâmetros (largura e tamanho dos filtros);
- Se o tamanho dos mapas diminui, a largura dos blocos aumenta proporcionalmente.
A observância desses princípios permite manter uma complexidade computacional aproximadamente constante em todos os níveis do modelo, o que simplifica bastante o processo de sua concepção. Basta definir um módulo padrão, e os demais blocos são formados automaticamente, garantindo padronização, configuração simplificada e facilidade na análise da arquitetura.
Os neurônios comuns em redes neurais artificiais realizam uma soma ponderada dos valores de entrada, o que constitui a operação principal nas camadas convolucionais e totalmente conectadas. Esse processo pode ser dividido em três etapas principais: divisão, transformação e agregação. No entanto, o ResNeXt introduz uma abordagem mais flexível em vez da transformação convencional, onde as funções de transformação podem ser mais complexas, chegando até a ser mini-modelos. Isso leva à ideia de "modelo dentro do neurônio" (Network-in-Neuron), que amplia as capacidades da arquitetura por meio de uma nova dimensão — o número de canais (cardinality). Diferente da largura ou profundidade da rede, o número de canais define a quantidade de transformações complexas independentes em cada bloco. Pesquisas experimentais mostram que aumentar esse parâmetro pode ser mais eficaz do que aumentar a profundidade ou largura da rede, pois proporciona um melhor equilíbrio entre desempenho e eficiência computacional.
Todos os blocos no ResNeXt têm a mesma estrutura, utilizando o módulo bottleneck. Ele é composto por:
- uma camada convolucional inicial 1×1, que reduz a dimensionalidade das características,
- uma camada convolucional principal 3×3, que executa o processamento central dos dados,
- uma camada convolucional final 1×1, que retorna a dimensionalidade original.
Essa abordagem permite reduzir a complexidade computacional mantendo a alta capacidade de representação do modelo. Além disso, o uso de conexões residuais ajuda a preservar os gradientes durante o treinamento, evitando seu desaparecimento, o que é fundamental em modelos profundos.
Uma das principais melhorias do ResNeXt é a utilização de convoluções em grupo (Grouped Convolutions). Nesse método, os dados de entrada são divididos em vários grupos independentes, cada um processado por um filtro convolucional separado, e os resultados são então combinados. Esse mecanismo permite reduzir o número de parâmetros do modelo, mantém alta largura de banda na rede e melhora a eficiência computacional sem perdas significativas de precisão.
Para garantir estabilidade na complexidade computacional ao alterar o número de grupos, o ResNeXt adapta a largura dos bottleneck-blocos, ajustando o número de canais nas camadas internas. Isso permite que o modelo seja escalado sem aumento excessivo nos custos computacionais.
O framework de treinamento multitarefa baseado em ResNeXt representa uma abordagem progressiva para a manipulação de dados financeiros, resolvendo o problema do uso conjunto de características e da modelagem cooperativa para diversas tarefas analíticas. Ele é construído a partir de três componentes estruturais principais:
- módulo de extração de características;
- módulo de aprendizado conjunto;
- camadas de saída especializadas para cada tarefa.
Essa abordagem permite integrar mecanismos eficientes de aprendizado profundo com séries temporais financeiras, garantindo alta precisão na previsão e adaptação do modelo às condições de mercado em constante mudança.
O módulo de extração de características é baseado na arquitetura ResNeXt, o que permite destacar de forma eficaz tanto as características locais quanto as globais dos dados financeiros. Na manipulação de dados financeiros multidimensionais, o número de grupos na arquitetura do modelo desempenha um papel crucial. Esse hiperparâmetro permite encontrar um equilíbrio ideal entre a representação detalhada das características e o custo computacional. Cada operação de convolução em grupo no ResNeXt identifica padrões específicos em diferentes grupos de canais, e depois os agrega em uma única representação.
Após passarem pelas camadas de transformações não lineares, as características formadas se tornam a base para o aprendizado multitarefa subsequente e para a adaptação do modelo a tarefas específicas. O módulo de aprendizado conjunto integra um mecanismo de separação de pesos, que permite projetar as características compartilhadas em espaços específicos de cada tarefa. Esse mecanismo garante que o modelo consiga gerar representações individuais para cada tarefa, evitando interferências entre elas e, ao mesmo tempo, assegurando um alto grau de compartilhamento de características. A divisão das tarefas em clusters, levando em conta as correlações entre elas, aumenta ainda mais a eficácia do mecanismo de aprendizado conjunto.
As camadas de saída de cada tarefa são perceptrons totalmente conectados, que projetam as características especializadas no espaço de previsões finais. As camadas de saída podem ser adaptadas de acordo com a natureza das tarefas a serem resolvidas. Especificamente, em tarefas de classificação utiliza-se a função de perda baseada em entropia cruzada, enquanto nas tarefas de regressão é aplicada o erro quadrático médio (MSE). Para o aprendizado conjunto de todas as tarefas, a função de perda final é representada como uma soma ponderada das perdas de cada tarefa.
O treinamento do modelo é realizado em várias etapas. Primeiramente, executa-se um pré-treinamento das submodelos para resolver tarefas individuais, a fim de garantir uma convergência eficiente dos MLP. Em seguida, o modelo é refinado dentro da arquitetura multitarefa, o que contribui para a melhoria de sua performance geral. A otimização é feita com o uso do algoritmo Adam, com ajuste dinâmico da taxa de aprendizado.
Implementação com MQL5
Após a análise dos aspectos teóricos do framework de aprendizado multitarefa baseado no ResNeXt, passamos à implementação de nossa visão sobre as abordagens propostas utilizando recursos do MQL5. E começaremos construindo os blocos básicos da arquitetura ResNeXt — os módulos bottleneck.
Módulo Bottleneck
Como mencionado anteriormente, o módulo Bottleneck é composto por três camadas convolucionais, cada uma desempenhando um papel fundamental no processamento dos dados brutos. A primeira camada é responsável pela redução da dimensionalidade do espaço de características, o que permite diminuir a complexidade computacional do processamento subsequente da informação.
A segunda camada convolucional executa a convolução principal com o objetivo de extrair características complexas e de alto nível, necessárias para a interpretação precisa dos dados brutos. Ela analisa as interações entre diferentes elementos dos dados, identificando padrões que podem ser críticos para as etapas seguintes da análise. Essa abordagem permite que o modelo se adapte às dependências não lineares nos dados financeiros, aumentando assim a precisão das previsões.
A última camada é responsável por restaurar a dimensionalidade original do tensor de dados, o que é essencial para manter toda a informação significativa. Na etapa anterior de extração de características, pode ocorrer uma redução na dimensionalidade do tensor ao longo do eixo de passos temporais. Isso é compensado pelo aumento na dimensionalidade do espaço de características, em conformidade com os princípios arquitetônicos do ResNeXt.
Com o objetivo de estabilizar o processo de treinamento, após cada camada convolucional é aplicada normalização em lote. Essa técnica reduz o deslocamento interno de covariância e acelera a convergência do modelo. A função de ativação utilizada é a ReLU, que aumenta a não linearidade do modelo, aprimorando sua capacidade de identificar dependências complexas nos dados e melhorando a generalização.
A arquitetura descrita acima é implementada dentro do objeto CNeuronResNeXtBottleneck, cuja estrutura está apresentada a seguir.
class CNeuronResNeXtBottleneck : public CNeuronConvOCL { protected: CNeuronConvOCL cProjectionIn; CNeuronBatchNormOCL cNormalizeIn; CNeuronTransposeRCDOCL cTransposeIn; CNeuronConvOCL cFeatureExtraction; CNeuronBatchNormOCL cNormalizeFeature; CNeuronTransposeRCDOCL cTransposeOut; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronResNeXtBottleneck(void){}; ~CNeuronResNeXtBottleneck(void){}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint chanels_in, uint chanels_out, uint window, uint step, uint units_count, uint group_size, uint groups, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronResNeXtBottleneck; } //--- methods for working with files virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual CLayerDescription* GetLayerInfo(void) override; virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; };
Como classe pai, utilizamos o objeto de camada convolucional, que será responsável por executar a função de restauração do espaço de características. Além disso, na estrutura apresentada, vemos uma série de objetos internos que desempenharão papéis fundamentais nos algoritmos que estamos desenvolvendo. Conheceremos melhor a funcionalidade desses objetos durante a construção dos métodos da nova classe.
Todos os objetos internos são declarados como estáticos, o que nos permite deixar vazios o construtor e o destrutor da classe. A inicialização de todos os objetos declarados e herdados é feita no método Init. Nos parâmetros desse método, recebemos um conjunto de constantes que fornecem uma descrição clara da arquitetura do módulo que está sendo criado.
bool CNeuronResNeXtBottleneck::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint chanels_in, uint chanels_out, uint window, uint step, uint units_count, uint group_size, uint groups, ENUM_OPTIMIZATION optimization_type, uint batch) { int units_out = ((int)units_count - (int)window + (int)step - 1) / (int)step + 1; if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, group_size * groups, group_size * groups, chanels_out, units_out, 1, optimization_type, batch)) return false;
No corpo do método, geralmente chamamos imediatamente o método de mesmo nome da classe pai, no qual já estão implementados os algoritmos de inicialização dos objetos e interfaces herdados. No entanto, neste caso, é importante destacar que a classe pai atua como a última camada convolucional do módulo. Ela recebe como entrada os dados após a extração de características, etapa durante a qual pode ter ocorrido alteração na dimensionalidade do tensor processado. Por isso, primeiro definimos o comprimento da sequência na saída do módulo, e só depois chamamos o método da classe pai.
Após a inicialização bem-sucedida dos objetos herdados, passamos a trabalhar com os objetos recém-declarados. Aqui, iniciamos com o bloco de projeção dos dados. Com a primeira camada convolucional, preparamos as projeções dos dados brutos para a quantidade exigida de grupos de trabalho.
//--- Projection In uint index = 0; if(!cProjectionIn.Init(0, index, OpenCL, chanels_in, chanels_in, group_size * groups, units_count, 1, optimization, iBatch)) return false; index++; if(!cNormalizeIn.Init(0, index, OpenCL, cProjectionIn.Neurons(), iBatch, optimization)) return false; cNormalizeIn.SetActivationFunction(LReLU);
As projeções obtidas são normalizadas e adicionamos LReLU como função de ativação.
É importante observar que, como resultado dessas operações, obtemos os dados na forma de um tensor tridimensional [Time, Group, Dimension]. Para fins de construção do algoritmo de processamento independente de grupos individuais, movemos a dimensão do identificador de grupo para a primeira posição usando o objeto de transposição do tensor tridimensional.
index++; if(!cTransposeIn.Init(0, index, OpenCL, units_count, groups, group_size, optimization, iBatch)) return false; cTransposeIn.SetActivationFunction((ENUM_ACTIVATION)cNormalizeIn.Activation());
Em seguida, temos o bloco de extração de características. Nele, usamos uma camada convolucional com o número de grupos definido como a quantidade de sequências independentes. Isso permite “não misturar” os valores de grupos diferentes. Cada grupo utiliza sua própria matriz de parâmetros treináveis.
//--- Feature Extraction index++; if(!cFeatureExtraction.Init(0, index, OpenCL, group_size * window, group_size * step, group_size, units_out, groups, optimization, iBatch)) return false;
Também é importante notar que, nos parâmetros do método, recebemos o tamanho da janela da convolução e seu passo na dimensão dos passos temporais. Por isso, ao passarmos os parâmetros para o método de inicialização da camada convolucional interna, multiplicamos os respectivos parâmetros pelo tamanho do grupo.
Após a camada convolucional, adicionamos a normalização em lote com a função de ativação LReLU.
index++; if(!cNormalizeFeature.Init(0, index, OpenCL, cFeatureExtraction.Neurons(), iBatch, optimization)) return false; cNormalizeFeature.SetActivationFunction(LReLU);
O último bloco de projeção reversa do espaço de características inclui apenas o objeto de transposição do tensor tridimensional, que combina os grupos em uma única sequência. A projeção direta dos dados, como já mencionado anteriormente, é realizada por meio dos recursos herdados da classe pai.
//--- Projection Out index++; if(!cTransposeOut.Init(0, index, OpenCL, groups, units_out, group_size, optimization, iBatch)) return false; cTransposeOut.SetActivationFunction((ENUM_ACTIVATION)cNormalizeFeature.Activation()); //--- return true; }
Resta apenas retornar o resultado lógico da execução das operações para o programa que chamou o método e concluir o funcionamento do método de inicialização do objeto.
A próxima etapa do nosso trabalho é a construção do algoritmo de propagação para frente, que implementamos no método feedForward.
bool CNeuronResNeXtBottleneck::feedForward(CNeuronBaseOCL *NeuronOCL) { //--- Projection In if(!cProjectionIn.FeedForward(NeuronOCL)) return false;
Nos parâmetros do método, recebemos um ponteiro para o objeto de dados brutos, que é imediatamente passado para o método de mesmo nome da primeira camada convolucional interna de projeção de dados. Não realizamos a verificação da validade do pointeiro recebido, pois essa verificação já está implementada no método da camada interna e, neste caso, tal controle seria redundante.
Os resultados da projeção são normalizados e transpostos para a forma de grupos distintos.
if(!cNormalizeIn.FeedForward(cProjectionIn.AsObject())) return false; if(!cTransposeIn.FeedForward(cNormalizeIn.AsObject())) return false;
No bloco de extração de características, realizamos operações de convolução em grupo e normalizamos os resultados obtidos.
//--- Feature Extraction if(!cFeatureExtraction.FeedForward(cTransposeIn.AsObject())) return false; if(!cNormalizeFeature.FeedForward(cFeatureExtraction.AsObject())) return false;
As características extraídas de grupos distintos são novamente transpostas para uma única sequência multidimensional e projetadas no espaço de características definido usando os recursos da classe pai.
//--- Projection Out if(!cTransposeOut.FeedForward(cNormalizeFeature.AsObject())) return false; return CNeuronConvOCL::feedForward(cTransposeOut.AsObject()); }
O resultado lógico da execução das operações é retornado para o programa chamador e finalizamos o funcionamento do método.
Como você pode perceber, o algoritmo do método de propagação para frente tem uma natureza linear. E da mesma forma o gradiente do erro se propaga linearmente. Portanto, sugiro deixar os métodos de propagação reversa para estudo independente. O código completo do objeto apresentado e de todos os seus métodos pode ser encontrado no anexo.
Módulo de conexões residuais
A arquitetura ResNeXt é caracterizada pela presença de conexões residuais em cada módulo Bottleneck, o que contribui para uma propagação mais eficaz do gradiente de erro durante a propagação reversa. Essas conexões permitem que o modelo reutilize características previamente extraídas, melhorando a convergência e reduzindo o risco de desaparecimento do gradiente. Como resultado, o modelo é capaz de realizar aprendizado em maior profundidade sem aumento significativo nos custos computacionais.
É importante destacar que, na saída do módulo Bottleneck, é formado um tensor cujo tamanho total permanece aproximadamente o mesmo, embora suas dimensões individuais se alterem. A redução no número de passos temporais é compensada pelo aumento na dimensionalidade do espaço de características, permitindo que o modelo preserve informações essenciais e leve em conta dependências de longo prazo nos dados. Para garantir a correta organização do fluxo das conexões residuais, é utilizado um módulo especial de projeção dos dados brutos nas dimensões correspondentes, assegurando a integração correta das informações entre as camadas do modelo. Isso evita problemas de incompatibilidade de dimensões e mantém a estabilidade do treinamento mesmo em arquiteturas profundas.
No contexto do nosso trabalho, criamos tal módulo na forma do objeto CNeuronResNeXtResidual, cuja estrutura está apresentada abaixo.
class CNeuronResNeXtResidual: public CNeuronConvOCL { protected: CNeuronTransposeOCL cTransposeIn; CNeuronConvOCL cProjectionTime; CNeuronBatchNormOCL cNormalizeTime; CNeuronTransposeOCL cTransposeOut; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronResNeXtResidual(void){}; ~CNeuronResNeXtResidual(void){}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint chanels_in, uint chanels_out, uint units_in, uint units_out, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronResNeXtResidual; } //--- methods for working with files virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual CLayerDescription* GetLayerInfo(void) override; virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; };
Durante o desenvolvimento desse objeto, utilizamos abordagens semelhantes àquelas usadas na construção dos módulos Bottleneck, mas adaptadas às demais dimensões do tensor de entrada.
Na estrutura apresentada do objeto, observamos vários objetos internos, cuja funcionalidade será detalhada no processo de implementação dos métodos da nova classe. Todos os objetos internos são declarados como estáticos. Isso nos permite manter o construtor e o destrutor da classe vazios. A inicialização de todos os objetos, incluindo os herdados, é realizada no módulo Init, cujos parâmetros fornecem um conjunto de constantes que permitem interpretar com clareza a arquitetura do objeto criado.
bool CNeuronResNeXtResidual::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint chanels_in, uint chanels_out, uint units_in, uint units_out, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, chanels_in, chanels_in, chanels_out, units_out, 1, optimization_type, batch)) return false;
No corpo do método, chamamos imediatamente o método de mesmo nome da classe pai, passando o conjunto necessário de parâmetros. De forma análoga ao módulo Bottleneck, usamos uma camada convolucional como classe pai. Ela também desempenha a função de projetar os dados para uma nova dimensão de características.
Após a execução bem-sucedida das operações da classe pai referentes à inicialização dos objetos e interfaces herdados, passamos a trabalhar com os objetos recém-declarados. Para facilitar o trabalho com os dados na dimensão dos passos temporais, realizamos primeiramente a transposição dos dados brutos.
int index=0; if(!cTransposeIn.Init(0, index, OpenCL, units_in, chanels_in, optimization, iBatch)) return false;
Em seguida, com o auxílio da camada convolucional, realizamos a projeção das sequências unitárias individuais para a dimensão especificada.
index++; if(!cProjectionTime.Init(0, index, OpenCL, units_in, units_in, units_out, chanels_in, 1, optimization, iBatch)) return false;
Os resultados obtidos são normalizados, de maneira análoga ao módulo Bottleneck. No entanto, neste caso, não utilizamos função de ativação. Afinal, estamos construindo um módulo de conexões residuais e é necessário transmitir todas as informações sem perdas.
index++; if(!cNormalizeTime.Init(0, index, OpenCL, cProjectionTime.Neurons(), iBatch, optimization)) return false;
Depois, precisamos ajustar o espaço de características. Para isso, realizamos a transposição reversa dos dados. A projeção propriamente dita é feita pelos recursos da classe pai.
index++; if(!cTransposeOut.Init(0, index, OpenCL, chanels_in, units_out, optimization, iBatch)) return false; //--- return true; }
Resta apenas retornar o resultado lógico da execução das operações para o programa chamador e finalizar o funcionamento do método de inicialização do novo objeto.
Na etapa seguinte, passamos à construção do algoritmo de propagação para frente dentro do método feedForward.
bool CNeuronResNeXtResidual::feedForward(CNeuronBaseOCL *NeuronOCL) { //--- Projection Timeline if(!cTransposeIn.FeedForward(NeuronOCL)) return false;
Nos parâmetros do método, recebemos um ponteiro para o objeto que contém os dados brutos. Esse ponteiro é passado ao método de mesmo nome da camada interna de transposição de dados, que converte os dados para o formato de sequências unitárias.
Em seguida, é necessário modificar a dimensionalidade dessas sequências unitárias para o tamanho definido. Para esse fim, é utilizada a camada convolucional.
if(!cProjectionTime.FeedForward(cTransposeIn.AsObject())) return false;
Os dados obtidos são normalizados.
if(!cNormalizeTime.FeedForward(cProjectionTime.AsObject())) return false;
Após isso, realizamos a transposição reversa dos dados e a projeção no espaço de características.
//--- Projection Chanels if(!cTransposeOut.FeedForward(cNormalizeTime.AsObject())) return false; return CNeuronConvOCL::feedForward(cTransposeOut.AsObject()); }
A projeção final é realizada pelos meios da classe pai. O resultado lógico da execução das operações é retornado ao programa chamador, e finalizamos a execução do método de propagação para frente.
É fácil perceber que o algoritmo do método de propagação para frente apresenta uma estrutura linear. Isso leva à linearidade nos fluxos de distribuição dos gradientes de erro durante as operações de propagação reversa. Portanto, os métodos de propagação reversa sugiro deixar para estudo independente, tal como foi feito com o objeto CNeuronResNeXtBottleneck. O código completo dos objetos mencionados e de todos os seus módulos pode ser encontrado no anexo.
Bloco ResNeXt
Acima, criamos objetos individuais que representam dois fluxos de informação do framework ResNeXt. Agora chegou o momento de unificar esses objetos em uma única estrutura, o que permitirá um trabalho mais eficiente com os dados. Para esse fim, criaremos o objeto CNeuronResNeXtBlock, que servirá como o bloco principal para o processamento posterior da informação. A estrutura deste objeto é apresentada abaixo.
class CNeuronResNeXtBlock : public CNeuronBaseOCL { protected: uint iChanelsOut; CNeuronResNeXtBottleneck cBottleneck; CNeuronResNeXtResidual cResidual; CBufferFloat cBuffer; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronResNeXtBlock(void){}; ~CNeuronResNeXtBlock(void){}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint chanels_in, uint chanels_out, uint window, uint step, uint units_count, uint group_size, uint groups, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronResNeXtBlock; } //--- methods for working with files virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual CLayerDescription* GetLayerInfo(void) override; virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; };
Na estrutura apresentada, vemos objetos já familiares e um conjunto conhecido de métodos virtuais, que nos caberá sobrescrever.
Todos os objetos internos são declarados como estáticos, o que nos permite deixar vazios o construtor e o destrutor da classe. A inicialização de todos os objetos declarados e herdados é realizada, como de costume, no método Init. A estrutura de parâmetros desse método é totalmente herdada do objeto CNeuronResNeXtBottleneck.
bool CNeuronResNeXtBlock::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint chanels_in, uint chanels_out, uint window, uint step, uint units_count, uint group_size, uint groups, ENUM_OPTIMIZATION optimization_type, uint batch) { int units_out = ((int)units_count - (int)window + (int)step - 1) / (int)step + 1; if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, units_out * chanels_out, optimization_type, batch)) return false;
No corpo do método, primeiro definimos a dimensionalidade da sequência na saída do bloco e, em seguida, inicializamos as interfaces básicas herdadas do objeto pai.
Após a execução bem-sucedida das operações do método de inicialização da classe pai, salvamos os parâmetros necessários em variáveis do nosso objeto.
iChanelsOut = chanels_out;
E inicializamos os objetos internos dos fluxos de informação que construímos anteriormente.
int index = 0; if(!cBottleneck.Init(0, index, OpenCL, chanels_in, chanels_out, window, step, units_count, group_size, groups, optimization, iBatch)) return false; index++; if(!cResidual.Init(0, index, OpenCL, chanels_in, chanels_out, units_count, units_out, optimization, iBatch)) return false;
Na saída do bloco, espera-se obter a soma dos valores dos dois fluxos de informação. Portanto, podemos encaminhar completamente o gradiente de erro recebido para ambos os fluxos de dados. Como é habitual nesses casos, para evitar operações de cópia desnecessária de dados, realizamos a substituição dos ponteiros para os respectivos buffers de dados.
if(!cResidual.SetGradient(cBottleneck.getGradient(), true)) return false; if(!SetGradient(cBottleneck.getGradient(), true)) return false; //--- return true; }
Agora só nos resta retornar o resultado lógico da execução das operações para o programa chamador e finalizar o método.
A seguir, passamos à construção dos algoritmos de propagação para frente dentro do método feedForward. Aqui, tudo é bastante direto.
bool CNeuronResNeXtBlock::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cBottleneck.FeedForward(NeuronOCL)) return false; if(!cResidual.FeedForward(NeuronOCL)) return false;
Nos parâmetros do método, recebemos um ponteiro para o objeto de dados brutos, que passamos imediatamente aos métodos de mesmo nome dos dois objetos dos fluxos de informação. Os resultados obtidos são somados e normalizados.
if(!SumAndNormilize(cBottleneck.getOutput(), cResidual.getOutput(), Output, iChanelsOut, true, 0, 0, 0, 1)) return false; //--- result return true; }
O resultado lógico da execução das operações é retornado ao programa chamador, e finalizamos a execução do método de propagação para frente.
Embora à primeira vista a estrutura possa parecer bastante simples, na verdade ela incorpora dois fluxos de informação, o que adiciona certa complexidade na execução das operações de distribuição dos gradientes de erro. O algoritmo responsável por esse processo está implementado no método calcInputGradients.
bool CNeuronResNeXtBlock::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false;
Nos parâmetros do método, recebemos um ponteiro para o objeto de dados brutos utilizado durante a propagação para frente. No entanto, neste caso, precisamos transmitir a ele o gradiente de erro de acordo com a influência dos dados brutos sobre o resultado final do modelo. Só é possível transmitir os dados a um objeto válido. Portanto, antes de continuar, realizamos a verificação da validade do ponteiro recebido.
Após a validação bem-sucedida no bloco de controle, realizamos a transmissão do gradiente de erro por um dos fluxos de informação.
if(!NeuronOCL.calcHiddenGradients(cBottleneck.AsObject())) return false;
Em seguida, antes de transmitir o gradiente de erro do segundo fluxo de informação, precisamos salvar os dados já obtidos. No entanto, não faremos uma cópia completa dos dados. Em vez disso, utilizaremos o mecanismo de substituição de ponteiros para os buffers de dados. O ponteiro para o buffer de gradientes de erro dos dados brutos é armazenado em uma variável local.
CBufferFloat *temp = NeuronOCL.getGradient();
Em seguida, verificamos se o buffer auxiliar está compatível com a dimensionalidade do buffer de gradientes. Se necessário, ajustamos o buffer auxiliar.
if(cBuffer.GetOpenCL() != OpenCL || cBuffer.Total() != temp.Total()) { if(!cBuffer.BufferInitLike(temp)) return false; }
E passamos seu ponteiro para o objeto de dados brutos.
if(!NeuronOCL.SetGradient(GetPointer(cBuffer), false)) return false;
Agora podemos tranquilamente transmitir o gradiente de erro pelo segundo fluxo de informação sem risco de perda de dados.
if(!NeuronOCL.calcHiddenGradients(cResidual.AsObject())) return false;
Os valores dos dois fluxos de informação são somados e os ponteiros para os buffers de dados são restaurados ao estado original.
if(!SumAndNormilize(temp, NeuronOCL.getGradient(), temp, 1, false, 0, 0, 0, 1)) return false; if(!NeuronOCL.SetGradient(temp, false)) return false; //--- return true; }
Depois disso, retornamos o resultado lógico da execução das operações ao programa chamador e finalizamos o método de distribuição do gradiente de erro.
Com isso, encerramos a análise dos algoritmos de construção dos métodos do objeto bloco ResNeXt. O código completo do referido objeto e de todos os seus métodos pode ser consultado no anexo.
Chegamos ao fim do artigo, mas nosso trabalho ainda não terminou. Faremos uma breve pausa e continuaremos o trabalho iniciado no próximo artigo.
Considerações finais
Neste artigo, conhecemos o framework de aprendizado multitarefa baseado na arquitetura ResNeXt, proposto para o processamento de dados financeiros. Esse framework permite extrair e processar eficientemente características, otimizando tarefas de classificação e regressão em cenários com alta dimensionalidade e séries temporais.
Na parte prática do artigo, foram construídos os elementos básicos da arquitetura ResNeXt. No próximo artigo, será abordada a criação do framework de aprendizado multitarefa, além da verificação da eficácia dos métodos implementados em dados históricos reais.
Referências
- Aggregated Residual Transformations for Deep Neural Networks
- Collaborative Optimization in Financial Data Mining Through Deep Learning and ResNeXt
- Outros artigos da série
Programas utilizados no artigo
| # | Nome | Tipo | Descrição |
|---|---|---|---|
| 1 | Research.mq5 | Expert Advisor | EA de coleta de exemplos |
| 2 | ResearchRealORL.mq5 | Expert Advisor | EA de coleta de exemplos com o método Real-ORL |
| 3 | Study.mq5 | Expert Advisor | EA de treinamento de modelos |
| 4 | Test.mq5 | Expert Advisor | EA para teste do modelo |
| 5 | Trajectory.mqh | Biblioteca de classe | Estrutura de descrição do estado do sistema e da arquitetura dos modelos |
| 6 | NeuroNet.mqh | Biblioteca de classe | Biblioteca de classes para criação da rede neural |
| 7 | NeuroNet.cl | Biblioteca | Biblioteca com código de programa em OpenCL |
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/17142
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.
Analisando o código binário dos preços no mercado (Parte II): Convertendo para BIP39 e criando um modelo GPT
Algoritmo de otimização Royal Flush — Royal Flush Optimization (RFO)
Redes neurais em trading: Treinamento multitarefa baseado no modelo ResNeXt (Conclusão)
Redes neurais em trading: Transformador hierárquico com duas torres (Conclusão)
- 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
Dmitry, você tem um grande número de artigos sobre redes neurais.
Você prefere ganhar dinheiro escrevendo artigos em vez de negociando.
Será que é impossível ganhar dinheiro com uma rede neural?