preview
Rede neural na prática: Retro propagação manual

Rede neural na prática: Retro propagação manual

MetaTrader 5Aprendizado de máquina |
46 0
Daniel Jose
Daniel Jose

Introdução

No artigo anterior Rede neural na prática: Iniciando a corrente, começamos a esboçar aquilo que seria a nossa primeira rede perceptron de uso mais geral. Tal rede, na verdade, não é exatamente uma rede, mas sim a tentativa de ligação de dois perceptrons em série. O objetivo seria demonstrar como funciona o mecanismo, que faria, ou melhor dizendo, permitiria a perceptrons que aparentemente estariam isolados, pudessem trabalhar em conjunto, como se fossem uma única entidade. Apesar de podermos utilizar um número arbitrário de perceptrons, no momento, sugiro que façamos uso apenas de dois. Já que um número maior, traria certas complicações desnecessárias no momento. Até mesmo o número de entradas e saídas deverá permanecer como sendo o número mínimo necessário. Pelo simples fato de que quanto mais informação tivermos para ser analisado, mais complicado se torna entender como as coisas funcionam.

Você pode ter notado, que os perceptrons estavam tentando convergir para uma solução. Contudo, estavam tentando fazer isto de uma forma totalmente individualizada. Isto por que, até o atual momento, o que se encontra de fato implementado, nos possibilita apenas construir algo similar ao que pode ser visto logo abaixo.

Imagem 01

O objetivo deste artigo será de demonstrar como poderemos fazer com que a nossa tentativa de ligar dois ou mais perceptrons possa funcionar. Isto de forma que eles passem a trabalhar em conjunto, e não como sendo coisas individuais.


É uma rede perceptron, ou um perceptron único?

O título deste tópico, pode parecer um tanto extravagante. Isto por que muitos podem imaginar, que não tem como comparar um perceptron com uma rede perceptron. Bem, até certo ponto isto pode de fato ser verdade. Mas não é verdade, quando precisamos pensar em uma forma de fazer diversos perceptrons funcionarem em conjunto.

Talvez entender o porquê deste titulo do tópico, precisaremos antes, fazer outra pergunta: aquilo que vemos na imagem 01, de fato é algo que funciona? De certa forma, sim, mas não da maneira como seria o esperado. Isto por que, os perceptrons não estão conversando, ou melhor dizendo, trocando informações entre si. Tendo suas decisões tomadas de maneira completamente individualizada. O que para nosso propósito, não gerará nenhum tipo de coisa realmente útil. Neste momento, vem a pergunta: "Como fazer os perceptrons trabalharem em conjunto?".

Bem, está de fato, é a grande pergunta. Contudo, ela não tem uma única resposta. Diferente do que muitos dizem, ou podem de maneira errada estar imaginando. A resposta não é necessariamente a retro propagação. Apesar de que, a retro propagação, ser uma solução para a grande parte dos problemas, ela nem sempre é o que implementamos na realidade. Existem situações onde ela não é empregada. Então, tudo depende de cada caso específico. E de qual o proposito específico para a criação da rede perceptron em questão.

Mas antes de nos aprofundarmos nesta questão. Quero propor uma coisa, e esta foi vista no início desta sequência de artigos, quando expliquemos como a equação de reta, é usada para gerar uma convergência para melhores valores das constantes empregadas. Basicamente isto acaba por resultar em uma regressão linear. Conforme fomos progredindo, acabamos por desenvolver um perceptron simples. Mas perfeitamente funcional, dentro daquela que era a proposta primária. Com o progresso, em algum momento chegaremos no que seria uma rede perceptron. Contudo, antes que isto ocorra, quero propor uma pequena pausa. E experimentar uma modificação, que lhe ajudará a entender como podemos implementar a comunicação entre os perceptrons. É algo que explicará diversas questões envolvidas, isto quanto precisamos de alguma forma forçar a convergência em uma rede perceptron.

Sei que parece muito estranho, e ainda mais para quem está tentando entender como as coisas funcionam. De que, bem lá, no fundo, uma rede perceptron, não será na maior parte das vezes diferente de um perceptron isolado. Existem casos em que uma rede perceptron, não se parece em nada com o que será explicado aqui. Mas em outros, ela será muito próxima do que iremos ver em breve sendo implementado.

Para entender o que será feito, mas principalmente, por que, será preciso darmos alguns passos para trás. Isto por que, da forma como o nosso perceptron se encontra montado. É um tanto quanto difícil explicar, e de forma que fique claro para você, por que precisamos de outro modelo de implementação para o perceptron. Abandonando assim esta que estamos atualmente utilizando. É bom que isto fique bastante claro, antes de prosseguirmos. A nossa atual implementação, não está de forma alguma errada. Ela está apenas um tanto quanto confusa, para que uma explicação adequada possa ser dada, a fim de que fique fácil de entender como perceptrons podem trabalhar em conjunto.

Muito bem, então para começar precisamos recorrer a um pouco de matemática. Mas não precisa ficar apavorado, o que será visto aqui, é algo bastante simples, nível básico mesmo.

Todo e qualquer sistema, que utiliza um perceptron, que faz uso de uma matemática a fim de encontrar uma equação que represente um conjunto de dados. Basicamente a expressão usada é esta vista logo abaixo:

Imagem 02

Aqui temos apenas uma entrada e uma saída. Que é justamente o valor de < x >. Tanto o valor de < a > que é o coeficiente de inclinação, que é comumente chamado de peso, quando tratamos de perceptrons, e o < b > que é o ponto de intersecção, conhecemos aqui como sendo viés, tem valores fixos. Isto tanto faz se estamos usando um perceptron, ou uma rede perceptron qualquer. Tudo pode ser resumido a esta expressão vista na imagem 02.

Agora preste atenção. Se o valor de < x > for 10, e o valor de f(x), for 20. Você pode facilmente perceber que < a > deveria ser igual a dois e < b > deveria ser igual a zero. Isto para uma única, e somente uma única entrada. Conforme mudamos o valor de < x >, esta percepção de que < a > deveria ser dois e < b > deveria ser zero, se torna cada vez mais e mais forte. Acabando por fim fazer com que concluamos que a expressão que representa nosso banco de dados seria:

Imagem 03

Ok, mas onde você que chegar com isto? Bem, se você conseguiu entender esta ideia acima, e o que foi explicado nos artigos anteriores. Deve ter notado que o perceptron, consegue encontrar a expressão que representa o banco de dados, simplesmente comparando o valor de saída com aquele que fornecemos como sendo o valor esperado. E ele consegue fazer isto mudando os valores de peso e viés internos. Mas o que aconteceria se colocássemos dois perceptrons, ligados como vimos sendo feito no artigo anterior, e replicado na imagem 01? Qual seria de fato a expressão que cada perceptron deveria usar? Bem, a expressão seria algo do tipo mostrado abaixo.

Imagem 04

Agora as coisas começaram a ficar um tanto quanto mais interessantes. Observe o seguinte f(xout) é o resultado estimado. Já < xin > é o valor de entrada. E aqui temos dois perceptrons, por assim dizer, que estão ligados em sequência. Ou seja, a entrada de um é a saída do anterior. Por isto temos < a1 > e < b1 > que representam o perceptron de entrada. Assim como < a2 > e < b2 > representam o perceptron de saída.

Agora não é tão simples saber quais os valores para serem colocados nas constantes, certo? Já que a variação de uma, implica na variação de todo o conjunto, dificultando fazer com que o mesmo banco usando para gerar a imagem 03, já não use os valores dois e zero, como peso e viés respectivamente.

E menciono a pouco, é aqui, onde a coisa realmente fica interessante. Lembrando de que esta é modelagem a mais simples de todas. Isto depois da que faz uso de apenas um único perceptron. Outro fato, é que estamos ignorando qualquer tipo de função de ativação aqui, já que elas visam, modificar a equação da reta. Mas isto será explicado em outro momento futuro.

Agora preste atenção a uma coisa nesta expressão da imagem 04, note que ela se parece muito com as anteriores. E as expressões anteriores, seriam de fato um perceptron, que já foi explicado. Assim, começamos a reduzir o problema, a um problema, do qual já sabemos como lidar. Ou seja, usamos o perceptron, a fim de gerar um resultado. E dependendo do erro entre o valor estimado, e o valor reportado pelo perceptron, ajustamos os valores de pesos e viés. Isto fará com que nossa rede passe a se comporta como se fosse um único perceptron.

Estou tentando fazer tudo de forma o mais gradual quanto é possível ser feito. Isto para que a didática se mantenha em um patamar bastante elevado e todos consigam entender. Neste ponto, quem já tem um bom conhecimento em programação, logo começa a imaginar uma solução e já consegue implementar muito rapidamente o que precisa ser feito. Tornando inclusive, a implementação já adequada para modelar uma topologia mais elaborada. Onde não estaríamos restritos a uma sequência única de perceptrons sendo feita.

Contudo, aqui iremos mais devagar, visto que desconheço o nível de programação de cada um. Portanto, considerando que tenha ficado bastante claro, de que a modelagem presente na classe C_Neuron, não está de forma alguma errada, precisando apenas ser adaptada, para cumprir o que é de nosso interesse, nos permitindo assim, controlar como a retro propagação acontecerá, e como ela acontecerá. Podemos começar a pensar nas mudanças que se fazem necessárias, mas para separar devidamente as coisas, vamos iniciar um novo tópico.


Uma leve mudança, mas com resultados profundos

Apesar de ter resultados divertidos, em termos gerais, o que vamos começar fazendo aqui, é um tanto quanto chato. Já que não é de fato, algo assim tão complicado. E que exija um grau de experiência assombrosa. O que faremos será simplesmente, modificar o código visto no artigo anterior, a fim de promover o que foi visto e explicado no tópico anterior. Isto, sim, é algo totalmente sem graça, já que é muito simples de ser feito.

A primeira coisa, será remover o código de treinamento que está sendo utilizado. E vamos passar a utilizar outro código, a fim de tornar a modelagem mais parecida com a que foi explicado no começo deste artigo. Ou seja, a função Learning, que se encontra na classe C_Neuron, não será utilizada. Criaremos uma função local para fazer este mesmo trabalho. Mas não se preocupe, vamos fazer isto devagar. Passo a passo. Assim você conseguirá acompanhar e entender como as coisas vão surgindo naturalmente. Então o código presente no artigo anterior, será modificado para este visto abaixo.

//+------------------------------------------------------------------+
#property copyright "Daniel Jose"
#property description "Experiencing a simple chain of neurons"
//+------------------------------------------------------------------+
#define def_NO_MSG
//+------------------------------------------------------------------+
#include <Neural Network\C_Neuron.mqh>
//+------------------------------------------------------------------+
//Training expression: f(x) = (w0 * 2)
//+------------------------------------------------------------------+
double Train[] {
                10, 20
               };
//+------------------------------------------------------------------+
#define nColumns    2
#define nLines      Train.Size() / nColumns
//+------------------------------------------------------------------+
void SimpleChain(const uchar nNeurons)
{
    C_Neuron *neuron[];
    double tmp[], in, out;
 
    if ((!nNeurons) || (Train.Size() > 2))
        return;

    ArrayResize(neuron, nNeurons);
    for (uchar c = 0; c < nNeurons; c++)
        neuron[c] = new C_Neuron(nColumns - 1, C_Neuron::Identity);

    ArrayResize(tmp, Train.Size());
    in = out = Train[0];

    for (uchar c = 0; c < nNeurons; c++)
    {
        tmp[0] = (c > 0 ? out : in);
        PrintFormat("Input value in the neuron #%02d: %.8f", c, tmp[0]);
        out = (*neuron[c]).Perceptron(tmp);
    }

    PrintFormat("Final exit: %.8f || Expected: %.8f", out, Train[1]);

    for (uchar c = 0; c < nNeurons; c++)
        delete neuron[c];

    ArrayFree(tmp);
    ArrayFree(neuron);
}
//+------------------------------------------------------------------+
void OnStart()
{
    Print("************************************");
    Print("A simple chain of neurons...");
    Print("************************************");

    SimpleChain(2);
}
//+------------------------------------------------------------------+

Código 01

Claro que se você tentar executar isto, não terá absolutamente nenhum resultado grandioso. Você apenas verá o que é mostrado na imagem abaixo, ou seja, o mesmo de antes.

Imagem 05

Agora quero que você, pense no seguinte: se pedimos para os perceptrons ajustarem seus pesos e viés, o que acontecerá? Obviamente teremos um valor diferente saindo de cada um dos perceptrons. Mas vamos pensar de uma forma um pouco mais profunda. E se ao invés de mudar os valores, de maneira diferenciada, fizermos um pedido para ambos perceptrons, mudarem os pesos e viés, ao mesmo tempo, e na mesma amplitude. O que aconteceria neste caso? Hum, não sei responder a isto. Pois bem, para fazer isto, precisamos adicionar um novo procedimento a classe C_Neuron. Já que da maneira como ela se encontra, não conseguiremos fazer este tipo de coisa ainda.

Mas este novo procedimento é algo que você pode ver no fragmento logo abaixo.

//+------------------------------------------------------------------+
        inline void BackPropagation(const double errBias, const double errWeight)
        {
            m_Infos.Bias -= errBias;
            for (uint c = 0, m = m_Infos.Weight.Size(); c < m; c++)
                m_Infos.Weight[c] -= errWeight;
        }
//+------------------------------------------------------------------+

Fragmento 01

O nome do procedimento é bem sugestivo, mas de fato, aqui, estamos dizendo ao perceptron o quanto ele deverá modificar os pesos e viés. Mas como isto será acessado? Bem, voltando ao código principal, podemos ver como as deverão ocorrer. Como não precisamos mexer muito no código, vamos apenas focar, no fragmento principal, visto abaixo.

17. //+------------------------------------------------------------------+
18. void SimpleChain(const uchar nNeurons, const double errBias, const double errWeight)
19. {
20.     C_Neuron *neuron[];
21.     double tmp[], in, out;
22.  
23.     if ((!nNeurons) || (Train.Size() > 2))
24.         return;
25. 
26.     ArrayResize(neuron, nNeurons);
27.     for (uchar c = 0; c < nNeurons; c++)
28.         neuron[c] = new C_Neuron(nColumns - 1, C_Neuron::Identity);
29. 
30.     ArrayResize(tmp, Train.Size());
31.     in = out = Train[0];
32.     for (uchar count = 1; count < 3; count++)
33.     {
34.         PrintFormat("====== Interaction #%02d ======", count);
35.         for (uchar c = 0; c < nNeurons; c++)
36.         {
37.             tmp[0] = (c > 0 ? out : in);
38.             PrintFormat("Input value in the neuron #%02d: %.8f", c, tmp[0]);
39.             out = (*neuron[c]).Perceptron(tmp);
40.         }
41.         PrintFormat("Final exit: %.8f || Expected: %.8f", out, Train[1]);
42.         for (uchar c = 0; c < nNeurons; c++)
43.             (*neuron[c]).BackPropagation(errBias, errWeight);
44.     }
45. 
46.     for (uchar c = 0; c < nNeurons; c++)
47.         delete neuron[c];
48. 
49.     ArrayFree(tmp);
50.     ArrayFree(neuron);
51. }
52. //+------------------------------------------------------------------+

Fragmento 02

Muito bem, a ideia é forçar ambos perceptrons a ajustarem o valor de viés e peso ao mesmo tempo. Isto será conseguido, passando valores como sendo errBias e errWeight. Como faremos apenas, e tão somente, duas interações, o objetivo não será o de chegar em um valor perfeito, mas simplesmente testar uma hipótese. Note que na primeira interação, temos os valores pelo que o perceptron criou de maneira pseudo randômica. Mas na linha quarenta e dois, entramos em um laço que em como objetivo, forçar todos os perceptrons a ajustarem seus valores de maneira idêntica. Ou seja, não importa o erro que cada perceptron esteja tendo de maneira individual. Todos serão ajustados, pelo novo procedimento que adicionamos na classe C_Neuron.

Com isto na segunda interação, teremos um valor diferente sendo reportado pelo perceptron, isto na linha trinta e nove. Olhando este valor, você poderá visualizar a tentativa dos perceptrons em ajustar adequadamente os pesos e viés. Abaixo podemos observar um típico resultado da execução do código.

Imagem 06

Nosso objetivo primário, é fazer com que o valor, mostrado na região marcada em verde, sejam os mais próximos um do outro. Note que precisamos ajustar o valor de viés. Já que pelos valores de treinamento, o que temos seria: f(x) = 2 * x + 0. Ou seja, o viés deverá ser o mais próximo possível de ser igual a zero. Já o valor de peso, deverá ser o mais próximo de ser igual a dois. Mas como fiz isto nesta imagem 06? Bem, se você notar, verá que o valor do resultado está invertido. Além de estar bem próximo daquele que seria de fato ideal. Isto indica que os valores precisam ser invertidos. Assim, invertendo-os, e ajustando um pouco os valores, com paciência e calma. Podemos chegar nesta imagem vista abaixo.

Imagem 07

Portanto, não precisamos de muita coisa sendo feita, o que necessitamos, é que o perceptron passe a fazer, exatamente o que acabamos de fazer de forma manual, mas de forma automática, sem nossa intervenção. Mas antes, vamos fazer uma última mudança no código, para deixar a coisa ainda mais interessante. Com o objetivo, de justamente não precisarmos compilar o código a cada nova mudança que experimentemos nos valores. Assim, passaremos a poder analisar algumas situações bem inusitadas, e ver que tipo de resultado elas poderão gerar, dependendo de como façamos algumas escolhas. O proposito é ter as seguintes informações, que poderão ser ajustadas de forma interativa, como mostrado na imagem abaixo.

Imagem 08

A graça desta abordagem, está justamente na interação que passaremos a fazer. É bem verdade, que podemos melhorar ainda mais esta interação. No entanto, deixo isto para que você pense em formas mais elaboradas de gerir esta interação. De qualquer forma, esta simples abordagem é melhor do que ficar mudando e recompilado o código a todo momento. Assim, um exemplo de execução pode ser observado logo abaixo.

Imagem 09

Neste caso estamos usando a função de ativação identidade, ao mesmo tempo que fazemos uso de dois perceptrons. E sem mudar nada, podemos fazer outro teste, e ter como um resultado o que é visto na imagem a seguir.

Imagem 10

Agora estamos fazendo uso de cinco perceptrons ligados em cascata. Porém, desta vez, estamos usando a função eLU, como sendo a função de ativação. É importante notar, que a função escolhida será aplicada em todos os perceptrons. Depois veremos como fazer a escolha para que a função de ativação, de cada perceptron seja diferente das de outros perceptrons. Explicar como isto pode ser feito, ficará para outro momento futuro.

Agora, você pode achar que para termos este nível de interação, o código precisou de grandes mudanças. Contudo, veja logo abaixo, e responda: O código precisou mudar o quanto você imaginava?

//+------------------------------------------------------------------+
#property copyright "Daniel Jose"
#property script_show_inputs
#property description "Experiencing a simple chain of neurons"
//+------------------------------------------------------------------+
#define def_NO_MSG
//+------------------------------------------------------------------+
#include <Neural Network\C_Neuron.mqh>
//+------------------------------------------------------------------+
//Training expression: f(x) = (w0 * 2)
//+------------------------------------------------------------------+
double Train[] {
                10, 20
               };
//+------------------------------------------------------------------+
#define nColumns    2
#define nLines      Train.Size() / nColumns
//+------------------------------------------------------------------+
input C_Neuron::eFnActivate user00 = C_Neuron::Identity;    //Activation function
input uchar                 user01 = 2;                     //Number of neurons
input double                user02 = 1.0;                   //Bias Error
input double                user03 = 1.0;                   //Weight Error
//+------------------------------------------------------------------+
void SimpleChain(const uchar nNeurons, const double errBias, const double errWeight)
{
    C_Neuron *neuron[];
    double tmp[], in, out;
 
    if ((!nNeurons) || (Train.Size() > 2))
        return;

    ArrayResize(neuron, nNeurons);
    for (uchar c = 0; c < nNeurons; c++)
        neuron[c] = new C_Neuron(nColumns - 1, user00);

    ArrayResize(tmp, Train.Size());
    in = out = Train[0];
    for (uchar count = 1; count < 3; count++)
    {
        PrintFormat("====== Interaction #%02d ======", count);
        for (uchar c = 0; c < nNeurons; c++)
        {
            tmp[0] = (c > 0 ? out : in);
            PrintFormat("Input value in the neuron #%02d: %.8f", c, tmp[0]);
            out = (*neuron[c]).Perceptron(tmp);
        }
        PrintFormat("Final exit: %.8f || Expected: %.8f", out, Train[1]);
        for (uchar c = 0; c < nNeurons; c++)
            (*neuron[c]).BackPropagation(errBias, errWeight);
    }

    for (uchar c = 0; c < nNeurons; c++)
        delete neuron[c];

    ArrayFree(tmp);
    ArrayFree(neuron);
}
//+------------------------------------------------------------------+
void OnStart()
{
    Print("************************************");
    Print("A simple chain of neurons...");
    Print("************************************");
    Print("Parameters:");
    Print("Activation function: ", EnumToString(user00));
    Print("Bias Error: ", user02);
    Print("Weight Error:", user03);
    Print("************************************");

    SimpleChain(user01, user02, user03);
}
//+------------------------------------------------------------------+

Código 02

Perfeito. Mas antes de terminarmos o artigo, quero propor uma última mudança, e tornará a interação ainda maior. Como pretendo explorar um pouco mais este tipo de coisa, visto que, quando melhor você entender o que está acontecendo aqui, mais simples será entender o que será visto depois. Vamos fazer o seguinte, abriremos um novo tópico, para assim separar adequadamente os assuntos.


Quanto mais interativo, melhor

Apesar de que a implementação feita, usando o script, ser de certa forma agradável. Mais hora, menos hora, ela acaba se tornando um tanto quanto monótona. Podemos deixar a coisa, um pouco mais agradável de ser utilizada na prática. Para isto, vamos adicionar um novo modelo de interação, onde poderemos fazer uso das teclas de direção como forma de controlar a mudança dos valores. O código em si, não é complicado, sendo, na verdade, uma modificação de um código mais antigo, já visto aqui nesta sequência. Contudo, a ideia, neste momento, será utilizada de uma forma um pouco diferente, visando justamente deixar menos monótono o uso da aplicação.

O código será composto de duas partes. Uma que estará em um arquivo de cabeçalho, e outra que estará em um indicador. A parte referente ao arquivo de cabeçalho, que pode ser vista no fragmento abaixo, estará contida no arquivo C_SimpleChain, que no anexo poderá ser visto integralmente. Contudo, precisamos explicar este fragmento visto a seguir.

33. //+------------------------------------------------------------------+
34.         bool Interaction(const double errBias, const double errWeight, string &arr[])
35.         {
36.             double tmp[], in, out;
37.             uchar cArr = 0;
38. 
39.             if ((m_Info.nNeurons == 0) || (m_Info.nColumns == 0))
40.                 return false;
41. 
42.             ArrayResize(neuron, m_Info.nNeurons);
43.             for (uchar c = 0; c < m_Info.nNeurons; c++)
44.                 neuron[c] = new C_Neuron(m_Info.nColumns - 1, m_Info.fn);
45.             ArrayResize(arr, (m_Info.nNeurons + 2) * 2);
46.             ArrayResize(tmp, m_Info.Train.Size());
47.             in = out = m_Info.Train[0];
48.             for (uchar count = 1; count < 3; count++)
49.             {
50.                 arr[cArr++] = StringFormat("====== Interaction #%02d ======", count);
51.                 for (uchar c = 0; c < m_Info.nNeurons; c++)
52.                 {
53.                     tmp[0] = (c > 0 ? out : in);
54.                     arr[cArr++] = StringFormat("Input value in the neuron #%02d: %.8f", c, tmp[0]);
55.                     out = (*neuron[c]).Perceptron(tmp);
56.                 }
57.                 arr[cArr] = StringFormat("Final exit: %.8f || Expected: %.8f", out, m_Info.Train[1]);
58.                 for (uchar c = 0; c < m_Info.nNeurons; c++)
59.                     (*neuron[c]).BackPropagation(errBias, errWeight);
60.             }
61. 
62.             for (uchar c = 0; c < m_Info.nNeurons; c++)
63.                 delete neuron[c];
64.             ArrayFree(neuron);
65. 
66.             return true;
67.         }
68. //+------------------------------------------------------------------+

Fragmento 03

Observe, que na linha trinta e quatro, temos um array de retorno, que utiliza o tipo string. Isto nos permitirá direcionar as informações colhidas, para um local mais adequado depois. Minha sugestão é que você não tente mexer neste código, antes de realmente entender como ele funciona. Já que não estou fazendo testes de range, a fim de garantir que não teremos tentativas de acesso em uma região invalida da memória. Assim, qualquer coisa fora do normal, fará o código quebrar, já que ele é muito sensível a pequenas mudanças de sua estrutura. Porém, quero chamar a sua atenção, para o laço na linha quarenta e oito. Aqui fazemos o mesmo que estava sendo feita no script, do tópico anterior. No entanto, se você notar, na linha quarenta e três, cada chamada de interação, estaremos reconstruindo a rede perceptron.

Preste atenção a este fato. A rede perceptron criada, somente existirá, dentro desta função. Isto por que, estaremos usando esta rede em um indicador. E como o indicador, não será recolocado no gráfico a cada nova interação, se a rede não fosse destruída, não conseguiríamos forçar o tipo de interação que queremos. Note que no final, isto na linha cinquenta e oito, fazemos aquilo que poderíamos chamar de retro propagação, da mesma forma, que era feita no script. Mas como a interação aqui será constante. A coisa toda, pode ficar, um tanto quanto confusa, isto caso você não entenda, que a rede somente existe dentro deste procedimento visto no fragmento 03. Já que na linha sessenta e dois entramos em um laço que destruirá a rede, aqui construída.

Talvez este sistema mereça um artigo focado apenas na explicação do mesmo. Mas como grande parte do código, foi devidamente explicado no começo deste artigo. Não vejo necessidade de um artigo apenas para explicar o código completo. Mas isto não quer dizer que não devemos explicar com um pouco mais de detalhes, o código do indicador. Isto por conta, de que ele contém algumas partes que podem gerar alguma dúvida. Porém, tais dúvidas, não surgirão em quem já tem familiaridade com a programação MQL5. Por conta disto, a explicação que será dada aqui, visa atender pessoas que estejam começando a programar, e queira entender como o indicador funciona.

Abaixo temos o código na íntegra. Então se você se senti inseguro com relação ao como o código funciona. Preste atenção a explicação.

001. //+------------------------------------------------------------------+
002. #property copyright "Daniel Jose"
003. #property indicator_chart_window
004. #property indicator_plots 0
005. //+------------------------------------------------------------------+
006. #define def_NO_MSG
007. //+------------------------------------------------------------------+
008. #include <Canvas\Canvas.mqh>
009. #include <Neural Network\C_SimplesChain.mqh> 
010. //+------------------------------------------------------------------+
011. input C_Neuron::eFnActivate user00 = C_Neuron::Identity;    //Activation function
012. input uchar   user01 = 2;                                   //Number of neurons
013. input double  user02 = 1e-3;                                //Learning Rate
014. //+------------------------------------------------------------------+
015. CCanvas *canvas;
016. C_SimpleChain *gl_Chain;
017. //+------------------------------------------------------------------+
018. double Train[] {
019.                 10, 20
020.                };
021. //+------------------------------------------------------------------+
022. #define nColumns    2
023. #define nLines      Train.Size() / nColumns
024. //+------------------------------------------------------------------+
025. struct st_00
026. {
027.     int     x,
028.             y,
029.             sizeLine;
030.     uint    height;
031.     double  errBias, errWeight;
032. }global;
033. //+------------------------------------------------------------------+
034. void NewPoint(const char lr, const char ud)
035. {
036. #define macroScale(a, b, c) MathAbs(MathAbs(a/b) > c ? c/a : 1/b)
037. 
038.     double sx, sy;
039.     string SzArr[];
040. 
041.     (*canvas).Erase(ColorToARGB(clrWhite, 255));
042.     (*canvas).LineVertical(global.x, global.y - global.sizeLine, global.y + global.sizeLine, ColorToARGB(clrRoyalBlue, 255));
043.     (*canvas).LineHorizontal(global.x - global.sizeLine, global.x + global.sizeLine, global.y, ColorToARGB(clrRoyalBlue, 255));
044.     
045.     global.errBias += (user02 * lr);
046.     global.errWeight += (user02 * ud);
047. 
048.     if (!(*gl_Chain).Interaction(global.errBias, global.errWeight, SzArr)) return;
049.     
050.     sx = macroScale(global.errBias, user02, global.sizeLine);
051.     sy = macroScale(global.errWeight, user02, global.sizeLine);
052. 
053.     (*canvas).FillCircle(global.x + (int)(global.errBias * sx), global.y - (int)(global.errWeight * sy), 5, ColorToARGB(clrRed, 255));
054.     (*canvas).FillCircle(global.x + (int)(global.errBias * sx), global.y - (int)(global.errWeight * sy), 3, ColorToARGB(clrBlack, 255));
055. 
056.     (*canvas).TextOut(5, global.height * 0, StringFormat("Activation function: %s", EnumToString(user00)), ColorToARGB(clrDarkSlateGray));
057.     (*canvas).TextOut(5, global.height * 2, StringFormat("Bias Error   : %.8f", global.errBias), ColorToARGB(clrMediumBlue));
058.     (*canvas).TextOut(5, global.height * 3, StringFormat("Weight Error : %.8f", global.errWeight), ColorToARGB(clrMediumBlue));
059.     for(uint c = 0, l = 5, m = SzArr.Size(), t = m - 2; c < m; c++, l+=(c == t ? 2 : 1))
060.         (*canvas).TextOut(5, global.height * l, SzArr[c], ColorToARGB(c == t ? clrPurple : clrBlack));
061. 
062.     (*canvas).Update(true);
063. 
064.     ArrayFree(SzArr);
065. 
066. #undef macroScale
067. }
068. //+------------------------------------------------------------------+
069. int OnInit()
070. {
071.     uint w;
072. 
073.     ZeroMemory(global);
074.     ChartSetInteger(0, CHART_SHOW_DATE_SCALE, false);
075.     ChartSetInteger(0, CHART_SHOW_PRICE_SCALE, false);
076.     TextGetSize("W", w, global.height);
077. 
078.     canvas = NULL;
079. 
080.     gl_Chain = new C_SimpleChain(user00, (uchar)(user01 < 1 ? 1 : user01), Train, nColumns);
081. 
082.     return INIT_SUCCEEDED;
083. }
084. //+------------------------------------------------------------------+
085. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
086. {
087.    return rates_total;
088. }
089. //+------------------------------------------------------------------+
090. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
091. {
092.     switch (id)
093.     {
094.         case CHARTEVENT_CHART_CHANGE:
095.             if (canvas != NULL)
096.             {
097.                    (*canvas).Destroy();                
098.                 delete canvas;
099.             }
100.             
101.             canvas = new CCanvas;
102. 
103.             global.x = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0);
104.             global.y = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, 0);
105. 
106.             (*canvas).CreateBitmapLabel("BL", 0, 0, global.x, global.y, COLOR_FORMAT_ARGB_NORMALIZE);
107.             global.x /= 2;
108.             global.y /= 2;
109.             global.sizeLine = MathMin(global.y, global.x) - 10;
110. 
111.             NewPoint(0, 0);
112.             break;
113.         case CHARTEVENT_KEYDOWN:
114.             if (TerminalInfoInteger(TERMINAL_KEYSTATE_LEFT))
115.                 NewPoint(-1, 0);
116.             if (TerminalInfoInteger(TERMINAL_KEYSTATE_RIGHT))
117.                 NewPoint(1, 0);
118.             if (TerminalInfoInteger(TERMINAL_KEYSTATE_UP))
119.                 NewPoint(0, 1);
120.             if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN))
121.                 NewPoint(0, -1);
122.             break;
123.     }
124. }
125. //+------------------------------------------------------------------+
126. void OnDeinit(const int reason)
127. {
128.     delete gl_Chain;
129.    (*canvas).Destroy();
130.     delete canvas;
131.     ChartSetInteger(0, CHART_SHOW_DATE_SCALE, true);
132.     ChartSetInteger(0, CHART_SHOW_PRICE_SCALE, true);
133. }
134. //+------------------------------------------------------------------+

Código 03

Este código quando executado, produzirá o que é visto no vídeo abaixo.


Vídeo 01

Repare como a interação aqui é muito maior que antes, quando estávamos fazendo uso do script. Mas este indicador, não deverá ser utilizado solidamente. Você deve utilizá-lo como um complemento, a fim de conseguir entender o que está acontecendo internamente na rede perceptron.

Pois bem, na linha seis, temos uma definição que já foi explicada em outro artigo nesta sequência. Já na linha dezoito temos o array de treinamento. Um detalhe: você pode mudar os valores neste array, contudo, NÃO MUDE A QUANTIDADE DE VALORES. Caso contrário, terá problemas com este indicador. Isto por que ele não está sendo implementado, para permitir mais de uma entrada na rede que estará sendo construída. Assim como também não está sendo pensado para ter mais de uma saída.

Basicamente tudo que você, seja como usuário, seja como programador iniciante, pode de fato mexer, são nos valores, que aparecem no momento em que você for experimentar este indicador no MetaTrader 5. Veja o vídeo para entender melhor. Fora os detalhes mostrados no vídeo, e voltando a atenção ao código, na linha cento e treze, temos a captura de algumas teclas. Estas informam o quanto deverá ser ajustado o valor de viés e dos pesos. Também temos um evento, na linha noventa e quatro, que ajuda a redimensionar as coisas ao gráfico. Algo simples que acredito que você, mesmo sendo iniciante, conseguirá entender o funcionamento.

Agora veja a linha oitenta, nela criamos a cadeia de perceptrons, formando assim uma pequena rede simples. Neste ponto, fazemos um teste a fim de evitar que o usuário coloque um valor negativo, ou mesmo um valor igual a zero. Já que não faz sentido uma rede com tais valores. Então caso isto ocorra, forçamos a rede a ser construída com pelo menos um perceptron.

Agora podemos ir ao cerne deste indicador, que é justamente o procedimento na linha trinta e quatro. Este é um procedimento bastante direto, sem lenga, lenga. Na linha quarenta e um, limpamos o bitmap da imagem a ser mostrada. As linhas quarenta e dois, e quarenta e três, criam os eixos cartesianos. As linhas quarenta e cinco, e quarenta e seis, ajustam os valores de erro, ou modificação do viés e do peso. Veja que o passo, é dado pelo valor que você pode ajustar ao adicionar o indicador no gráfico. Este seria a tal taxa de aprendizagem. Já na linha quarenta e oito, é onde executamos um ciclo de duas interações com a rede perceptron.

A primeira interação mostra os valores iniciais. Já na segunda interação, vemos os valores já modificados pelo ajuste que estamos fazendo. Isto foi explicado no decorrer deste artigo. Uma vez feito isto, as próximas linhas, que vão da linha cinquenta até a linha sessenta, criarão todas as informações a serem apresentadas. Na linha sessenta e dois, apresentamos a informação no gráfico do ativo. Já na linha sessenta e quatro, devolvemos a memória alocada pela classe C_SimpleChain, para o sistema.

Grande parte deste código, é bem direta. Mas observe a linha cinquenta e nove, onde temos um laço for, pegamos algumas informações, que C_SimpleChain colocou no array de string. Neste ponto podemos apresentar tais informações ao usuário. Desta maneira, a classe C_SimpleChain, não precisará saber o que faremos com tais informações. Podemos lançar estas informações para qualquer local, ou mesmo as converter as mesmas, para ter outros dados que poderão serem usados aqui em outro momento.


Considerações finais

Este artigo, teve como objetivo apresentar de forma bem sucinta e simples, um mecanismo que permitiria a um conjunto discreto de perceptrons, trabalharem em conjunto. Tal mecanismo, poderia a grosso modo, ser chamado de retro propagação. Contudo, ele definitivamente não tem tal objetivo, sendo, na verdade, apenas uma forma interativa de ajuste dos pesos e vieses de todos perceptrons de uma só vez.

O pronto, a ser de fato compreendido aqui, é entender que a simples mudança no código, permitiu que saíssemos daquilo que era a imagem 01 e passamos a ter algo similar ao que é visto na imagem abaixo.

Imagem 11

Esta simples mudança na estrutura, que a princípio pode soar como algo banal, permite que você veja, que podemos implementar qualquer funcionalidade. Desde que saibamos o que estamos fazendo. No próximo artigo veremos como este mecanismo, gerado neste artigo, pode nos ajudar a entender melhor o funcionamento daquilo que de fato seria a retro propagação.
Arquivo MQ5 Descrição
Scripts\Example 01  Demonstração básica
Indicators\Example 01  Demonstração básica

Arquivos anexados |
Do básico ao intermediário: Arquivo template (II) Do básico ao intermediário: Arquivo template (II)
O artigo mostra como aplicar indicadores a gráficos criados por OBJ_CHART usando templates, quando ChartIndicatorAdd não funciona nesse contexto. Explicamos como salvar um template com o indicador, recuperar o ID do gráfico e aplicar ChartApplyTemplate ao destino correto. Você aprenderá a integrar indicadores como recurso, automatizar a configuração do gráfico embutido e entender limitações práticas, como a não listagem de indicadores na janela padrão.
Rede neural na prática: Iniciando a corrente Rede neural na prática: Iniciando a corrente
Chegou a hora de começarmos a ver e entender como ligar perceptrons em uma cadeia. Isto a fim de conseguir implementar o que conhecemos como sendo rede perceptron. Esta rede é a base de tudo aquilo que você conhece e utiliza como sendo algum tipo de "inteligência artificial". Mas construir tal rede envolve diversos desafios. Aqui vamos começar a ver que desafios são estes.
Estratégias de Reversão à Média com RSI2 de Larry Connors para Day Trading Estratégias de Reversão à Média com RSI2 de Larry Connors para Day Trading
Larry Connors é um trader e autor renomado, mais conhecido por seu trabalho em trading quantitativo e estratégias como o RSI de 2 períodos (RSI2), que ajuda a identificar condições de sobrecompra e sobrevenda de curto prazo no mercado. Neste artigo, primeiro explicaremos a motivação por trás de nossa pesquisa, depois recriaremos três das estratégias mais famosas de Connors em MQL5 e as aplicaremos ao trading intradiário do CFD do índice S&P 500.
Do básico ao intermediário: Arquivo template (I) Do básico ao intermediário: Arquivo template (I)
O artigo explica como transferir configurações do gráfico principal para um gráfico embutido em OBJCHART, considerando que apenas parte do template é herdada. Mostramos como sincronizar cores e propriedades, reagir a mudanças de tamanho e aplicar ajustes ao gráfico interno usando seu chart ID e chamadas explícitas de atualização. O leitor aprende a reproduzir a aparência e o comportamento do gráfico principal no OBJCHART de forma previsível.