
Redes neurais em trading: Modelo adaptativo multiagente (MASA)
Introdução
As tecnologias computacionais vêm se tornando parte essencial da análise financeira, propondo novas abordagens para resolver problemas complexos. Nos últimos anos, o aprendizado por reforço tem demonstrado sua eficácia na gestão dinâmica de portfólios de investimentos em mercados financeiros turbulentos. No entanto, os métodos existentes frequentemente se concentram em maximizar a rentabilidade, negligenciando o gerenciamento de riscos, especialmente em contextos de incerteza causados por pandemias, desastres naturais e conflitos regionais.
Para superar essa limitação, o estudo "Developing A Multi-Agent and Self-Adaptive Framework with Deep Reinforcement Learning for Dynamic Portfolio Risk Management" propôs o framework adaptativo multiagente MASA (Multi-Agent and Self-Adaptive). Ele é baseado na integração única de dois agentes que interagem: o primeiro otimiza a rentabilidade usando o algoritmo TD3, e o segundo minimiza os riscos com algoritmos evolutivos ou outros métodos de otimização. Adicionalmente, o MASA incorpora um observador de mercado, que utiliza redes neurais profundas para analisar tendências do mercado e transmiti-las como feedback.
Os autores do MASA testaram o modelo com dados dos índices CSI 300, Dow Jones Industrial Average (DJIA) e S&P 500 ao longo dos últimos 10 anos. Os resultados obtidos demonstram a superioridade do MASA em relação às abordagens tradicionais de aprendizado por reforço na gestão de portfólios.
1. Algoritmo MASA
Com o objetivo de superar a armadilha dos métodos de aprendizado por reforço, que tendem a focar na otimização da rentabilidade dos investimentos, os autores do framework propõem uma estrutura adaptativa multiagente (MASA), na qual dois agentes interativos e reativos (um baseado em RL, e outro em um algoritmo de otimização alternativo) são utilizados para implementar um novo esquema de aprendizado por reforço multiagente. Esse esquema busca equilibrar dinamicamente o compromisso entre a rentabilidade geral do portfólio recentemente ajustado e os riscos potenciais, especialmente em condições de grande turbulência nos mercados financeiros.
Na arquitetura proposta pelos autores do framework, o agente de aprendizado por reforço baseado no algoritmo TD3, otimiza a rentabilidade geral do portfólio de investimentos atual. Ao mesmo tempo, o agente baseado em um algoritmo de otimização alternativo trabalha na adaptação adicional do portfólio de investimentos retornado pelo agente de aprendizado por reforço, com o objetivo de minimizar seus riscos potenciais após considerar a avaliação da tendência de mercado fornecida pelo observador de mercado.
Basicamente, graças à separação clara de funções entre os agentes, o modelo aprende e se adapta continuamente ao ambiente financeiro subjacente. A estrutura multiagente MASA pode alcançar portfólios de investimento mais equilibrados tanto em termos de rentabilidade quanto de riscos potenciais, em comparação com portfólios obtidos por abordagens baseadas apenas em RL.
Vale destacar que a estrutura MASA adota um modelo computacional fracamente acoplado e em pipeline entre os três agentes interativos e inteligentes. Dessa forma, a abordagem geral baseada em aprendizado por reforço multiagente é mais estável e confiável, já que a estrutura como um todo continua funcionando mesmo que qualquer agente individual falhe.
Antes do início do processo de treinamento iterativo do modelo, todas as informações relevantes são inicializadas, incluindo a política de aprendizado por reforço, informações sobre o estado do mercado armazenadas no agente Market Observer, e assim por diante.
Durante o treinamento, as informações sobre o estado atual do mercado Ot (por exemplo, a tendência de baixa ou de alta mais recente do mercado financeiro subjacente nos últimos dias de negociação) serão coletadas como dados principais para análise posterior pelo agente de observação do mercado. Além disso, a recompensa pela ação At−1,Final executada anteriormente será obtida como feedback para o algoritmo de aprendizado por reforço e para a revisão da política comportamental do agente de aprendizado por reforço.
Em seguida, é chamado o agente observador de mercado, para calcular a fronteira de risco proposta σs,t e o vetor de mercado Vm,t, como algumas características adicionais para atualização do agente de aprendizado por reforço e do Controlador conforme as condições de mercado mais recentes.
Para manter a flexibilidade e adaptabilidade da estrutura MASA, proposta, podem ser utilizados diversos métodos, incluindo abordagens algorítmicas ou redes neurais profundas. Ainda mais importante, deve-se observar que tanto os agentes baseados em RL quanto os agentes baseados em algoritmos alternativos de otimização já estão protegidos com informações atuais de mercado como o feedback mais valioso extraído do ambiente de negociação existente. As informações fornecidas pelo agente de observação do mercado sobre a conjuntura de mercado prevista são utilizadas exclusivamente como dado complementar para rápida adaptação e aumento da performance tanto do agente de aprendizado por reforço quanto do agente controlador, especialmente quando as condições de mercado recentes são altamente voláteis.
Nas piores situações, quando as características fornecidas pelo agente de monitoramento de mercado podem estar incorretas devido a “ruídos” e induzirem os demais agentes ao erro na definição de ações ideais em determinados dias de negociação, o caráter adaptativo do mecanismo de recompensa baseado em RL permite a adaptação ao ambiente de negociação subjacente ao longo das iterações seguintes do treinamento. Além disso, a capacidade de autocorreção do agente inteligente de monitoramento de mercado durante o treinamento ajuda a garantir que tais ruídos enganosos possam ser corrigidos de forma eficaz e oportuna ao longo de períodos mais longos de negociação.
É interessante notar que, de acordo com os experimentos realizados pelos autores do framework, foram observadas melhorias bastante expressivas no desempenho final dos agentes, tanto os baseados em aprendizado por reforço quanto os que utilizam otimizadores alternativos, mesmo quando se adota uma abordagem algorítmica relativamente simples para a implementação do agente observador de mercado dentro da estrutura MASA nos conjuntos de dados complexos CSI 300, DJIA e S&P 500 ao longo dos últimos 10 anos.
É evidente que, para compreender profundamente o impacto final da informação fornecida pelo agente de observação do mercado sobre os dois outros agentes inteligentes, aos quais deve ser aplicada a estrutura MASA, é necessário conduzir uma análise detalhada com conjuntos de dados mais complexos ou em outras áreas aplicadas.
Após a chamada do agente de observação do mercado, o agente de aprendizado por reforço será ativado para gerar a ação atual At,RL como os pesos do portfólio, os quais poderão ser revisados posteriormente pelo Controlador com base em um algoritmo de otimização alternativo, após considerar sua própria estratégia de gestão de riscos e as condições de mercado propostas pelo agente observador. De forma geral, graças à adoção deste modelo computacional fracamente acoplado e em pipeline, o ambiente resultante MASA continuará funcionando como um sistema MAS, confiável, mesmo no caso de falha de algum agente individual.
Com o auxílio do mecanismo orientador baseado em recompensa adotado pelo framework MASA para reagir de forma cuidadosa a um ambiente em constante mudança, os agentes de tomada de decisão podem aumentar iterativamente o portfólio de investimentos atual, considerando tanto os objetivos de rentabilidade geral quanto os riscos potenciais, com base no valioso feedback do agente de observação do mercado. Ao mesmo tempo, o mecanismo orientador baseado em recompensa utiliza uma medida de divergência baseada em entropia para promover a diversidade nos conjuntos de ações geradas como uma estratégia inteligente e adaptável, para atender às necessidades de um ambiente extremamente volátil nos diversos mercados financeiros.
A visualização do framework MASA desenvolvida pelos autores é apresentada a seguir.
2. Implementação com MQL5
Após examinarmos os aspectos teóricos do framework MASA, passamos à parte prática do nosso trabalho, na qual implementamos nossa própria visão das abordagens propostas utilizando os recursos do MQL5.
Como mencionado anteriormente, o framework MASA inclui 3 agentes. Para tornar o código mais claro e legível, criaremos um objeto para cada agente, que posteriormente será integrado em uma estrutura única.
2.1 Agente de observação do mercado
Começamos criando o agente de observação do mercado. Os autores do framework MASA mencionam a possibilidade de empregar diferentes algoritmos de análise de mercado, desde os métodos analíticos mais simples até modelos profundos complexos. A principal função do agente de observação do mercado é identificar as tendências principais com o objetivo de prever o movimento mais provável que virá a seguir.
Em nossa implementação, adotaremos uma abordagem híbrida. Inicialmente, utilizaremos um algoritmo de representação segmentada linear da série temporal para identificar as tendências atuais. Em seguida, analisaremos as dependências entre as tendências identificadas de sequências unitárias individuais no módulo de atenção com codificação relativa. Por fim, na saída do agente, tentaremos prever o comportamento mais provável do mercado para um determinado horizonte de planejamento utilizando uma MLP.
O algoritmo complexo do agente de observação do mercado descrito acima será implementado dentro de um novo objeto chamado CNeuronMarketObserver. Sua estrutura é apresentada a seguir.
class CNeuronMarketObserver : public CNeuronRMAT { public: CNeuronMarketObserver(void) {}; ~CNeuronMarketObserver(void) {}; virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint heads, uint layers, uint forecast, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronMarketObserver; } };
É fácil perceber que o algoritmo descrito possui uma estrutura linear. Por esse motivo, escolhemos como classe-mãe do nosso novo objeto a CNeuronRMAT, dentro da qual já está organizado o funcionamento de um modelo linear simples. Isso nos permite focar apenas na criação da nova estrutura do agente de observação do mercado dentro do método de inicialização Init. Toda a funcionalidade principal já está organizada nos métodos da classe-mãe.
Nos parâmetros do método de inicialização do objeto Init, recebemos as constantes principais que definem a arquitetura do nosso agente de observação do mercado. Entre elas:
- window — tamanho do vetor de descrição de um elemento da sequência (quantidade de séries temporais unitárias);
- window_key — dimensionalidade das entidades internas do bloco de atenção (Query, Key, Value);
- units_count — profundidade do histórico analisado;
- heads — número de cabeças de atenção;
- layers — número de camadas no bloco de atenção;
- forecast — horizonte de previsão do movimento futuro.
bool CNeuronMarketObserver::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint heads, uint layers, uint forecast, ENUM_OPTIMIZATION optimization_type, uint batch) { //--- Init parent object if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * forecast, optimization_type, batch)) return false;
No corpo do método, chamamos imediatamente o método de mesmo nome da camada totalmente conectada base, que é a classe-mãe comum para todas as camadas neurais da nossa biblioteca. Dentro do método da classe-mãe, está organizada a inicialização das interfaces básicas do nosso objeto.
Atenção para dois pontos. Primeiro, utilizamos o método de inicialização da classe base, e não da classe diretamente superior. Isso porque a arquitetura do nosso agente é bastante diferente da do objeto pai.
Segundo, ao chamar o método da classe-mãe, especificamos o tamanho do objeto como o produto do horizonte de planejamento pelo tamanho do vetor de descrição de um elemento da sequência. Esse é exatamente o tensor que esperamos obter como resultado do funcionamento do nosso agente de observação do mercado.
Em seguida, limpamos o array dinâmico de ponteiros para os objetos internos.
//--- Clear layers' array
cLayers.Clear();
cLayers.SetOpenCL(OpenCL);
Com isso, concluímos a preparação inicial e passamos à organização direta da estrutura do nosso agente de observação do mercado.
Na entrada do modelo, esperamos receber uma série temporal multimodal sob a forma de uma sequência de vetores que descrevem estados individuais do sistema (neste caso, barras). Por isso, para organizar o processamento por sequência unitária, precisamos transpor os dados recebidos.
//--- Tranpose input data int lay_count = 0; CNeuronTransposeOCL *transp = new CNeuronTransposeOCL(); if(!transp || !transp.Init(0, lay_count, OpenCL, units_count, window, optimization, iBatch) || !cLayers.Add(transp)) { delete transp; return false; }
Depois, transformamos essas sequências unitárias para a representação segmentada linear.
//--- Piecewise linear representation lay_count++; CNeuronPLROCL *plr = new CNeuronPLROCL(); if(!plr || !plr.Init(0, lay_count, OpenCL, units_count, window, false, optimization, iBatch) || !cLayers.Add(plr)) { delete plr; return false; }
Para analisar as dependências entre as diferentes sequências unitárias, utilizaremos o módulo de atenção com codificação relativa que já está implementado, especificando a quantidade necessária de camadas internas.
//--- Self-Attention for Variables lay_count++; CNeuronRMAT *att = new CNeuronRMAT(); if(!att || !att.Init(0, lay_count, OpenCL, units_count, window_key, window, heads, layers, optimization, iBatch) || !cLayers.Add(att)) { delete att; return false; }
Com base nos dados obtidos na saída do bloco de atenção, tentaremos prever os valores futuros de cada sequência unitária. Para isso, usaremos um bloco convolucional com conexão residual, o CResidualConv, como uma MLP destinada à previsão independente de cada série temporal unitária.
//--- Forecast mapping lay_count++; CResidualConv *conv = new CResidualConv(); if(!conv || !conv.Init(0, lay_count, OpenCL, units_count, forecast, window, optimization, iBatch) || !cLayers.Add(conv)) { delete conv; return false; }
Por fim, precisamos apenas realizar a transformação reversa dos dados, para apresentá-los na mesma dimensionalidade dos dados originais.
//--- Back transpose forecast lay_count++; transp = new CNeuronTransposeOCL(); if(!transp || !transp.Init(0, lay_count, OpenCL, window, forecast, optimization, iBatch) || !cLayers.Add(transp)) { delete transp; return false; }
Para reduzir o número de operações de cópia de dados, aplicamos a técnica consolidada de substituição de ponteiros para os buffers das interfaces externas de troca de informação.
if(!SetOutput(transp.getOutput(), true) || !SetGradient(transp.getGradient(), true)) return false; //--- return true; }
Concluímos então a execução do método de inicialização, retornando o resultado lógico da operação para o programa que fez a chamada.
Como já foi mencionado anteriormente, toda a funcionalidade principal desta classe é herdada do objeto pai. Sendo assim, concluímos o trabalho sobre o agente de observação do mercado. O código completo desta classe pode ser consultado no anexo.
2.2 Agente de aprendizado por reforço
Na etapa seguinte, passamos à construção do agente de aprendizado por reforço. Na estrutura do framework MASA, ele opera em paralelo com o agente de observação do mercado e realiza uma análise independente da situação de mercado, seguida da tomada de decisão conforme a política de comportamento aprendida.
Os autores do framework MASA propõem o uso de um modelo treinado com o algoritmo TD3 como agente de aprendizado por reforço. Nós iremos nos afastar um pouco dessa proposta de implementação e adotar uma arquitetura diferente para o nosso agente de aprendizado por reforço. Para a análise independente do estado atual do ambiente, utilizaremos o framework PSformer. Já a tomada de decisão com base nessa análise ficará a cargo de um pequeno perceptron que utiliza abordagens de SAM otimização.
O algoritmo do nosso agente de aprendizado por reforço será implementado em um novo objeto chamado CNeuronRLAgent, cuja estrutura está apresentada a seguir.
class CNeuronRLAgent : public CNeuronRMAT { public: CNeuronRLAgent(void) {}; ~CNeuronRLAgent(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint units_count, uint segments, float rho, uint layers, uint n_actions, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) const override { return defNeuronRLAgent; } };
Como se pode notar, utilizamos aqui a mesma abordagem de herança funcional de modelo linear a partir do objeto pai CNeuronRMAT. Sendo assim, basta especificar a nova arquitetura do módulo no método de inicialização Init.
bool CNeuronRLAgent::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint units_count, uint segments, float rho, uint layers, uint n_actions, ENUM_OPTIMIZATION optimization_type, uint batch) { //--- Init parent object if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, n_actions, optimization_type, batch)) return false;
A estrutura dos parâmetros do método é muito semelhante àquela apresentada na construção do agente de observação do mercado. Mas há algumas particularidades. Por exemplo, o horizonte de previsão forecast é substituído pelo espaço de ações do agente n_actions. Além disso, foram adicionados parâmetros específicos como o número de segmentos (segments) e o coeficiente da área de suavização (rho).
Como anteriormente, no corpo do método chamamos o método homônimo da classe ancestral (camada totalmente conectada base), mas agora especificando o espaço de ações do nosso agente de aprendizado por reforço como o tamanho do tensor de saída do objeto.
Depois disso, limpamos o array dinâmico de ponteiros para os objetos internos.
//--- Clear layers' array
cLayers.Clear();
cLayers.SetOpenCL(OpenCL);
Os dados brutos recebidos na entrada do modelo são passados diretamente para o PSformer, cuja quantidade de camadas é criada dentro de um laço.
//--- State observation int lay_count = 0; for(uint i = 0; i < layers; i++) { CNeuronPSformer *psf = new CNeuronPSformer(); if(!psf || !psf.Init(0, lay_count, OpenCL, window, units_count, segments, rho, optimization,iBatch)|| !cLayers.Add(psf)) { delete psf; return false; } lay_count++; }
Em seguida, o nosso agente de aprendizado por reforço toma uma decisão sobre a ação ideal, passando os resultados da análise do estado atual do ambiente pelo bloco de tomada de decisão, que é composto por camadas convolucionais e totalmente conectadas. Primeiro, a camada convolucional reduz a dimensionalidade do tensor de dados de entrada.
CNeuronConvSAMOCL *conv = new CNeuronConvSAMOCL(); if(!conv || !conv.Init(n_actions, lay_count, OpenCL, window, window, 1, units_count, 1, optimization, iBatch) || !cLayers.Add(conv)) { delete conv; return false; } conv.SetActivationFunction(GELU); lay_count++;
Depois, a camada totalmente conectada gera o tensor de ações.
CNeuronBaseSAMOCL *flat = new CNeuronBaseSAMOCL(); if(!flat || !flat.Init(0, lay_count, OpenCL, n_actions, optimization, iBatch) || !cLayers.Add(flat)) { delete flat; return false; } SetActivationFunction(SIGMOID);
Observe que, neste caso, não utilizamos uma política estocástica do Ator. No entanto, não descartamos a possibilidade de sua utilização, o que será discutido mais adiante.
Para o vetor de ações, utilizamos por padrão a função de ativação sigmoide, limitando o intervalo de valores entre 0 e 1. Ainda assim, a função de ativação pode ser alterada externamente a partir do programa chamador.
Agora, basta realizarmos a substituição dos ponteiros para os buffers de dados das interfaces e concluir a execução do método, retornando o resultado lógico da operação para o programa que realizou a chamada.
if(!SetOutput(flat.getOutput(), true) || !SetGradient(flat.getGradient(), true)) return false; //--- return true; }
Com isso, encerramos a análise do objeto do agente de aprendizado por reforço. O código completo desta classe e de todos os seus métodos pode ser consultado no anexo.
2.3 Controlador
Acima, já construímos os objetos de dois dos três agentes. Agora precisamos organizar a funcionalidade do terceiro agente, o Controlador. Sua função é avaliar os riscos e ajustar as ações do agente de aprendizado por reforço com base na análise do estado do ambiente realizada pelo agente de observação do mercado.
Como é fácil perceber, a principal diferença deste último agente é a presença de duas fontes de dados brutos. Precisamos, portanto, analisar as dependências e a influência de uma fonte sobre os valores da outra. Em minha opinião, a estrutura do decodificador Transformer é ideal para essa tarefa. No entanto, em vez dos módulos tradicionais de Self- e Cross-Attention, utilizaremos módulos equivalentes com codificação relativa.
Para implementar esse algoritmo, criaremos um novo objeto chamado CNeuronControlAgent, que herdará a funcionalidade básica da mesma classe CNeuronRMAT. Contudo, a presença de duas fontes de dados exigirá um trabalho adicional de sobrescrita de métodos. A estrutura dessa nova classe é apresentada a seguir.
class CNeuronControlAgent : public CNeuronRMAT { protected: virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override { return false; } virtual bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override { return false; } virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return false; } virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override; public: CNeuronControlAgent(void) {}; ~CNeuronControlAgent(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint heads, uint window_kv, uint units_kv, uint layers, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronControlAgent; } };
Como anteriormente, a estrutura dos objetos internos do nosso agente é definida no método de inicialização Init, cujos parâmetros contêm as constantes conhecidas que descrevem a arquitetura do decodificador Transformer.
bool CNeuronControlAgent::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint heads, uint window_kv, uint units_kv, uint layers, ENUM_OPTIMIZATION optimization_type, uint batch) { //--- Init parent object if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch)) return false; //--- Clear layers' array cLayers.Clear(); cLayers.SetOpenCL(OpenCL);
No corpo do método, de forma análoga aos dois casos anteriores, chamamos o método homônimo da camada totalmente conectada base, especificando a dimensionalidade dos resultados no nível do tensor dos dados principais. Em seguida, limpamos o array dinâmico de ponteiros para os objetos internos.
Depois, passamos à criação da arquitetura do nosso agente controlador. E aqui é importante lembrar que, na saída do agente de observação do mercado, obtemos a previsão do movimento de mercado futuro na forma de uma série temporal multimodal, representada por uma sequência de vetores que descrevem estados individuais do ambiente (no nosso caso, barras).
Aqui podemos considerar duas abordagens: comparar as ações do agente de aprendizado por reforço com cada barra, ou com sequências unitárias separadas. Nesse caso, entendemos que o agente de observação do mercado nos forneceu apenas dados de previsão, cuja probabilidade de realização está longe de 100%. Existe a possibilidade de desvios em absolutamente todos os valores.
Vamos pensar logicamente. Que utilidade terá o vetor de descrição de uma vela prevista, onde há uma alta chance de desvios em cada elemento? Essa é uma questão controversa e difícil de responder sem conhecer a precisão de cada previsão.
Por outro lado, se olharmos para uma sequência unitária individual, além dos valores em si, podemos identificar uma tendência do movimento futuro. E como uma tendência não é formada por um único valor, mas por um conjunto deles, é razoável esperar sua confirmação, mesmo que alguns elementos se desviem.
Além disso, todas as sequências unitárias do nosso conjunto multimodal de séries temporais apresentam certas dependências entre si. E, quando uma tendência prevista em uma sequência unitária é confirmada pelos valores de outra, a probabilidade dessa previsão aumenta.
Considerando isso, decidiu-se analisar a dependência das ações do agente em relação aos valores previstos das sequências unitárias. Para isso, primeiro transferimos os dados da segunda fonte para uma camada neural base especialmente preparada.
int lay_count = 0; CNeuronBaseOCL *flat = new CNeuronBaseOCL(); if(!flat || !flat.Init(0, lay_count, OpenCL, window_kv * units_kv, optimization, iBatch) || !cLayers.Add(flat)) { delete flat; return false; }
Em seguida, reformatamos os dados para a representação de sequências unitárias.
lay_count++; CNeuronTransposeOCL *transp = new CNeuronTransposeOCL(); if(!transp || !transp.Init(0, lay_count, OpenCL, units_kv, window_kv, optimization, iBatch) || !cLayers.Add(transp)) { delete transp; return false; } lay_count++;
Depois, construímos a arquitetura do nosso decodificador, criando o número necessário de camadas dentro de um laço. O número de iterações desse laço é definido pelos parâmetros externos do método de inicialização.
//--- Attention Action To Observation for(uint i = 0; i < layers; i++) { if(units_count > 1) { CNeuronRelativeSelfAttention *self = new CNeuronRelativeSelfAttention(); if(!self || !self.Init(0, lay_count, OpenCL, window, window_key, units_count, heads, optimization, iBatch) || !cLayers.Add(self)) { delete self; return false; } lay_count++; }
Vale observar que, na entrada do decodificador Transformer tradicional, os dados são inicialmente processados por um módulo de Self-Attention. Esse módulo analisa as dependências entre os elementos dos dados de entrada. Em nossa implementação, o substituímos por um módulo equivalente que utiliza codificação relativa. No entanto, só criamos esse módulo se a sequência de entrada contiver mais de um elemento. Afinal, com apenas um elemento, não há o que analisar em termos de dependência, e o módulo de Self-Attention se torna redundante.
O próximo passo é criar o módulo de atenção cruzada, no qual são analisadas as dependências entre os elementos das duas fontes de dados.
CNeuronRelativeCrossAttention *cross = new CNeuronRelativeCrossAttention(); if(!cross || !cross.Init(0, lay_count, OpenCL, window, window_key, units_count, heads, units_kv, window_kv, optimization, iBatch) || !cLayers.Add(cross)) { delete cross; return false; } lay_count++;
Cada camada do decodificador é finalizada por um bloco FeedForward, que aqui é representado por um bloco convolucional com realimentação.
CResidualConv *ffn = new CResidualConv(); if(!ffn || !ffn.Init(0, lay_count, OpenCL, window, window, units_count, optimization, iBatch) || !cLayers.Add(ffn)) { delete ffn; return false; } lay_count++; }
Feito isso, passamos para a próxima iteração do laço e criamos uma nova camada do decodificador.
Vale mencionar que, assim como na arquitetura do decodificador Transformer, a saída do bloco convolucional com realimentação passa por normalização. No entanto, pode ser necessário restringir o espaço de ações do Ator a um intervalo definido, o que normalmente é feito por uma função de ativação. Por isso, após a criação das camadas necessárias do decodificador, adicionamos mais uma camada convolucional, especificando a função de ativação desejada.
CNeuronConvSAMOCL *conv = new CNeuronConvSAMOCL(); if(!conv || !conv.Init(0, lay_count, OpenCL, window, window, window, units_count, 1, optimization, iBatch) || !cLayers.Add(conv)) { delete conv; return false; } SetActivationFunction(SIGMOID);
Assim como no agente de aprendizado por reforço, utilizamos por padrão a função de ativação sigmoide. Ainda assim, ela pode ser alterada externamente pelo programa.
Ao final do método de inicialização, fazemos a substituição dos ponteiros para os buffers das interfaces e retornamos o resultado lógico da operação para o programa chamador.
//--- if(!SetOutput(conv.getOutput(), true) || !SetGradient(conv.getGradient(), true)) return false; //--- return true; }
Depois de concluir a inicialização do objeto, passamos à construção dos algoritmos de propagação para frente, que implementamos no método feedForward.
bool CNeuronControlAgent::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) { if(!SecondInput) return false;
Nos parâmetros do método, recebemos ponteiros para dois objetos de dados de entrada. O fluxo principal de dados é apresentado como uma camada neural, e os dados adicionais são passados por meio de um buffer. Para facilitar o trabalho com ambas as fontes, substituímos o buffer de dados de saída de uma camada interna criada especificamente, utilizando o objeto recebido nos parâmetros.
CNeuronBaseOCL *second = cLayers[0]; if(!second) return false; if(!second.SetOutput(SecondInput, true)) return false;
Em seguida, transpondo o tensor da segunda fonte de dados, representamos a série temporal multimodal como uma sequência de séries unitárias.
second = cLayers[1]; if(!second || !second.FeedForward(cLayers[0])) return false;
Organizamos então um laço que percorre sequencialmente as camadas neurais internas, chamando seus métodos de propagação para frente e passando os dados de ambas as fontes.
CNeuronBaseOCL *first = NeuronOCL; CNeuronBaseOCL *main = NULL; for(int i = 2; i < cLayers.Total(); i++) { main = cLayers[i]; if(!main || !main.FeedForward(first, second.getOutput())) return false; first = main; } //--- return true; }
Após a execução bem-sucedida de todas as iterações, basta retornarmos o resultado lógico da operação ao programa chamador e concluir o método.
Como se pode notar, o algoritmo do método de propagação para frente é bastante simples. Isso se deve ao uso de blocos já prontos para construir uma arquitetura mais complexa.
No entanto, existe certa complexidade na construção do método de distribuição do gradiente de erro, relacionada ao uso da segunda fonte de dados. Isso ocorre porque, pela via principal, os dados são passados sequencialmente de uma camada interna para a próxima. Já a segunda fonte de dados é comum a todas as camadas do decodificador, mais precisamente, a todos os módulos de atenção cruzada. Portanto, o gradiente de erro da segunda fonte de dados precisa ser reunido a partir de todos os módulos de atenção cruzada. Vamos analisar como isso é resolvido no código.
O algoritmo de distribuição do gradiente de erro é implementado no método calcInputGradients. Nos parâmetros deste método, recebemos ponteiros para os objetos das duas fontes de dados e seus respectivos gradientes de erro. Nosso objetivo é distribuir o gradiente de erro entre as fontes, de acordo com sua contribuição para o resultado.
bool CNeuronControlAgent::calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = -1) { if(!NeuronOCL || !SecondGradient) return false;
No corpo do método, verificamos imediatamente a validade dos ponteiros recebidos. Afinal, não podemos enviar dados para objetos inexistentes.
Assim como na propagação para frente, a segunda fonte de dados é representada por buffers. E fazemos a substituição dos ponteiros na camada interna correspondente.
CNeuronBaseOCL *main = cLayers[0]; if(!main) return false; if(!main.SetGradient(SecondGradient, true)) return false; main.SetActivationFunction(SecondActivation); //--- CNeuronBaseOCL *second = cLayers[1]; if(!second) return false; second.SetActivationFunction(SecondActivation);
Além disso, sincronizamos as funções de ativação da camada interna e da camada de transposição de dados com a função de ativação da fonte original. Isso é necessário para a transmissão correta do gradiente de erro.
Depois, a função de segunda fonte de dados passa a ser desempenhada pela nossa camada interna de transposição. Para facilitar o trabalho, salvamos os ponteiros dos objetos de interface dela em variáveis locais.
CBufferFloat *second_out = second.getOutput(); CBufferFloat *second_gr = second.getGradient(); CBufferFloat *temp = second.getPrevOutput(); if(!second_gr.Fill(0)) return false;
E limpamos obrigatoriamente o buffer de gradientes de erro de dados previamente acumulados.
Em seguida, organizamos um laço para percorrer as camadas neurais internas em ordem reversa. Vale destacar que, dentro desse laço, operamos apenas com os objetos do decodificador.
Lembrando que os dois primeiros objetos internos são usados para tratar a segunda fonte de dados.
for(int i = cLayers.Total() - 2; i >= 2; i--) { main = cLayers[i]; if(!main) return false;
Dentro do laço, retiramos do array o ponteiro para o objeto da camada correspondente e verificamos sua validade.
Precisamos lembrar que nem todos os objetos do decodificador trabalham com duas fontes de dados. Por isso, o algoritmo se ramifica conforme o tipo do objeto que transmite o gradiente de erro. No caso de um módulo de atenção cruzada, primeiro transmitimos o gradiente da segunda fonte de dados da camada atual para um buffer temporário. Em seguida, somamos os valores obtidos com os dados previamente acumulados.
if(cLayers[i + 1].Type() == defNeuronRelativeCrossAttention) { if(!main.calcHiddenGradients(cLayers[i + 1], second_out, temp, SecondActivation) || !SumAndNormilize(temp, second_gr, second_gr, 1, false, 0, 0, 0, 1)) return false; }
Nos demais casos, transmitimos apenas o gradiente pela via principal e passamos para a próxima iteração do laço.
else { if(!main.calcHiddenGradients(cLayers[i + 1])) return false; } }
Após executar todas as iterações, precisamos transmitir o gradiente de erro para o nível dos dados de entrada. Primeiro, seguimos pela via principal e enviamos o gradiente ao primeiro fluxo de dados.
if(!NeuronOCL.calcHiddenGradients(main.AsObject(), second_out, temp, SecondActivation)) return false;
Mas devemos lembrar que a primeira camada do decodificador pode ser tanto um módulo de Self-Attention quanto de Cross-Attention. No segundo caso, duas fontes de dados são utilizadas. Portanto, precisamos verificar o tipo do objeto que transmite o gradiente de erro e, se necessário, adicionar o gradiente da segunda fonte aos valores acumulados.
if(main.Type() == defNeuronRelativeCrossAttention) { if(!SumAndNormilize(temp, second_gr, second_gr, 1, false, 0, 0, 0, 1)) return false; }
Agora transmitimos todo o gradiente acumulado pela segunda via de dados até o respectivo nível da fonte correspondente.
main = cLayers[0]; if(!main.calcHiddenGradients(second.AsObject())) return false; //--- return true; }
Em seguida, encerramos o método, retornando previamente o resultado lógico da operação ao programa que realizou a chamada.
O algoritmo do método de atualização dos parâmetros do modelo, updateInputWeights, é bastante simples. Nele, apenas chamamos em laço os métodos de mesmo nome nos objetos internos que contêm parâmetros treináveis. Não vamos nos aprofundar em sua análise neste momento. Mas vale lembrar que a arquitetura construída para esse objeto utiliza componentes com SAM otimização. Portanto, a iteração sobre os objetos internos deve ser feita em ordem reversa.
Com isso, encerramos a análise dos algoritmos de construção dos métodos do agente controlador. O código completo da nova classe e de todos os seus métodos pode ser consultado no anexo.
Hoje trabalhamos bastante, mas nosso trabalho ainda não terminou. Faremos apenas uma pequena pausa e, no próximo artigo, levaremos tudo a uma conclusão lógica.
Considerações finais
Exploramos uma abordagem inovadora para o gerenciamento de portfólios de investimento em mercados financeiros instáveis: a estrutura adaptativa multiagente MASA. O framework proposto combina com sucesso os benefícios dos algoritmos de aprendizado por reforço para otimização da rentabilidade, com métodos adaptativos de otimização para minimização de riscos, além de incluir um módulo de observação de mercado para análise de tendências atuais.
Na parte prática, implementamos cada um dos agentes propostos utilizando MQL5 como módulos separados. No próximo artigo, uniremos esses módulos em uma estrutura integrada e avaliaremos a eficácia das soluções implementadas em dados históricos reais.
Links
- Developing A Multi-Agent and Self-Adaptive Framework with Deep Reinforcement Learning for Dynamic Portfolio Risk Management
- Outros artigos da série
Programas utilizados neste artigo
# | Nome | Tipo | Descrição |
---|---|---|---|
1 | Research.mq5 | Expert Advisor | EA para coleta de amostras |
2 | ResearchRealORL.mq5 | Expert Advisor | EA de coleta de amostras com método Real-ORL |
3 | Study.mq5 | Expert Advisor | EA para treinamento de modelos |
4 | Test.mq5 | Expert Advisor | EA para teste de modelos |
5 | Trajectory.mqh | Biblioteca de classe | Estrutura de descrição do estado do sistema |
6 | NeuroNet.mqh | Biblioteca de classe | Biblioteca de classes para criação de rede neural |
7 | NeuroNet.cl | Biblioteca | Biblioteca de código do programa OpenCL |
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/16537
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.





- 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