Redes neurais de maneira fácil (Parte 41): Modelos Hierárquicos
Introdução
Neste artigo, exploraremos a aplicação do aprendizado hierárquico por reforço na negociação. Propomos usar essa abordagem para criar um modelo de negociação hierárquico que seja capaz de tomar decisões ótimas em diferentes níveis e se adaptar a diversas condições de mercado.
No artigo, descreveremos a arquitetura do modelo hierárquico, incluindo diferentes níveis de tomada de decisão, como a definição de pontos de entrada e saída de negociações. Também apresentaremos métodos de treinamento do modelo hierárquico que combinam o aprendizado por reforço em nível global e em níveis locais.
O uso de aprendizado hierárquico é um dos enfoques específicos para o treinamento desses modelos. Isso contribui para aumentar a capacidade do modelo de generalização e sua adaptabilidade às condições cambiantes do mercado.
1. Vantagens dos modelos hierárquicos
Nos últimos anos, o uso de modelos hierárquicos no campo da negociação tem recebido cada vez mais atenção e pesquisa. O aprendizado hierárquico é um método poderoso que permite modelar estruturas de tomada de decisão hierárquicas complexas. No contexto da negociação, isso pode trazer várias vantagens significativas.
A primeira vantagem reside na capacidade do modelo hierárquico de se adaptar a diferentes condições de mercado. O modelo pode analisar fatores macroeconômicos em níveis mais elevados, como eventos políticos ou indicadores econômicos, enquanto ao mesmo tempo considera fatores microeconômicos em níveis mais baixos, como análise técnica ou informações específicas de ativos. Isso permite que o modelo tome decisões mais informadas e se adapte a diferentes situações no mercado.
A segunda vantagem está relacionada ao uso mais eficiente das informações disponíveis. Os modelos hierárquicos permitem modelar e usar informações em diferentes níveis da hierarquia. As estratégias de alto nível podem considerar tendências gerais e padrões, enquanto as estratégias de baixo nível podem levar em conta dados mais precisos e de rápida mudança. Isso permite que o modelo obtenha uma visão mais completa do mercado e tome decisões mais fundamentadas.
A terceira vantagem dos modelos hierárquicos está na capacidade de distribuir eficazmente os recursos computacionais. Estratégias de alto nível podem ser treinadas em escalas de tempo mais amplas, enquanto estratégias de baixo nível podem ser mais sensíveis a dados que mudam rapidamente em escalas de tempo menores. Isso permite o uso eficiente dos recursos de computação e acelera o processo de treinamento do modelo.
A quarta vantagem está relacionada à melhoria da estabilidade e portabilidade das estratégias. Modelos hierárquicos têm maior capacidade de generalização, pois são capazes de modelar conceitos abstratos e dependências em diferentes níveis hierárquicos. Isso permite a criação de estratégias robustas que podem ser aplicadas com sucesso em diferentes condições e adaptadas a diversos mercados e ativos.
A quinta vantagem do uso de modelos hierárquicos está na capacidade de decompor tarefas complexas de negociação em tarefas mais simples. Isso reduz a complexidade da tarefa e simplifica o processo de treinamento. Cada nível hierárquico pode lidar com aspectos específicos da negociação, como a definição de pontos de entrada e saída de negociações, gerenciamento de riscos ou alocação de portfólio. Isso contribui para um treinamento mais eficaz do modelo e melhora a qualidade de suas decisões.
Por fim, o uso de modelos hierárquicos promove uma melhor interpretabilidade dos resultados e das decisões tomadas. Como o modelo possui uma estrutura hierárquica clara, é mais fácil compreender quais fatores e variáveis influenciam as decisões em cada nível. Isso permite que traders e pesquisadores compreendam melhor as razões e resultados de suas estratégias, bem como façam ajustes necessários.
Portanto, o uso de modelos hierárquicos em tarefas de negociação oferece várias vantagens, incluindo adaptação às condições de mercado, uso eficiente de informações, distribuição de recursos computacionais, estabilidade e portabilidade das estratégias, decomposição de tarefas complexas e uma melhor interpretabilidade dos resultados. Essas vantagens tornam os modelos hierárquicos uma ferramenta poderosa para o desenvolvimento de estratégias de negociação bem-sucedidas.
A aplicação de modelos hierárquicos na negociação requer abordagens especiais de treinamento. Métodos tradicionais de treinamento usados em modelos de um único nível nem sempre são adequados para modelos hierárquicos devido à sua estrutura complexa e às interconexões entre os níveis.
O uso de aprendizado hierárquico é um dos enfoques específicos para o treinamento desses modelos. Nesse caso, o modelo é treinado em etapas em diferentes níveis hierárquicos, começando nos níveis mais baixos e progredindo gradualmente para os mais altos. Durante o treinamento em cada nível, o modelo utiliza informações obtidas nos níveis anteriores, permitindo-lhe capturar dependências e conceitos mais abstratos nos níveis superiores da hierarquia.
Além disso, uma abordagem importante envolve a combinação de aprendizado por reforço e aprendizado supervisionado. Nesse cenário, o modelo é treinado com base nas recompensas obtidas durante a execução de tarefas de reforço, bem como em exemplos de treinamento fornecidos em cada nível hierárquico. Essa abordagem permite que o modelo aprenda com a experiência de outros agentes e aplique conhecimentos já adquiridos nos níveis mais altos da hierarquia.
Um aspecto crucial no treinamento de modelos hierárquicos é a capacidade de se adaptar a condições em constante mudança. O modelo deve ser flexível e capaz de se ajustar rapidamente a novas condições de mercado e mudanças nos dados. Para isso, pode-se empregar o aprendizado dinâmico, que envolve a regularização periódica do modelo e a atualização de seus parâmetros com base em novos dados.
Um exemplo notável de algoritmo de treinamento de modelos hierárquicos na negociação é o Scheduled Auxiliary Control (SAC-X).
O algoritmo Scheduled Auxiliary Control (SAC-X) é um método de aprendizado por reforço que utiliza uma estrutura hierárquica para tomada de decisões e representa uma nova abordagem para resolver problemas com recompensas esparsas. Ele se baseia em quatro princípios fundamentais:
- Cada par de estado-ação é acompanhado por um vetor de recompensas, composto por recompensas externas (geralmente esparsas) e recompensas internas auxiliares (também geralmente esparsas).
- Cada entrada de recompensa é atribuída a uma política chamada de intenção, que é treinada para maximizar a recompensa acumulada correspondente.
- Há um planejador de alto nível que escolhe e executa intenções individuais com o objetivo de melhorar o desempenho do agente nas tarefas externas.
- O aprendizado ocorre fora das políticas (de forma assíncrona em relação à execução das políticas), e a experiência entre intenções é compartilhada para o uso eficiente das informações.
O algoritmo SAC-X utiliza esses princípios para resolver eficazmente problemas com recompensas esparsas. Vetores de recompensas permitem o aprendizado em diferentes aspectos da tarefa e a criação de várias intenções, cada uma maximizando sua própria recompensa. O planejador coordena a execução das intenções, selecionando a estratégia ótima para alcançar as metas externas. O treinamento ocorre fora da política, permitindo o uso de experiência de várias intenções para um aprendizado eficaz.
Essa abordagem permite que o agente resolva eficazmente problemas com recompensas esparsas, aprendendo com recompensas internas e externas. O uso do planejador também facilita a coordenação de ações e envolve a troca de experiências entre as intenções, contribuindo para a utilização eficiente de informações e aumentando o desempenho geral do agente.
O SAC-X proporciona um aprendizado mais eficiente e flexível do agente em ambientes com recompensas esparsas. Sua característica fundamental é o uso de recompensas internas auxiliares, que ajudam a superar a escassez de recompensas e facilitam o treinamento em tarefas com recompensas baixas.
Durante o treinamento do SAC-X, cada intenção possui sua própria política que maximiza a recompensa auxiliar correspondente. Um planejador determina quais intenções serão escolhidas e executadas a qualquer momento. Isso permite ao agente aprender em diversos aspectos da tarefa e usar efetivamente as informações disponíveis para alcançar resultados ótimos.
Uma das principais vantagens do SAC-X é sua capacidade de trabalhar com várias tarefas externas diferentes. O algoritmo pode ser ajustado para funcionar com diferentes funções-alvo e se adaptar a ambientes e tarefas variadas. Isso permite que o SAC-X seja aplicado em uma ampla gama de domínios.
Além disso, a troca de experiência assíncrona entre as intenções contribui para a utilização eficaz das informações. O agente pode aprender com intenções bem-sucedidas e usar esse conhecimento para melhorar seu desempenho. Isso permite que o agente encontre estratégias ótimas para resolver tarefas complexas de maneira mais rápida e eficaz.
Em resumo, o algoritmo Scheduled Auxiliary Control (SAC-X) representa uma abordagem inovadora para o treinamento de agentes em ambientes com recompensas esparsas. Ele combina o uso de recompensas auxiliares internas e externas, um planejador e aprendizado assíncrono para alcançar alto desempenho e adaptabilidade do agente. O SAC-X oferece novas possibilidades para resolver problemas complexos e pode ser aplicado em diversas aplicações onde recompensas esparsas são um desafio.
O procedimento do SAC-X pode ser descrito da seguinte forma:
- Inicialização: começa com a inicialização de políticas para cada intenção e seus vetores de recompensa correspondentes. Também é inicializado o planejador, que escolherá e executará intenções.
- Laço de treinamento:
- Coleta de experiência: O agente interage com o ambiente, executando ações com base nas intenções selecionadas. Ele coleta experiência na forma de estados, ações, recompensas externas e recompensas internas auxiliares.
- Atualização das intenções: Para cada intenção, ocorre uma atualização da política correspondente usando a experiência coletada. A política é ajustada para maximizar a recompensa auxiliar cumulativa associada a essa intenção.
- Planejamento: O planejador escolhe qual intenção será executada no próximo passo com base no estado atual e nas intenções anteriores. O objetivo do planejador é melhorar o desempenho geral do agente nas tarefas externas.
- Treinamento assíncrono: As políticas e o planejador são atualizados de forma assíncrona, permitindo ao agente utilizar eficientemente as informações e experiências adquiridas.
- Conclusão: O algoritmo continua o ciclo de treinamento até atingir um critério de parada definido, como atingir um desempenho específico ou um número predeterminado de iterações.
O algoritmo SAC-X permite que o agente utilize efetivamente recompensas auxiliares internas e externas para aprendizado e escolha as melhores intenções para alcançar resultados ótimos em tarefas externas. Isso resolve o problema da escassez de recompensas e melhora o desempenho do agente em ambientes com recompensas baixas.
2. Implementação em MQL5
O algoritmo Scheduled Auxiliary Control (SAC-X) envolve o treinamento assíncrono de agentes com a capacidade de compartilhar experiências entre diferentes agentes. Para realizar esse processo, assim como no artigo anterior, dividiremos todo o processo de treinamento em 2 etapas:
- Coleta de experiência
- Políticas de treinamento (estratégias de comportamento dos agentes)
Para coletar experiência, primeiro criaremos 2 estruturas. Na primeira estrutura SState, registraremos a descrição de um estado individual do sistema. Ela conterá apenas um array estático para armazenar valores de ponto flutuante.
struct SState { float state[HistoryBars * 12 + 9]; //--- SState(void); //--- bool Save(int file_handle); bool Load(int file_handle); //--- overloading void operator=(const SState &obj) { ArrayCopy(state, obj.state); } };
Para maior comodidade, criaremos métodos de trabalho com arquivos Save e Load na estrutura. O código dos métodos é bastante simples e você pode examiná-los por conta própria no anexo.
A segunda estrutura, STrajectory, conterá todas as informações sobre a experiência acumulada do agente em um único episódio. Nela, você pode notar 3 arrays estáticos:
- States — um array de estados. Este é um array de estruturas previamente criadas onde todos os estados visitados pelo agente serão registrados.
- Actions — um array de ações realizadas pelo agente.
- Revards — um array de recompensas obtidas da ambiente externo.
Além disso, adicionaremos 3 variáveis:
- Total — o número de estados visitados.
- DiscountFactor — o fator de desconto.
- CumCounted — uma sinalizador que indica se o recálculo da recompensa cumulativa deve ser realizado levando em consideração o fator de desconto.
struct STrajectory { SState States[Buffer_Size]; int Actions[Buffer_Size]; float Revards[Buffer_Size]; int Total; float DiscountFactor; bool CumCounted; //--- STrajectory(void); //--- bool Add(SState &state, int action, float reward); void CumRevards(void); //--- bool Save(int file_handle); bool Load(int file_handle); };
Ao contrário da estrutura de descrição de estados individuais discutida anteriormente, nesta estrutura, criaremos um construtor. Nele, inicializaremos os arrays e as variáveis com valores iniciais.
STrajectory::STrajectory(void) : Total(0), DiscountFactor(0.99f), CumCounted(false) { ArrayInitialize(Actions, -1); ArrayInitialize(Revards, 0); }
Observe que no construtor definimos o número total de estados visitados como "0" e o sinalizador CumCounted para calcular a recompensa acumulada como falso. Calcularemos a recompensa acumulada antes de salvar os dados no arquivo. Esses valores serão necessários durante o treinamento do modelo.
Usaremos o método Add para adicionar conjuntos de estado-ação-recompensa ao banco de dados.
bool STrajectory::Add(SState &state, int action, float reward) { if(Total + 1 >= ArraySize(Actions)) return false; States[Total] = state; Actions[Total] = action; if(Total > 0) Revards[Total - 1] = reward; Total++; //--- return true; }
Observe que salvamos a recompensa para o estado anterior. Isso ocorre porque ela é obtida durante a transição do estado anterior para o estado atual ao executar a ação escolhida pelo agente no estado anterior. Dessa forma, mantemos a relação causal entre a ação e a recompensa.
O método de cálculo da recompensa acumulada CumRevards é bastante simples. No entanto, observe o controle do sinalizador CumCounted concluído. Isso é crucial, pois impede o cálculo repetido da recompensa acumulada, o que pode distorcer fundamentalmente os dados do conjunto de treinamento e, como resultado, o treinamento geral do modelo.
void STrajectory::CumRevards(void) { if(CumCounted) return; //--- for(int i = Buffer_Size - 2; i >= 0; i--) Revards[i] += Revards[i + 1] * DiscountFactor; CumCounted = true; }
Quanto aos métodos de trabalho com arquivos, sugiro que você os examine por conta própria no anexo. Agora, vamos passar para os "trabalhadores" reais - nossos EAs.
O primeiro Expert Advisor para coleta de experiência será criado no arquivo Research.mq5. Planejamos executar este EA no modo de otimização do testador de estratégias para coletar experiência em várias passagens do agente através de um episódio de treinamento com dados históricos. Exatamente a mesma abordagem foi utilizada na Fase 1 no artigo anterior. Assim como no EA "Fasa1.mql5", usaremos os métodos OnTester, OnTesterInit, OnTesterPass e OnTesterDeinit para coletar e salvar informações de diferentes passagens em um único buffer de experiência. A diferença é que agora usaremos nosso modelo para escolher as ações, em vez de um gerador de números aleatórios como no EA mencionado.
Os parâmetros externos do nosso EA foram copiados das versões anteriores sem alterações. Neles, especificamos o período de tempo de trabalho e os parâmetros dos indicadores utilizados.
//+------------------------------------------------------------------+ //| Input parameters | //+------------------------------------------------------------------+ input ENUM_TIMEFRAMES TimeFrame = PERIOD_H1; //--- input group "---- RSI ----" input int RSIPeriod = 14; //Period input ENUM_APPLIED_PRICE RSIPrice = PRICE_CLOSE; //Applied price //--- input group "---- CCI ----" input int CCIPeriod = 14; //Period input ENUM_APPLIED_PRICE CCIPrice = PRICE_TYPICAL; //Applied price //--- input group "---- ATR ----" input int ATRPeriod = 14; //Period //--- input group "---- MACD ----" input int FastPeriod = 12; //Fast input int SlowPeriod = 26; //Slow input int SignalPeriod = 9; //Signal input ENUM_APPLIED_PRICE MACDPrice = PRICE_CLOSE; //Applied price input int Agent=1;
Para fins de execução do otimizador de estratégias, adicionaremos o parâmetro Agent. No código do EA, ele não é usado e serve apenas para controlar o número de agentes no otimizador do testador de estratégias.
Na área de variáveis globais, declararemos um elemento da estrutura SState para registrar o estado atual do sistema. Uma estrutura de trajetória STrajectory para armazenar a experiência do agente atual. E declararemos um array estático de trajetórias com um elemento, que usaremos para transferir experiência entre quadros.
SState sState; STrajectory Base; STrajectory Buffer[]; STrajectory Frame[1]; CNet Actor; CFQF Schedule; int Models = 1;
Aqui também definiremos variáveis para criar duas redes neurais: Agente e Planejador. Quero esclarecer que usaremos vários agentes dentro de um único modelo de agente. No entanto, discutiremos isso em mais detalhes ao descrever a arquitetura dos modelos.
No método de inicialização do EA, não há inovações. Inicializamos objetos de indicadores, a classe de negociação. Carregamos modelos pré-treinados. E se não os encontrarmos, criamos novos com parâmetros aleatórios. Você pode examinar o código completo do método no anexo.
Gostaria de me concentrar no método de descrição da arquitetura dos modelos CreateDescriptions. Pretendemos treinar nossos agentes de intenções usando o método Ator-Crítico. Portanto, criaremos descrições para três modelos:
- Agente (Ator)
- Crítico
- Agendador (modelo do nível superior da hierarquia).
Não se assuste pelo fato de variáveis globais terem sido declaradas para 2 modelos ao criar a descrição de 3 modelos. A razão é que na fase de coleta de dados não treinaremos os modelos. Portanto, a funcionalidade do crítico não é usada. Portanto, não criamos seu modelo.
Ao mesmo tempo, para criar modelos comparáveis, criamos um método comum para declarar a arquitetura dos modelos. Que usaremos tanto na fase de coleta de dados quanto na fase de treinamento dos modelos.
Nos parâmetros do método, recebemos ponteiros para 3 objetos para passar as arquiteturas dos modelos criados. No corpo do método, verificamos a relevância dos ponteiros recebidos e, se necessário, criamos novos objetos.
bool CreateDescriptions(CArrayObj *actor, CArrayObj *critic, CArrayObj *scheduler) { //--- if(!actor) { actor = new CArrayObj(); if(!actor) return false; } //--- if(!critic) { critic = new CArrayObj(); if(!critic) return false; } //--- if(!scheduler) { scheduler = new CArrayObj(); if(!scheduler) return false; }
Primeiro, criamos uma descrição da arquitetura do Ator (agente). Como sempre, começamos com uma camada totalmente conectada. E depois disso, há uma camada de normalização de dados.
//--- Actor actor.Clear(); CLayerDescription *descr; //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; int prev_count = descr.count = (int)(HistoryBars * 12 + 9); descr.window = 0; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; } //--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = 1000; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
Em seguida, coloquei outra camada totalmente conectada.
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = 300; descr.optimization = ADAM; descr.activation = SIGMOID; if(!actor.Add(descr)) { delete descr; return false; }
A seguir, a camada convolucional tentará identificar certos padrões nos dados.
//--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = 100; descr.window = 3; descr.step = 3; descr.window_out = 2; descr.activation = LReLU; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
E processaremos seus resultados com uma camada totalmente conectada.
//--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = 100; descr.optimization = ADAM; descr.activation = SIGMOID; if(!actor.Add(descr)) { delete descr; return false; }
Em seguida, adicionaremos mais uma camada convolucional.
//--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = 50; descr.window = 2; descr.step = 2; descr.window_out = 4; descr.activation = SIGMOID; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
Como resultado, esse "bolo em camadas" reduzirá a dimensionalidade dos dados para 100 elementos. Essa arquitetura servirá para o pré-processamento dos dados.
A seguir, precisaremos criar vários agentes de intenção. Para evitar criar várias modelos, usaremos nossos desenvolvimentos anteriores e utilizaremos a classe de camada neural totalmente conectada multi-modelo CNeuronMultiModel. Primeiro, criaremos uma camada totalmente conectada com um tamanho adequado.
//--- layer 6 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = 1000; descr.optimization = ADAM; descr.activation = TANH; if(!actor.Add(descr)) { delete descr; return false; }
E então criaremos 2 camadas neurais totalmente conectadas multi-modelo ocultas, cada uma com 10 modelos.
//--- layer 7 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMultiModels; descr.count = 200; descr.window = 100; descr.step = 10; descr.activation = TANH; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; } //--- layer 8 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMultiModels; descr.count = 50; descr.window = 200; descr.step = 10; descr.activation = TANH; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
Na fase final do modelamento, criaremos uma camada de saída de resultados que possui um recurso especial. A saída de nosso Ator deve ser um distribuição probabilística de ações. Quando examinamos o método de gradiente de política, lidamos com problemas semelhantes, normalizando a saída com a função SoftMax para um único vetor de resultados. Agora, precisamos normalizar os resultados de 10 modelos.
Graças ao uso de nossa camada neural totalmente conectada multi-modelo, os resultados de todos os 10 modelos são armazenados em um único array. Podemos usar nossa camada CNeuronSoftMaxOCL para normalizar os dados. Na inicialização dessa camada, especificamos que precisamos normalizar um array composta por 10 linhas.
//--- layer 9 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMultiModels; descr.count = 4; descr.window = 50; descr.step = 10; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; } //--- layer 10 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronSoftMaxOCL; descr.count = 4; descr.step = 10; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
Desenvolvemos um modelo com um único bloco de pré-processamento de dados, seguido por 10 atores em paralelo (agentes de intenção). Cada ator tem uma distribuição probabilística de ações na saída.
Da mesma forma, o modelo do crítico é criado com 10 críticos na saída. No entanto, na saída do crítico, esperamos obter o valor da função de valor (value) para cada ação. Portanto, no modelo do crítico, não usamos a camada SoftMax.
No contexto deste algoritmo, o modelo do planejador é uma modelo clássica de um único nível. No entanto, dentro deste algoritmo, o planejador não seleciona a ação do agente, mas escolhe um Ator específico de nosso grupo para seguir sua política na situação atual. O planejador tem a capacidade de avaliar o estado atual do sistema para escolher o agente de intenção apropriado. Ele também pode solicitar estados dos agentes para tomar decisões.
Nesta implementação, propomos fornecer ao planejador um vetor concatenado contendo o estado atual do sistema analisado e o vetor de resultados do pool de Atores. Isso permite ao planejador usar informações sobre o estado do sistema e as estimativas dos Atores para selecionar o Ator de intenção apropriado.
Na descrição do modelo do planejador, especificaremos uma camada de dados de entrada do tamanho correspondente.
//--- Scheduler scheduler.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = (int)(HistoryBars * 12 + 9+40); descr.window = 0; descr.activation = None; descr.optimization = ADAM; if(!scheduler.Add(descr)) { delete descr; return false; }
Em seguida, segue-se uma camada de normalização dos dados de entrada.
//--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = 1000; descr.activation = None; descr.optimization = ADAM; if(!scheduler.Add(descr)) { delete descr; return false; }
Para o processamento dos dados de entrada, é aplicada uma abordagem modular, semelhante à descrita anteriormente.
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = 300; descr.optimization = ADAM; descr.activation = SIGMOID; if(!scheduler.Add(descr)) { delete descr; return false; } //--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = 100; descr.window = 3; descr.step = 3; descr.window_out = 2; descr.activation = LReLU; descr.optimization = ADAM; if(!scheduler.Add(descr)) { delete descr; return false; } //--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = 100; descr.optimization = ADAM; descr.activation = SIGMOID; if(!scheduler.Add(descr)) { delete descr; return false; } //--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = 50; descr.window = 2; descr.step = 2; descr.window_out = 4; descr.activation = SIGMOID; descr.optimization = ADAM; if(!scheduler.Add(descr)) { delete descr; return false; }
No bloco de tomada de decisões, usamos um perceptron com duas camadas ocultas. Este é um modelo de rede neural multicamada que permite processar e analisar os dados de entrada usando vários níveis de abstração e características de alto nível. O uso de duas camadas ocultas dá ao modelo maior expressividade e capacidade de capturar dependências complexas entre os dados de entrada e as decisões de saída.
Na saída deste perceptron, aplicamos uma função de quantil completamente parametrizável. A função de quantil nos permite modelar a distribuição condicional da variável de destino com base nos dados de entrada. Em vez de prever um único valor, ela fornece informações sobre a probabilidade de a variável de destino estar em uma faixa específica.
O tamanho da camada de resultados no bloco de tomada de decisões corresponde ao tamanho de nosso pool de agentes. Isso significa que cada elemento do vetor de resultados representa a probabilidade ou estimativa correspondente a um agente no pool. Isso nos permite escolher o melhor agente ou combinação de agentes com base em suas estimativas e probabilidades.
//--- layer 6 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = 100; descr.optimization = ADAM; descr.activation = TANH; if(!scheduler.Add(descr)) { delete descr; return false; } //--- layer 7 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = 100; descr.optimization = ADAM; descr.activation = TANH; if(!scheduler.Add(descr)) { delete descr; return false; } //--- layer 8 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronFQF; descr.count = 10; descr.window_out = 32; descr.optimization = ADAM; if(!scheduler.Add(descr)) { delete descr; return false; }
As arquiteturas de modelos criadas fornecem uma ampla variedade de possibilidades para avaliar o estado atual do sistema e tomar decisões ótimas. Graças ao uso de redes neurais multicamada, os modelos são capazes de analisar vários aspectos dos dados de entrada e identificar características de alto nível que podem estar relacionadas a estratégias eficazes e tomada de decisões.
Isso permite que os modelos resolvam eficazmente problemas com quantidades limitadas de dados ou recompensas esparsas, bem como se adaptem a condições e cenários em constante mudança.
O método OnTick merece menção adicional. No início, verificamos se uma nova vela foi aberta e coletamos parâmetros para o estado atual do sistema. Este processo é repetido sem alterações para os EAs de várias edições seguidas, e não vou me deter nele. Em seguida, prosseguimos com a propagação de dois modelos e a seleção da ação do agente com base em seus resultados.
Primeiro, realizamos uma propagação pelo pool de Atores de intenção.
State1.AssignArray(sState.state); if(!Actor.feedForward(GetPointer(State1), 12, true)) return;
Os resultados obtidos na propagação dos agentes são concatenados com a descrição atual do estado do sistema e alimentados ao planejador para avaliação.
Actor.getResults(Result); State1.AddArray(Result); if(!Schedule.feedForward(GetPointer(State1),12,true)) return;
Após a propagação de ambos os modelos, aplicamos amostragem para selecionar um agente de intenção específico com base em suas distribuições. Em seguida, a partir do agente selecionado, amostramos uma ação específica de sua distribuição de probabilidade.
int act = GetAction(Result, Schedule.getSample(), Models);
É importante observar que estamos usando um modelo com parâmetros fixos em todas as passagens, sem treinamento. Portanto, a escolha gananciosa de um agente e ação com alta probabilidade levarão à repetição da mesma trajetória em cada passagem. A amostragem de valores aleatórios das distribuições nos permite explorar o ambiente circundante e obter diferentes trajetórias em cada passagem. Ao mesmo tempo, as restrições impostas pela distribuição permitem conduzir explorações em direções específicas.
No final da função, executamos a ação selecionada pelo agente e salvamos os dados para treinamento posterior.
switch(act) { case 0: if(!Trade.Buy(Symb.LotsMin(), Symb.Name())) act = 3; break; case 1: if(!Trade.Sell(Symb.LotsMin(), Symb.Name())) act = 3; break; case 2: for(int i = PositionsTotal() - 1; i >= 0; i--) if(PositionGetSymbol(i) == Symb.Name()) if(!Trade.PositionClose(PositionGetInteger(POSITION_IDENTIFIER))) { act = 3; break; } break; } //--- float reward = 0; if(Base.Total > 0) reward = ((sState.state[240] + sState.state[241]) - (Base.States[Base.Total - 1].state[240] + Base.States[Base.Total - 1].state[241])) / 10; if(!Base.Add(sState, act, reward)) ExpertRemove(); //--- }
Após cada passagem, as informações sobre as ações realizadas, estados do sistema visitados e recompensas obtidas são armazenadas em um único buffer para o treinamento posterior dos modelos. Essas operações são realizadas nos métodos OnTester, OnTesterInit, OnTesterPass e OnTesterDeinit, cujo princípio de construção foi detalhadamente descrito no artigo sobre o algoritmo Go-Explore.
O código completo do EA e todos os seus métodos estão disponíveis no anexo.
Após a criação do EA para coleta de experiência, o executamos no modo de otimização do testador de estratégia e passamos para o trabalho no EA de treinamento dos modelos Study.mq5. Nos parâmetros externos deste EA, apenas especificamos o número de iterações de treinamento.
//+------------------------------------------------------------------+ //| Input parameters | //+------------------------------------------------------------------+ input int Iterations = 100000;
No bloco de variáveis globais, já especificamos três modelos: Ator, Crítico e Planejador. A arquitetura dos modelos foi descrita anteriormente.
STrajectory Buffer[]; CNet Actor; CNet Critic; CFQF Scheduler;
No método OnInit, primeiro carregamos o conjunto de treinamento que o EA anterior nos forneceu.
int OnInit() { //--- ResetLastError(); if(!LoadTotalBase()) { PrintFormat("Error of load study data: %d", GetLastError()); return INIT_FAILED; }
Carregamos modelos pré-treinados ou criamos novos modelos.
//--- load models float temp; if(!Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) || !Critic.Load(FileName + "Crt.nnw", temp, temp, temp, dtStudied, true) || !Scheduler.Load(FileName + "Sch.nnw", dtStudied, true)) { CArrayObj *actor = new CArrayObj(); CArrayObj *critic = new CArrayObj(); CArrayObj *schedule = new CArrayObj(); if(!CreateDescriptions(actor, critic, schedule)) { delete actor; delete critic; delete schedule; return INIT_FAILED; } if(!Actor.Create(actor) || !Critic.Create(critic) || !Scheduler.Create(schedule)) { delete actor; delete critic; delete schedule; return INIT_FAILED; } delete actor; delete critic; delete schedule; } Scheduler.getResults(SchedulerResult); Models = (int)SchedulerResult.Size(); Actor.getResults(ActorResult); Scheduler.SetUpdateTarget(Iterations); if(ActorResult.Size() % Models != 0) { PrintFormat("The scope of the scheduler does not match the scope of the Agent (%d <> %d)", Models, ActorResult.Size()); return INIT_FAILED; }
E inicializamos o evento de início do processo de treinamento.
//--- if(!EventChartCustom(ChartID(), 1, 0, 0, "Init")) { PrintFormat("Error of create study event: %d", GetLastError()); return INIT_FAILED; } //--- return(INIT_SUCCEEDED); }
No método Train, organizamos o processo de treinamento em si. É importante notar que o conjunto de treinamento consiste em várias passagens, e, na implementação atual, armazenamos os estados em uma estrutura de trajetória sequencial em vez de combiná-los em um único banco de dados. Isso significa que, para escolher um estado aleatório do sistema, precisamos primeiro selecionar uma passagem do array e, em seguida, escolher um estado dessa passagem.
Estritamente falando, não associamos passagens e ações a agentes de intenção específicos. Em vez disso, todos os agentes são treinados em um conjunto de exemplos comum. Esse método nos permite criar políticas de agentes intercambiáveis e sequenciais, onde cada agente pode continuar a política a partir de qualquer estado do sistema, independentemente da política que foi aplicada antes de atingir esse estado.
No início do método, realizamos um pequeno trabalho preparatório: determinamos o número de passagens na base de exemplos e salvamos o valor do contador de ticks para controlar o tempo do processo de treinamento.
void Train(void) { int total_tr = ArraySize(Buffer); uint ticks = GetTickCount();
Após a preparação, organizamos um laço para o processo de treinamento dos modelos.
for(int iter = 0; (iter < Iterations && !IsStopped()); iter ++) { int tr = (int)(((double)MathRand() / 32767.0) * (total_tr - 1)); int i = 0; i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2));
Dentro do laço de treinamento, primeiro escolhemos uma passagem da base de exemplos de treinamento, como mencionado anteriormente. Em seguida, selecionamos aleatoriamente um estado da passagem selecionada. Esse estado é usado como dados de entrada para a propagação dos modelos Ator e Crítico.
State1.AssignArray(Buffer[tr].States[i].state); if(IsStopped()) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); ExpertRemove(); return; } if(!Actor.feedForward(GetPointer(State1), 12, true) || !Critic.feedForward(GetPointer(State1), 12, true)) return;
Os resultados obtidos na propagação são descarregados nos vetores correspondentes.
Actor.getResults(ActorResult); Critic.getResults(CriticResult);
O vetor de resultados obtidos na propagação do Ator é concatenado com o vetor de estado do sistema. Esse vetor combinado é então alimentado como entrada para o modelo Planejador para análise e avaliação.
State1.AddArray(ActorResult); if(!Scheduler.feedForward(GetPointer(State1), 12, true)) return;
Após a propagação do Planejador, aplicamos a escolha gananciosa do agente de intenção.
Scheduler.getResults(SchedulerResult); int agent = Scheduler.getAction(); if(agent < 0) { iter--; continue; }
É importante notar que no início do treinamento, podemos usar amostragem para explorar ao máximo o ambiente. No entanto, à medida que o Planejador é treinado e melhora sua estratégia, mudamos para a escolha gananciosa do agente. Isso ocorre porque o Planejador se torna mais experiente e capaz de avaliar estados do sistema com mais precisão, além de escolher o agente mais adequado para atingir os objetivos estabelecidos.
Não tomamos a decisão de escolher uma ação, pois a base de exemplos já contém informações sobre as ações executadas e as recompensas correspondentes. Com base nesses dados, formamos vetores de recompensa para cada modelo e realizamos uma retropropagação sequencial para cada um deles. Primeiro, realizamos a retropropagação do Planejador.
int actions = (int)(ActorResult.Size() / SchedulerResult.Size()); float max_value = CriticResult[agent * actions]; for(int j = 1; j < actions; j++) max_value = MathMax(max_value, CriticResult[agent * actions + j]); SchedulerResult[agent] = Buffer[tr].Revards[i]; Result.AssignArray(SchedulerResult); //--- if(!Scheduler.backProp(GetPointer(Result),0.0f,NULL)) return;
Em seguida, chamamos o método de retropropagação do Crítico.
int agent_action = agent * actions + Buffer[tr].Actions[i]; CriticResult[agent_action] = Buffer[tr].Revards[i]; Result.AssignArray(CriticResult); //--- if(!Critic.backProp(GetPointer(Result))) return;
E, por fim, seguimos com o modelo dos agentes de intenção.
ActorResult.Fill(0); ActorResult[agent_action] = Buffer[tr].Revards[i] - max_value; Result.AssignArray(ActorResult); //--- if(!Actor.backProp(GetPointer(Result))) return;
No final das iterações do laço, verificamos o tempo de treinamento e exibimos informações para o usuário sobre o processo de treinamento a cada 0.5 segundos.
if(GetTickCount() - ticks > 500) { string str = StringFormat("Actor %.2f%% -> Error %.8f\n", iter * 100.0 / (double)(Iterations), Actor.getRecentAverageError()); str += StringFormat("Critic %.2f%% -> Error %.8f\n", iter * 100.0 / (double)(Iterations), Critic.getRecentAverageError()); str += StringFormat("Scheduler %.2f%% -> Error %.8f\n", iter * 100.0 / (double)(Iterations), Scheduler.getRecentAverageError()); Comment(str); ticks = GetTickCount(); } }
Após a conclusão do processo de treinamento do modelo, registramos os resultados alcançados no log e encerramos a operação do EA.
Comment(""); //--- PrintFormat("%s -> %d -> %10.7f", __FUNCTION__, __LINE__, Actor.getRecentAverageError()); PrintFormat("%s -> %d -> %10.7f", __FUNCTION__, __LINE__, Critic.getRecentAverageError()); PrintFormat("%s -> %d -> %10.7f", __FUNCTION__, __LINE__, Scheduler.getRecentAverageError()); ExpertRemove(); //--- }
Você pode encontrar o código completo do EA no anexo. Todos os arquivos desta modelo estão destacados no arquivo compactado na pasta SAC.
O processo de treinamento do modelo consiste em iterações em que coletamos exemplos no modo de otimização e iniciamos o processo de treinamento em tempo real no gráfico. Se o resultado do treinamento não corresponder às nossas expectativas, repetimos a operação de coleta de exemplos e o treinamento dos modelos. Essas operações são repetidas até alcançarmos o resultado ideal que corresponda aos nossos objetivos de treinamento.
As iterações subsequentes de coleta de exemplos e treinamento dos modelos são uma parte integral do processo de treinamento. Elas nos permitem melhorar os modelos, adaptá-los a condições em mudança e buscar resultados ideais. Cada iteração fornece novos dados e oportunidades para melhorar os modelos, permitindo-nos abordar eficazmente os problemas e alcançar nossos objetivos.
É importante observar que o processo de treinamento pode ser iterativo e pode exigir várias rodadas antes de alcançarmos o resultado desejado. Isso ocorre porque o treinamento de modelos é um processo complexo que requer ajustes constantes e melhorias. Devemos estar preparados para uma abordagem iterativa e estar dispostos a repetir as operações de coleta de exemplos e treinamento até atingirmos nossos objetivos e obtermos resultados ideais.
O sistema organizado dessa maneira, onde a base de exemplos é constantemente complementada a cada iteração de coleta de exemplos, nos oferece uma vantagem significativa. Isso nos permite criar uma base de exemplos completa que pode melhorar significativamente o treinamento do modelo e sua capacidade de tomar decisões ideais.
No entanto, é importante considerar que o aumento do tamanho da base de exemplos tem suas consequências. Em primeiro lugar, o processamento e a análise de um volume maior de dados podem levar mais tempo e exigir recursos computacionais mais substanciais. Isso pode resultar em um aumento no tempo das iterações de treinamento do modelo. Em segundo lugar, o aumento do tamanho da base de exemplos pode aumentar a complexidade do treinamento, pois os modelos precisam processar mais dados e se adaptar a cenários diversos.
3. Teste
Os resultados do treinamento do modelo com dados históricos do EURUSD em um período de tempo H1 para os primeiros 4 meses de 2023 mostraram que o modelo é capaz de gerar lucros tanto na amostra de treinamento quanto fora dela. Foram realizadas mais de 10 iterações de coleta de exemplos e treinamento do modelo, incluindo de 8 a 24 passagens de otimização em cada iteração. No total, mais de 200 passagens foram coletadas e o processo de treinamento incluiu de 100.000 a 10.000.000 de iterações.
Para verificar os resultados do treinamento do modelo, foi criado o EA Test.mq5, que usou a escolha gananciosa do agente e das ações em vez da amostragem. Isso permitiu testar o funcionamento do modelo e eliminar o fator de aleatoriedade.
No gráfico abaixo, estão apresentados os resultados do modelo fora do conjunto de treinamento. Em um curto período de tempo, o modelo conseguiu obter um pequeno lucro. O fator de lucro foi de 1.19 e o fator de recuperação foi de 0.46.
No entanto, vale ressaltar que o gráfico de saldo apresenta zonas de prejuízo, o que pode indicar a necessidade de realizar iterações adicionais no treinamento do modelo. Isso pode ajudar a melhorar sua capacidade de gerar lucro e reduzir o nível de risco nas negociações.
Considerações finais
Podemos enfatizar a eficácia do método de Controle Auxiliar Agendado (SAC-X) no treinamento de modelos de agentes de intenção para os mercados financeiros. O SAC-X representa um desenvolvimento do método clássico de aprendizado por reforço, levando em consideração a natureza específica dos dados financeiros e os requisitos das estratégias de negociação.
Uma das principais características do SAC-X é o uso de várias modelos (Ator, Crítico, Planejador) para avaliar o estado do sistema e tomar decisões. Isso permite considerar diversos aspectos da negociação e criar uma política de agente mais flexível e adaptável.
Outro aspecto importante do SAC-X é o uso do planejador para analisar o estado do sistema e escolher o melhor agente de intenção. Isso aumenta a eficiência e a precisão na tomada de decisões, proporcionando resultados de negociação mais estáveis.
Os testes do SAC-X com dados históricos do EURUSD mostraram sua capacidade de gerar lucros tanto no conjunto de treinamento quanto fora dele. No entanto, é importante observar que em alguns casos foram identificadas zonas de prejuízo no gráfico de saldo, o que pode indicar a necessidade de treinamento adicional do modelo.
Em resumo, o método de Controle Auxiliar Agendado (SAC-X) é uma ferramenta poderosa para treinar modelos de agentes de intenção na área financeira. Ele leva em consideração a especificidade dos dados de mercado, permite criar estratégias de negociação adaptáveis e flexíveis, e demonstra potencial para alcançar negociações estáveis e lucrativas. Pesquisas futuras e melhorias no SAC-X podem levar a resultados ainda melhores e à expansão de seu uso nos mercados financeiros.
Referências
Programas utilizados no artigo
# | Nome | Tipo | Descrição |
---|---|---|---|
1 | Research.mq5 | EA | EA de coleta de exemplos |
2 | Study.mql5 | EA | EA para treinamento de modelos |
3 | Test.mq5 | EA | EA para teste do modelo |
4 | Trajectory.mqh | Biblioteca de classe | Estrutura de descrição do estado do sistema |
5 | FQF.mqh | Biblioteca de classe | Biblioteca de classes de preparação de modelos totalmente parametrizada |
6 | NeuroNet.mqh | Biblioteca de classe | Biblioteca de classes para a criação de uma rede neural |
7 | NeuroNet.cl | Biblioteca | Biblioteca do código do programa OpenCL |
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/12605
- 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