English Русский 中文 Español Deutsch 日本語
preview
Redes neurais de maneira fácil (Parte 25): Exercícios práticos de transferência de aprendizado

Redes neurais de maneira fácil (Parte 25): Exercícios práticos de transferência de aprendizado

MetaTrader 5Integração | 21 novembro 2022, 09:36
227 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Conteúdo


Introdução

Continuamos a estudar a tecnologia de transferência de aprendizado. Nos dois artigos anteriores, criamos uma ferramenta para criar e editar modelos de redes neurais. E será ela que hoje nos ajudará a transferir parte do modelo pré-treinado para um novo modelo e complementá-lo com novas camadas de toma de decisão. Este enfoque deverá, potencialmente, nos ajudar a treinar o modelo assim criado para resolver novos problemas mais rapidamente. Neste artigo, sugiro que você avalie os benefícios dessa abordagem na prática e, ao mesmo tempo, verifique a usabilidade da nossa ferramenta.


1. Questões gerais de preparação para o teste

Neste artigo, queremos avaliar os benefícios do uso da tecnologia transferência de aprendizado. E, para isso, nada melhor do que comparar o processo de aprendizado de dois modelos para resolver o mesmo problema. Dito isso, tomaremos um modelo "puro" iniciado por pesos aleatórios. E vamos criar um segundo modelo usando a tecnologia transferência de aprendizado.

E se como tarefa podemos usar a busca de fractais, como ao testar todos os modelos prévios em métodos de aprendizado supervisionado, então que usaremos como modelo doador para a tecnologia de transferência de aprendizado? E aqui podemos nos lembrar dos autocodificadores. Foram eles os que treinamos como doadores para o transferência de aprendizado. Ao estudar autocodificadores, criamos e treinamos 2 modelos de autocodificadores variacionais. No primeiro, o codificador era construído usando camadas neurais totalmente conectadas. E, no segundo, usamos um codificador em blocos LSTM recorrentes. Bem, agora podemos usar esses dois modelos como doadores. E, paralelamente, podemos verificar a eficácia de cada uma das abordagens mencionadas.

Já tomamos a primeira decisão fundamental na preparação dos próximos testes, isto é, como modelos doadores, usaremos autocodificadores variacionais treinados quando do estudo dos tópicos relacionados.

A segunda questão conceitual é como testaremos os modelos. Ao resolver isso, devemos pensar nas condições mais iguais para todos os modelos. Somente assim, poderemos excluir a influência de outros fatores e avaliar verdadeiramente a influência das particularidades de construção dos modelos.

E aqui, "particularidades de construção" é provavelmente a chave. Então, como avaliar os benefícios da transferência de aprendizado em modelos essencialmente diferentes? Na verdade, a situação não é clara e evidente. Vamos lembrar o que o autocodificador aprende. Sua arquitetura é tal que esperamos receber os dados de entrada na saída do modelo. Quando isso acontece, os dados de entrada são compactados pelo codificador até o "gargalo" do estado latente e depois restaurados pelo decodificador. O que quer dizer que simplesmente comprimimos os dados de entrada. Nesse caso, podemos considerar modelos com arquiteturas idênticas quando a arquitetura do modelo após o bloco codificador emprestado for igual à arquitetura do modelo de referência.

Por outro lado, juntamente com a compactação de dados, o codificador realiza o pré-processamento dos dados. Algumas características são destacadas e outras, pelo contrário, são zeradas. E nesta interpretação, para alinhar as arquiteturas dos dois modelos, precisamos criar uma cópia exata do modelo, mas já inicializado com pesos aleatórios.

E como há diferenças na compreensão do problema, realizaremos testes com ambas as abordagens para resolvê-lo.

E a próxima pergunta é sobre a ferramenta de teste. Se anteriormente criávamos um Expert Advisor separado para testar cada modelo, e isso era explicado pelo fato de que cada vez descrevíamos e criávamos o modelo no bloco de inicialização do Expert Advisor, agora a situação mudou. Criamos, pode-se dizer, uma ferramenta universal para elaborar modelos. Com ela, podemos criar diferentes arquiteturas de modelos e salvá-las em um arquivo. E então podemos enviar o modelo criado para qualquer Expert Advisor para treinamento e/ou uso.

Consequentemente, agora podemos criar um Expert Advisor no qual treinaremos todos os modelos. Assim, conseguimos oferecer as condições mais idênticas para os modelos de teste.

E a questão do ambiente de teste permanece sem resolver. Com quais dados testaremos nossos modelos? Aqui a resposta é clara e evidente, para o treinamento dos modelos usaremos um ambiente semelhante ao usado para treinar autocodificadores. Afinal, lembramos que as redes neurais são muito sensíveis aos dados de entrada. E elas funcionarão corretamente apenas com os dados usados para seu treinamento. Portanto, para usar a tecnologia de transferência de aprendizado, devemos usar dados de entrada semelhantes à amostra de treinamento do modelo doador.

Parece que decidimos sobre todas as questões-chave e podemos proceder à preparação para os testes.


2. Criando um Expert Advisor para teste

E iniciaremos nosso trabalho preparatório criando um Expert Advisor para testar modelos. Para fazer isso, realizaremos o EA modelo "check_net.mq5". Nele, vamos primeiro adicionar as bibliotecas:

  • NeuroNet.mqh - nossa biblioteca para criar redes neurais;
  • SymbolInfo.mqh - biblioteca padrão para acessar dados de instrumentos de negociação;
  • Oscilators.mqh - biblioteca padrão para trabalhar com osciladores.
E vamos declarar uma enumeração para facilitar o trabalho com sinais.

//+------------------------------------------------------------------+
//| Includes                                                         |
//+------------------------------------------------------------------+
#include "..\..\NeuroNet_DNG\NeuroNet.mqh"
#include <Trade\SymbolInfo.mqh>
#include <Indicators\Oscilators.mqh>
//---
enum ENUM_SIGNAL
  {
   Sell = -1,
   Undefine = 0,
   Buy = 1
  };

O próximo passo é declarar as variáveis globais do nosso EA. Aqui especificamos o arquivo do modelo, o período gráfico e o período de treinamento do modelo. Também exibiremos todos os parâmetros dos indicadores usados. Ao fazer isso, dividiremos os parâmetros do indicador em grupos, o que tornará o menu do nosso Expert Advisor mais legível.

//+------------------------------------------------------------------+
//|   input parameters                                               |
//+------------------------------------------------------------------+
input int                  StudyPeriod =  2;            //Study period, years
input string               FileName = "EURUSD_i_PERIOD_H1_test_rnn";
ENUM_TIMEFRAMES            TimeFrame   =  PERIOD_CURRENT;
//---
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

Em seguida, vamos declarar as instâncias dos objetos usados. E, ao fazer isso, excluímos ao máximo o uso de objetos dinâmicos. Isso simplificará um pouco o código removendo operações desnecessárias de criação/validação de objetos. Os nomes dos objetos correspondem tanto quanto possível ao seu conteúdo. Isso minimiza o risco de confusão de variáveis e melhora a legibilidade do código do nosso programa.

CSymbolInfo          Symb;
CNet                 Net;
CBufferFloat        *TempData;
CiRSI                RSI;
CiCCI                CCI;
CiATR                ATR;
CiMACD               MACD;
CBufferFloat         Fractals;

Também declararemos as variáveis globais do nosso EA. Agora não vou me alongar na descrição da funcionalidade de cada uma delas. Vamos conhecer para que servem quando analisemos os algoritmos das funções do Expert Advisor criado.

uint                 HistoryBars =  40;            //Depth of history
MqlRates             Rates[];
float                dError;
float                dUndefine;
float                dForecast;
float                dPrevSignal;
datetime             dtStudied;
bool                 bEventStudy;

Aqui podemos notar a variável de tamanho dos dados de entrada em barras, variável essa que costumávamos especificar nos parâmetros externos do EA. Ocultar este parâmetro e transferi-lo para variáveis globais é uma medida forçada. Na verdade, anteriormente descrevíamos a arquitetura do modelo criado na função de inicialização do EA. E este parâmetro era um dos hiperparâmetros do modelo, que o usuário especificava ao iniciar o Expert Advisor. Agora vamos usar os modelos criados anteriormente. Bem, o parâmetro da profundidade do histórico analisado deve corresponder ao modelo carregado. Mas como o usuário pode usar o modelo "às cegas" e não conhecer esse parâmetro, corremos o risco de uma incompatibilidade entre o parâmetro especificado e o modelo carregado. Para eliminar esse risco, decidi recalcular o parâmetro a partir do tamanho da camada de dados de entrada, do modelo carregado.

Vamos proceder à análise dos algoritmos das funções do Expert Advisor. Começaremos este trabalho com o método de inicialização do Expert Advisor OnInit. No corpo deste método, primeiro carregaremos o modelo do arquivo especificado nos parâmetros do EA. Aqui podemos prestar atenção a 2 pontos que diferem de operações semelhantes nos Expert Advisors apresentados anteriormente.

Primeiro, a rejeição de ponteiros dinâmicos torna desnecessária tanto a criação de uma nova instância do objeto de modelo quando a validação do ponteiro.

Segundo, se o modelo não for lido com sucesso a partir do arquivo, informamos o usuário e saímos da função com o resultado INIT_PARAMETERS_INCORRECT. E, com isso, terminamos o trabalho do EA. Como mencionado acima, estamos criando um Expert Advisor para trabalhar com distintos modelos criados anteriormente. Portanto, não há um modelo padrão. E, na ausência de um modelo, não teremos nada a ensinar, e o trabalho ulterior do EA não fará nenhum sentido. Portanto, após informar o usuário, encerramos o EA.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   ResetLastError();
   if(!Net.Load(FileName + ".nnw", dError, dUndefine, dForecast, dtStudied, false))
     {
      printf("%s - %d -> Error of read %s prev Net %d", __FUNCTION__, __LINE__, FileName + ".nnw", GetLastError());
      return INIT_PARAMETERS_INCORRECT;
     }

Após carregar o modelo com sucesso, calculamos o tamanho da profundidade do histórico analisado e armazenamos o valor resultante na variável HistoryBars. No entanto, vamos verificar o tamanho da camada de resultados. Deve conter 3 neurônios de acordo com o número de possíveis resultados do modelo.

   if(!Net.GetLayerOutput(0, TempData))
      return INIT_FAILED;
   HistoryBars = TempData.Total() / 12;
   Net.getResults(TempData);
   if(TempData.Total() != 3)
      return INIT_PARAMETERS_INCORRECT;

Após passar com sucesso os controles acima, inicializamos os objetos para trabalhar com indicadores.

   if(!Symb.Name(_Symbol))
      return INIT_FAILED;
   Symb.Refresh();
   if(!RSI.Create(Symb.Name(), TimeFrame, RSIPeriod, RSIPrice))
      return INIT_FAILED;
   if(!CCI.Create(Symb.Name(), TimeFrame, CCIPeriod, CCIPrice))
      return INIT_FAILED;
   if(!ATR.Create(Symb.Name(), TimeFrame, ATRPeriod))
      return INIT_FAILED;
   if(!MACD.Create(Symb.Name(), TimeFrame, FastPeriod, SlowPeriod, SignalPeriod, MACDPrice))
      return INIT_FAILED;

Claro, não nos esquecemos de controlar o processo de execução de cada uma das operações.

Após a inicialização de todos os objetos, geraremos um evento personalizado, para o qual transferiremos o controle e o método de treinamento do modelo. Escreveremos o resultado da geração de evento personalizado na variável bEventStudy, que funcionará como um sinalizador para iniciar o processo de treinamento do modelo.

A operação de geração de eventos personalizados nos permite terminar o método de inicialização do Expert Advisor, bem como, em paralelo, inicializar o processo de treinamento do modelo sem esperar por um novo tick. Assim, tornamos o início do processo de aprendizado do modelo independente da volatilidade do mercado.

   bEventStudy = EventChartCustom(ChartID(), 1, (long)MathMax(0, MathMin(iTime(Symb.Name(), PERIOD_CURRENT,
                                  (int)(100 * Net.recentAverageSmoothingFactor * (dForecast >= 70 ? 1 : 10))), dtStudied)),

                                  0, "Init");
//---
   return(INIT_SUCCEEDED);
  }

No método de desinicialização do EA, excluiremos apenas o único objeto dinâmico usado no EA. Aqui, novamente, a rejeição do uso de objetos dinâmicos se refletiu.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(TempData) != POINTER_INVALID)
      delete TempData;
  }

Todos os eventos do gráfico são processados na função OnChartEvent. Incluindo nosso evento personalizado. Portanto, nesta função, estamos aguardando a ocorrência de um evento de usuário, que pode ser identificado pelo seu ID. A identificação do evento do usuário começa em 1000. Ao gerar um evento personalizado, atribuímos a ele o ID "1". Isso significa que nesta função devemos receber um evento com o identificador "1001". Quando tal evento ocorre, chamamos o procedimento de treinamento do nosso modelo Train.

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if(id == 1001)
      Train(lparam);
  }

Vamos dar uma olhada mais de perto na elaboração do algoritmo, provavelmente a principal função do nosso Expert Advisor é treinar o modelo Train. Nos parâmetros, esta função recebe um valor, a data de início do período de treinamento. E primeiro verificamos se esta data está fora dos limites do período de treinamento especificado pelo usuário nos parâmetros externos do EA. Se a data recebida não corresponder ao período especificado pelo usuário, mudamos a data para o início do período de treinamento especificado.

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;

Em seguida, preparamos as variáveis locais.

   double prev_er = DBL_MAX;
   datetime bar_time = 0;
   bool stop = IsStopped();

E carregamos os dados históricos. Ao fazer isso, carregamos os dados de cotações e indicadores. Aqui é importante manter os buffers dos indicadores e as cotações carregadas em sincronia. Desse modo, primeiro carregamos as cotações para o período especificado. Determinamos o número de barras carregadas. E carregamos o mesmo período para todos os indicadores utilizados.

   int bars = CopyRates(Symb.Name(), TimeFrame, st_time, TimeCurrent(), Rates);
   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);

Depois de carregar a amostra de treinamento, pegaremos os últimos 300 elementos do número total de elementos da amostra de treinamento para validação após cada época de treinamento. E então criamos um sistema de loops do processo de aprendizado. Aqui, o loop externo fará a contagem regressiva das épocas de treinamento e controlará se o processo de treinamento do modelo deve continuar. No corpo do loop, atualizaremos os valores dos sinalizadores:

  • prev_er - erro de modelo na época anterior;
  • stop - geração do evento de encerramento do programa pelo usuário.

   MqlDateTime sTime;
   int total = (int)(bars - MathMax(HistoryBars, 0) - 300);
   do
     {
      prev_er = dError;
      stop = IsStopped();

Em um loop aninhado, iteramos sobre os elementos da amostra de treinamento e alimentamos a rede neural com eles. Como planejamos usar modelos recorrentes sensíveis à sequência de dados de entrada, somos forçados a abandonar a escolha de um próximo elemento aleatório da sequência. Em vez disso, usaremos a sequência histórica de elementos.

Aqui verificamos imediatamente se há suficientes dados do elemento atual para elaborar o padrão. Se não houver dados suficientes, vamos para o próximo elemento.

      for(int it = total; it > 1 && !stop; t--)
        {
         TempData.Clear();
         int i = it + 299;
         int r = i + (int)HistoryBars;
         if(r > bars)
            continue;

Com informações suficientes, formamos um padrão para alimentar o modelo. Ao fazer isso, controlamos a disponibilidade de dados nos buffers do indicador. Se os valores do indicador não estiverem definidos, passamos para o próximo elemento.

         for(int b = 0; b < (int)HistoryBars; b++)
           {
            int bar_t = r - b;
            float open = (float)Rates[bar_t].open;
            TimeToStruct(Rates[bar_t].time, sTime);
            float rsi = (float)RSI.Main(bar_t);
            float cci = (float)CCI.Main(bar_t);
            float atr = (float)ATR.Main(bar_t);
            float macd = (float)MACD.Main(bar_t);
            float sign = (float)MACD.Signal(bar_t);
            if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
               continue;
            //---
            if(!TempData.Add((float)Rates[bar_t].close - open) || !TempData.Add((float)Rates[bar_t].high - open) ||
               !TempData.Add((float)Rates[bar_t].low - open) || !TempData.Add((float)Rates[bar_t].tick_volume / 1000.0f) ||
               !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 a formação de padrão bem-sucedida, chamamos o método de propagação do nosso modelo. E então solicitamos o resultado da propagação.

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

Aplicamos a função SortMax aos resultados do modelo para transferir os valores obtidos para o escopo das probabilidades. 

         float sum = 0;
         for(int res = 0; res < 3; res++)
           {
            float temp = exp(TempData.At(res));
            sum += temp;
            TempData.Update(res, temp);
           }
         for(int res = 0; (res < 3 && sum > 0); res++)
            TempData.Update(res, TempData.At(res) / sum);
         //---
         switch(TempData.Maximum(0, 3))
           {
            case 1:
               dPrevSignal = (TempData[1] != TempData[2] ? TempData[1] : 0);
               break;
            case 2:
               dPrevSignal = -TempData[2];
               break;
            default:
               dPrevSignal = 0;
               break;
           }

Depois disso, exibimos informações sobre o processo de aprendizado no gráfico.

         if((GetTickCount64() - last_tick) >= 250)
           {
            string s = StringFormat("Study -> Era %d -> %.2f -> Undefine %.2f%% foracast %.2f%%\n %d of %d -> %.2f%% \n
                                     Error %.2f\n%s -> %.2f ->> Buy %.5f - Sell %.5f - Undef %.5f", count, dError, 
                                     dUndefine, dForecast, total - it - 1, total, 
                                     (double)(total - it - 1.0) / (total) * 100, Net.getRecentAverageError(),
                                      EnumToString(DoubleToSignal(dPrevSignal)), dPrevSignal, TempData[1], TempData[2], TempData[0]);
            Comment(s);
            last_tick = GetTickCount64();
           }

A propagação no processo de treinamento do modelo é seguida pela retropropagação. E aqui vamos primeiro criar os valores alvo e passá-los para o método de retropropagação do nosso modelo. E calculamos imediatamente as estatísticas do processo de aprendizado.

         stop = IsStopped();
         if(!stop)
           {
            TempData.Clear();
            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);
            TempData.Add(!(buy || sell));
            TempData.Add(buy);
            TempData.Add(sell);
            Net.backProp(TempData);
            ENUM_SIGNAL signal = DoubleToSignal(dPrevSignal);
            if(signal != Undefine)
              {
               if((signal == Sell && sell) || (signal == Buy && buy))
                  dForecast += (100 - dForecast) / Net.recentAverageSmoothingFactor;
               else
                  dForecast -= dForecast / Net.recentAverageSmoothingFactor;
               dUndefine -= dUndefine / Net.recentAverageSmoothingFactor;
              }
            else
              {
               if(!(buy || sell))
                  dUndefine += (100 - dUndefine) / Net.recentAverageSmoothingFactor;
              }
           }
        }

Assim concluímos a iteração do loop aninhado sobre os elementos da amostra de treinamento em uma época do treinamento do modelo. E então efetuamos a validação para avaliar o comportamento do modelo com dados que não estão incluídos no conjunto de treinamento. Para isso, nos últimos 300 elementos, fazemos um loop semelhante, mas apenas com propagação. Durante o processo de validação, não faremos retropropagação e atualizaremos as matrizes de peso.

      count++;
      for(int i = 0; i < 300; i++)
        {
         TempData.Clear();
         int r = i + (int)HistoryBars;
         if(r > bars)
            continue;
         //---
         for(int b = 0; b < (int)HistoryBars; b++)
           {
            int bar_t = r - b;
            float open = (float)Rates[bar_t].open;
            TimeToStruct(Rates[bar_t].time, sTime);
            float rsi = (float)RSI.Main(bar_t);
            float cci = (float)CCI.Main(bar_t);
            float atr = (float)ATR.Main(bar_t);
            float macd = (float)MACD.Main(bar_t);
            float sign = (float)MACD.Signal(bar_t);
            if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
               continue;
            //---
            if(!TempData.Add((float)Rates[bar_t].close - open) || !TempData.Add((float)Rates[bar_t].high - open) ||
               !TempData.Add((float)Rates[bar_t].low - open) || !TempData.Add((float)Rates[bar_t].tick_volume / 1000.0f) ||
               !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;
         Net.feedForward(TempData, 12, true);
         Net.getResults(TempData);
         //---
         float sum = 0;
         for(int res = 0; res < 3; res++)
           {
            float temp = exp(TempData.At(res));
            sum += temp;
            TempData.Update(res, temp);
           }
         for(int res = 0; (res < 3 && sum > 0); res++)
            TempData.Update(res, TempData.At(res) / sum);
         //---
         switch(TempData.Maximum(0, 3))
           {
            case 1:
               dPrevSignal = (TempData[1] != TempData[2] ? TempData[1] : 0);
               break;
            case 2:
               dPrevSignal = (TempData[1] != TempData[2] ? -TempData[2] : 0);
               break;
            default:
               dPrevSignal = 0;
               break;
           }

Depois de realizar uma propagação com os validados, exibiremos os sinais do nosso modelo no gráfico para uma avaliação visual de seu desempenho.

         if(DoubleToSignal(dPrevSignal) == Undefine)
            DeleteObject(Rates[i].time);
         else
            DrawObject(Rates[i].time, dPrevSignal, Rates[i].high, Rates[i].low);
        }

E no final de cada época, salvamos o estado atual do modelo. Aqui também adicionamos o erro do modelo atual ao arquivo para controlar a dinâmica do processo de aprendizado.

      if(!stop)
        {
         dError = Net.getRecentAverageError();
         Net.Save(FileName + ".nnw", dError, dUndefine, dForecast, Rates[0].time, false);
         printf("Era %d -> error %.2f %% forecast %.2f", count, dError, dForecast);
         int h = FileOpen(FileName + ".csv", FILE_READ | FILE_WRITE | FILE_CSV);
         if(h != INVALID_HANDLE)
           {
            FileSeek(h, 0, SEEK_END);
            FileWrite(h, eta, count, dError, dUndefine, dForecast);
            FileFlush(h);
            FileClose(h);
           }
        }
     }
   while(!(dError < 0.01 && (prev_er - dError) < 0.01) && !stop);

Em seguida, avaliamos a mudança no erro do modelo ao longo da última época de treinamento e decidimos se continuamos o treinamento. Se for tomada a decisão de continuar, repetimos as iterações cíclicas da nova época de aprendizado.

Após a conclusão do processo de treinamento do modelo, limpamos a área de comentários no gráfico e inicializamos o encerramento do Expert Advisor, pois ele completou sua tarefa de treinar o modelo e sua presença adicional na memória não faz sentido.

   Comment("");
   ExpertRemove();
  }

As funções auxiliares para exibir rótulos no gráfico e excluí-los foram completamente aproveitadas dos Expert Advisors considerados anteriormente, e não repetirei seu algoritmo agora. Você pode conhecer o conteúdo completo de todas as funções do EA no anexo.


3. Elaborando modelos para teste

Após criar uma ferramenta para testar modelos, devemos preparar a base para teste, ou seja, criar aqueles modelos que vamos treinar. E aqui não vamos mais programar. Como todo o trabalho de programação para a criação de modelos já foi elaborado nos 2 artigos anteriores, agora vamos aproveitar os resultados e criar modelos usando nossa ferramenta.

Para fazer isso, iniciamos o Expert Advisor "NetCreator" criado anteriormente. Nele, abriremos um modelo de um autocodificador pré-treinado utilizando um codificador recorrente baseado em blocos LSTM. Anteriormente, nós o salvávamos no arquivo "EURUSD_i_PERIOD_H1_rnn_vae.nnw". Tomaremos apenas o codificador a partir deste modelo. Portanto, no bloco esquerdo do modelo pré-treinado, encontraremos a camada de estado latente do autocodificador variacional VAE. No meu caso, ele é o oitavo. Portanto, copiarei apenas as 7 primeiras camadas neurais do modelo doador.

Existem 3 maneiras de selecionar o número necessário de camadas para copiar em nossa ferramenta. Você pode usar os botões da área "Transfer Layers" ou os botões "↑" e "↓" do teclado. Ou você pode simplesmente clicar na descrição da última camada copiada na descrição do modelo doador.

Simultaneamente com a mudança no número de camadas copiadas, a descrição do modelo criado no bloco direito de nossa ferramenta também muda. Na minha opinião, é bastante conveniente e informativo. Você vê imediatamente como suas ações afetam a arquitetura do modelo que está sendo criado.

Em seguida, temos que complementar nosso novo modelo com várias camadas neurais de tomada de decisão para uma tarefa de aprendizado específica. Aqui não compliquei muito, pois nesses testes a principal tarefa é avaliar a eficácia das abordagens. 2 camadas neurais totalmente conectadas com 500 elementos e tangente hiperbólica como função de ativação foram adicionadas.

Adicionar novas camadas neurais acabou sendo uma tarefa bastante simples. Primeiro, selecionamos o tipo de camada neural. Uma camada neural totalmente conectada corresponde a "Dense". Especificamos o número de neurônios na camada, a função de ativação e o método de atualização de parâmetro. Se você selecionar um tipo diferente de camada neural, preencherá os campos relevantes a ela. Após especificar todos os dados necessários, pressionamos o botão "ADD LAYER".

E eis mais um ponto interessante e conveniente. Se você precisar adicionar várias camadas neurais idênticas, não será necessário inserir novamente os dados. Basta pressionar o botão novamente "ADD LAYER". Isso é o que eu fiz. Para adicionar a 2ª camada neural, não inseri mais dados, senão simplesmente pressionei o botão para adicionar uma nova camada.

A camada de resultados também está totalmente conectada e contém 3 elementos, de acordo com os requisitos do Expert Advisor criado acima. Sigmoid é usado como função de ativação para a camada de resultado.

Nossas camadas neurais anteriores também estavam totalmente conectadas. Portanto, só podemos alterar o número de neurônios e a função de ativação. Em seguida, adicionamos uma camada ao nosso modelo.

Agora só temos que salvar nosso novo modelo em um arquivo. Para fazer isso, pressionamos o botão "SAVE MODEL" e especificamos o nome do arquivo do novo modelo "EURUSD_i_PERIOD_H1_test_rnn.nnw". Observe que você pode especificar o nome do arquivo sem a extensão. Ela será adicionada automaticamente.

Você pode ver todo o processo de criação de modelo no gif abaixo.

Usando a ferramenta de criação de modelos

O primeiro modelo foi criado. E passamos para a criação do segundo modelo. Para isso, como doador, carregamos um autocodificador variacional com um codificador totalmente conectado desde o arquivo "EURUSD_i_PERIOD_H1_vae.nnw". E então outra surpresa nos esperava. Depois de carregar o novo modelo doador, não removemos as camadas neurais adicionadas. Assim, elas foram adicionadas automaticamente ao modelo carregado. Nós apenas temos que escolher o número de camadas neurais para copiar do modelo doador para o novo modelo. E nosso próximo modelo está pronto.

Devo dizer que, com base no modelo de autocodificador mais recente, criei não um, mas dois modelos. Criei o primeiro modelo por analogia com o anterior. Peguei o codificador do modelo doador e adicionei as 3 camadas criadas anteriormente. Para o segundo modelo, a partir do doador peguei apenas a camada de dados de entrada e a camada de normalização de lote. A elas eu adicionei as mesmas 3 camadas neurais totalmente conectadas. O último modelo será usado como guia para o treinamento de um novo modelo. Decidi que a camada de normalização de lote pré-treinada nos serviria como uma preparação de dados de entrada brutos. E isso deve aumentar a convergência do novo modelo. Quando isso acontece, excluímos a compactação de dados. E podemos assumir que o último modelo está completamente preenchido com pesos aleatórios.

Como discutimos acima, existem diferentes maneiras de avaliar os conceitos de impacto da arquitetura de um modelo pré-treinado. Portanto, para mais um teste, criei com outro modelo. Peguei as arquiteturas do modelo recém-criado usando o codificador com blocos LSTM e o repliquei completamente no novo modelo. Mas sem copiar o codificador do modelo doador. Assim, obtive uma arquitetura de modelo completamente idêntica, mas já inicializada com pesos aleatórios.


4. Resultado dos testes

E agora que criamos todos os modelos que precisamos para nossos testes, passaremos a treiná-los.

Treinamos os modelos pelo método de aprendizado supervisionado com a preservação dos parâmetros de treinamento utilizados anteriormente. Assim, o treinamento é realizado em um intervalo de tempo a longo dos últimos 2 anos. Usamos o instrumento EURUSD e o timeframe H1. Os parâmetros de todos os indicadores foram usados definidos no Expert Advisor por padrão.

Para a pureza do experimento, treinamos todos os modelos simultaneamente em um terminal em gráficos diferentes.

Convém dizer que não é aconselhável treinar vários modelos ao mesmo tempo, uma vez que isso reduz significativamente a taxa de aprendizado de cada um deles. Como você sabe, em nossos modelos utilizamos a tecnologia OpenCL para paralelizar o processo computacional e aproveitar ao máximo os recursos disponíveis. E com o treinamento paralelo de vários modelos, teremos que compartilhar os recursos disponíveis entre todos os modelos. E isso significa cortar os recursos disponíveis para cada um deles e, como resultado, um aumento no tempo de treinamento para cada um deles. Mas demos esse passo intencionalmente para oferecer as condições mais idênticas para treinar todos os modelos comparados.

Teste 1

Para o primeiro teste, usamos dois modelos com codificadores pré-treinados e um modelo pequeno totalmente conectado com uma camada de normalização de lote emprestada e 2 camadas ocultas totalmente conectadas.

Os resultados do teste do modelo são mostrados no gráfico abaixo.

Comparação da dinâmica do aprendizado do modelo

Como pode ser visto no gráfico acima, o modelo com codificador recursivo pré-treinado mostra uma clara vantagem. Praticamente desde as primeiras épocas de treinamento, seu erro diminuiu em um ritmo significativamente mais rápido.

O modelo com codificador totalmente conectado também apresentou tendência de redução do erro durante o processo de aprendizado, porém a um ritmo mais lento.

Um modelo totalmente totalmente conectado com 2 camadas ocultas inicializadas por valores aleatórios em seu fundo parece não ser treinável em absoluto. A julgar pelo gráfico apresentado, fica-se com a impressão de que seu erro está congelado no lugar.

Dinâmica de erro de modelo totalmente conectado

Mas quando se observa em detalhes, percebe-se uma tendência para a diminuição do erro. Embora esse declínio ocorra em um ritmo muito mais lento. Obviamente, tal modelo é muito simples para resolver tais problemas.

A partir disso podemos concluir que o desempenho do modelo ainda é muito influenciado pelo processamento dos dados de entrada pelo codificador pré-treinado. E a arquitetura de tal codificador tem um impacto significativo na operação de todo o modelo.

Separadamente, deve-se falar sobre a velocidade dos modelos de aprendizado. Claro, o modelo mais simples mostrava o tempo mínimo para passar uma época. Mas vale a pena notar que a taxa de aprendizado do modelo com um codificador recorrente acabou sendo muito próxima. Na minha opinião, isso foi influenciado por uma série de fatores.

Em primeiro lugar, a arquitetura do modelo recorrente nos permitiu reduzir em 4 vezes a janela de dados analisados e, ao mesmo tempo, o número de conexões interneurais. Como resultado, o custo de seu processamento é reduzido. Ao mesmo tempo, a arquitetura recorrente implica custos de recursos adicionais para a retropropagação da distribuição do gradiente de erro. Por isso desativamos a retropropagação para camadas neurais pré-treinadas. Tudo isso junto nos permitiu reduzir significativamente o custo de retreinamento do modelo.

O modelo com um codificador totalmente conectado está um pouco em termos de velocidade de aprendizado.

Teste 2

No segundo teste, decidimos minimizar as diferenças arquitetônicas entre os modelos, bem como treinar dois modelos recorrentes com a mesma arquitetura. Apenas um modelo usa um codificador recorrente pré-treinado. E o segundo modelo é totalmente inicializado com pesos aleatórios. O treinamento foi realizado com a preservação de todos os parâmetros de teste especificados no primeiro teste.

Os resultados do teste são mostrados no gráfico abaixo. Como você pode ver, o modelo pré-treinado começou com um erro menor. Mas logo ambos os modelos se estabilizaram e seus valores ficaram bem próximos. Isso confirma a conclusão anterior de que a arquitetura do codificador tem um impacto significativo no desempenho de todo o modelo.

Comparação de dinâmicas de aprendizado de modelos recorrentes

Também vale a pena notar a velocidade de aprendizado. Durante os testes, o modelo pré-treinado mostrou 6 vezes menos tempo para passar uma época. Claro, aqui levamos em conta o tempo puro sem levar em conta o custo de treinamento do autocodificador.


Considerações finais

O trabalho realizado nos permite concluir uma série de vantagens derivadas da utilização da tecnologia transferência de aprendizado. Em primeiro lugar, esta tecnologia realmente funciona. Sua aplicação permite reutilizar blocos de modelos já treinados para resolver novos problemas. A única condição é a relação de igualdade dos dados de entrada. O uso de blocos pré-treinados com dados de entrada inadequados não produzirá resultados.

O uso da tecnologia permite reduzir o tempo de treinamento de um novo modelo. É verdade que, durante o processo de teste, o tempo puro foi medido sem levar em consideração o custo de treinamento do autocodificador. Provavelmente, se somarmos o tempo gasto no treinamento do autocodificador, os custos serão iguais. E talvez até devido à arquitetura de decodificador mais complexa treinar um modelo "puro" será mais rápido. Portanto, o uso do transferência de aprendizado pode ser justificado quando um bloco deve ser usado para resolver vários problemas, ou quando o treinamento do modelo como um todo não é possível por algum motivo. Por exemplo, o modelo pode ser muito complexo e durante o processo de aprendizado o gradiente de erro decai e não atinge todas as camadas.

Além disso, o uso de tecnologia pode ser justificado no processo de encontrar o modelo ótimo, quando gradativamente complicamos o modelo na busca do valor ótimo de erro.


Referências

  1. Redes neurais de maneira fácil (Parte 20): autocodificadores
  2. Redes neurais de maneira fácil (Parte 21): autocodificadores variacionais (VAE)
  3. Redes neurais de maneira fácil (Parte 22): aprendizado não supervisionado de modelos recorrentes
  4. Redes neurais de maneira fácil (Parte 23): criando uma ferramenta para transferência de aprendizado
  5. Redes neurais de maneira fácil (Parte 24): melhorando a ferramenta para transferência de aprendizado

Programas utilizados no artigo

# Nome Tipo Descrição
1 check_net.mq5  EA Expert Advisor para treinamento adicional de modelos 
2 NetCreator.mq5 EA Ferramenta para construção de modelos
3 NetCreatotPanel.mqh Biblioteca de classe Biblioteca da classe para criação da ferramenta
4 NeuroNet.mqh Biblioteca de classe Biblioteca das classes para criar uma rede neural
5 NeuroNet.cl Biblioteca Biblioteca do código do programa OpenCL


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

Arquivos anexados |
MQL5.zip (78.84 KB)
Redes neurais de maneira fácil (Parte 26): aprendizado por reforço Redes neurais de maneira fácil (Parte 26): aprendizado por reforço
Continuamos a estudar métodos de aprendizado de máquina. Com este artigo, começamos outro grande tópico chamado aprendizado por reforço. Essa abordagem permite que os modelos estabeleçam certas estratégias para resolver as tarefas. E esperamos que essa propriedade inerente ao aprendizado de reforço abra novos horizontes para a construção de estratégias de negociação.
Operações com Matrizes e Vetores em MQL5 Operações com Matrizes e Vetores em MQL5
Matrizes e vetores foram introduzidos na MQL5 para operações eficientes com soluções matemáticas. Os novos tipos oferecem métodos integrados para a criação de código conciso e compreensível que se aproxima da notação matemática. Os arrays fornecem recursos extensos, mas há muitos casos em que as matrizes são muito mais eficientes.
Redes neurais de maneira fácil (Parte 27): Aprendizado Q profundo (DQN) Redes neurais de maneira fácil (Parte 27): Aprendizado Q profundo (DQN)
Continuamos nosso estudo sobre aprendizado por reforço. E, neste artigo, vamos nos familiarizar com o método de aprendizado Q profundo. Com esse método, a equipe do DeepMind criou um modelo que pode superar um humano ao jogar jogos do Atari. Acho que será útil avaliar as possibilidades de tal tecnologia para resolver problemas de negociação.
DoEasy. Controles (Parte 15): Objeto WinForms TabControl - múltiplas fileiras de cabeçalhos de guias, métodos de manuseio de guias DoEasy. Controles (Parte 15): Objeto WinForms TabControl - múltiplas fileiras de cabeçalhos de guias, métodos de manuseio de guias
Neste artigo, continuaremos trabalhando no objeto WinForm TabControl, e para tal criaremos a classe do objeto-campo de guia, tornaremos possível colocar cabeçalhos de guias em várias linhas e adicionaremos métodos para trabalhar com as guias do objeto.