English Русский 中文 Español Deutsch 日本語
preview
Redes neurais de maneira fácil (Parte 23): Criando uma ferramenta para transferência de aprendizado

Redes neurais de maneira fácil (Parte 23): Criando uma ferramenta para transferência de aprendizado

MetaTrader 5Sistemas de negociação | 15 novembro 2022, 15:43
323 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Conteúdo


Introdução

Continuamos nossa imersão no mundo da inteligência artificial. Hoje eu gostaria de apresentar a transferência de aprendizado (Transfer Learning, em inglês). De uma forma ou de outra, mencionamos repetidamente essa tecnologia, mas nunca a usamos. Porém, esta é uma ferramenta bastante poderosa que permite aumentar a eficiência do desenvolvimento de redes neurais e reduzir o custo de treiná-las.


1. Por que a transferência de aprendizado é necessária?

O que é a transferência de aprendizado e por que ela é necessária? Em um sentido geral, a transferência de aprendizado é um método de aprendizado de máquina no qual o conhecimento de um modelo treinado para resolver um problema é reutilizado como base para a resolução de novos problemas. Claro que, para resolver novos problemas, o modelo é preliminarmente treinado com novos dados. E normalmente, com o modelo doador adequado, o reaprendizado é muito mais rápido e com melhores resultados do que o treinamento de um modelo similar a partir do zero.

Nesse caso, o modelo doador pode ser usado tanto total quanto parcialmente.

Em parte, pode-se chamar de uso dessa tecnologia, quando utilizamos os resultados de agrupamento e compressão de dados como processamento preliminar dos dados iniciais para a rede neural. Neste caso, fizemos pleno uso do modelo pré-treinado. Mas ao construir um modelo para resolver novos problemas, não realizamos treinamento adicional do modelo doador. Usamos apenas para pré-processar os dados iniciais "brutos" e treinamos um novo modelo com base eles.

Se nos lembrarmos de como começamos a estudar autocodificadores, também falamos sobre a possibilidade de usar a transferência de aprendizado após treinar o modelo. Mas, neste caso, não podemos usar o autocodificador completamente como um modelo doador, porque o treinamos para compactar os dados de entrada e depois restaurá-los a partir da sua forma compactada. É por isso que usar todo o autocodificador como um modelo doador não tem sentido. Para o pré-processamento de dados, é muito mais eficiente usar apenas o codificador. Nesse caso, o modelo geral será menor e a eficiência das camadas subsequentes será maior, pois serão necessários menos pesos treináveis para processar a mesma quantidade de informações.

Bem, o uso da transferência de aprendizado está longe de se limitar ao uso de resultados de aprendizagem não supervisionados. Pense em quantas vezes você começou a treinar seu modelo novamente quando tudo o que você precisava fazer era adicionar ou remover uma camada neural. Mas, afinal, parte das camadas neurais poderia ter sido reutilizada.

Existe outra área de aplicação desta tecnologia. Devido ao problema do gradiente de desvanecimento, é quase impossível treinar completamente um modelo profundo. Usar a transferência de aprendizado nos permite treinar camadas neurais em blocos e aumentar gradualmente o tamanho do modelo.

Certamente, você pode encontrar outros usos para essa tecnologia. E sugiro que passemos a estudar uma ferramenta para sua aplicação.


2. Criando uma ferramenta

Ao construir uma ferramenta, sugiro que se definamos seus objetivos. Antes de tudo, vamos lembrar como salvamos nossos modelos treinados. Todos eles são salvos em um único arquivo binário. Adicionalmente a isso, cada objeto do nosso modelo possui sua própria estrutura estrita para registro de dados. É por isso que será difícil simplesmente remover parte dos dados do arquivo no editor. Portanto, precisamos carregar todo o modelo treinado a partir do arquivo, modificá-lo tanto quanto necessário e salvar o novo modelo em um novo arquivo ou sobrescrever o antigo. É dada preferência ao novo arquivo, pois o modelo doador pode continuar a ser utilizado para as tarefas sobre as quais ele foi treinado.

O segundo ponto é que nossas redes neurais funcionam bem apenas com os dados sobre os que foram treinadas. Se usarmos dados completamente novos, podemos obter resultados imprevisíveis. Isso também se aplica a camadas neuronais individuais. Portanto, para a transferência de aprendizado, podemos usar apenas camadas neurais subsequentes, começando pela camada de dados de entrada. É impossível remover algum bloco do meio ou da parte final do modelo, o que significa que podemos pegar todo o modelo doador ou algumas de suas primeiras camadas, adicionar diferentes camadas neurais a ele e salvar o novo modelo.

Ao fazer isso, precisamos garantir a plena funcionalidade do novo modelo tanto no modo de treinamento quanto no modo de uso real. Obviamente, antes do uso real, o novo modelo deve ser treinado.

Há um outro ponto a ser considerado aqui. As camadas neurais do modelo doador mantêm seus pesos, e junto com eles, todo o seu conhecimento adquirido na fase de treinamento preliminar do modelo. As novas camadas neurais receberão pesos aleatórios, assim como quando o modelo foi inicializado. Se começarmos a treinar o novo modelo como antes, desequilibraremos as camadas neurais treinadas anteriormente, juntamente com o treinamento das novas camadas. Por isso, devemos primeiro bloquear o treinamento das camadas neurais do modelo doador e treinar apenas novas camadas.


2.1 Desenho

É necessário entender que não precisamos apenas de um produto informático que pega o modelo doador inicial, processa-o de alguma forma e o salva novamente em um novo arquivo. Afinal, o número de camadas copiadas e a arquitetura do modelo criado são sempre individuais. Sendo assim, precisamos é de uma ferramenta que permita ao usuário ajustar cada modelo individualmente de forma rápida e conveniente. Em outras palavras, precisamos de uma ferramenta com uma interface de usuário conveniente. E vamos começar a trabalhar nisso, procedendo ao design da interface do usuário.

Bem, eu vejo 3 blocos que se destacam claramente. No primeiro bloco, trabalharemos com um modelo doador. Aqui precisamos da capacidade de selecionar um arquivo com um modelo treinado. Após carregar um modelo a partir de um arquivo, a ferramenta deve nos apresentar uma descrição da arquitetura do modelo carregado. Afinal, o usuário precisa entender qual modelo está carregado e quais camadas neurais ele irá copiar. Aqui, informaremos à ferramenta o número de camadas a serem copiadas. Como mencionado acima, copiaremos as camadas neurais em uma linha, começando pela camada de dados de entrada.

No segundo bloco, elaboramos a adição de camadas neurais. Aqui vamos criar campos para inserir informações sobre a camada neural que está sendo criada. Como no código de software, descreveremos cada camada neural uma a uma e a acrescentaremos à arquitetura do novo modelo.

Já o terceiro bloco exibirá a arquitetura completa do modelo que está sendo criado, com a opção de especificar um arquivo para salvá-lo. Abaixo podemos ver um exemplo de projeto da ferramenta a ser criada.

Design da ferramenta

Tanto o design da ferramenta quanto sua implementação são apresentados apenas para fins de demonstração. Você sempre pode alterá-los a seu critério para melhor atender às suas necessidades.


2.2 Realizando a interface do usuário 

Uma vez decidido o design de nossa ferramenta, podemos começar a elaborá-la. Para fazer isso, criaremos uma nova classe CNetCreatorPanel que herda a classe base do aplicativo de diálogo CAppDialog.

Cada controle em nosso painel será criado por um objeto separado. Portanto, o número de objetos que declararemos em nossa nova classe será bastante grande. Por conveniência, vamos dividi-los em blocos.

No primeiro bloco, vamos declarar objetos relacionados à visualização do modelo pré-treinado:

  • m_edPTModel - elemento para inserir o nome do arquivo de um modelo pré-treinado;
  • m_edPTModelLayers - elemento de exibição do número total de camadas neurais presentes no modelo pré-treinado;
  • m_spPTModelLayers - número de camadas neurais copiadas para o novo modelo;
  • m_lstPTMode - elemento que exibe a arquitetura do modelo pré-treinado.
class CNetCreatorPanel : protected CAppDialog
  {
protected:
   //--- pre-trained model
   CEdit             m_edPTModel;
   CEdit             m_edPTModelLayers;
   CSpinEdit         m_spPTModelLayers;
   CListView         m_lstPTModel;
   CNetModify        m_Model;   
   CArrayObj*        m_arPTModelDescription;

Aqui vamos declarar os objetos para trabalhar com o modelo pré-treinado:

  • m_Model - objeto do próprio modelo pré-treinado;
  • m_arPTModelDescription - array dinâmico que descreve a arquitetura do modelo pré-treinado.

Preste atenção em 2 pontos. Declaramos todos os objetos como estáticos, exceto o array dinâmico da descrição da arquitetura do modelo. O uso de objetos estáticos nos permitirá transferir o trabalho de memória para o sistema, uma vez que a criação e exclusão de objetos estáticos é realizada em conjunto com o objeto que os contém e não requer trabalho adicional do programador. Mas, desta forma, só podemos criar objetos na estrutura da nossa classe. Como receberemos a descrição da arquitetura a partir do nosso modelo pré-treinado, declaramos dado objeto por meio de um ponteiro dinâmico.

E aqui vem o segundo ponto. Para declarar um objeto de modelo pré-treinado, usamos a classe CNetModify. Já para modelos de rede neural, criamos anteriormente a classe CNet. Isso se deve ao fato de que precisamos de funcionalidades adicionais por parte de nossa rede neural. E para gerá-las, criaremos uma nova classe CNetModify como sucessora da nossa classe CNet. Mas voltaremos a isso ao descrever a funcionalidade de nossa ferramenta.

O próximo bloco contém objetos para descrever a nova camada neural que está sendo criada. E todos eles estão em sintonia com os elementos da nossa classe CLayerDescription para descrever a arquitetura da camada neural. Portanto, não nos deteremos na descrição de cada elemento. A única coisa que vale a pena notar é a criação de dois botões para adicionar uma nova camada neural e remover uma já criada. Só podemos excluir as camadas neurais adicionadas. Para controlar o número de camadas neurais copiadas, usaremos os elementos do bloco anterior.

   //--- add layers
   CComboBox         m_cbNewNeuronType;
   CEdit             m_edCount;
   CEdit             m_edWindow;
   CEdit             m_edWindowOut;
   CEdit             m_edStep;
   CEdit             m_edLayers;
   CEdit             m_edBatch;
   CEdit             m_edProbability;
   CComboBox         m_cbActivation;
   CComboBox         m_cbOptimization;
   CButton           m_btAddLayer;
   CButton           m_btDeleteLayer;

E esse é o último bloco de objetos do novo modelo. Contém apenas 3 elementos. Entre eles estão um objeto para exibir a arquitetura geral do modelo que está sendo criado, um botão para salvar o novo modelo e um array dinâmico descrevendo a arquitetura das camadas neurais adicionadas. Neste caso, criamos um objeto de array dinâmico estático descrevendo a arquitetura das camadas neurais a serem adicionadas, m_arAddLayers. Vamos criar a arquitetura das camadas neurais adicionadas dentro de nossa ferramenta. E certamente podemos criar esse objeto como estático.

   //--- new model
   CListView         m_lstNewModel;
   CButton           m_btSave;
   CArrayObj         m_arAddLayers;

A lista de métodos públicos de nossa classe será bem básica. Eles incluem um construtor e um destruidor de classe, um método de criação de barra de ferramentas e um manipulador de eventos.

Também sobrescrevemos 3 métodos da classe pai. Mas isso poderia ter sido evitado usando herança pública.

public:
                     CNetCreatorPanel();
                    ~CNetCreatorPanel();
   //--- main application dialog creation and destroy
   virtual bool      Create(const long chart, const string name, const int subwin, const int x1, const int y1);
   //--- chart event handler
   virtual bool      OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam);
 
   virtual void      Destroy(const int reason = REASON_PROGRAM) override { CAppDialog::Destroy(reason); }
   bool              Run(void) { return CAppDialog::Run();}
   void              ChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
     {               CAppDialog::ChartEvent(id, lparam, dparam, sparam); }
  };

Graças ao uso de objetos estáticos, o construtor e o destruidor de nossa classe estão praticamente vazios.

A criação dos elementos de nossa interface e seu arranjo são realizados no método de criação da caixa de diálogo Create. Mas antes de proceder à descrição desse método, realizaremos um pequeno trabalho preparatório.

Em primeiro lugar, definiremos uma série de constantes que nos ajudarão a organizar adequadamente o espaço interno de nossa interface. Uma lista deles pode ser encontrada no anexo.

Além dos elementos de entrada, nossa interface também contém uma série de rótulos de texto para os quais não declaramos objetos. Isso é intencional para simplificar a estrutura de nossa classe. Afinal, eles são necessários apenas para visualização e não serão utilizados na criação da funcionalidade de nossa ferramenta. No entanto, precisaremos criar esses objetos. E o procedimento para criar tais objetos será repetido, exceto por alguns dados, como o texto do objeto e sua localização no painel. Portanto, para estruturar nosso código, criaremos um método CreateLabel separado para criar esses rótulos.

Nos parâmetros do método, passaremos o identificador do objeto, o texto do rótulo e suas coordenadas no painel.

No corpo do método, primeiro criamos um novo objeto rótulo e verificamos o resultado da operação. Em seguida, criamos um objeto no gráfico; passamos o conteúdo necessário para ele e adicionamos um ponteiro para o objeto criado ao array dinâmico da coleção de objetos da nossa interface.

É importante notar aqui que criamos um novo objeto com um ponteiro em uma variável privada. Durante a execução das operações desse método, verificamos o resultado das mesmas e, em caso de erro, excluímos o objeto criado. Mas após sair do método, não deixamos em nossa classe um ponteiro para o objeto criado para sua posterior remoção quando o programa for fechado. O ponto é que passamos um ponteiro para o objeto criado para a coleção de objetos em nossa caixa de diálogo, cuja funcionalidade completa já está elaborada na classe pai. Além disso, nessa coleção existe uma função para apagar todos os objetos da coleção no encerramento do programa. Então, por enquanto, podemos apenas passar o ponteiro para a coleção e esquecê-la.

bool CNetCreatorPanel::CreateLabel(const int id, const string text, const int x1, const int y1, const int x2, const int y2)
  {
   CLabel *tmp_label = new CLabel();
   if(!tmp_label)
      return false;
   if(!tmp_label.Create(m_chart_id, StringFormat("%s%d", LABEL_NAME, id), m_subwin, x1, y1, x2, y2))
     {
      delete tmp_label;
      return false;
     }
   if(!tmp_label.Text(text))
     {
      delete tmp_label;
      return false;
     }
   if(!Add(tmp_label))
     {
      delete tmp_label;
      return false;
     }
//---
   return true;
  }

Da mesma forma, criaremos um método para criar objetos de entrada de dados. Somente que aqui não criamos novos objetos, mas utilizamos aqueles criados anteriormente na classe. E passaremos seus ponteiros nos parâmetros do método.

bool CNetCreatorPanel::CreateEdit(const int id,
                                  CEdit& object,
                                  const int x1,
                                  const int y1,
                                  const int x2,
                                  const int y2,
                                  bool read_only)
  {
   if(!object.Create(m_chart_id, StringFormat("%s%d", EDIT_NAME, id), m_subwin, x1, y1, x2, y2))
      return false;
   if(!object.TextAlign(ALIGN_RIGHT))
      return false;
   if(!object.ReadOnly(read_only))
      return false;
   if(!Add(object))
      return false;
//---
   return true;
  }

Além disso, ao descrever a arquitetura das camadas neurais criadas, usamos enumerações e constantes. E para reduzir a "0" a probabilidade de o usuário inserir valores incorretos em tais elementos, criaremos controles especiais. Com esses controles, o usuário poderá selecionar apenas um elemento da lista. Vamos precisar de vários desses elementos. Bem, primeiro vamos criar um elemento para indicar o tipo de camada neural. Vamos realizar a implementação desta funcionalidade no método CreateComboBoxType. Como esse método é projetado para criar um elemento específico, não precisamos passar um ponteiro para o objeto nos parâmetros. Neste caso, basta especificar apenas as coordenadas do elemento que está sendo criado.

No corpo do método, criamos um elemento no gráfico nas coordenadas especificadas e verificamos o resultado da operação.

Em seguida, precisamos preencher o elemento com uma descrição de texto de cada elemento e seu ID numérico. Se pudermos usar o identificador do tipo de camada neural como um identificador numérico é porque não demos uma descrição de texto em nenhum lugar antes. Portanto, para converter um identificador numérico em uma descrição de texto, criaremos um método LayerTypeToString separado. Seu algoritmo é bastante simples. E sugiro que o conheça pessoalmente no anexo. Aqui usaremos apenas a chamada deste método para cada tipo de camada neural.

E no final do método, adicionaremos um ponteiro para o nosso objeto à coleção de objetos em nossa interface.

Observe que adicionamos objetos dinâmicos e estáticos à coleção. Isso se deve ao fato de que a funcionalidade da coleção é muito mais ampla do que o controle sobre a retirada de objetos após a conclusão do trabalho. Com isso, os elementos da coleção estão envolvidos tanto na determinação das coordenadas para a localização dos objetos no gráfico quanto no processamento de eventos. O propósito geral da coleção especificada está no funcionamento de todos os objetos como um único organismo inteiro.

bool CNetCreatorPanel::CreateComboBoxType(const int x1, const int y1, const int x2, const int y2)
  {
   if(!m_cbNewNeuronType.Create(m_chart_id, "cbNewNeuronType", m_subwin, x1, y1, x2, y2))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronBaseOCL), defNeuronBaseOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronConvOCL), defNeuronConvOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronProofOCL), defNeuronProofOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronLSTMOCL), defNeuronLSTMOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronAttentionOCL), defNeuronAttentionOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronMHAttentionOCL), defNeuronMHAttentionOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronMLMHAttentionOCL), defNeuronMLMHAttentionOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronDropoutOCL), defNeuronDropoutOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronBatchNormOCL), defNeuronBatchNormOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronVAEOCL), defNeuronVAEOCL))
      return false;
   if(!Add(m_cbNewNeuronType))
      return false;
//---
   return true;
  }

Da mesma forma, criaremos objetos para enumerações de funções de ativação e métodos de otimização de parâmetros. É importante notar apenas que para converter a enumeração em um formato de texto, usaremos a função padrão EnumToString. Portanto, podemos adicionar elementos à lista em um loop. O código completo dos métodos é apresentado no anexo.

Assim concluímos o trabalho preparatório e procedemos diretamente à criação de nossa interface de usuário. Essa funcionalidade é realizada no método Create. Observamos imediatamente que nos parâmetros do método recebemos apenas as coordenadas da localização do canto superior direito do nosso painel de interface. No entanto, para criar objetos, também precisaremos das dimensões do nosso painel. Para facilitar o uso e modificar rapidamente se necessário, defini as dimensões do painel através de constantes pré-definidas. E a criação do próprio painel é realizada por um método semelhante da classe pai. Vamos chamá-lo primeiro no corpo do nosso método.

bool CNetCreatorPanel::Create(const long chart, const string name, const int subwin, const int x1, const int y1)
  {
   if(!CAppDialog::Create(chart, name, subwin, x1, y1, x1 + PANEL_WIDTH, y1 + PANEL_HEIGHT))
      return false;

Em seguida, colocaremos os objetos de interface no painel criado. Vamos encaixar os objetos sequencialmente, começando pelo canto superior esquerdo. Ao fazer isso, vincularemos as coordenadas de cada novo objeto às coordenadas do objeto anterior. Essa abordagem nos permitirá construir objetos em uma estrutura uniforme.

Seguindo a lógica acima, começaremos a criar os objetos do grupo de trabalho com um modelo pré-treinado. E primeiro vamos criar um rótulo de grupo. Para fazer isso, determinaremos as coordenadas do rótulo e chamaremos o método CreateLabel criado anteriormente. Neste método, passaremos o texto do rótulo e suas coordenadas. E, é claro, não esqueçamos de acrescentar um identificador de rótulo exclusivo.

   int lx1 = INDENT_LEFT;
   int ly1 = INDENT_TOP;
   int lx2 = lx1 + LIST_WIDTH;
   int ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(0, "PreTrained model", lx1, ly1, lx2, ly2))
      return false;

Abaixo vamos criar um campo de entrada para nome de arquivo com modelo pré-treinado. Para fazer isso, vamos deslocar as coordenadas do objeto criado verticalmente e deixar as coordenadas horizontais inalteradas. Desta forma, os 2 objetos serão posicionados estritamente um abaixo do outro.

Aqui devemos acrescentar que não permitiremos que o usuário insira o nome do arquivo manualmente. Em vez disso, solicitaremos ao usuário que selecione um arquivo entre os existentes. Veremos a funcionalidade dessa ação um pouco mais tarde, mas por enquanto vamos tornar o campo do nome do arquivo somente leitura. Vamos criar um objeto chamando o método CreateEdit gerado anteriormente. E após criar o campo, adicionaremos uma mensagem a ele.

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateEdit(0, m_edPTModel, lx1, ly1, lx2, ly2, true))
      return false;
   if(!m_edPTModel.Text("Select file"))
      return false;

Abaixo indicamos o número total de camadas neurais no modelo treinado. Para fazer isso, criaremos um rótulo de texto e um campo de entrada (saída neste caso) para o número de camadas neurais. Este campo também será somente leitura.

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(1, "Layers Total", lx1, ly1, lx1 + EDIT_WIDTH, ly2))
      return false;
//---
   if(!CreateEdit(1, m_edPTModelLayers, lx2 - EDIT_WIDTH, ly1, lx2, ly2, true))
      return false;
   if(!m_edPTModelLayers.Text("0"))
      return false;

Da mesma forma, criaremos um rótulo e campos para inserir o número de camadas neurais a serem copiadas. E aqui precisamos de um mecanismo que limite o usuário na escolha do número de camadas neurais. Não deve ser inferior a "0" e não superior ao número total de camadas neurais no modelo. É bastante simples realizar isso usando uma instância de um objeto da classe CSpinEdit. Esta classe nos permite especificar um intervalo de valores válidos. O resto já está feito na classe.

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(2, "Transfer Layers", lx1, ly1, lx1 + EDIT_WIDTH, ly2))
      return false;
//---
   if(!m_spPTModelLayers.Create(m_chart_id, "spPTMCopyLayers", m_subwin, lx2 - 100, ly1, lx2, ly2))
      return false;
   m_spPTModelLayers.MinValue(0);
   m_spPTModelLayers.MaxValue(0);
   m_spPTModelLayers.Value(0);
   if(!Add(m_spPTModelLayers))
      return false;

Abaixo, precisamos apenas exibir uma janela para descrever a arquitetura do modelo pré-treinado. Observe que antes disso sempre deslocávamos as coordenadas dos objetos criados um nível abaixo. Neste caso, apenas deslocamos a borda superior para a parte inferior do objeto anterior. Definimos a borda inferior do objeto como a distância do recuo em relação à altura da nossa janela. Assim, esticamos o objeto até o tamanho da janela e obtivemos uma borda suave na parte inferior da interface criada.

   lx1 = INDENT_LEFT;
   lx2 = lx1 + LIST_WIDTH;
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ClientAreaHeight() - INDENT_BOTTOM;
   if(!m_lstPTModel.Create(m_chart_id, "lstPTModel", m_subwin, lx1, ly1, lx2, ly2))
      return false;
   if(!m_lstPTModel.VScrolled(true))
      return false;
   if(!Add(m_lstPTModel))
      return false;

Assim concluímos o trabalho no bloco de modelo pré-treinado e avançamos para o segundo bloco de objetos para descrever a arquitetura da camada neural adicionada. Também vamos criar os objetos deste bloco de cima para baixo. Ao definir as coordenadas para o novo objeto, vamos deslocar as coordenadas horizontalmente e definir a borda superior no nível do recuo em relação à borda superior da janela.

   lx1 = lx2 + CONTROLS_GAP_X;
   lx2 = lx1 + ADDS_WIDTH;
   ly1 = INDENT_TOP;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(3, "Add layer", lx1, ly1, lx2, ly2))
      return false;

Abaixo, na distância de deslocamento, criaremos uma caixa combinada para selecionar o tipo de camada neural a ser criada. Para fazer isso, usaremos o método criado acima. A largura deste objeto será igual à largura de todo o bloco.

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateComboBoxType(lx1, ly1, lx2, ly2))
      return false;

A seguir estão os elementos da descrição das arquiteturas da camada neural criada. Para cada elemento, criaremos 2 objetos a partir da classe de descrição da arquitetura da camada neural CLayerDescription: um rótulo de texto com o nome do elemento e um campo de entrada de valor. Para que nossos elementos sejam colocados no painel de interface em uma ordem estrita, alinharemos os rótulos de texto à esquerda e os campos de entrada à direita do nosso bloco. Ao fazer isso, o tamanho de todos os campos de entrada será o mesmo. Essa abordagem nos ajudará a construir uma espécie de tabela.

Não apresentarei agora o mesmo código para todos os nove elementos. Abaixo, por exemplo, está o código para criar 2 linhas da nossa tabela. Você pode ver o código completo no anexo.

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(4, "Neurons", lx1, ly1, lx1 + EDIT_WIDTH, ly2))
      return false;
//---
   if(!CreateEdit(2, m_edCount, lx2 - EDIT_WIDTH, ly1, lx2, ly2, false))
      return false;
   if(!m_edCount.Text((string)DEFAULT_NEURONS))
      return false;
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(5, "Activation", lx1, ly1, lx1 + EDIT_WIDTH, ly2))
      return false;
//---
   if(!CreateComboBoxActivation(lx2 - EDIT_WIDTH, ly1, lx2, ly2))
      return false;

Depois de criar elementos para descrever a arquitetura da camada neural adicionada, adicionaremos 2 botões: um para adicionar e outro para remover uma camada neural. Vamos posicionar os botões em uma linha, dividindo a largura do bloco entre eles pela metade.

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + BUTTON_HEIGHT;
   if(!m_btAddLayer.Create(m_chart_id, "btAddLayer", m_subwin, lx1, ly1, lx1 + ADDS_WIDTH / 2, ly2))
      return false;
   if(!m_btAddLayer.Text("ADD LAYER"))
      return false;
   m_btAddLayer.Locking(false);
   if(!Add(m_btAddLayer))
      return false;
//---
   if(!m_btDeleteLayer.Create(m_chart_id, "btDeleteLayer", m_subwin, lx2 - ADDS_WIDTH / 2, ly1, lx2, ly2))
      return false;
   if(!m_btDeleteLayer.Text("DELETE"))
      return false;
   m_btDeleteLayer.Locking(false);
   if(!Add(m_btDeleteLayer))
      return false;

E passamos ao terceiro e último bloco de descrição da arquitetura completa do modelo a ser criado. Aqui podemos encontrar todos os métodos usados acima.

Após criar todos os elementos, saímos do método com o resultado true. O código completo do EA pode ser encontrado no anexo.

Assim concluímos o arranjo dos elementos de nossa interface. E já podemos adicioná-la ao EA. Mas, no estado atual, será apenas uma bela imagem no gráfico do instrumento. Em seguida, temos que dar à nossa forma a funcionalidade necessária.


2.3 Preenchendo a ferramenta com funcionalidade

Continuamos a trabalhar na criação da nossa ferramenta e a próxima etapa será fornecer a interface com as funcionalidades necessárias. E antes de por mãos à obra, vamos mais uma vez falar sobre o algoritmo de trabalho que queremos para nossa ferramenta.

  1. Primeiro, precisamos abrir o arquivo com o modelo treinado salvo. Para fazer isso, o usuário clica no objeto para selecionar um arquivo. Isso abre uma caixa de diálogo na qual o usuário seleciona um arquivo existente com a extensão especificada.
  2. Após o usuário selecionar um arquivo, nossa ferramenta deve carregar o modelo do arquivo especificado e exibir informações sobre o modelo carregado (tipo e número de camadas de neurônios, número de neurônios em cada camada).
  3. Juntamente com a exibição de informações sobre o modelo carregado por padrão, todas as suas camadas neurais são configuradas para serem copiadas para o novo modelo. Ao mesmo tempo, as informações sobre eles são copiadas para o bloco de descrição do modelo criado.
  4. O usuário deve poder alterar manualmente o número de camadas neurais copiadas. Simultaneamente com a mudança no número de camadas neurais copiadas, a arquitetura do modelo criado deve ser modificada. O que será refletido no bloco que descreve a arquitetura do modelo criado.
  5. Após selecionar o número de camadas neurais copiadas, o usuário pode especificar manualmente o tipo e a arquitetura da nova camada neural e adicioná-la ao modelo criado pressionando o botão "ADD LAYER".
  6. Se alguma camada neural foi adicionada ao modelo por engano, o usuário pode selecionar tal camada neural no bloco que descreve a arquitetura do modelo que está sendo criado e excluí-la pressionando o botão "DELETE". Aqui é importante notar que apenas as camadas neurais adicionadas podem ser excluídas. Para remover as camadas do modelo doador, será necessário usar a ferramenta para alterar o número de camadas neurais copiadas.
  7. Após construir a arquitetura da rede neural criada, o usuário pressiona o botão "SAVE MODEL". Uma caixa de diálogo é aberta na frente dele, para selecionar um existente ou especificar o nome de um novo arquivo.

Este me parece que é um cenário lógico no qual se pode trabalhar com a ferramenta. Mas para elaborá-la, precisaremos trabalhar. Para começar, precisamos da funcionalidade que serve para obter informações sobre o modelo salvo. Anteriormente, não fornecíamos ao usuário informações sobre o modelo carregado. E para elaborar essa funcionalidade, precisaremos fazer alterações na classe de rede neural. Mas como essa funcionalidade não afeta a operação do modelo em si, vamos adicioná-la à nova classe CNetModify, que será uma sucessora direta da classe de modelo de rede neural CNet criada anteriormente.

A nova classe não deve criar novos objetos. Portanto, o construtor e o destruidor da classe permanecerão vazios. O método LayersTotal retorna o número de camadas neurais no modelo. E não há nada complicado em seu algoritmo, pois ele simplesmente retorna o tamanho do array. Seu código pode ser encontrado no anexo.

class CNetModify :  public CNet
  {
public:
                     CNetModify(void) {};
                    ~CNetModify(void) {};
   //---
   uint              LayersTotal(void);
   CArrayObj*        GetLayersDiscriptions(void);
  };

Proponho parar um pouco e nos debruçar sobre o método GetLayersDiscriptions para obter informações sobre as redes neurais utilizadas. Como resultado da execução deste método, espera-se receber um array dinâmico da descrição da arquitetura da rede neural, semelhante à descrição do modelo passada nos parâmetros do método do construtor do modelo. A complexidade de realizar esse processo também está no fato de não termos criado anteriormente métodos para obter hiperparâmetros de camadas neurais. Portanto, precisamos adicionar o método correspondente às classes da camada neural também. Para começar, adicionaremos um método GetLayerInfo à classe base das camadas neurais CNeuronBaseOCL.

O novo método não contém parâmetros e, após a execução, retornará o objeto de descrição da camada neural CLayerDescription. No corpo do método, primeiro criaremos uma instância do objeto de descrição da camada neural. E, em seguida, vamos preenchê-lo com os hiperparâmetros da camada neural atual. Depois disso, saímos do método e retornamos um ponteiro para o objeto criado para o programa chamador.

CLayerDescription* CNeuronBaseOCL::GetLayerInfo(void)
  {
   CLayerDescription* result = new CLayerDescription();
   if(!result)
      return result;
//---
   result.type = Type();
   result.count = Output.Total();
   result.optimization = optimization;
   result.activation = activation;
   result.batch = (int)(optimization == LS ? iBatch : 1);
   result.layers = 1;
//---
   return result;
  }

Ao adicionar um método à classe base da camada neural, em essência, adicionamos um método a todos os seus descendentes. Ou seja, todas as nossas camadas neurais receberam esse método. E agora podemos obter informações semelhantes de qualquer camada neural. Se esses dados forem suficientes para você, você poderá terminar de trabalhar com a camada neural e passar para o método de coleta de informações sobre o modelo.

Mas se você precisar de informações específicas para cada camada neural, precisamos substituir esse método em todas as camadas neurais. Abaixo está um exemplo de redefinição do método na camada de subamostragem, que permite obter dados sobre o tamanho da janela analisada e o incremento de sua movimentação. No corpo desse método, primeiro chamamos o método da classe pai para obter os hiperparâmetros subjacentes. E então complementamos o objeto de descrição da camada neural resultante com parâmetros específicos. E saímos do método retornando um ponteiro para o objeto de descrição da camada neural para o programa de chamada.

CLayerDescription* CNeuronProofOCL::GetLayerInfo(void)
  {
   CLayerDescription *result = CNeuronBaseOCL::GetLayerInfo();
   if(!result)
      return result;
   result.window = (int)iWindow;
   result.step = (int)iStep;
//---
   return result;
  }

No anexo, você pode encontrar métodos semelhantes para todos os tipos de camadas neurais discutidos anteriormente.

Agora conseguimos obter informações sobre os hiperparâmetros de cada camada neural. E podemos combinar isso em uma estrutura comum. Vamos retornar ao nosso método CNetModify::GetLayersDiscriptions e criar um array dinâmico nele para armazenar ponteiros para objetos de descrição de camada neural.

Em seguida, criaremos um laço sobre todas as camadas neurais. E, no corpo do laço, solicitaremos de cada camada neural um objeto para descrever sua arquitetura, para isso chamamos o método criado acima. Os objetos resultantes serão adicionados ao nosso array dinâmico.

Após executar todas as iterações do laço, obteremos um array dinâmico com uma descrição da arquitetura completa do modelo carregado. Isto é o que retornaremos ao programa de chamada quando o método estiver concluído.

CArrayObj* CNetModify::GetLayersDiscriptions(void)
  {
   CArrayObj* result = new CArrayObj();
   for(uint i = 0; i < LayersTotal(); i++)
     {
      CLayer* layer = layers.At(i);
      if(!layer)
         break;
      CNeuronBaseOCL* neuron = layer.At(0);
      if(!neuron)
         break;
      if(!result.Add(neuron.GetLayerInfo()))
         break;
     }
//---
   return result;
  }

Nesta fase, implementamos a possibilidade de obter uma descrição da arquitetura de um modelo previamente criado. E podemos proceder à elaboração do método de carregamento de modelo pré-treinado a partir de um arquivo especificado pelo usuário. Para construir a funcionalidade especificada, criaremos o método CNetCreatorPanel::LoadModel. Nos parâmetros, o método receberá o nome do arquivo para carregar o modelo.

No corpo do método, primeiro carregamos o modelo a partir do arquivo especificado. Observe que não verificamos o valor do parâmetro antes de chamar o método Load do modelo. Afinal, todos os controles já estão implementados no método de carregamento. Apenas verificamos o resultado da operação. E no caso de receber um erro ao carregar o modelo, exibiremos informações sobre ele no bloco de descrição do modelo carregado.

bool CNetCreatorPanel::LoadModel(string file_name)
  {
   float error, undefine, forecast;
   datetime time;
   ResetLastError();
   if(!m_Model.Load(file_name, error, undefine, forecast, time, false))
     {
      m_lstPTModel.ItemsClear();
      m_lstPTModel.ItemAdd("Error of load model", 0);
      m_lstPTModel.ItemAdd(file_name, 1);
      int err = GetLastError();
      if(err == 0)
         m_lstPTModel.ItemAdd("The file is damaged");
      else
         m_lstPTModel.ItemAdd(StringFormat("error id: %d", GetLastError()), 2);
      m_edPTModel.Text("Select file");
      return false;
     }

Após carregar o modelo com sucesso, exibiremos o nome do arquivo carregado e o número de camadas neurais nos elementos correspondentes da interface criada acima.

Excluímos a descrição do modelo carregado anteriormente, se houver. E chamaremos o método para coletar informações sobre a arquitetura do modelo carregado.

   m_edPTModel.Text(file_name);
   m_edPTModelLayers.Text((string)m_Model.LayersTotal());
   if(!!m_arPTModelDescription)
      delete m_arPTModelDescription;
   m_arPTModelDescription = m_Model.GetLayersDiscriptions();

Após receber as informações sobre o modelo carregado, criaremos um loop, em cujo corpo exibiremos as informações recebidas no bloco correspondente de nossa interface.

   m_lstPTModel.ItemsClear();
   int total = m_arPTModelDescription.Total();
   for(int i = 0; i < total; i++)
     {
      CLayerDescription* temp = m_arPTModelDescription.At(i);
      if(!temp)
         return false;
      //---
      string item = StringFormat("%s (units %d)", LayerTypeToString(temp.type), temp.count);
      if(!m_lstPTModel.AddItem(item, i))
         return false;
     }

No final do método, alteraremos a faixa de valores do número de camadas neurais que podem ser copiadas até atingir o tamanho total do modelo carregado. E dizemos à ferramenta para copiar completamente o modelo carregado. Então saímos do método.

   m_spPTModelLayers.MaxValue(total);
   m_spPTModelLayers.Value(total);
//---
   return true;
  }

Como você pode ver no método acima, o nome do arquivo para carregar dados calha nos parâmetros do programa de chamada. Também precisamos fornecer ao usuário a opção de selecionar independentemente o arquivo de modelo.

Vamos criar outro método OpenPreTrainedModel. No corpo deste método, fazemos apenas uma chamada para a função padrão FileSelectDialog, na qual a interface da caixa de diálogo do arquivo já está elaborada. Ao chamar esta função, especificaremos apenas as extensões de arquivo necessárias e o sinalizador FSD_FILE_MUST_EXIST, que indica que apenas um arquivo existente pode ser especificado.

Com determinadas configurações de sinalizador, esta função permite selecionar vários arquivos. Portanto, como resultado da execução, o FileSelectDialog retorna o número de arquivos selecionados. E seus nomes estão contidos em um array cujo o ponteiro é recebido pela função nos parâmetros.

Assim, quando o usuário selecionar um arquivo, passaremos seu nome nos parâmetros para o método acima. Caso contrário, exibiremos uma mensagem em nossa interface sobre a necessidade de selecionar um arquivo para carregamento de dados.

bool CNetCreatorPanel::OpenPreTrainedModel(void)
  {
   string filenames[];
   if(FileSelectDialog("Выберите файлы для загрузки", NULL,
                       "Neuron Net (*.nnw)|*.nnw|All files (*.*)|*.*",
                       FSD_FILE_MUST_EXIST, filenames, NULL) > 0)
     {
      if(!LoadModel(filenames[0]))
         return false;
     }
   else
      m_edPTModel.Text("Files not selected");
//---
   return true;
  }

Estamos avançando gradualmente e já criamos uma representação da interface da nossa ferramenta, assim como uma cadeia de métodos para selecionar um arquivo e carregar um modelo pré-treinado a partir dela. Mas até agora, esses 2 blocos do nosso programa não foram combinados em um único programa orgânico. Sim, no método de carregamento de dados, efetuamos a exibição de informações sobre o modelo de dados carregado no painel. Mas, por enquanto, é uma «estrada de mão única». Precisamos indicar o caminho de volta pelo qual nosso programa receberá informações sobre as ações do usuário e sua reação às informações de saída.

Para fazer isso, usaremos o manipulador de eventos. Nos herdeiros da classe CAppDialog, esse mecanismo é realizado por meio de macro-substituições. Para fazer isso, um bloco de macros é criado no código do programa, bloco esse que começa com a macro EVENT_MAP_BEGIN e termina com a macro EVENT_MAP_END. Entre eles estão várias macros correspondentes a vários eventos. No nosso caso, usaremos a macro ON_EVENT, que envolve o processamento de um evento por um identificador numérico. Em particular, para manipular o evento de clique do mouse no objeto nome do arquivo para carregar o modelo no corpo da macro, especificamos o evento ON_CLICK, o ponteiro do objeto m_edPTModel e o nome do método a ser chamado quando o evento OpenPreTrainedModel ocorrer . Assim, ao pressionar o botão do mouse sobre o objeto m_edPTModel, que corresponde ao campo para inserir o nome do arquivo de carregamento do modelo, o programa chamará o método OpenPreTrainedModel e, assim, iniciará a cadeia de métodos de carregamento do modelo pré-treinados criados acima.

EVENT_MAP_BEGIN(CNetCreatorPanel)
ON_EVENT(ON_CLICK, m_edPTModel, OpenPreTrainedModel)
ON_EVENT(ON_CLICK, m_btAddLayer, OnClickAddButton)
ON_EVENT(ON_CLICK, m_btDeleteLayer, OnClickDeleteButton)
ON_EVENT(ON_CLICK, m_btSave, OnClickSaveButton)
ON_EVENT(ON_CHANGE, m_spPTModelLayers, ChangeNumberOfLayers)
ON_EVENT(ON_CHANGE, m_lstPTModel, OnChangeListPTModel)
EVENT_MAP_END(CAppDialog)

Da mesma forma, descreveremos outros eventos e os métodos chamados quando isso acontece:

  • OnClickAddButton — método para manipular o botão "ADD LAYER";
  • OnClickDeleteButton  — método de processamento de pressionamento do botão "DELETE";
  • OnClickSaveButton  — método de processamento de processamento do botão "SAVE MODEL";
  • ChangeNumberOfLayers — método para lidar com o evento de alteração do número de camadas neurais copiadas;
  • OnChangeListPTModel — método para manipular o evento de clique do mouse na camada neural na lista de descrição da arquitetura do modelo carregado.

O código de todos esses métodos pode ser encontrado no anexo. Agora proponho nos debruçarmos sobre o método que serve para salvar o novo modelo, uma vez que sua implementação é bastante complicada e requer a criação de métodos adicionais na classe de modelo de rede neural CNetModify.

O algoritmo deste método pode ser dividido condicionalmente em 3 blocos:

  • copiar camadas neurais a partir de um modelo pré-treinado;
  • adicionar novas camadas neurais ao modelo;
  • salvar o modelo em um arquivo.

No momento, apenas o último ponto está implementado em nossa classe de rede neural. Não temos métodos para copiar camadas neurais de outro modelo, nem para adicionar novas camadas neurais a um modelo existente.

Vamos ponto a ponto. Bem, primeiro vamos criar um mecanismo para copiar camadas neurais. Sabemos muito bem que, dependendo da arquitetura da camada neural, ela pode conter um número diferente de objetos. Também precisamos de um algoritmo universal que nos permita copiar todos os tipos de camadas neurais com diferentes métodos de otimização de parâmetros. Ao fazer isso, copiar o modelo treinado envolve transferir não apenas a arquitetura, mas também todos os pesos. E então vem à mente uma pergunta sensata: por que quereríamos replicar completamente todos os elementos de cada camada neural? O que nos impede de simplesmente copiar o ponteiro para o objeto necessário da camada neural? Como você sabe, o uso de ponteiros permite acessar o mesmo objeto desde diferentes partes do código do programa. Bem, é esta particularidade que iremos utilizar. Vamos criar dois métodos. Um retornará um ponteiro para o objeto da camada neural por seu número na estrutura do modelo. E o outro adicionará um ponteiro para o objeto da camada neural à arquitetura do modelo.

CLayer* CNetModify::GetLayer(uint layer)
  {
   if(!layers || LayersTotal() <= layer)
      return NULL;
//---
   return layers.At(layer);
  }
bool CNetModify::AddLayer(CLayer *new_layer)
  {
   if(!new_layer)
      return false;
   if(!layers)
     {
      layers = new CArrayLayer();
      if(!layers)
         return false;
     }
//---
   return layers.Add(new_layer);
  }

Como copiaremos um bloco de camadas neurais sucessivas, transferindo ponteiros para camadas neurais para um novo modelo com preservação total de sua sequência, salvamos todos os relacionamentos entre essas camadas neurais.

O primeiro ponto está entendido. E seguimos em frente. O construtor do nosso modelo é perfeitamente capaz de criar um novo modelo de acordo com a descrição da arquitetura que lhe foi passada. Ao adicionar camadas neurais ao modelo, criamos uma descrição semelhante das camadas neurais. E, ao que parece, nós só precisamos adicionar novas camadas, o que o modelo já sabe fazer. Mas a dificuldade está na falta de uma ponte entre as camadas neurais copiadas e as recém-criadas.

A arquitetura de nossas camadas neurais é tal que os pesos de uma camada neural estão diretamente relacionados aos elementos de outra camada neural. Portanto, para manter o funcionamento do modelo no modo propagação e retropropagação, precisamos construir essa relação. Se observarmos o método de inicialização da classe base de nossa camada neural CNeuronBaseOCL, podemos notar entre seus parâmetros um indicador do número de neurônios na camada neural subsequente. Este parâmetro determina o tamanho da matriz criada de coeficientes de peso e buffers associados usados na otimização de parâmetros.

Primeiro, adicionaremos um método à classe CNeuronBaseOCL que ajustará a matriz de peso de acordo com o número especificado de neurônios na camada subsequente CNeuronBaseOCL::numOutputs.

Nos parâmetros do método, passaremos o número de neurônios na camada subsequente e o método de otimização de parâmetros.

No corpo do método, verificamos o número de elementos na camada neural subsequente recebida nos parâmetros e, se necessário, criamos uma matriz de coeficientes de peso dos tamanhos apropriados. Ao fazer isso, nós a preenchemos com coeficientes de peso aleatórios, uma vez que ela se relaciona à camada neural recém-adicionada. Para a matriz preenchida, criamos um buffer no contexto OpenCL e passamos o conteúdo da matriz para ele.

A passagem de dados para o contexto OpenCL é necessária porque nosso método de classe tentará carregar os dados desde o contexto antes de salvá-los no arquivo. E, em caso de erro, abortará o salvamento do modelo com resultado negativo. É claro que poderíamos fazer alterações nos métodos de nossas classes de camada neural. Mas, na minha opinião, tal «mão-de-obra» é mais cara do que transferir informações de e para o contexto OpenCL.

bool CNeuronBaseOCL::numOutputs(const uint outputs, ENUM_OPTIMIZATION optimization_type)
  {
   if(outputs > 0)
     {
      if(CheckPointer(Weights) == POINTER_INVALID)
        {
         Weights = new CBufferFloat();
         if(CheckPointer(Weights) == POINTER_INVALID)
            return false;
        }
      Weights.BufferFree();
      Weights.Clear();
      int count = (int)((Output.Total() + 1) * outputs);
      if(!Weights.Reserve(count))
         return false;
      float k = (float)(1 / sqrt(Output.Total() + 1));
      for(int i = 0; i < count; i++)
        {
         if(!Weights.Add((2 * GenerateWeight()*k - k)*WeightsMultiplier))
            return false;
        }
      if(!Weights.BufferCreate(OpenCL))
         return false;

Após criar a matriz de pesos, vamos criar os buffers de dados usados no processo de otimização de pesos.

Se não houver necessidade de uma matriz de coeficientes de peso e buffers de acompanhamento, os removeremos como desnecessários. E saímos do método.

O código completo do método pode ser encontrado no anexo.

Agora voltamos à classe CNetModify para criar um método para adicionar camadas neurais de acordo com a descrição de AddLayers fornecida. Nos parâmetros, o método recebe um ponteiro para um array dinâmico com uma descrição da arquitetura das camadas neurais adicionadas. E imediatamente no corpo do método verificamos os dados recebidos. Em primeiro lugar, o ponteiro resultante deve ser válido e conter uma descrição de pelo menos uma camada neural.

bool CNetModify::AddLayers(CArrayObj *new_layers)
  {
   if(!new_layers || new_layers.Total() <= 0)
      return false;
//---
   if(!layers || LayersTotal() <= 0)
     {
      Create(new_layers);
      return true;
     }

Em seguida, verificamos o número de camadas neurais que existem no modelo. Se não houver nenhum, simplesmente chamamos o construtor da classe pai. Ele criará um novo modelo com a arquitetura fornecida.

Se tivermos que adicionar camadas neurais a um modelo existente, primeiro declararemos as variáveis locais.

   CLayerDescription *desc = NULL, *next = NULL;
   CLayer *temp;
   int outputs;

Em seguida, faremos um pequeno trabalho preparatório e chamaremos o método criado acima para unir duas camadas neurais.

   int shift = (int)LayersTotal() - 1;
   CLayer* last_layer = layers.At(shift);
   if(!last_layer)
      return false;
//---
   CNeuronBaseOCL* neuron = last_layer.At(0);
   if(!neuron)
      return false;
//---
   desc = neuron.GetLayerInfo();
   next = new_layers.At(0);
   outputs = (next == NULL || (next.type != defNeuron && next.type != defNeuronBaseOCL) ? 0 : next.count);
   if(!neuron.numOutputs(outputs, next.optimization))
      return false;
   delete desc;

Além disso, da mesma forma que o construtor da classe pai, percorreremos o array dinâmico da descrição da arquitetura do modelo e adicionaremos sequencialmente todas as camadas neurais. O código deste bloco repete completamente o código do construtor da classe pai. Permita-me não repeti-lo neste artigo. O código completo do EA pode ser encontrado no anexo.

Vamos retornar à classe de nossa ferramenta CNetCreatorPanel e criar um método para manipular o evento de pressionar o botão de salvamento de modelo, que combinará os métodos acima para criar um novo modelo em uma única sequência.

No início do método OnClickSaveButton, solicitaremos ao usuário que especifique um arquivo para salvar o modelo. Para fazer isso, usaremos a função FileSelectDialog já conhecida por nós. Desta vez vamos mudar o sinalizador que indica que um arquivo está sendo criado para escrita. E especificamos o nome do arquivo padrão.

bool CNetCreatorPanel::OnClickSaveButton(void)
  {
   string filenames[];
   if(FileSelectDialog("Выберите файлы для сохранения", NULL,
                       "Neuron Net (*.nnw)|*.nnw|All files (*.*)|*.*",
                       FSD_WRITE_FILE, filenames, "NewModel.nnw") <= 0)
     {
      Print("File not selected");
      return false;
     }

Em seguida, criaremos uma nova instância da classe de rede neural e verificaremos o resultado da operação.

   string file_name = filenames[0];
   if(StringLen(file_name) - StringLen(EXTENSION) > StringFind(file_name, EXTENSION))
      file_name += EXTENSION;
   CNetModify* new_model = new CNetModify();
   if(!new_model)
      return false;

Após uma operação bem-sucedida de criação de um novo modelo, organizamos um laço de cópia do número necessário de camadas neurais. Ao mesmo tempo, para todas as camadas neurais copiadas, alteramos o sinalizador de aprendizado para false. Assim, desabilitamos o processo de atualização dos coeficientes de peso dessas camadas no processo de treinamento subsequente. Mais tarde, podemos alterar programaticamente esse sinalizador para todas as camadas neurais do modelo chamando literalmente um único método.

   int total = m_spPTModelLayers.Value();
   bool result = true;
   for(int i = 0; i < total && result; i++)
     {
      CLayer* temp = m_Model.GetLayer((uint)i);
      if(!temp)
        {
         result = false;
         break;
        }
      CNeuronBaseOCL* neuron = temp.At(0);
      neuron.TrainMode(false);
      if(!new_model.AddLayer(temp))
         result = false;
     }

Após terminar as iterações de cópia de camadas neurais, chamamos o método acima para adicionar camadas neurais, e assim completamos a criação de um novo modelo.

   new_model.SetOpenCL(m_Model.GetOpenCL());
   if(result && m_arAddLayers.Total() > 0)
      if(!new_model.AddLayers(GetPointer(m_arAddLayers)))
         result = false;

Depois disso, basta salvar o modelo criado.

   if(result && !new_model.Save(file_name, 1.0e37f, 100, 0, 0, false))
      result = false;
//---
   if(!!new_model)
      delete new_model;
   LoadModel(m_edPTModel.Text());
//---
   return result;
  }

Após salvar o modelo, podemos excluí-lo, pois o treinamento será realizado usando outro programa.

Aqui devemos lembrar que quando o modelo é excluído, as camadas neurais copiadas também serão excluídas, já que não copiamos dados para o novo modelo, mas apenas passamos ponteiros. E se o usuário quiser criar outro modelo baseado no já utilizado, ele precisará recarregá-lo. Vamos economizar o usuário de uma tarefa desnecessária e chamaremos o método para recarregar o modelo nós mesmos. E só depois disso saímos do método.

Neste ponto, concluímos o trabalho com nossa classe e passamos a testar o trabalho realizado.


3. Teste

Para testar a ferramenta criada, criaremos o Expert Advisor NetCreator.mq5. O código do EA é bastante simples e contém apenas a inclusão da classe CNetCreatorPanel criada acima. Em essência, a integração de uma classe em um Expert Advisor é realizada em 3 pontos. Inicialização e lançamento do modelo na função OnInit; destruição de classe na função OnDeinit e transferência de eventos para a classe no método OnChartEvent. O código para todos os pontos de integração é dado abaixo.

#include "NetCreatorPanel.mqh"
CNetCreatorPanel Panel;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   if(!Panel.Create(0, "NetCreator", 0, 50, 50))
      return INIT_FAILED;
   if(!Panel.Run())
      return INIT_FAILED;
//---
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
//---
   Panel.Destroy(reason);
  }
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if(id == CHARTEVENT_OBJECT_CLICK)
      Sleep(0);
   Panel.ChartEvent(id, lparam, dparam, sparam);
  }

Testes práticos confirmaram nossa expectativa de transferir camadas neurais de um modelo para outro com a possibilidade de adicionar novas camadas. Além disso, a ferramenta permite criar um modelo completamente novo, e assim fugir da descrição do modelo criado no código do programa.  


Considerações finais

Neste artigo, criamos uma ferramenta que permite transferir parte das camadas neurais de um modelo para outro. Ao fazer isso, podemos adicionar um número arbitrário de novas camadas de arquitetura arbitrária. Assim, convidamos todos a experimentarem seus modelos previamente treinados e verem como mudar a arquitetura pode afetar a produtividade do mesmo.

Você pode tentar combinar diferentes arquiteturas em um modelo e realizar uma série de experimentos diferentes para mudar a arquitetura do modelo. Enquanto isso, se você mantiver as arquiteturas das camadas de resultados e dados de entrada, poderá tentar "enfiar" uma arquitetura de modelo completamente nova em um Expert Advisor já existente. Treine o modelo e compare a influência da arquitetura e o erro do modelo.


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

Programas utilizados no artigo

# Nome Tipo Descrição
1 NetCreator.mq5 EA   Ferramenta de construção de modelos
2 NetCreatotPanel.mqh Biblioteca de classe Biblioteca de classe para criação de ferramentas
3 NeuroNet.mqh Biblioteca de classe Biblioteca de classe para a criação de uma rede neural
4 NeuroNet.cl Biblioteca Biblioteca de código do programa OpenCL


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

Arquivos anexados |
MQL5.zip (71.47 KB)
Como desenvolver um sistema de negociação baseado no indicador Bear's Power Como desenvolver um sistema de negociação baseado no indicador Bear's Power
Bem-vindo a um novo artigo em nossa série sobre como desenvolver um sistema de negociação com base nos indicadores técnicos mais populares, aqui está um novo artigo sobre como aprender a desenvolver um sistema de negociação pelo indicador técnico Bear's Power.
Redes neurais de maneira fácil (Parte 22): Aprendizado não supervisionado de modelos recorrentes Redes neurais de maneira fácil (Parte 22): Aprendizado não supervisionado de modelos recorrentes
Continuamos a estudar algoritmos de aprendizado não supervisionado. E agora proponho discutir as particularidades por trás do uso de autocodificadores para treinar modelos recorrentes.
DoEasy. Controles (Parte 14): Novo algoritmo para nomear elementos gráficos. Continuando o trabalho no objeto WinForms TabControl DoEasy. Controles (Parte 14): Novo algoritmo para nomear elementos gráficos. Continuando o trabalho no objeto WinForms TabControl
Neste artigo, elaboraremos um novo algoritmo para nomear todos os elementos gráficos que permitem criar gráficos personalizados, e continuaremos desenvolvendo o objeto WinForms TabControl.
Experiências com redes neurais (Parte 2): Otimização inteligente de redes neurais Experiências com redes neurais (Parte 2): Otimização inteligente de redes neurais
As redes neurais são tudo para nós. E vamos verificar na prática se é assim, indagando se MetaTrader 5 é uma ferramenta autossuficiente para implementar redes neurais na negociação. A explicação vai ser simples.