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

Redes neurais de maneira fácil (Parte 24): Melhorando a ferramenta para transferência de aprendizado

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

Conteúdo

Introdução

No artigo anterior desta série, criamos uma ferramenta que nos permite tirar partido da tecnologia de transferência de aprendizado (Transfer Learning, em inglês). Como resultado do enorme trabalho que fizemos, conseguimos uma ferramenta que permite editar modelos já treinados. E agora podemos obter qualquer número de camadas neurais de um modelo pré-treinado. Claro, existem condições limitantes. Tomamos apenas camadas sucessivas, tendo pegado primeiro a camada de dados de entrada. A razão para esta abordagem está na natureza das redes neurais. Elas funcionam bem apenas com dados de entrada semelhantes aos usados ao treinar o modelo.

Além do mencionado, a ferramenta criada permite não apenas editar modelos treinados, mas também criar modelos completamente novos. E isto nos poupa da necessidade de descrever a arquitetura do modelo no código do programa. Quer dizer, basta criarmos um modelo usando a ferramenta, e em seguida treinar e usar o modelo carregando a rede neural criada a partir de um arquivo. Assim, podemos experimentar diferentes arquiteturas sem alterar o código do programa. Nós nem precisamos recompilar o programa. Tudo o que devemos fazer é substituir o arquivo de modelo.

E, claro, queremos oferecer uma ferramenta tão útil quanto possível para o usuário. Neste artigo, tentaremos melhorar sua usabilidade.


1. Exibindo informações completas sobre a camada neural

E começaremos a melhorar a usabilidade de nossa ferramenta aumentando a quantidade de informações sobre cada camada neural. Como você lembra, no último artigo coletamos todas as informações possíveis sobre a arquitetura de cada camada neural do modelo treinado. Mas mostramos ao usuário apenas o tipo de camada neural e o número de neurônios na saída. Isso pode ser aceito quando trabalhamos com um modelo e lembramos de sua arquitetura. Mas quando você experimenta com um grande número de modelos, essa quantidade de informações obviamente não será suficiente.

Porém mais informações exigem mais espaço na janela de informações, em cujo caso uma barra de rolagem horizontal não seria provavelmente a melhor solução. É por isso que decidi exibir informações sobre cada camada neural em várias linhas. E aqui é importante que as informações a serem exibidas sejam fáceis de ler, sem que elas se misturem em um enorme confusão difícil de ser acompanhada. Para tal, podemos dividir o texto em blocos inserindo separadores entre as descrições de duas camadas neurais sucessivas.

Parece que a simples decisão de dividir o texto em várias linhas no processo de elaboração também exige abordagens não padronizadas. O ponto é que usamos a classe de lista CListView para exibir informações sobre a arquitetura do modelo. Nessa classe cada linha representa um elemento separado da lista. E, ao fazer isso, não permite exibir um mesmo elemento em várias linhas, bem como agrupar vários elementos em uma entidade. Adicionar essa funcionalidade exigirá alterações no algoritmo e na arquitetura de classe. Já na prática, isso resultará na criação de uma nova classe de objeto de controle. Nesta variante, podemos herdar da classe CListView ou criar um elemento completamente novo. Como você entende, este é um trabalho bastante volumoso e não foi incluído nos meus planos.

Por isso, decidi usar uma classe já existente, mas com alguns ajustes sem fazer alterações no próprio código da classe. Como mencionado acima, para a divisão visual em blocos de camadas neurais individuais, serão utilizados elementos separadores. São eles que nos permitirão dividir o array geral de texto que descreve a arquitetura do modelo em blocos separados de camadas neurais. E assim agrupar visualmente as informações para cada camada neural.

Mas, além do agrupamento visual, também precisamos entender no nível do programa a qual camada neural esse ou aquele elemento da lista pertence. Como você lembra, no último artigo fizemos mudanças no número de camadas neurais copiadas selecionando uma camada neural separada do modelo treinado com o mouse e excluindo a camada selecionada da lista de camadas neurais adicionadas ao novo modelo. Em ambos os casos, precisamos de uma compreensão clara da correspondência entre o elemento selecionado e a camada neural específica.

E aqui vamos lembrar que, ao adicionar cada elemento à lista, especificamos seu texto e algum valor numérico. É esse valor numérico que geralmente é usado para identificar rapidamente o elemento selecionado. Anteriormente, especificávamos valores individuais para cada elemento. Mas ninguém nos proíbe de usar um valor para vários elementos. Obviamente, essa abordagem dificultará a identificação de cada elemento da lista. E agora, não precisamos disso. Só precisamos identificar nosso grupo de elementos. Assim, com a ajuda desse funcional, podemos identificar não um único elemento, mas todo um grupo de elementos.

bool  AddItem( 
   const string  item,     // text 
   const long    value     // value 
   )

Na prática, essa decisão nos dá mais uma vantagem. A classe CListView tem um método SelectByValue. A principal funcionalidade deste método é selecionar um elemento pelo seu valor numérico. Seu algoritmo é construído de tal forma que encontra o primeiro elemento com o valor numérico especificado entre todos os elementos da lista e o seleciona. Isso significa que, ao preparar uma lista de seleção de eventos, podemos ler o valor de um elemento selecionado pelo usuário e solicitar à classe que selecione o primeiro elemento da lista com esse valor. Assim, visualizamos, para o usuário, o início do grupo de interesse. Eu acho que esse é um recurso bastante útil.

bool  SelectByValue( 
   const long  value     // value 
   )

E agora proponho olhar para a aplicação prática das abordagens descritas acima. E antes de tudo, precisamos fazer uma representação textual da descrição da arquitetura da camada neural para exibi-la no painel. Para fazer isso, vamos criar um método LayerDescriptionToString. Este método nos parâmetros recebe um ponteiro para o objeto da descrição da arquitetura da camada neural e um ponteiro para um array dinâmico de strings, no qual escreveremos uma descrição textual da camada neural. Quando isso acontece, cada elemento do array é uma linha separada na lista de descrições da arquitetura do modelo. Ou seja, se nos referirmos à terminologia usada acima, cada array é um grupo separado de elementos na lista para descrever uma camada neural. O uso de um array dinâmico nos permite organizar grupos de elementos de vários tamanhos, dependendo da necessidade de descrever uma determinada camada neural.

Nosso método retornará o número de elementos no array.

int CNetCreatorPanel::LayerDescriptionToString(const CLayerDescription *layer, string& result[])
  {
   if(!layer)
      return -1;

No corpo do método, primeiro verificamos se a descrição da arquitetura da camada neural está em conformidade com o ponteiro recebido.

Em seguida, preparamos uma variável local e limpar o array dinâmico resultante.

   string temp;
   ArrayFree(result);

E então criamos uma descrição de texto da camada neural dependendo do seu tipo. Atenção, não trabalharemos imediatamente com um array dinâmico de strings. Em vez disso, escreveremos toda a descrição em uma linha. E, ao fazer isso, nos locais onde as linhas são separadas, inseriremos um caractere separador. No exemplo acima, usei uma barra invertida "\". Além disso, para uma composição conveniente de texto com uma determinada marcação, usei a função StringFormat. Ela permite obter o texto formatado com o mínimo de esforço.

Após compilar uma descrição de string formatada da arquitetura da camada neural, usaremos a função StringSplit e dividiremos nosso texto em linhas. Esta função divide o texto em linhas onde o elemento delimitador especificado está presente. Elemento esse que adicionamos cuidadosamente ao texto na etapa anterior. Esta função tem a vantagem de aumentar também o tamanho do array dinâmico para o tamanho exigido. Então não temos que controlá-lo.

   switch(layer.type)
     {
      case defNeuronBaseOCL:
         temp = StringFormat("Dense (outputs %d, \activation %s, \optimization %s)", 
                layer.count, EnumToString(layer.activation), EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      case defNeuronConvOCL:
         temp = StringFormat("Conolution (outputs %d, \window %d, step %d, window out %d, \activation %s, \optimization %s)",
                layer.count * layer.window_out, layer.window, layer.step, layer.window_out, EnumToString(layer.activation),

                EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      case defNeuronProofOCL:
         temp = StringFormat("Proof (outputs %d, \window %d, step %d, \optimization %s)",
                layer.count, layer.window, layer.step, EnumToString(layer.activation), EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      case defNeuronAttentionOCL:
         temp = StringFormat("Self Attention (outputs %d, \units %s, window %d, \optimization %s)",
                layer.count * layer.window, layer.count, layer.window, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      case defNeuronMHAttentionOCL:
         temp = StringFormat("Multi-Head Attention (outputs %d, \units %s, window %d, heads %s, \optimization %s)",
                layer.count * layer.window, layer.count, layer.window, layer.step, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      case defNeuronMLMHAttentionOCL:
         temp = StringFormat("Multi-Layer MH Attention (outputs %d, \units %s, window %d, key size %d, \heads %s, layers %d,
                              \optimization %s)",
                layer.count * layer.window, layer.count, layer.window, layer.window_out, layer.step, layer.layers,

                EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      case defNeuronDropoutOCL:
         temp = StringFormat("Dropout (outputs %d, \probability %d, \optimization %s)",
                layer.count, layer.probability, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      case defNeuronBatchNormOCL:
         temp = StringFormat("Batchnorm (outputs %d, \batch size %d, \optimization %s)",
                layer.count, layer.batch, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      case defNeuronVAEOCL:
         temp = StringFormat("VAE (outputs %d)", layer.count);
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      case defNeuronLSTMOCL:
         temp = StringFormat("LSTM (outputs %d, \optimization %s)", layer.count, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      default:	
         temp = StringFormat("Unknown type %#x (outputs %d, \activation %s, \optimization %s)",
                layer.type, layer.count, EnumToString(layer.activation), EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
     }

Depois de compilar uma descrição para todas as camadas neurais conhecidas, não se esqueça de adicionar uma descrição padrão para tipos desconhecidos. Assim, informaremos o usuário sobre a descoberta de uma camada neural desconhecida e nos protegeremos da violação não intencional da integridade do modelo.

No final do método, retornamos o tamanho do array de resultados para o chamador.

//---
   return ArraySize(result);
  }

Em seguida, tratamos do método LoadModel, que já conhecemos no artigo anterior. Não alteraremos todo o método, mas apenas o corpo do loop para adicionar elementos à lista. Como antes, no corpo do loop, primeiro obtemos, a partir do array dinâmico, um ponteiro para o objeto de descrição da próxima camada neural. E verificamos imediatamente a validade do ponteiro recebido.

   for(int i = 0; i < total; i++)
     {
      CLayerDescription* temp = m_arPTModelDescription.At(i);
      if(!temp)
         return false;

Em seguida, preparamos um arary dinâmico de strings e chamamos o método descrito acima para gerar uma descrição de texto da camada neural LayerDescriptionToString. Depois que o método for concluído, obteremos um array de descrições de string e o número de elementos nela. Se ocorrer um erro, o método retornará um array vazio e "-1" em vez do tamanho do array. Notificaremos o usuário sobre a ocorrência do erro e encerraremos o método.

      string items[];
      int total_items = LayerDescriptionToString(temp, items);
      if(total_items < 0)
        {
         printf("%s %d Error at layer %d: %d", __FUNCSIG__, __LINE__, i, GetLastError());
         return false;
        }

Se compusermos com sucesso a descrição de texto da camada neural, primeiro adicionaremos um elemento separador de bloco. E então, em um loop aninhado, exibiremos todo o conteúdo do texto array-descrição da camada neural.

      if(!m_lstPTModel.AddItem(StringFormat("____ Layer %d ____", i + 1), i + 1))
         return false;
      for(int it = 0; it < total_items; it++)
         if(!m_lstPTModel.AddItem(items[it], i + 1))
            return false;
     }

Aqui é importante notar que, ao especificar o identificador do grupo, adicionamos 1 ao número ordinal da camada neural na matriz dinâmica da descrição da arquitetura do modelo. Esta é uma etapa necessária porque a indexação dos elementos na matriz começa em "0". E ao especificar "0" como identificador numérico, a classe CListView irá substituí-lo automaticamente pelo número total de elementos na lista. E não gostaríamos de receber um valor aleatório em vez de um ID de grupo.

O código do método LoadModel é então deixado inalterado. E seu código completo pode ser encontrado no anexo. Lá você também pode encontrar os códigos de todos os métodos e classes utilizados no programa descrito. Em particular, você pode ver adições semelhantes ao método para exibir a descrição do novo modelo ChangeNumberOfLayers.

Observe que, no método ChangeNumberOfLayers, as informações sobre o modelo que está sendo criado são coletadas de 2 arrays dinâmicos que descrevem a arquitetura dos modelos. A primeira descreve a arquitetura do modelo doador. Dele tomamos a descrição das camadas neurais copiadas. E o segundo array contém uma descrição das redes neurais adicionadas.

Após exibir a descrição da arquitetura dos modelos, manuseamos os métodos de tratamento de eventos de mudança de estado das listas criadas.

ON_EVENT(ON_CHANGE, m_lstPTModel, OnChangeListPTModel)
ON_EVENT(ON_CHANGE, m_lstNewModel, OnChangeListNewModel)

Conforme descrito acima, quando o usuário seleciona qualquer linha da lista, moveremos a seleção para a primeira linha do bloco especificado. Para fazer isso, basta obter o ID do grupo do elemento selecionado pelo usuário e dizer ao programa para selecionar o primeiro elemento com o identificador fornecido. Esta operação é realizada pelo método SelectByValue.

bool CNetCreatorPanel::OnChangeListNewModel(void)
  {
   long value = m_lstNewModel.Value();
//---
   return m_lstNewModel.SelectByValue(value);
  }

Assim, expandimos a quantidade de informações exibidas sobre a arquitetura do modelo. Ao mesmo tempo, tornamos a quantidade de informação de saída minimamente suficiente e dependente do tipo de camada neural. Isso significa que ao usuário são exibidas apenas informações relevantes para a descrição da arquitetura de cada camada neural específica. E informações extras não sobrecarregam a janela de informações.


2. Ativando campos de entrada usados e desativando não utilizados

A próxima coisa que teremos de fazer é modificar os campos de entrada de dados. Não importa o quão estranho possa parecer, mas há um campo bastante grande para a imaginação. Provavelmente a primeira coisa que chama sua atenção é a quantidade de informações de entrada. No painel, criamos campos de entrada para todos os elementos da classe de descrição da arquitetura da camada neural CLayerDescription. Não posso dizer que é ruim. Assim, o usuário vê todos os dados especificados e pode alterá-los em qualquer ordem e quantas vezes quiser antes de adicionar uma camada. Mas você e eu sabemos que nem todos esses campos são relevantes para todas as camadas neurais.

Por exemplo, para especificar os parâmetros de uma camada neural totalmente conectada, basta especificar apenas 3: o número de neurônios, a função de ativação e o método de otimização de parâmetros. Os outros parâmetros são irrelevantes. Ao mesmo tempo, para a camada neural convolucional, devemos especificar o tamanho da janela de dados de entrada e seu incremento. E o número de elementos na saída da camada neural dependerá do tamanho do buffer de dados de entrada e dos dois parâmetros mencionados acima.

E no bloco recorrente LSTM, as funções de ativação são definidas pela arquitetura do bloco, e não há necessidade de especificá-las. 

Claro, o usuário pode conhecer todas essas nuances. Mas uma ferramenta bem projetada deve alertar o usuário contra possíveis erros "automáticos". E aqui existem 2 opções preventivas. Podemos remover elementos irrelevantes do painel ou simplesmente torná-los não editáveis.

Cada opção tem seus prós e contras. As vantagens da primeira opção incluem a redução do número de campos de entrada no painel. Assim, o painel pode ser mais compacto. A desvantagem seria uma elaboração mais complexa, uma vez que precisaremos mover os elementos ao redor do painel a cada vez. Ao fazer isso, o movimento constante de objetos pode confundir o usuário e levar a erros durante a operação.

Na minha opinião, o uso desse método se justifica quando você precisa inserir uma grande quantidade de dados. E a remoção de objetos desnecessários tornará o painel mais compacto e evitará a confusão desnecessária de elementos.

A segunda opção é aceitável para um pequeno número de elementos, quando podemos organizar facilmente todos os elementos em um painel de uma só vez e é fácil de usar. Com isso, não confundimos o usuário movendo coisas pelo painel desnecessariamente. Como resultado, após várias iterações, o usuário lembra visualmente de sua localização e, praticamente, de forma instintiva pode trabalhar com eles, o que melhora o desempenho geral.

Já colocamos todos os campos de entrada no painel da interface de nossa ferramenta. Por isso vejo a segunda opção como uma solução adequada.

Bem, aí está a primeira solução a nível de arquitetura. Mas iremos um pouco mais longe. Nosso painel tem tanto um campo de lista suspensa quanto um campo para inserir valores diretamente. Se o usuário só pode selecionar uma opção no campo de lista suspensa, então ele pode inserir fisicamente qualquer texto nos campos campo para inserir valores diretamente.

Esperamos obter um valor inteiro lá. E é bastante lógico adicionar uma verificação das informações inseridas antes de passá-las para o objeto de descrição da arquitetura da camada neural criada. E para que o usuário possa ver por si mesmo como as informações são percebidas por nossa ferramenta, faremos uma verificação de se as informações inseridas estão corretas assim que o usuário inserir texto. E após verificar no campo de entrada, substituiremos as informações inseridas pelo usuário pelas informações aceitas pela ferramenta. Assim, o usuário poderá ver a diferença entre as informações inseridas e lidas, e, se necessário, tomar medidas corretivas.

Eis aqui outro ponto importante. Ao descrever a arquitetura da camada neural na classe CLayerDescription, temos elementos de dupla finalidade. Portanto, o elemento step para as camadas convolucional e de subamostra indica o incremento da janela de dados de entrada. Ao mesmo tempo, também é usado para indicar o número de pontos de atenção ao descrever camadas neurais de atenção.

Da mesma forma, o parâmetro window_out indica o número de filtros na camada convolucional e o tamanho da camada de chaves interna no bloco de atenção.

Para tornar nossa interface mais amigável, o correto é alterar os rótulos de texto ao escolher o tipo apropriado de camada neural.

Isso não deve ser confundido com o problema de reorganizar elementos na janela de interface descrita acima. Nesse caso, o campo de entrada permanece inalterado. Apenas a mensagem informativa ao lado da janela é alterada. Se o usuário não prestar atenção à alteração na legenda e inserir os dados automaticamente no campo correspondente, isso não levará a erros na organização do modelo, uma vez que a informação em qualquer caso irá calhar no elemento desejado da descrição da arquitetura da camada neural criada.

Para implementar as soluções acima, precisamos dar um passo atrás e fazer algum trabalho preparatório.

Em primeiro lugar, ao criar rótulos de texto no painel de interface de nossa ferramenta, não salvamos ponteiros para os objetos correspondentes. E agora, quando precisarmos alterar o texto de alguns deles, teremos que procurá-los no array geral de objetos. Para não fazer isso, retornaremos ao método de criação de rótulo de texto CreateLabel e, ao concluir as operações do método, ao invés de um resultado lógico, retornaremos um ponteiro para o objeto criado.

CLabel* 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 NULL;
   if(!tmp_label.Create(m_chart_id, StringFormat("%s%d", LABEL_NAME, id), m_subwin, x1, y1, x2, y2))
     {
      delete tmp_label;
      return NULL;
     }
   if(!tmp_label.Text(text))
     {
      delete tmp_label;
      return NULL;
     }
   if(!Add(tmp_label))
     {
      delete tmp_label;
      return NULL;
     }
//---
   return tmp_label;
  }

Obviamente, não armazenaremos ponteiros para todos os rótulos. Salvaremos apenas 2 objetos de interesse para nós. Para fazer isso, vamos declarar duas variáveis adicionais. Embora ponteiros dinâmicos para objetos sejam usados, não os adicionaremos ao destruidor de nossa classe de ferramentas. Esses objetos, como antes, serão excluídos da matriz de todos os objetos da ferramenta. Mas, ao mesmo tempo, teremos acesso direto aos objetos de que precisamos.

   CLabel*           m_lbWindowOut;
   CLabel*           m_lbStepHeads;

Vamos escrever ponteiros para novas variáveis no método Create de nossa classe, onde faremos as pequenas alterações abaixo. O restante do código do método permaneceu inalterado. O código completo do script pode ser encontrado no anexo.

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;
//---
...............
...............
//---
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   m_lbStepHeads = CreateLabel(8, "Step", lx1, ly1, lx1 + EDIT_WIDTH, ly2);
   if(!m_lbStepHeads)
      return false;
//---
...............
...............
//---
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   m_lbWindowOut = CreateLabel(9, "Window Out", lx1, ly1, lx1 + EDIT_WIDTH, ly2);
   if(!m_lbWindowOut)
      return false;
//---
...............
...............
//---
   return true;
  }

Na próxima etapa do nosso trabalho preparatório, criaremos um método para alterar o status do campo de entrada. É importante dizer que a classe CEdit padrão já possui um método ReadOnly para alterar o status do objeto. Mas este método não oferece visualização do status do objeto. Apenas bloqueia a possibilidade de entrada de dados. E gostaríamos de disponibilizar objetos visualmente separáveis, isto é, «para entrada de dados» e «não para entrada de dados». Não vamos inventar algo novo aqui. E realçamos os objetos com uma cor de fundo. Para campos editáveis, faremos com que a cor de fundo do objeto seja branca. E para objetos imutáveis, faremos o fundo semelhante à cor do painel.

Esta funcionalidade será executada no método EditReedOnly. Nos parâmetros do método, passaremos um ponteiro para o objeto e um novo sinalizador de status. No corpo do método, passaremos o sinalizador recebido para o método ReadOnly do objeto de entrada e definiremos o plano de fundo do objeto de acordo com o sinalizador especificado.

bool CNetCreatorPanel::EditReedOnly(CEdit& object, const bool flag)
  {
   if(!object.ReadOnly(flag))
      return false;
   if(!object.ColorBackground(flag ? CONTROLS_DIALOG_COLOR_CLIENT_BG : CONTROLS_EDIT_COLOR_BG))
      return false;
//---
   return true;
  }

Agora vamos voltar nossa atenção para as funções de ativação, ou melhor, para a lista suspensa de funções de ativação disponíveis. Aqui, é importante notar que nem todas elas para todos os tipos de camadas neurais são relevantes para esta lista suspensa. Como em algumas arquiteturas o tipo de função de ativação é claramente definido e não é alterado por esta lista. Um exemplo disso é o bloco LSTM, camada de subamostra, blocos de atenção. No entanto, a classe CComboBox não oferece um método que bloqueie a funcionalidade da classe. Por isso, usaremos uma solução alternativa e modificaremos a lista de funções de ativação disponíveis caso a caso. Para fazer isso, criaremos métodos separados para preencher a lista de funções de ativação disponíveis.

Na verdade, existem apenas dois desses métodos. Um, por assim dizer, é geral, indicando as funções de ativação ActivationListMain. E o segundo é um ActivationListEmpty "vazio" com apenas uma opção "None" disponível.

Para entender o algoritmo de construção do método, vamos considerar o código do método ActivationListMain. No início do método, limparemos completamente a lista existente de elementos das funções de ativação disponíveis. E, em seguida, preenchemos a lista de elementos em um loop usando o método ItemAdd e a função EnumToString.

Observe aqui que a codificação dos elementos na enumeração da função de ativação começa com "-1" para None. E o próximo na função de enumeração da tangente hiperbólica TANH tem o índice "0". E isso não é totalmente bom pelo motivo indicado acima ao descrever o preenchimento da lista de descrições da arquitetura da rede neural. Afinal, a lista suspensa é a classe CListView já conhecida por nós. Portanto, como antes, para excluir o valor nulo do identificador de lista, simplesmente adicionamos uma pequena constante ao identificador de enumeração.

Após preencher a lista de funções de ativação disponíveis, definiremos o valor padrão e sairemos do método.

bool CNetCreatorPanel::ActivationListMain(void)
  {
   if(!m_cbActivation.ItemsClear())
      return false;
   for(int i = -1; i < 3; i++)
      if(!m_cbActivation.ItemAdd(EnumToString((ENUM_ACTIVATION)i), i + 2))
         return false;
   if(!m_cbActivation.SelectByValue((int)DEFAULT_ACTIVATION + 2))
      return false;
//---
   return true;
  }

E mais um método que precisamos nos ajudará a automatizar um pouco o trabalho do usuário. Já mencionamos acima que no caso de modelos convolucionais ou blocos de atenção, o número de elementos na saída do modelo depende do tamanho da janela dos dados de entrada analisados e do incremento de seu movimento. E aqui, para eliminar possíveis erros e reduzir o trabalho manual do usuário, decidi fechar o campo de entrada para o número de blocos e preenchê-lo com um método SetCounts separado.

Nos parâmetros deste método, passamos o tipo da camada neural criada. O método retornará o resultado lógico das operações.

bool CNetCreatorPanel::SetCounts(const uint position, const uint type)
  {
   const uint position = m_arAddLayers.Total();

E no corpo do método, primeiro determinamos o número de elementos na saída da camada anterior. E aqui devemos entender que a camada anterior pode estar em um dos dois arrays dinâmicos: descrições da arquitetura do modelo doador ou descrições da arquitetura para adicionar novas camadas neurais. Determinar de onde precisamos pegar a última camada neural é bastante simples. Sempre adicionaremos uma camada neural ao final da lista. Portanto, tomaremos uma camada do modelo doador somente se o array de novas camadas neurais estiver vazio. E, seguindo essa lógica, verificamos o tamanho do array dinâmico de novas camadas neurais. E dependendo de seu tamanho, pediremos ao array correspondente um ponteiro para a camada neural anterior.

   CLayerDescription *prev;
   if(position <= 0)
     {
      if(!m_arPTModelDescription || m_spPTModelLayers.Value() <= 0)
         return false;
      prev = m_arPTModelDescription.At(m_spPTModelLayers.Value() - 1);
      if(!prev)
         return false;
     }
   else
     {
      if(m_arAddLayers.Total() < (int)position)
         return false;
      prev = m_arAddLayers.At(position - 1);
     }
   if(!prev)
      return false;

A seguir, contaremos o número de elementos no buffer de resultados da camada anterior de acordo com seu tipo. Se de repente o tamanho do buffer não for maior que "0", saímos do método com um resultado false.

   int outputs = prev.count;
   switch(prev.type)
     {
      case defNeuronAttentionOCL:
      case defNeuronMHAttentionOCL:
      case defNeuronMLMHAttentionOCL:
         outputs *= prev.window;
         break;
      case defNeuronConvOCL:
         outputs *= prev.window_out;
         break;
     }
//---
   if(outputs <= 0)
      return false;

Em seguida, lemos na interface os valores do tamanho da janela dos dados de entrada analisados e seu incremento. E também preparamos uma variável para registrar o resultado do cálculo.

   int counts = 0;
   int window = (int)StringToInteger(m_edWindow.Text());
   int step = (int)StringToInteger(m_edStep.Text());

Calcularemos diretamente o número de elementos dependendo do tipo de camada neural que está sendo criada. Para calcular o número de elementos das camadas convolucional e de subamostra, precisamos do tamanho da janela de dados de entrada analisada e seu incremento.

   switch(type)
     {
      case defNeuronConvOCL:
      case defNeuronProofOCL:
         if(step <= 0)
            break;
         counts = (outputs - window - 1 + 2 * step) / step;
         break;

Ao usar blocos de atenção, o tamanho do incremento é igual ao tamanho da janela. E de acordo com as regras da matemática, a fórmula é ligeiramente reduzida.

      case defNeuronAttentionOCL:
      case defNeuronMHAttentionOCL:
      case defNeuronMLMHAttentionOCL:
         if(window <= 0)
            break;
         counts = (outputs + window - 1) / window;
         break;

E no caso de usar a camada latente do autocodificador variacional, o tamanho da camada será exatamente 2 vezes menor que o anterior.

      case defNeuronVAEOCL:
         counts = outputs / 2;
         break;

Para todos os outros casos, definiremos o tamanho da camada neural para ser igual ao tamanho da camada anterior. Isto pode ser usado ao declarar uma camada de normalização de lote ou Dropout.

      default:
         counts = outputs;
         break;
     }
//---
   return m_edCount.Text((string)counts);
  }

Transferiremos o valor recebido para o elemento de interface correspondente.

Agora temos ferramentas suficientes para efetuar as mudanças de interface dependendo da escolha do tipo de camada neural a ser criada. Então vamos ver como podemos fazer isso. Essa funcionalidade é implementada no método OnChangeNeuronType. O nome do método não é acidental. Afinal, vamos chamá-lo toda vez que o usuário alterar o tipo da camada neural adicionada.

O método especificado não contém parâmetros e retorna o resultado lógico da operação. No corpo do método, primeiro definimos o tipo de camada neural a ser criada, que o usuário escolheu.

bool CNetCreatorPanel::OnChangeNeuronType(void)
  {
   long type = m_cbNewNeuronType.Value();

Além disso, nosso algoritmo se ramifica dependendo do tipo selecionado de camada neural. O algoritmo para cada camada neural será semelhante. Mas quase todas as camadas neurais têm suas próprias nuances. Para uma camada neural totalmente conectada, deixamos apenas um campo de entrada para o número de neurônios ativos e carregamos a lista completa de funções de ativação possíveis.

   switch((int)type)
     {
      case defNeuronBaseOCL:
         if(!EditReedOnly(m_edCount, false) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!ActivationListMain())
            return false;
         break;

No caso de uma camada convolucional, estarão ativos outros 3 campos de entrada, o tamanho da janela de dados de entrada analisada, seu incremento, assim como o tamanho da janela de resultados (número de filtros). Ao mesmo tempo, atualizamos o valor de dois rótulos de texto e começamos a recalcular o número de elementos da camada neural dependendo do tamanho da janela de dados de entrada e de seu incremento. Vale a pena dizer que contamos o número de elementos para um filtro. Portanto, o resultado do cálculo não depende do número de filtros utilizados.

      case defNeuronConvOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, false) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, false))
            return false;
         if(!m_lbStepHeads.Text("Step"))
            return false;
         if(!m_lbWindowOut.Text("Window Out"))
            return false;
         if(!ActivationListMain())
            return false;
         if(!SetCounts(defNeuronConvOCL))
            return false;
         break;

Para a camada de subamostragem, não especificamos o número de filtros e a função de ativação. Em nossa implementação, sempre usamos o valor máximo como função de ativação da camada de subamostra. Portanto, limpamos a lista de funções de ativação disponíveis. Mas, como no caso da camada convolucional, começamos a calcular o número de elementos da camada criada.

      case defNeuronProofOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, false) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!m_lbStepHeads.Text("Step"))
            return false;
         if(!SetCounts(defNeuronProofOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

Ao declarar um bloco LSTM, a lista de funções de ativação não é utilizada, e nós a limpamos. Ao mesmo tempo, apenas o campo para o número de elementos na camada neural está aberto para entrada de dados.

      case defNeuronLSTMOCL:
         if(!EditReedOnly(m_edCount, false) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

Para inicializar a camada Dropout, basta especificar apenas os valores para a probabilidade de expulsão de neurônios A função de ativação não é usada. E o número de elementos é igual ao tamanho da camada neural anterior.

      case defNeuronDropoutOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, false) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!SetCounts(defNeuronDropoutOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

Uma abordagem semelhante é adotada ao declarar uma camada de normalização de lote, somente que neste caso especificamos o tamanho do lote.

      case defNeuronBatchNormOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, false) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!SetCounts(defNeuronBatchNormOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

Dependendo do método de atenção, ativamos os campos de entrada de número de pontos de atenção e camadas neurais no bloco. Ao mesmo tempo, alteramos os rótulos de texto dos campos de entrada correspondentes.

      case defNeuronAttentionOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!SetCounts(defNeuronAttentionOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;
      case defNeuronMHAttentionOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, false) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!m_lbStepHeads.Text("Heads"))
            return false;
         if(!SetCounts(defNeuronMHAttentionOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;
      case defNeuronMLMHAttentionOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, false) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, false) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, false))
            return false;
         if(!m_lbStepHeads.Text("Heads"))
            return false;
         if(!m_lbWindowOut.Text("Keys size"))
            return false;
         if(!SetCounts(defNeuronMLMHAttentionOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

E para a camada latente do autocodificador variacional, não precisamos inserir dados. Só precisamos selecionar o tipo de camada e adicioná-lo ao modelo.

      case defNeuronVAEOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!ActivationListEmpty())
            return false;
         if(!SetCounts(defNeuronVAEOCL))
            return false;
         break;

Se o tipo de camada neural especificado nos parâmetros não for encontrado, concluímos o método com o resultado false.

      default:
         return false;
         break;
     }
//---
   return true;
  }

Se todas as operações do método forem concluídas com sucesso, saímos com o resultado true.

Agora só temos que elaborar o lançamento do método descrito no momento certo. Para fazer isso, usaremos o evento de alteração de valor do elemento de seleção de tipo de camada neural e adicionaremos o manipulador de eventos apropriado.

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)
ON_EVENT(ON_CHANGE, m_lstNewModel, OnChangeListNewModel)
ON_EVENT(ON_CHANGE, m_cbNewNeuronType, OnChangeNeuronType)
EVENT_MAP_END(CAppDialog)

Ao implementar os métodos descritos acima, fazemos a inclusão e desativação de campos de entrada dependendo do tipo de camada neural selecionada. Mas acima, também discutimos o controle de entrada de dados.

Em um exame mais detalhado, podemos ver que em todos os campos de entrada de dados, esperamos receber números inteiros estritamente maiores que zero. A única exceção é o valor da probabilidade de expulsão de elementos na camada Dropout. Aqui é permitido um valor real entre 0 e 1. Portanto, precisamos de 2 métodos para validar os dados inseridos. Um para probabilidade e outro para todos os outros elementos.

O algoritmo de ambos os métodos é bastante simples. Primeiro, lemos o valor do texto inserido pelo usuário. Vamos convertê-lo em um valor numérico. Verificamos se corresponde com o intervalo de valores válidos. E enviamos o valor recebido de volta para a janela correspondente da interface. E cabe ao usuário assegurar-se de que os dados sejam interpretados corretamente.

bool CNetCreatorPanel::OnEndEditProbability(void)
  {
   double value = StringToDouble(m_edProbability.Text());
   return m_edProbability.Text(DoubleToString(fmax(0, fmin(1, value)), 2));
  }
bool CNetCreatorPanel::OnEndEdit(CEdit& object)
  {
   long value = StringToInteger(object.Text());
   return object.Text((string)fmax(1, value));
  }

É importante notar que se, ao verificar se a entrada do valor de probabilidade é correta, identificarmos claramente o campo de entrada, então, para identificar o objeto no segundo método, passaremos um ponteiro para o objeto correspondente nos parâmetros do método. E aqui reside outro desafio. As macros de tratamento de eventos sugeridas não contêm uma macro adequada que passasse um ponteiro para o objeto que acionou o evento para o método de tratamento de eventos. Portanto, tivemos que adicionar essa macro.

#define ON_EVENT_CONTROL(event,control,handler)          if(id==(event+CHARTEVENT_CUSTOM) && lparam==control.Id()) \
                                                              { handler(control); return(true); }

E aqui devemos lembrar que entre os campos de entrada a serem verificados pode estar o tamanho da janela de dados de entrada analisada e seu incremento. E como você se lembra, o número de elementos na camada neural dependerá desses parâmetros. Isso significa que ao alterar seus valores, precisaremos recalcular o tamanho da camada neural criada. Somente aqui o modelo de manipulação de eventos que usamos permite apenas um manipulador para cada evento. Ao fazer isso, podemos usar um manipulador para atender a diferentes eventos. Por isso, vamos criar outro método que primeiro verificará os valores nos campos de entrada referentes ao tamanho da janela e ao incremento. E então ele chamará o método para recalcular o tamanho da camada neural, levando em consideração o tipo selecionado de camada neural.

bool CNetCreatorPanel::OnChangeWindowStep(void)
  {
   if(!OnEndEdit(m_edWindow) || !OnEndEdit(m_edStep))
      return false;
   return SetCounts((uint)m_cbNewNeuronType.Value());
  }

Agora só temos que completar nosso mapa do manipulador de eventos. Isso facilita a execução do manipulador de eventos certo no momento certo.

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)
ON_EVENT(ON_CHANGE, m_lstNewModel, OnChangeListNewModel)
ON_EVENT(ON_CHANGE, m_cbNewNeuronType, OnChangeNeuronType)
ON_EVENT(ON_END_EDIT, m_edWindow, OnChangeWindowStep)
ON_EVENT(ON_END_EDIT, m_edStep, OnChangeWindowStep)
ON_EVENT(ON_END_EDIT, m_edProbability, OnEndEditProbability)
ON_EVENT_CONTROL(ON_END_EDIT, m_edCount, OnEndEdit)
ON_EVENT_CONTROL(ON_END_EDIT, m_edWindowOut, OnEndEdit)
ON_EVENT_CONTROL(ON_END_EDIT, m_edLayers, OnEndEdit)
ON_EVENT_CONTROL(ON_END_EDIT, m_edBatch, OnEndEdit)
EVENT_MAP_END(CAppDialog)


3. Adicionando manipulação de eventos de teclado

Fizemos um ótimo trabalho ao tornar nossa ferramenta de transferência de aprendizado muito mais amigável e intuitiva. Mas todas as nossas melhorias diziam respeito à interface e facilitavam o uso com um mouse ou com o toque, se o seu computador suportar este recurso. Mas ainda não temos a opção de usar um teclado para operar nossa ferramenta. Por exemplo, pode ser conveniente usar as setas para cima e para baixo para alterar o número de camadas neurais a serem copiadas. Ou pressionar a tecla Delete chamará o método para excluir a camada neural selecionada do modelo que está sendo criado.

Não vou me aprofundar neste tópico agora. Mostrarei apenas como adicionar processamento de teclas com manipuladores de eventos existentes em apenas algumas linhas de código.

Como você pode ver, todos os três recursos propostos acima já estão realizados no código da nossa ferramenta. E eles são executados quando um determinado evento ocorre. Para excluir a camada neural selecionada, criamos um botão separado no painel de interface. E a alteração do número de camadas neurais copiadas é realizada pressionando os botões do objeto CSpinEdit.

Do lado técnico, pressionar os botões do teclado é o mesmo que pressionar os botões do mouse ou movê-lo. Isso também é tratado pela função OnChartEvent do seu programa. Isso significa que ela chama o método ChartEvent de nossa classe.

Quando ocorrer um evento de pressionamento de tecla, receberemos o ID do evento CHARTEVENT_KEYDOWN e a variável lparam armazenará o ID da tecla pressionada.

Usando esta propriedade, podemos brincar com o teclado e determinar os identificadores de todas as teclas que nos interessam. Por exemplo, aqui estão os códigos das teclas mencionadas acima.

#define KEY_UP                               38
#define KEY_DOWN                             40
#define KEY_DELETE                           46

Agora vamos passar para o método ChartEvent da nossa classe. Como você se lembra, chamamos um método semelhante da classe pai. Agora vamos adicionar uma verificação de identificador de evento e a visibilidade de nossa ferramenta para ele. Executaremos o manipulador de eventos somente quando a interface de nossa ferramenta estiver visível. Afinal, o usuário deve ver o que está acontecendo no painel e controlar visualmente o processo.

Se passamos na primeira etapa de verificação, verificamos o código da tecla pressionada. Se houver uma tecla correspondente em nossa lista, geramos um evento personalizado que corresponde a uma ação semelhante no painel de nossa interface.

Por exemplo, ao pressionar a tecla Delete no teclado, geramos um evento de pressionamento do botão DELETE no painel de nossa interface. 

void CNetCreatorPanel::ChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   CAppDialog::ChartEvent(id, lparam, dparam, sparam);
   if(id == CHARTEVENT_KEYDOWN && m_spPTModelLayers.IsVisible())
     {
      switch((int)lparam)
        {
         case KEY_UP:
            EventChartCustom(CONTROLS_SELF_MESSAGE, ON_CLICK, m_spPTModelLayers.Id() + 2, 0.0, m_spPTModelLayers.Name() + "Inc");
            break;
         case KEY_DOWN:
            EventChartCustom(CONTROLS_SELF_MESSAGE, ON_CLICK, m_spPTModelLayers.Id() + 3, 0.0, m_spPTModelLayers.Name() + "Dec");
            break;
         case KEY_DELETE:
            EventChartCustom(CONTROLS_SELF_MESSAGE, ON_CLICK, m_btDeleteLayer.Id(), 0.0, m_btDeleteLayer.Name());
            break;
        }
     }
  }

E depois disso saímos do método. Em seguida, deixamos o programa manipular o evento gerado com os manipuladores e métodos de eventos já existentes.

Obviamente, essa abordagem só é possível se houver manipuladores apropriados no programa. No entanto, ninguém nos incomoda em criar novos manipuladores de eventos e gerar eventos exclusivos para eles.


Considerações finais

Neste artigo, analisamos várias opções para melhorar a usabilidade da interface do usuário. Você pode verificar a qualidade das abordagens utilizadas ao testar a ferramenta no anexo do artigo. Espero que você ache esta ferramenta útil. E ficarei grato se você compartilhar suas impressões e desejos para melhorar a ferramenta no tópico apropriado do fórum.

Referências

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

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 das classes 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/11306

Arquivos anexados |
MQL5.zip (74.22 KB)
DoEasy. Controles (Parte 15): Objeto WinForms TabControl - múltiplas fileiras de cabeçalhos de guias, métodos de manuseio de guias DoEasy. Controles (Parte 15): Objeto WinForms TabControl - múltiplas fileiras de cabeçalhos de guias, métodos de manuseio de guias
Neste artigo, continuaremos trabalhando no objeto WinForm TabControl, e para tal criaremos a classe do objeto-campo de guia, tornaremos possível colocar cabeçalhos de guias em várias linhas e adicionaremos métodos para trabalhar com as guias do objeto.
Aprendendo a construindo um EA que opera de forma automática (Parte 06): Tipos de contas (I) Aprendendo a construindo um EA que opera de forma automática (Parte 06): Tipos de contas (I)
Aprenda como criar um EA que opera de forma automática, isto de forma simples e o mais seguro possível. Nosso EA, até o momento consegue trabalhar, em qualquer tipo de situação, mas para torná-lo automatizado, ele não está adequado, precisamos fazer algumas coisas.
Operações com Matrizes e Vetores em MQL5 Operações com Matrizes e Vetores em MQL5
Matrizes e vetores foram introduzidos na MQL5 para operações eficientes com soluções matemáticas. Os novos tipos oferecem métodos integrados para a criação de código conciso e compreensível que se aproxima da notação matemática. Os arrays fornecem recursos extensos, mas há muitos casos em que as matrizes são muito mais eficientes.
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.