English Русский 中文 Español Deutsch 日本語
preview
Redes neurais de maneira fácil (Parte 20): autocodificadores

Redes neurais de maneira fácil (Parte 20): autocodificadores

MetaTrader 5Sistemas de negociação | 2 novembro 2022, 16:17
194 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Conteúdo


      Introdução

      Continuamos a mergulhar em métodos de aprendizado não supervisionado. Em artigos anteriores, já encontramos algoritmos para agrupamento, compactação de dados e busca de regras de associação. Mas os algoritmos de aprendizado não supervisionados discutidos anteriormente não usam redes neurais. Neste artigo, voltamos ao uso de redes neurais. E quero apresentar a vocês como funcionam os autocodificadores.


      1. Arquitetura do autocodificador

      Antes de passar para a descrição da arquitetura do autocodificador, vamos relembrar como treinamos redes neurais ao estudar algoritmos de aprendizado supervisionado. Pegamos pares de dados rotulados a partir do padrão e do resultado alvo, e otimizamos os coeficientes de peso de forma a minimizar o erro entre o resultado da rede neural e o valor alvo.

      Vamos pensar no que uma rede neural pode aprender nesse caso? Ela aprenderá exatamente o que lhe pedimos, isto é, encontrar caraterísticas das quais o resultado alvo depende. Ao mesmo tempo, ela anulará a influência de características que não afetam o resultado alvo estudado ou sua influência não será significativa. Ou seja, o treinamento do modelo ocorre em uma direção estreita. E não há nada de errado com isso. O modelo faz perfeitamente o que queremos dele.

      Mas há um outro lado da moeda. Ainda não usamos, mas já mencionamos os conceitos de Transfer Learning, que é o uso de um modelo pré-treinado para resolver novos problemas. E, neste caso, obteremos bons resultados apenas se os valores alvo anteriores e novos dependerem das mesmas caraterísticas. Caso contrário, podemos simplesmente não ter o suficiente dessas caraterísticas, caraterísticas essas cuja influência o modelo anulou no estágio anterior de treinamento.

      Como isso afetará a solução de nossas tarefas? Vamos lembrar que nosso mundo não é estático e está em constante mudança. Os drivers do mercado de hoje no futuro podem desaparecer em segundo plano, e o mercado será impulsionado por outros drivers, limitando a vida útil do nosso modelo. Claro que isso não é segredo para ninguém. Os operadores há muito tempo revisam suas estratégias com certa frequência. Isso também é confirmado pela lucratividade dos EAs construídos usando métodos clássicos para descrever uma ou outra estratégia de negociação.

      Nós, no início do estudo das redes neurais, acreditávamos com razão que o uso da inteligência artificial prolongaria a vida útil do modelo e que seu treinamento periódico ajudaria a obter lucro o maior tempo possível.

      Para minimizar os riscos associados à particularidade acima dita, recorremos ao aprendizado de representação, uma das áreas do aprendizado não supervisionado. O aprendizado de representação combina um conjunto de algoritmos que extraem automaticamente caraterísticas a partir de dados brutos de entrada. É bom mencionar que os algoritmos de agrupamento e redução de dimensionalidade que consideramos também se relacionam ao aprendizado de representação. Neles usamos transformações lineares. Para estudar formas mais complexas, eles usam autocodificadores.

      No caso geral, o autocodificador é uma rede neural composta por 2 blocos - codificador e decodificador. A camada de dados iniciais do codificador e a camada de resultado do decodificador contêm o mesmo número de elementos. Existe uma camada oculta entre o codificador e o decodificador que geralmente é menor que os dados iniciais. No processo de aprendizado, os neurônios dessa camada formam um estado latente (oculto) que pode descrever os dados iniciais de forma compactada.

      Autocodificador

      Isso nos lembra do problema de compactação de dados que resolvemos usando o método do componente principal. Mas falaremos sobre a diferença de abordagens um pouco mais tarde.

      Como mencionado acima, um autocodificador é uma rede neural e é treinado pelo método de retropropagação já conhecido por nós. O truque é que, como estamos usando dados não rotulados, treinamos o modelo para primeiro compactar os dados com o codificador até o tamanho do estado latente. E então no decodificador treinamos para restaurar os dados ao seu estado inicial com perda mínima de informações.

      Assim, treinamos o autocodificador com o já conhecido método de retropropagação. E como resultados alvo, usamos a própria amostra de treinamento.

      A mesma arquitetura de camadas neurais pode ser diferente. Na versão mais simples, estas podem ser camadas totalmente conectadas. Modelos convolucionais são amplamente utilizados para extrair características de imagens.

      Além disso, modelos recorrentes e algoritmos de atenção podem ser usados para trabalhar com sequências. Mas planejo analisar tais estudos em artigos futuros.


      2. Problemas clássicos resolvidos por autocodificadores

      Apesar da abordagem bastante fora do padrão para o treinamento de autocodificadores, seu uso pode ser encontrado na resolução de uma variedade de problemas. Em primeiro lugar, como mencionado anteriormente, eles consistem na compactação e pré-processamento de dados.

      Os algoritmos de compressão de dados podem ser divididos em 2 tipos:

      • compressão com perda;
      • compressão sem perdas.

      Um exemplo de compressão de dados com perdas é a análise de componentes principais (PCA) que discutimos anteriormente. Lembre-se, ao escolher o número de componentes, prestamos atenção ao nível máximo de perda de informações.

      Um exemplo de compactação de dados sem perdas pode ser os diferentes arquivadores. Não queremos perder nenhuma parte dos dados iniciais após a descompactação.

      Teoricamente, podemos treinar um autocodificador em qualquer dado, e usar o codificador para compactar os dados e o decodificador para restaurar os dados. Neste caso, apenas o estado latente é transmitido. Dependendo do nível de complexidade do modelo do autocodificador, a compactação de dados pode ser com ou sem perdas. Obviamente, a compactação de dados sem perdas exigirá modelos mais complexos. No entanto, é amplamente utilizado em telecomunicações para melhorar a qualidade da transmissão de dados, além de reduzir a quantidade de tráfego usado. Isso permite aumentar a taxa de transferência das redes usadas.

      Ao lado da compactação vem o pré-processamento dos dados iniciais. Por exemplo, a compressão de dados por autocodificadores é usada para combater a chamada "maldição da dimensionalidade". Além disso, muitos métodos de aprendizado de máquina funcionam melhor e mais rápido com dados de menor dimensão. As mesmas redes neurais, com uma dimensão de dados de entrada menor, conterão menos pesos treináveis. Isso significa que elas vão aprender e trabalhar mais rápido com menos propensão ao retreinamento.

      A próxima tarefa resolvida com a ajuda de autocodificadores é limpar os dados iniciais, isto é, eliminar o ruído. E existem 2 maneiras de resolver este problema. A primeira, como no caso do PCA, é a compactação de dados com perdas. Sendo que durante a compressão, esperamos que percamos não mais não menos do que o "ruído".

      A segunda maneira é amplamente utilizada no processamento de imagens. Aqui, imagens iniciais de boa qualidade (sem ruído) são usadas e diferentes distorções (ruído) são adicionadas a elas. As imagens distorcidas são alimentadas na entrada do autocodificador. E o modelo é treinado para obter a máxima semelhança com a imagem inicial de boa qualidade. Vale a pena abordar a escolha de distorções adicionais com extrema cautela. Elas devem ser parecidas ao "ruído natural". Caso contrário, há uma alta probabilidade de que em condições reais o modelo não funcione corretamente.

      Além disso, o uso de autocodificadores permite não apenas remover ruídos das imagens, como também remover ou adicionar objetos à imagem. Em particular, se pegarmos 2 imagens que diferem apenas em um objeto e alimentarmos com elas a entrada do codificador, então o vetor de diferença entre os estados latentes de duas imagens corresponderá a um objeto que está apenas em uma imagem. Assim, adicionando o vetor resultante ao estado latente de qualquer outra imagem, podemos adicionar um objeto à imagem. Da mesma forma, subtraindo o vetor do estado latente, removeremos o objeto da imagem.

      Técnicas de aprendizado de autocodificadores separadas permitem separar o estado latente em conteúdo e estilo de imagem. A substituição de conteúdo com preservação de estilo permite obter uma nova imagem no resultado do decodificador, imagem essa que combinará o conteúdo de uma imagem e o estilo da outra. O avanço desse tipo de experiências permite o uso de autocodificadores treinados para geração de imagens.

      Como você pode ver, a gama de tarefas resolvidas usando autocodificadors é bastante ampla. E nem todos eles podem ser usados na negociação. Pelo menos, não vejo agora como aplicar essa habilidade de gerar algo. Talvez você tenha algumas ideias originais e consiga implementá-las.


      3. Comparação entre o autocodificador e o PCA

      Como mencionado acima, as tarefas resolvidas pelos autocodificadores «fazem eco» parcialmente nos algoritmos considerados anteriormente. Por exemplo, os autocodificadores e o método do componente principal são capazes de compactar os dados (reduzir a dimensionalidade) e remover o ruído dos dados iniciais. Por que precisamos de outra ferramenta para resolver os mesmos problemas? Vamos ver qual é a diferença entre as abordagens e os resultados de cada uma.

      Primeiro, vamos relembrar o algoritmo do método de componente principal. Este é um método puramente matemático para extrair os componentes principais, e é baseado em fórmulas matemáticas estritas. E ao usar tal método com os mesmos dados iniciais, temos a garantia de obter o mesmo resultado. O mesmo não pode ser dito sobre autocodificadores.

      Um autocodificador é uma rede neural, que é inicializada com pesos aleatórios e treinada iterativamente pelo método gradiente descendente. Sim, as mesmas fórmulas matemáticas são usadas no treinamento. Mas por várias razões, como resultado de vários treinamentos do mesmo modelo, mesmo com o mesmo conjunto de treinamento, é muito provável que obtenhamos resultados completamente diferentes. Eles podem dar a mesma precisão e ainda ser completamente diferentes.

      O segundo aspecto é o tipo de transformações. No PCA, usamos transformações lineares na forma de multiplicação de matrizes. Enquanto, em redes neurais, geralmente usamos funções de ativação não lineares. Isso significa que as transformações no autocodificador serão mais complexas.

      Sim, podemos comparar a análise de componentes principais com um autocodificador de três camadas sem usar uma função de ativação. Mas neste caso, mesmo quando o número de elementos da camada oculta é igual ao número de componentes principais, não há garantia de obter o mesmo resultado. Pelo contrário, neste caso, o PCA garante o melhor resultado.

      Autocodificador - PCA

      Além disso, o cálculo dos componentes principais será muito mais rápido do que treinar o modelo do autocodificador. Portanto, se um relacionamento linear for claramente rastreado em seus dados, é melhor usar o método do componente principal para compactá-los. Para tarefas mais complexas, um autocodificador funcionará melhor.


      4. Usos potenciais para autocodificadores na negociação

      Tendo nos familiarizado com a parte teórica do trabalho dos algoritmos de autocodificadores, vamos pensar em como podemos usar seus recursos em nossas estratégias de negociação. A primeira coisa que vem à mente é, obviamente, o pré-processamento de dados: compactação de dados e remoção de ruído. Já realizamos experimentos semelhantes com o método de componentes principais, e podemos fazer uma análise comparativa.

      É difícil para mim agora imaginar como você pode usar as habilidades generativas de um autocodificador. E a utilidade de termos gráficos falsos é questionável. Embora, é claro, você possa tentar treinar um autocodificador e obter resultados de decodificador com uma ligeira mudança de tempo à frente. Isso, certamente, não diferirá muito dos métodos discutidos anteriormente de treinamento supervisionado. Mas o valor de tal abordagem só pode ser avaliado experimentalmente.

      Também podemos tentar avaliar a dinâmica das mudanças da situação do mercado. Não importa o quão banal possa parecer, porque toda negociação é baseada tanto no monitoramento das mudanças na situação do mercado como na tentativa de prever o movimento futuro. Acima, já descrevi a abordagem quando, graças ao trabalho com o estado latente, um objeto era adicionado ou removido da imagem. Por que não aproveitamos esta propriedade? Só não distorceremos a situação do mercado no resultado do decodificador. Tentaremos apenas avaliar a dinâmica do mercado pelo vetor da diferença entre dois estados latentes sucessivos.

      E, claro, não nos esquecemos do Transfer Learning. Foi aqui que nosso artigo começou. Graças à tecnologia de aprendizado do autocodificador, podemos treinar o modelo para extrair caraterísticas a partir dos dados iniciais, para, então, pegarmos apenas o codificador. Vamos adicionar várias camadas de tomada de decisão e treinar o modelo supervisionado para resolver nossos problemas. Ao fazer isso, deve-se lembrar que ao treinar um autocodificador, o estado latente contém todas as caraterísticas a partir dos dados iniciais. Portanto, uma vez que um codificador é treinado, podemos usá-lo para resolver diversos problemas. Claro, se tais problemas usarem os mesmos dados iniciais.

      Assim, designamos um certo conjunto de tarefas para nossos experimentos. Devo dizer imediatamente que todo o seu tamanho está além do escopo de um artigo. Mas não temos medo das dificuldades e começamos nossa parte prática.


      5. Experimentos práticos

      E agora chegou a hora em que vamos dedicar um pouco de tempo a experimentos práticos. Primeiro, vamos criar e treinar um autocodificador simples usando camadas totalmente conectadas. Ao construir nosso modelo de autocodificador, usaremos nossa biblioteca de camadas neurais construída durante nosso estudo de métodos de aprendizado supervisionado.

      Mas, antes de proceder diretamente à criação do código, vamos pensar no que e como treinaremos nosso autocodificador. Parece que falamos muito acima sobre autocodificadores, e sobre que são treinados para retornar os dados iniciais. E aqui está a pergunta novamente, de fato, quando se trata de dados homogêneos, tudo fica claro. Pegamos e treinamos o modelo para obter os dados iniciais. Mas não temos exatamente os mesmos dados iniciais. Podemos alimentar o modelo com dados de preços e com leituras de indicadores. Sendo que as leituras de vários indicadores têm dados muito diferentes. Já falamos sobre isso mais de uma vez ao treinar modelos supervisionados. Em seguida, prestamos atenção ao fato de que dados de diferentes amplitudes têm um efeito diferente no resultado do modelo. Mas agora o assunto é ainda mais complicado pelo fato de que na camada de resultados do decodificador devemos especificar a função de ativação. E esta função de ativação deve ser capaz de retornar todo o intervalo de diferentes valores iniciais.

      Minha solução foi a mesma do caso do aprendizado supervisionado - fazer normalização dos dados iniciais. Isso pode ser um processo separado ou usar uma camada de normalização em lote.

      A primeira camada oculta do nosso autocodificador será a camada de normalização de lote. E vamos treinar o autocodificador de forma que o decodificador retorne dados normalizados. Para a camada de resultados do decodificador, usaremos a tangente hiperbólica como função de ativação. Isso nos permitirá normalizar os resultados entre -1 e 1.

      A solução teórica foi encontrada. Mas para implementá-lo na prática, precisaremos ter acesso aos resultados da primeira camada oculta do nosso modelo a cada iteração do treinamento do modelo. Nós não olhamos dentro de nossos modelos ainda. Os estados ocultos de nossas redes neurais sempre foram uma "caixa preta". E para gerar o processo de aprendizagem, precisamos abri-la. Para fazer isso, em nossa classe para organizar o trabalho de redes neurais CNet, adicionaremos um método para obter os valores do buffer de resultado de qualquer camada oculta GetLayerOutput.

      Nos parâmetros do nosso novo método, passaremos o número ordinal da camada necessária e um ponteiro para um buffer para escrever os resultados.

      No corpo do método, como de costume, criamos um pequeno bloco para verificação dos dados recebidos. Nesse caso, verificamos se há um buffer de camada atualizado do nosso modelo. E também verificamos se o número de série especificado da camada neural necessária está dentro do intervalo de camadas neurais do nosso modelo. Observe que não verificamos a possibilidade de especificar erroneamente um número de camada ordinal negativo. Em vez disso, usamos uma variável inteira sem sinal para obter o parâmetro. Assim, seu valor será sempre não negativo. Isso significa que, no bloco de controle, basta verificarmos o limite superior do número de camadas neurais no modelo.

      Depois de passar com sucesso o bloco de controles, obtemos um ponteiro para a camada neural especificada em uma variável local. E verificamos imediatamente a validade do ponteiro recebido.

      Na próxima etapa do nosso método, verificamos a validade do ponteiro para o buffer de resultado recebido nos parâmetros. E se necessário, iniciamos a criação de um novo buffer de dados.

      Em seguida, solicitamos os valores do buffer de resultado da camada neural correspondente. E claro, não nos esquecemos de verificar o resultado das operações em cada etapa.

      bool CNet::GetLayerOutput(uint layer, CBufferDouble *&result)
        {
         if(!layers || layers.Total() <= (int)layer)
            return false;
         CLayer *Layer = layers.At(layer);
         if(!Layer)
            return false;
      //---
         if(!result)
           {
            result = new CBufferDouble();
            if(!result)
               return false;
           }
      //---
         CNeuronBaseOCL *temp = Layer.At(0);
         if(!temp || temp.getOutputVal(result) <= 0)
            return false;
      //---
         return true;
        }
      

      Isso conclui o trabalho preparatório e podemos começar a construir nosso primeiro autocodificador. Para implementá-lo, criaremos o Expert Advisor "ae.mq5", que será construído de acordo com o template de Expert Advisors do modelo de aprendizado supervisionado.

      Como dados iniciais, usaremos cotações de preços e dados de 4 indicadores: RSI, CCI, ATR и MACD. Usamos os mesmos dados para testar todos os modelos anteriores. Todos os parâmetros do indicador são especificados nos parâmetros externos do EA. Na função OnInit, inicializamos instâncias de objetos para trabalhar com indicadores.

      int OnInit()
        {
      //---
         Symb = new CSymbolInfo();
         if(CheckPointer(Symb) == POINTER_INVALID || !Symb.Name(_Symbol))
            return INIT_FAILED;
         Symb.Refresh();
      //---
         RSI = new CiRSI();
         if(CheckPointer(RSI) == POINTER_INVALID || !RSI.Create(Symb.Name(), TimeFrame, RSIPeriod, RSIPrice))
            return INIT_FAILED;
      //---
         CCI = new CiCCI();
         if(CheckPointer(CCI) == POINTER_INVALID || !CCI.Create(Symb.Name(), TimeFrame, CCIPeriod, CCIPrice))
            return INIT_FAILED;
      //---
         ATR = new CiATR();
         if(CheckPointer(ATR) == POINTER_INVALID || !ATR.Create(Symb.Name(), TimeFrame, ATRPeriod))
            return INIT_FAILED;
      //---
         MACD = new CiMACD();
         if(CheckPointer(MACD) == POINTER_INVALID || !MACD.Create(Symb.Name(), TimeFrame, FastPeriod, SlowPeriod, SignalPeriod, MACDPrice))
            return INIT_FAILED;
      

      Em seguida, temos que especificar a arquitetura do nosso autocodificador. O algoritmo e os princípios de construção de uma rede neural são totalmente consistentes com aqueles usados por nós ao construir modelos para aprendizado supervisionado. A única diferença está na arquitetura da própria rede neural.

      Deixe-me lembrá-lo de que, para passar a arquitetura da rede neural para o método de inicialização do nosso modelo, criaremos uma matriz dinâmica de objetos CArrayObj. Cada objeto desta matriz irá descrever uma camada neural. E sua sequência na matriz corresponderá à sequência de camadas neurais no modelo. Para descrever a arquitetura da camada neural, usaremos objetos CLayerDescription especialmente criados.

      class CLayerDescription    :  public CObject
        {
      public:
         /** Constructor */
                           CLayerDescription(void);
         /** Destructor */~CLayerDescription(void) {};
         //---
         int               type;          ///< Type of neurons in layer (\ref ObjectTypes)
         int               count;         ///< Number of neurons
         int               window;        ///< Size of input window
         int               window_out;    ///< Size of output window
         int               step;          ///< Step size
         int               layers;        ///< Layers count
         int               batch;         ///< Batch Size
         ENUM_ACTIVATION   activation;    ///< Type of activation function (#ENUM_ACTIVATION)
         ENUM_OPTIMIZATION optimization;  ///< Type of optimization method (#ENUM_OPTIMIZATION)
         double            probability;   ///< Probability of neurons shutdown, only Dropout used
        };
      

      A primeira camada, como sempre, é a camada de dados iniciais, que declaramos como uma camada totalmente conectada. Para cada descrição de cada vela, precisamos de 12 elementos. Portanto, o tamanho da camada será 12 vezes a profundidade histórica de um único padrão. Para a camada de dados iniciais, não usamos uma função de ativação.

         Net = new CNet(NULL);
         ResetLastError();
         double temp1, temp2;
         if(CheckPointer(Net) == POINTER_INVALID || !Net.Load(FileName + ".nnw", dError, temp1, temp2, dtStudied, false))
           {
            printf("%s - %d -> Error of read %s prev Net %d", __FUNCTION__, __LINE__, FileName + ".nnw", GetLastError());
            CArrayObj *Topology = new CArrayObj();
            if(CheckPointer(Topology) == POINTER_INVALID)
               return INIT_FAILED;
            //--- 0
            CLayerDescription *desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            int prev = desc.count = (int)HistoryBars * 12;
            desc.type = defNeuronBaseOCL;
            desc.activation = None;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      

      Depois de descrever a arquitetura da camada neural, adicionamo-la ao array dinâmico da descrição da arquitetura do modelo.

      A próxima camada que temos é uma camada de normalização em lote. Como você se lembra, discutimos a necessidade de sua criação um pouco mais acima. O número de elementos na camada de normalização em lote é igual ao número de neurônios na camada anterior. Aqui também não usaremos a função de ativação. Vamos indicar o tamanho do pacote de normalização em 1000 elementos e o método de otimização dos parâmetros treinados. E adicionamos descrições de mais uma camada neural à nossa matriz dinâmica de descrições de arquitetura de modelo.

            //--- 1
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            desc.count = prev;
            desc.batch = 1000;
            desc.type = defNeuronBatchNormOCL;
            desc.activation = None;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      

      Certifique-se de lembrar o índice da camada de normalização na arquitetura do nosso autocodificador.

      Em seguida, começamos a construir o codificador do nosso autocodificador. Nele, reduziremos gradualmente o tamanho das camadas neurais para 2 elementos do estado latente. Sua arquitetura pode lembrá-lo de um funil.

      Todas as camadas do codificador neural usam a tangente hiperbólica como função de ativação. E para ativar o estado latente, usei o sigmóide.

      Deve-se dizer que ao construir um autocodificador, não há requisitos especiais para o número de camadas neurais ou para as funções de ativação usadas. Aqui, os mesmos princípios são usados na construção de qualquer modelo de rede neural. Portanto, sugiro que você experimente diferentes arquiteturas ao construir seu modelo de autocodificador.

            //--- 2
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            prev = desc.count = (int)HistoryBars;
            desc.type = defNeuronBaseOCL;
            desc.activation = TANH;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      
            //--- 3
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            prev = desc.count = prev / 2;
            desc.type = defNeuronBaseOCL;
            desc.activation = TANH;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      
            //--- 4
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            prev = desc.count = prev / 2;
            desc.type = defNeuronBaseOCL;
            desc.activation = TANH;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      
            //--- 5
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            desc.count = 2;
            desc.type = defNeuronBaseOCL;
            desc.activation = SIGMOID;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      

      Em seguida, devemos especificar a arquitetura do decodificador. Ao contrário do codificador, aqui aumentaremos gradualmente o número de elementos nas camadas neurais. Muitas vezes você pode ver que a arquitetura do decodificador é uma imagem espelhada do codificador. Decidi mudar o número de camadas neurais e os neurônios que elas contêm. Neste caso, devemos necessariamente observar a igualdade do número de neurônios na camada de normalização em lote e na camada de resultados do decodificador.

            //--- 6
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            desc.count = (int) HistoryBars;
            desc.type = defNeuronBaseOCL;
            desc.activation = TANH;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      
            //--- 7
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            desc.count = (int) HistoryBars * 4;
            desc.type = defNeuronBaseOCL;
            desc.activation = TANH;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      
            //--- 8
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            desc.count = (int) HistoryBars * 12;
            desc.type = defNeuronBaseOCL;
            desc.activation = TANH;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      

      Após criar a descrição da arquitetura do modelo, criaremos a rede neural do nosso autocodificador. Para isso, criaremos uma nova instância do objeto de rede neural, passando a descrição do nosso autocodificador para seu construtor.

            delete Net;
            Net = new CNet(Topology);
            delete Topology;
            if(CheckPointer(Net) == POINTER_INVALID)
               return INIT_FAILED;
            dError = DBL_MAX;
           }
      

      E antes da conclusão da função de inicialização do EA, criaremos um buffer de dados temporários e um evento para iniciar o treinamento do modelo.

         TempData = new CBufferDouble();
         if(CheckPointer(TempData) == POINTER_INVALID)
            return INIT_FAILED;
      //---
         bEventStudy = EventChartCustom(ChartID(), 1, (long)MathMax(0, MathMin(iTime(Symb.Name(), 
                                        PERIOD_CURRENT, (int)(100 * Net.recentAverageSmoothingFactor * 10)),
                                        dtStudied)), 0, "Init");
      //---
         return(INIT_SUCCEEDED);
        }
      

      O código completo de todos os métodos e funções pode ser encontrado no anexo.

      E, claro, depois de criar nosso autocodificador, precisamos treiná-lo. O treinamento de modelos em nosso modelo de EA é realizado na função Train. Nos parâmetros, a função recebe a data de início do período de treinamento. No corpo da função, criaremos variáveis locais e definiremos o período de aprendizado.

      void Train(datetime StartTrainBar = 0)
        {
         int count = 0;
      //---
         MqlDateTime start_time;
         TimeCurrent(start_time);
         start_time.year -= StudyPeriod;
         if(start_time.year <= 0)
            start_time.year = 1900;
         datetime st_time = StructToTime(start_time);
         dtStudied = MathMax(StartTrainBar, st_time);
         ulong last_tick = 0;
      
         double prev_er = DBL_MAX;
         datetime bar_time = 0;
         bool stop = IsStopped();
         CArrayDouble *loss = new CArrayDouble();
         MqlDateTime sTime;
      

      Depois disso, carregaremos os dados históricos para treinar o modelo.

         int bars = CopyRates(Symb.Name(), TimeFrame, st_time, TimeCurrent(), Rates);
         prev_er = dError;
      //---
         if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
           {
            ExpertRemove();
            return;
           }
         if(!ArraySetAsSeries(Rates, true))
           {
            ExpertRemove();
            return;
           }
         RSI.Refresh(OBJ_ALL_PERIODS);
         CCI.Refresh(OBJ_ALL_PERIODS);
         ATR.Refresh(OBJ_ALL_PERIODS);
         MACD.Refresh(OBJ_ALL_PERIODS);
      

      O modelo é treinado diretamente em um sistema de loops aninhados. O loop externo contará as épocas de treinamento. E o loop interno irá iterar sobre os dados históricos dentro da época de aprendizado.

      No corpo do loop externo, armazenaremos o valor do erro da época de treinamento anterior. Ele será usado para controlar a dinâmica de aprendizagem. Se a dinâmica de mudança do erro após a conclusão da próxima época de aprendizado não for significativa, o processo de aprendizado será interrompido. E verificamos o sinalizador para interrupção do programa pelo usuário. Em seguida, criamos um loop aninhado.

         int total = (int)(bars - MathMax(HistoryBars, 0));
         do
           {
            //---
            stop = IsStopped();
            prev_er = dError;
            for(int it = total - 1; it >= 0 && !stop; it--)
              {
               int i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (total));
               if((GetTickCount64() - last_tick) >= 250)
                 {
                  com = StringFormat("Study -> Era %d -> %.6f\n %d of %d -> %.2f%% \nError %.5f",
                                     count, prev_er, bars - it + 1, bars,
                                     (double)(bars - it + 1.0) / bars * 100, Net.getRecentAverageError());
                  Comment(com);
                  last_tick = GetTickCount64();
                 }
      

      No corpo do loop aninhado, primeiro exibiremos informações sobre o processo de aprendizado no gráfico na forma de comentários. E determinamos aleatoriamente o próximo padrão para treinar o modelo. Em seguida, preenchemos o buffer temporário com dados históricos.

               TempData.Clear();
               int r = i + (int)HistoryBars;
               if(r > bars)
                  continue;
               //---
               for(int b = 0; b < (int)HistoryBars; b++)
                 {
                  int bar_t = r - b;
                  double open = Rates[bar_t].open;
                  TimeToStruct(Rates[bar_t].time, sTime);
                  double rsi = RSI.Main(bar_t);
                  double cci = CCI.Main(bar_t);
                  double atr = ATR.Main(bar_t);
                  double macd = MACD.Main(bar_t);
                  double sign = MACD.Signal(bar_t);
                  if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE ||
                     macd == EMPTY_VALUE || sign == EMPTY_VALUE)
                     continue;
                  //---
                  if(!TempData.Add(Rates[bar_t].close - open) || !TempData.Add(Rates[bar_t].high - open) ||
                     !TempData.Add(Rates[bar_t].low - open) || !TempData.Add((double)Rates[bar_t].tick_volume / 1000.0) ||
                     !TempData.Add(sTime.hour) || !TempData.Add(sTime.day_of_week) || !TempData.Add(sTime.mon) ||
                     !TempData.Add(rsi) || !TempData.Add(cci) || !TempData.Add(atr) || !TempData.Add(macd) || !TempData.Add(sign))
                     break;
                 }
               if(TempData.Total() < (int)HistoryBars * 12)
                  continue;
      

      Após coletar os dados históricos, chamamos o método de passagem direta do nosso autocodificador. Ao fazer isso, nos parâmetros do método, passaremos os dados históricos coletados.

               Net.feedForward(TempData, 12, true);
               TempData.Clear();
      

      Na próxima etapa, para treinar o modelo, precisamos chamar o método de retropropagação do modelo. Nos parâmetros deste método, usamos para transferir um buffer de resultados alvo. Agora, como dissemos anteriormente, o resultado alvo para nosso autocodificador será os dados iniciais normalizados. Para fazer isso, precisamos primeiro obter os resultados de nossa camada de normalização em lote e, em seguida, passá-los para o método backtrace do modelo. Deixe-me lembrá-lo que o índice da camada de normalização de lote em nosso modelo é "1".

               if(!Net.GetLayerOutput(1, TempData))
                  break;
               Net.backProp(TempData);
               stop = IsStopped();
              }
      

      Após a conclusão do método de retropropagação, verificamos o sinalizador para interromper a execução do programa pelo usuário e passamos para a próxima iteração do loop aninhado.

      Após a conclusão da próxima época de treinamento, salvaremos o resultado atual do treinamento do modelo. E o valor do erro atual do modelo será tanto exibido no log para informar ao usuário como salvo no buffer da dinâmica do processo de aprendizagem.

      E antes de rodar uma nova época de treinamento, verificaremos a viabilidade de treinar ainda mais o modelo.

            if(!stop)
              {
               dError = Net.getRecentAverageError();
               Net.Save(FileName + ".nnw", dError, 0, 0, dtStudied, false);
               printf("Era %d -> error %.5f %%", count, dError);
               loss.Add(dError);
               count++;
              }
           }
         while(!(dError < 0.01 && (prev_er - dError) < 0.01) && !stop);
      

      Após a conclusão do processo de treinamento, vamos salvar a dinâmica do erro durante todo o treinamento do modelo em um arquivo e chamar a função de encerramento forçado do nosso EA de treinamento do modelo.

         Comment("Write dinamic of error");
         int handle = FileOpen("ae_loss.csv", FILE_WRITE | FILE_CSV | FILE_ANSI, ",", CP_UTF8);
         if(handle == INVALID_HANDLE)
           {
            PrintFormat("Error of open loss file: %d", GetLastError());
            delete loss;
            return;
           }
         for(int i = 0; i < loss.Total(); i++)
            if(FileWrite(handle, loss.At(i)) <= 0)
               break;
         FileClose(handle);
         PrintFormat("The dynamics of the error change is saved to a file %s\\%s",
                     TerminalInfoString(TERMINAL_DATA_PATH), "ae_loss.csv");
         delete loss;
         Comment("");
         ExpertRemove();
        }
      

      Devo dizer que na versão acima, uso a função ExpertRemove para completar o trabalho do EA, pois sua tarefa era treinar o modelo. Se seus EAs tiverem outras tarefas, você precisará remover essa função do código. Ou mova-a para o local onde seu EA realizará todas as tarefas atribuídas a ele.

      O código completo do EA e todas as classes utilizadas podem ser encontrados no anexo.

      Depois de criar o Expert Advisor, podemos testá-lo com dados reais. Treinamos o autocodificador usando instrumento EURUSD, período gráfico H1 no histórico dos últimos 15 anos. Assim, o treinamento do autocodificador ocorreu sobre uma amostra de treinamento de mais de 92.000 padrões retirados de 40 velas A dinâmica do erro no processo de aprendizagem é mostrada no gráfico abaixo.

      Dinâmica de erro de treinamento do autocodificador

      Como pode ser visto no gráfico, em apenas 10 épocas, o erro quadrático médio caiu para 0,28 e depois continuou seu lento declínio. Ou seja, o autocodificador é capaz de compactar informações de 480 caraterísticas (40 velas * 12 caraterísticas por vela) até 2 elementos do estado latente preservando 78% das informações. Devo dizer que ao usar o PCA, um pouco menos de 25% dos dados semelhantes são salvos nos 2 primeiros componentes.

      Eu não peguei acidentalmente o tamanho do estado latente em 2 elementos. Isso nos dá a oportunidade de visualizá-lo e compará-lo com uma representação semelhante obtida usando o método dos componentes principais. Para preparar esses dados, modificamos ligeiramente o Expert Advisor criado acima. As principais mudanças afetarão a função de treinamento do modelo Train. O início da função, que inclui o processo de criação de uma amostra de treinamento, permaneceu inalterado.

      Imediatamente após a criação da amostra de treinamento, adicionaremos o treinamento do objeto de método do componente principal.

      void Train(datetime StartTrainBar = 0)
        {
      //---
          Процесс создания обучающей выборки без изменений
      //---
         if(!PCA.Study(data))
           {
            printf("Ошибка выполнения %d", GetLastError());
            return;
           }
      

      Além disso, no EA discutido acima, criamos um sistema de 2 loops aninhados para treinamento de modelo. Agora não iremos treinar novamente o autocodificador, mas usaremos o modelo previamente treinado. Portanto, não precisamos mais de um sistema de loops aninhados. Aqui, um ciclo de enumeração de elementos da amostra de treinamento é suficiente para nós. Deve-se dizer também que não visualizaremos o estado latente para todos os 92 mil padrões. Tal quantidade só vai complicar a percepção da informação. Eu decidi renderizar apenas 1000 padrões. Você pode repetir meus experimentos com seu próprio número de padrões para visualização.

      Devido ao fato de eu ter decidido não visualizar a amostra inteira, então seleciono aleatoriamente um padrão para visualização da amostra de treinamento. E eu preencho o buffer temporário com as caraterísticas do padrão selecionado.

           {
            //---
            stop = IsStopped();
            bool add_loop = false;
            for(int it = 0; i < 1000 && !stop; i++)
              {
               if((GetTickCount64() - last_tick) >= 250)
                 {
                  com = StringFormat("Calculation -> %d of %d -> %.2f%%", it + 1, 1000, (double)(it + 1.0) / 1000 * 100);
                  Comment(com);
                  last_tick = GetTickCount64();
                 }
               int i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (total));
               TempData.Clear();
               int r = i + (int)HistoryBars;
               if(r > bars)
                  continue;
               //---
               for(int b = 0; b < (int)HistoryBars; b++)
                 {
                  int bar_t = r - b;
                  double open = Rates[bar_t].open;
                  TimeToStruct(Rates[bar_t].time, sTime);
                  double rsi = RSI.Main(bar_t);
                  double cci = CCI.Main(bar_t);
                  double atr = ATR.Main(bar_t);
                  double macd = MACD.Main(bar_t);
                  double sign = MACD.Signal(bar_t);
                  if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
                     continue;
                  //---
                  if(!TempData.Add(Rates[bar_t].close - open) || !TempData.Add(Rates[bar_t].high - open) ||
                     !TempData.Add(Rates[bar_t].low - open) || !TempData.Add((double)Rates[bar_t].tick_volume / 1000.0) ||
                     !TempData.Add(sTime.hour) || !TempData.Add(sTime.day_of_week) || !TempData.Add(sTime.mon) ||
                     !TempData.Add(rsi) || !TempData.Add(cci) || !TempData.Add(atr) || !TempData.Add(macd) || !TempData.Add(sign))
                     break;
                 }
               if(TempData.Total() < (int)HistoryBars * 12)
                  continue;
      

      Depois de receber as informações sobre o padrão renderizado, chamamos o método de passagem direta do nosso autocodificador e aqui compactamos as informações usando o método do componente principal. Depois disso, obtemos o valor do buffer dos resultados do estado latente do nosso autocodificador.

               Net.feedForward(TempData, 12, true);
               data = PCA.ReduceM(TempData);
               TempData.Clear();
               if(!Net.GetLayerOutput(5, TempData))
                  break;
      

      Quero lembrar que anteriormente, ao testar todos os modelos considerados, verificamos sua capacidade de prever a formação de um fractal. Agora, para a separação visual dos padrões, faremos uma separação de cores dos padrões no gráfico. Portanto, precisamos especificar a que tipo o padrão renderizado pertence. Para entender isso, vamos verificar a formação de um fractal após o padrão.

               bool sell = (Rates[i - 1].high <= Rates[i].high && Rates[i + 1].high < Rates[i].high);
               bool buy = (Rates[i - 1].low >= Rates[i].low && Rates[i + 1].low > Rates[i].low);
               if(buy && sell)
                  buy = sell = false;
      

      E salvamos os dados recebidos em um arquivo para visualização posterior. Então passamos para o próximo padrão.

               FileWrite(handle, (buy ? DoubleToString(TempData.At(0)) : " "), (buy ? DoubleToString(TempData.At(1)) : " "),
                         (sell ? DoubleToString(TempData.At(0)) : " "), (sell ? DoubleToString(TempData.At(1)) : " "),
                         (!(buy || sell) ? DoubleToString(TempData.At(0)) : " "),
                         (!(buy || sell) ? DoubleToString(TempData.At(1)) : " "),
                         (buy ? DoubleToString(data[0, 0]) : " "), (buy ? DoubleToString(data[0, 1]) : " "),
                         (sell ? DoubleToString(data[0, 0]) : " "), (sell ? DoubleToString(data[0, 1]) : " "),
                         (!(buy || sell) ? DoubleToString(data[0, 0]) : " "),
                         (!(buy || sell) ? DoubleToString(data[0, 1]) : " "));
               stop = IsStopped();
              }
           }
      

      Após a conclusão de todas as iterações do ciclo, limpamos o campo de comentários no gráfico e encerramos o Expert Advisor.

         Comment("");
         ExpertRemove();
        }
      

      O código completo do EA pode ser encontrado no anexo.

      Como resultado do trabalho do Expert Advisor, foi gerado o arquivo "AE_latent.csv", no qual foram coletados os dados do estado latente do autocodificador e os dois primeiros componentes para os padrões correspondentes. Os 2 gráficos abaixo foram traçados a partir dos dados do arquivo.

      Visualização do estado latente do autocodificador Visualização dos 2 primeiros componentes principais

      Como se pode ver, em ambos os gráficos apresentados não há uma divisão clara dos padrões nos grupos desejados. No entanto, os dados de latência do autocodificador estão próximos de 0,5 em ambos os eixos. Quero lembrar que para a camada neural do estado latente, usamos o sigmóide como função de ativação. E esta retorna valores no intervalo de 0 a 1. Assim, o centro da distribuição obtida não está longe do centro do intervalo de valores da função.

      Além disso, a compactação de dados usando o método do componente principal fornece valores bastante grandes. Ao mesmo tempo, os valores ao longo dos eixos diferem de 6 a 7 vezes. E o centro de distribuição fica aproximadamente no ponto [18000, 130000]. Além disso, os limites superiores e inferiores lineares pronunciados do intervalo são dignos de nota.

      A partir da análise dos gráficos apresentados, eu escolheria o autocodificador como o instrumento para pré-processar os dados antes de passá-los para a entrada da rede neural de tomada de decisão.


      Considerações finais

      Neste artigo, conhecemos os autocodificadores, que são amplamente utilizados para resolver vários problemas. Construímos nosso primeiro autocodificador usando camadas totalmente conectadas e comparamos seu desempenho com a análise de componentes principais. Os resultados dos testes mostraram a vantagem de usar um autocodificador na resolução de problemas não lineares. Mas o tópico de realização e uso de autocodificadores é bastante extenso e excede o tamanho de um artigo. No próximo artigo, proponho considerar várias heurísticas para melhorar a eficiência dos autocodificadores.

      Ficarei feliz em responder a todas as suas perguntas no tópico do fórum dedicado a este artigo.


      Referências

      1. Redes neurais de maneira fácil (Parte 14): agrupamento de dados
      2. Redes neurais de maneira fácil (Parte 15): agrupamento de dados via MQL5
      3. Redes neurais de maneira fácil (Parte 16): uso prático do agrupamento
      4. Redes neurais de maneira fácil (Parte 17): redução de dimensionalidade
      5. Redes neurais de maneira fácil (Parte 18): regras de associação
      6. Redes neurais de maneira fácil (Parte 19): Regras de associação usando MQL5


      Programas utilizados no artigo

      # Nome Tipo Descrição
      1 ae.mq5 EA   EA de treinamento do autocodificador 
      2 ae2.mq5 EA EA de preparação de dados para visualização 
      2 NeuroNet.mqh Biblioteca de classe Biblioteca de classes para a criação de uma rede neural
      3 NeuroNet.cl Biblioteca Biblioteca de códigos do programa OpenCL


      Traduzido do russo pela MetaQuotes Ltd.
      Artigo original: https://www.mql5.com/ru/articles/11172

      Arquivos anexados |
      MQL5.zip (67.49 KB)
      Redes neurais de maneira fácil (Parte 21): Autocodificadores variacionais (VAE) Redes neurais de maneira fácil (Parte 21): Autocodificadores variacionais (VAE)
      No último artigo, analisamos o algoritmo do autocodificador. Como qualquer outro algoritmo, tem suas vantagens e desvantagens. Na implementação original, o autocodificador executa a tarefa de separar os objetos da amostra de treinamento o máximo possível. E falaremos sobre como lidar com algumas de suas deficiências neste artigo.
      Indicador CCI. Atualizações e novos recursos Indicador CCI. Atualizações e novos recursos
      Neste artigo, considerarei a possibilidade de atualizar o indicador CCI. Além disso, eu apresentarei uma modificação do indicador.
      Funcionalidades do assistente MQL5 que você precisa conhecer (Parte 02): Mapas de Kohonen Funcionalidades do assistente MQL5 que você precisa conhecer (Parte 02): Mapas de Kohonen
      Esta série de artigos propõe que o Assistente MQL5 deve ser um pilar para os traders. Por quê? Porque o trader não economiza apenas o tempo desenvolvendo suas novas ideias com o Assistente MQL5, mas reduz bastante os erros de desenvolvimento de código duplicado; ele está finalmente preparado para canalizar sua energia nas poucas áreas críticas de sua filosofia de negociação.
      Como desenvolver um sistema de negociação baseado no indicador Índice de Força Como desenvolver um sistema de negociação baseado no indicador Índice de Força
      Seja bem-vindo a este novo artigo em nossa série sobre como desenvolver um sistema de negociação com base no indicador técnico mais popular. Neste artigo, nós aprenderemos sobre um novo indicador técnico e como criar um sistema de negociação usando o indicador Índice de Força.