Redes neurais de maneira fácil (Parte 25): Exercícios práticos de transferência de aprendizado
Conteúdo
- Introdução
- 1. Questões gerais de preparação para o teste
- 2. Criando um Expert Advisor para teste
- 3. Elaborando modelos para teste
- 4. Resultado dos testes
- Considerações finais
- Referências
- Programas utilizados no artigo
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.
//+------------------------------------------------------------------+ //| 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.
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.
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.
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.
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
- Redes neurais de maneira fácil (Parte 20): autocodificadores
- Redes neurais de maneira fácil (Parte 21): autocodificadores variacionais (VAE)
- Redes neurais de maneira fácil (Parte 22): aprendizado não supervisionado de modelos recorrentes
- Redes neurais de maneira fácil (Parte 23): criando uma ferramenta para transferência de aprendizado
- 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
- 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