English Русский 中文 Español Deutsch 日本語 Português 한국어 Français Türkçe
Growing Neural Gas: Implementazione in MQL5

Growing Neural Gas: Implementazione in MQL5

MetaTrader 5Esempi | 16 dicembre 2021, 11:21
74 0
Alexey Subbotin
Alexey Subbotin

Introduzione

Negli anni '90 i ricercatori di artificial neural networks giunsero alla conclusione che era necessario sviluppare una nuova classe di questi meccanismi di calcolo, la cui caratteristica sarebbe l'assenza di una topologia fissa di strati di rete. Ciò significa che il numero e la disposizione dei neuroni artificiali nello spazio delle caratteristiche non è pre-specificato, ma viene calcolato nel processo di apprendimento di tali modelli in conformità con le caratteristiche dei dati di input, adattandosi indipendentemente ad essi.

La ragione dell'emergere di tali idee era una serie di problemi pratici sulla compressione ostacolata e la quantizzazione vettoriale dei parametri di input, come il riconoscimento vocale e delle immagini, la classificazione e il riconoscimento di modelli astratti.

Poiché a quel tempo mappe auto-organizzanti e apprendimento hebbiano erano già noti (in particolare, erano già noti algoritmi che producono topologizzazione di rete, cioè creano un insieme di connessioni tra neuroni, formando uno strato "framework"), e sono stati elaborati approcci per apprendimento competitivo "morbido" (in tali procedure si verifica l'adattamento del peso non solo del neurone "vincitore", ma anche dei suoi "vicini"), il passo logico è stato quello di combinare questi metodi, che è stato fatto nel 1995 da uno scienziato tedesco Bernd Fritzke che ha creato l'ormai popolare algoritmo "Growing neural gas" (, GNG).

Il metodo si rivelò un successo, così che apparvero una serie di sue modifiche; uno di questi era l'adattamento per l'apprendimento supervisionato (Supervised-GNG). Come notato dall'autore, S-GNG ha mostrato un'efficienza significativamente maggiore nella classificazione dei dati rispetto, ad esempio, a una rete di funzioni di base radiali, grazie alla capacità di ottimizzare la topologia nelle aree dello spazio di input che sono difficili da classificare. Senza dubbio, GNG è superiore al clustering "K-means".

È interessante notare che nel 2001 Fritzke ha concluso la sua carriera di scienziato presso l'università della Ruhr (Bochum, Germania) dopo aver ricevuto un'offerta di lavoro presso la borsa tedesca (Deutsche Bӧrse). Bene, questo fatto è stato un altro motivo per selezionare il suo algoritmo come base per scrivere questo articolo.

1. Growing Neural Gas

Quindi, GNG è un algoritmo che consente di implementare clustering adattivo dei dati di input, cioè non solo dividere lo spazio in cluster, ma anche determinare il loro numero richiesto in base alle caratteristiche dei dati.

Partendo da soli due neuroni, l'algoritmo cambia costantemente (per lo più aumenta) il numero di essi, creando al contempo un insieme di connessioni tra neuroni che meglio corrisponde alla distribuzione dei vettori di input, utilizzando l'approccio dell'apprendimento Hebbiano competitivo. Ogni neurone ha una variabile interna che accumula il cosiddetto "errore locale". Le connessioni tra i nodi sono caratterizzate da una variabile chiamata "età".

Lo pseudo codice GNG è simile al 19:

  1. Inizializzazione: Creare due nodi con i vettori di pesi, consentiti dalla distribuzione dei vettori di input, e valori zero di errori locali; connettere i nodi impostandone l'età su 0.
  2. Inserisci un vettore in una rete neurale.
  3. Trovare due neuroni e più vicini a , cioè nodi con vettore di peso e tale che è minimo, ed è secondo valore minimo di distanza tra tutti i nodi.
  4. Aggiorna l'errore locale del neurone vincitore aggiungendo ad esso la distanza quadrata tra i vettori e:


  5. Spostare il neurone vincitore e tutti i suoi vicini topologici (cioè tutti i neuroni che hanno una connessione con il vincitore) nella direzione del vettore di input per distanze uguali alle quote e da una piena.


  6. Aumenta di 1 l'età di tutte le connessioni in uscita dal vincitore.
  7. Se i due neuroni migliori e sono collegati, impostare l'età della loro connessione a zero. Altrimenti crea una connessione tra di loro.
  8. Rimuovere le connessioni con un'età maggiore di . Se questo si traduce in neuroni che non hanno più bordi emananti, rimuovi anche quei neuroni.
  9. Se il numero dell'iterazione corrente è un multiplo di , e la dimensione limite della rete non è stata raggiunta, inserire un nuovo neurone come segue:

    • Determinare un neurone  con un errore locale più grande.
    • Determinare tra i vicini del neurone con un errore massimo.
    • Creare un nodo "al centro" tra e :

    • Sostituite lo spigolo tra e con lo spigolo tra e , e .
    • Diminuire gli errori dei neuroni e , impostare il valore dell'errore di neurone .

  10. Diminuire gli errori di tutti i neuroni per la frazione .

  11. Se un criterio di arresto non è ancora soddisfatto, continuare con la fase 2.

Consideriamo come il growing neural gas si adatta alle caratteristiche dello spazio di input.

Prima di tutto, presta attenzione all'aumento della variabile di errore del vincitore alla fase 4. Questa procedura porta al fatto che i nodi che vincono più spesso, cioè quelli nelle vicinanze di cui appare il maggior numero di segnali di ingresso, hanno l'errore maggiore, e quindi queste aree sono i primi candidati alla "compattazione" aggiungendo nuovi nodi.

Lo spostamento dei nodi nella direzione del vettore di ingresso nel passaggio 5 significa che il vincitore cerca di "mediare" la sua posizione tra i segnali di ingresso situati nelle sue vicinanze. In questo caso, il miglior neurone "tira" i suoi vicini nella direzione del segnale ( viene scelto come regola).

Spiego l'operazione con i bordi tra i neuroni nelle fasi 6-8. Il significato di invecchiamento e rimozione di vecchie connessioni è che la topologia della rete dovrebbe essere massimamente vicina alla cosiddetta triangolazione di Delaunay, cioè una triangolazione (suddivisione in triangoli) di neuroni in cui, tra l'altro, l'angolo minimo di tutti gli angoli dei triangoli nella triangolazione è massimizzato (evitando triangoli "magri").

In parole povere, la triangolazione di Delaunay corrisponde alla più "bella", nel senso di massima entropia, topologizzazione dello strato. Va notato che la struttura topologica è richiesta non come unità separata, ma quando viene utilizzata per determinare la posizione di nuovi nodi quando vengono inseriti nel passaggio 8 - si trovano sempre nel mezzo di un bordo.

Il passo p è una correzione delle variabili di errore di tutti i neuroni nello strato. Questo per garantire che la rete "dimentichi" i vecchi vettori di input e risponda meglio a quelli nuovi. In questo modo, abbiamo la possibilità di utilizzare il growing neural gas per l'adattamento delle reti neurali per le time-dependent, vale a dire, distribuzioni lentamente alla deriva dei segnali di entrata. Questo, tuttavia, non gli dà la possibilità di tenere traccia dei rapidi cambiamenti nelle caratteristiche degli input (vedi maggiori dettagli di seguito nella sezione in cui vengono discussi gli svantaggi dell'algoritmo).

Forse dovremmo considerare separatamente il criterio di arresto. L'algoritmo lascia il posto alla fantasia degli sviluppatori di sistemi di analisi. Le opzioni possibili sono: verificare l'efficienza della rete sul set di test, analizzare la dinamica dell'errore medio dei neuroni, limitare la complessità della rete, ecc.

A scopo informativo lavoreremo con l'opzione più semplice - perché lo scopo di questo articolo è quello di dimostrare non solo l'algoritmo stesso, ma le possibilità della sua implementazione per mezzo di MQL5; continueremo l'apprendimento del livello fino a quando non esauriremo gli input (naturalmente il loro numero è predefinito).

2. Selezione del Metodo per Organizzare i Dati

Quando si programma l'algoritmo dovremo ovviamente fare i conti con la necessità di memorizzare quelli che vengono chiamati "set". Avremo due insiemi: un insieme di neuroni e un insieme di bordi tra di loro. Mentre entrambe le strutture si evolveranno nel corso del programma (e stiamo pianificando sia di aggiungere che di rimuovere elementi), dovremmo anche fornire meccanismi per questo.

Naturalmente, potremmo provare a utilizzare array dinamici di oggetti, ma dovremmo eseguire numerose operazioni di copia-spostamento dei dati, che essenzialmente rallenterebbero il programma. Un'opzione più adatta per lavorare con le astrazioni con le proprietà specificate sono i grafici del programma e la loro versione più semplice: un elenco collegato.

Ricorderò ai nostri lettori il principio di funzionamento della lista collegata (Fig. 1). Gli oggetti della classe base contengono un puntatore allo stesso oggetto di uno dei membri, che consente di combinarli in strutture lineari, indipendentemente dall'ordine fisico degli oggetti in memoria. Inoltre, c'è la classe "carriage", che incapsula la procedura di spostamento attraverso l'elenco, l'aggiunta, l'inserimento e l'eliminazione di nodi, la ricerca, il confronto e l'ordinamento e, se necessario, altre procedure.


Figura 1. Rappresentazione schematica dell'organizzazione di liste lineari collegate

Specialisti di MetaQuotes Software Corp. hanno già implementato elenchi collegati degli oggetti della classe CObject in una libreria standard. Il codice di programma corrispondente si trova nel file di intestazione List.mqh, che si trova in MQL5\Include\Arrays del pacchetto di consegna standard di MetaTrader 5.

Non reinventeremo la ruota e ci fideremo della qualifica dei programmatori rispettati di MetaQuotes, prendendo le classi CObject e CList come base delle nostre strutture di dati. Qui useremo uno dei pilastri dell'approccio orientato agli oggetti: il meccanismo di ereditarietà.

3. Programmazione del Modello

Per prima cosa definiamo la forma software del concetto di "neurone artificiale".

Una delle regole di etichetta quando si sviluppano applicazioni OOP è quella di iniziare sempre a programmare con le strutture di dati più comuni. Anche quando stai scrivendo solo per te stesso, ma soprattutto se si presume che i codici saranno disponibili per altri programmatori, dovresti tenere presente il fatto che in futuro gli sviluppatori possono avere idee diverse per lo sviluppo e la modifica della logica del programma; e non puoi sapere in anticipo in quale luogo verranno apportate le modifiche.

Il principio dell'OOP implica che altri sviluppatori non dovranno esaminare le tue classi, ma dovrebbero essere in grado di ereditare strutture di dati dai dati disponibili nel giusto posto della gerarchia. Pertanto, la prima classe scritta dovrebbe essere il più astratta possibile, e le specifiche dovrebbero essere aggiunte ai livelli inferiori, quando si è più vicini "alla terra peccaminosa".

Quando applicato al nostro problema, questo significa che iniziamo a scrivere un programma con la definizione della classe CCustomNeuron ("una sorta di neurone"), che, come tutti i neuroni artificiali avrà un certo numero di sinapsi (pesi di input) e il valore di output. Sarà in grado di inizializzare (assegnare valori ai pesi), calcolare il valore del segnale alla sua uscita e persino adattare i suoi pesi di un valore specificato.

Difficilmente possiamo ottenere più astrazione (tenendo conto del fatto che ereditiamo la nostra classe da un CObject massimamente generalizzato ) - tutti i neuroni devono essere in grado di eseguire le azioni specificate.

Per descrivere i dati creare un file di intestazione Neurons.mqh, inserendolo nella cartella Include\GNG.

//+------------------------------------------------------------------+
//| a base class to introduce object-neurons                |
//+------------------------------------------------------------------+
class CCustomNeuron:public CObject
  {
protected:
   int               m_synapses;
   double            m_weights[];
public:
   double            NET;
                     CCustomNeuron();
                    ~CCustomNeuron(){};
   void              ZeroInit(int synapses);
   int               Synapses();
   void              Init(double &weights[]);
   void              Weights(double &weights[]);
   void              AdaptWeights(double &delta[]);
   virtual void       ProcessVector(double &in[]) {return;}
   virtual int        Type() const          { return(TYPE_CUSTOM_NEURON);}
  };
//+------------------------------------------------------------------+
//| constructor                                                      |
//+------------------------------------------------------------------+
void CCustomNeuron::CCustomNeuron()
  {
   m_synapses=0;
   NET=0;
  }
//+------------------------------------------------------------------+
//| returns the dimension of the input vector of a neuron            |
//| INPUT: no                                                        |
//| OUTPUT: number of "synapses" of the neuron                       |
//+------------------------------------------------------------------+
int CCustomNeuron::Synapses()
  {
   return m_synapses;
  }
//+------------------------------------------------------------------+
//| initializing neuron with a zero vector of weights.               |
//| INPUT: synapses - number of synapses (input weights)             |
//| OUTPUT: no                                                       |
//+------------------------------------------------------------------+
void CCustomNeuron::ZeroInit(int synapses)
  {
   if(synapses<1) return;
   m_synapses=synapses;
   ArrayResize(m_weights,m_synapses);
   ArrayInitialize(m_weights,0);
   NET=0;
  }
//+------------------------------------------------------------------+
//| initializing neuron weights with a set vector.                   |
//| INPUT: weights - data vector                                     |
//| OUTPUT: no                                                       |
//+------------------------------------------------------------------+
void CCustomNeuron::Init(double &weights[])
  {
   if(ArraySize(weights)<1) return;
   m_synapses=ArraySize(weights);
   ArrayResize(m_weights,m_synapses);
   ArrayCopy(m_weights,weights);
   NET=0;
  }
//+------------------------------------------------------------------+
//| obtaining vector of neuron weights.                              |
//| INPUT: no                                                        |
//| OUTPUT: weights - result                                         |                        
//+------------------------------------------------------------------+
void CCustomNeuron::Weights(double &weights[])
  {
   ArrayResize(weights,m_synapses);
   ArrayCopy(weights,m_weights);
  }
//+------------------------------------------------------------------+
//| change weights of the neuron by a specified value                |
//| INPUT: delta - correcting vector                                 |
//| OUTPUT: no                                                       |
//+------------------------------------------------------------------+
void CCustomNeuron::AdaptWeights(double &delta[])
  {
   if(ArraySize(delta)!=m_synapses) return;
   for(int i=0;i<m_synapses;i++) m_weights[i]+=delta[i];
   NET=0;
  }

Le funzioni definite nella classe sono molto semplici, quindi non è necessario includere qui le loro descrizioni dettagliate. Si noti che abbiamo definito la funzione di elaborazione dei dati di input ProcessVector(double &in[]) (il valore di output qui è calcolato come di un normale percettore) con il modificatore virtuale.

Ciò significa che nel caso in cui il metodo venga ridefinito da classi derivate, verrà scelta la procedura appropriata in base alla classe di oggetti effettiva in fase di esecuzione in fase di esecuzione, il che aumenta la sua flessibilità, anche nel senso di interazione dell'utente, e riduce i costi di manodopera per la programmazione.

Nonostante il fatto che apparentemente non abbiamo fatto nulla per organizzare i neuroni in una lista collegata, in realtà è già successo nel momento in cui abbiamo sottolineato che la nuova classe eredita da CObject. Quindi, ora i membri privati della nostra classe sono m_first_node, m_curr_node e m_last_node , che sono del tipo e del punto, "puntatore a CObject", rispettivamente, al primo, attuale e all'ultimo elemento della lista. Abbiamo anche tutte le funzioni necessarie per navigare nell'elenco.

Ora è il momento di delineare le differenze di un neurone dello strato GNG dagli altri colleghi definendo la classe CGNGNeuron:

//+------------------------------------------------------------------+
//| a separate neuron of the GNG network                             |
//+------------------------------------------------------------------+
class CGNGNeuron:public CCustomNeuron
  {
public:
   int               uid;
   double            E;
   double            U;
   double            error;
                    CGNGNeuron();
   virtual void      ProcessVector(double &in[]);
  };
//+------------------------------------------------------------------+
//| constructor                                                      |
//+------------------------------------------------------------------+
CGNGNeuron::CGNGNeuron()
  {
   E=0;
   U=0;
   error=0;
  }
//+------------------------------------------------------------------+
//| calculating "distance" from the neuron to the input vector       |
//| INPUT: in - data vector                                          |
//| OUTPUT: no                                                       |
//| REMARK: the current "distance" is placed in the error variable,  |
//|         "local error" is contained in another variable,          |
//|         which is called E                                        |
//+------------------------------------------------------------------+
void CGNGNeuron::ProcessVector(double &in[])
  {
   if(ArraySize(in)!=m_synapses) return;

   error=0;
   NET=0;
   for(int i=0;i<m_synapses;i++)
     {
      error+=(in[i]-m_weights[i])*(in[i]-m_weights[i]);
     }
  }

Quindi, come puoi vedere, queste differenze sono in presenza di campi:

  • error– il quadrato corrente di distanza dal vettore di input al vettore dei pesi neuronali,
  • E – una variabile che accumula l'errore locale e un ID univoco,
  • uid – è necessario per permetterci di unire ulteriormente i neuroni con connessioni in coppie (la semplice indicizzazione esistente nella classe CList non è sufficiente, perché dovremo aggiungere ed eliminare neuroni, il che porterà a confusione nella numerazione).

La funzione ProcessVector(...) è cambiata, ora calcola il valore del campo di error.

Non prestare attenzione al campo U finora, il suo significato sarà spiegato più avanti nella sezione "Modifica dell'algoritmo".

Il passo successivo è scrivere una classe che rappresenti una connessione tra due neuroni.

//+------------------------------------------------------------------+
//| class defining connection (edge) between two neurons             |
//+------------------------------------------------------------------+
class CGNGConnection:public CObject
  {
public:
   int               uid1;
   int               uid2;
   int               age;
                     CGNGConnection();
   virtual int       Type() const          { return(TYPE_GNG_CONNECTION);}
  };
//+------------------------------------------------------------------+
//| constructor                                                      |
//+------------------------------------------------------------------+
CGNGConnection::CGNGConnection()
  {
   age=0;
  }

Non c'è nulla di difficile qui: un bordo ha due estremità (neuroni specificati dagli identificatori uid1 e uid2) e un'età inizialmente uguale a zero.

Ora lavoreremo con classi "carrelli" di elenchi collegati, che conterranno le possibilità necessarie per implementare l'algoritmo GNG.

Prima di tutto ereditare una classe di neuroni da CList:

//+------------------------------------------------------------------+
//| linked list of neurons                                           |
//+------------------------------------------------------------------+
class CGNGNeuronList:public CList
  {
public:
   //--- constructor   
                     CGNGNeuronList() {MathSrand(TimeLocal());}
   CGNGNeuron       *Append();
   void              Init(double &v1[],double &v2[]);
   CGNGNeuron       *Find(int uid);
   void              FindWinners(CGNGNeuron *&Winner,CGNGNeuron *&SecondWinner);
  };
//+------------------------------------------------------------------+
//| adds an "empty" neuron at the end of the list                    |
//| INPUT: no                                                        |
//| OUTPUT: pointer at a new neuron                                  |
//+------------------------------------------------------------------+
CGNGNeuron *CGNGNeuronList::Append()
  {
   if(m_first_node==NULL)
     {
      m_first_node= new CGNGNeuron;
      m_last_node = m_first_node;
     }
   else
     {
      GetLastNode();
      m_last_node=new CGNGNeuron;
      m_curr_node.Next(m_last_node);
      m_last_node.Prev(m_curr_node);
     }
   m_curr_node=m_last_node;
   m_curr_idx=m_data_total++;

   while(true)
     {
      int rnd=MathRand();
      if(!CheckPointer(Find(rnd)))
        {
         ((CGNGNeuron *)m_curr_node).uid=rnd;
         break;
        }
     }
//---
   return(m_curr_node);
  }
//+------------------------------------------------------------------+
//| initializing list by way of creating two neurons set             |
//| by vectors of weights                                            |
//| INPUT: v1,v2 - vectors of weights                                |
//| OUTPUT: no                                                       |
//+------------------------------------------------------------------+
void CGNGNeuronList::Init(double &v1[],double &v2[])
  {
   Clear();
   Append();
   ((CGNGNeuron *)m_curr_node).Init(v1);
   Append();
   ((CGNGNeuron *)m_curr_node).Init(v2);
  }
//+------------------------------------------------------------------+
//| search for a neuron by uid                                       |
//| INPUT: uid - a unique ID of the neuron                           |
//| OUTPUT: pointer at the neuron if successful, otherwise NULL      |
//+------------------------------------------------------------------+
CGNGNeuron *CGNGNeuronList::Find(int uid)
  {
   if(!GetFirstNode()) return(NULL);
   do
     {
      if(((CGNGNeuron *)m_curr_node).uid==uid)
         return(m_curr_node);
     }
   while(CheckPointer(GetNextNode()));
   return(NULL);
  }
//+------------------------------------------------------------------+
//| search for two "best" neurons in terms of minimal current error  |
//| INPUT: no                                                        |
//| OUTPUT: Winner - neuron "closest" to the input vector            |
//|         SecondWinner - second "closest" neuron                   |
//+------------------------------------------------------------------+
void CGNGNeuronList::FindWinners(CGNGNeuron *&Winner,CGNGNeuron *&SecondWinner)
  {
   double err_min=0;
   Winner=NULL;
   if(!CheckPointer(GetFirstNode())) return;
   do
     {
      if(!CheckPointer(Winner) || ((CGNGNeuron *)m_curr_node).error<err_min)
        {
         err_min= ((CGNGNeuron *)m_curr_node).error;
         Winner = m_curr_node;
        }
     }
   while(CheckPointer(GetNextNode()));

   err_min=0;
   SecondWinner=NULL;
   GetFirstNode();
   do
     {
      if(m_curr_node!=Winner)
         if(!CheckPointer(SecondWinner) || ((CGNGNeuron *)m_curr_node).error<err_min)
           {
            err_min=((CGNGNeuron *)m_curr_node).error;
            SecondWinner=m_curr_node;
           }
     }
   while(CheckPointer(GetNextNode()));
   m_curr_node=Winner;
  }

Nel costruttore della classe viene inizializzato un generatore di numeri pseudocasuali: verrà utilizzato per assegnare gli elementi degli identificatori univoci dell'elenco.

Chiariamo il significato dei metodi di classe:

  • Il metodo Append() è un'aggiunta alla funzionalità della classe CList. Quando lo si chiama, un nodo viene aggiunto alla fine dell'elenco o il primo nodo viene creato se la lussuria è vuota.
  • La funzione Init(double &v1[],double &v2[]) deve il suo aspetto all'algoritmo GNG. Ricorda, la crescita della rete inizia con due neuroni, quindi questa firma sarebbe più conveniente per noi. Nel corpo della funzione, quando si utilizzano ID m_curr_node, m_first_node,m_last_node è necessario convertire esplicitamente quindi in tipo CGNGNeuron *, se vogliamo utilizzare la funzionalità di questa classe (le variabili specificate sono state ereditate da CList, quindi nominalmente puntano a CObject).
  • La funzione Find(int uid), poiché deriva dal suo nome, cerca un neurone in base al suo ID e restituisce un puntatore all'elemento trovato o NULL se non riesce a trovarlo.
  • FindWinners(CGNGNeuron *&Winner,CGNGNeuron *&SecondWinner) – anche parte dell'algoritmo. Dovremo cercare un vincitore nell'elenco dei neuroni, e quello accanto ad esso in termini di vicinanza al vettore di input, questo è ciò per cui usiamo questa funzione. Si noti che i parametri vengono passati a questa funzione per riferimento in modo che ulteriormente possiamo scrivere lì i valori restituiti (*& significa "riferimento a un puntatore" - questa è una sintassi corretta, quella inversa &* significa "puntatore a un riferimento" che è vietato: il compilatore genererà un errore in questo caso).

La classe successiva è un elenco di connessioni tra neuroni.

//+------------------------------------------------------------------+
//| a linked list of connections between neurons                     |
//+------------------------------------------------------------------+
class CGNGConnectionList:public CList
  {
public:
   CGNGConnection   *Append();
   void              Init(int uid1,int uid2);
   CGNGConnection   *Find(int uid1,int uid2);
   CGNGConnection   *FindFirstConnection(int uid);
   CGNGConnection   *FindNextConnection(int uid);
  };
//+------------------------------------------------------------------+
//| adds an "empty" connection at the end of the list                |
//| INPUT: no                                                        |
//| OUTPUT: pointer at a new binding                                 |
//+------------------------------------------------------------------+
CGNGConnection *CGNGConnectionList::Append()
  {
   if(m_first_node==NULL)
     {
      m_first_node= new CGNGConnection;
      m_last_node = m_first_node;
     }
   else
     {
      GetLastNode();
      m_last_node=new CGNGConnection;
      m_curr_node.Next(m_last_node);
      m_last_node.Prev(m_curr_node);
     }
   m_curr_node=m_last_node;
   m_curr_idx=m_data_total++;
//---
   return(m_curr_node);
  }
//+------------------------------------------------------------------+
//| initialize the list by creating one connection                   |
//| INPUT: uid1,uid2 - IDs of neurons for the connection             |
//| OUTPUT: no                                                       |
//+------------------------------------------------------------------+
void CGNGConnectionList::Init(int uid1,int uid2)
  {
   Append();
   ((CGNGConnection *)m_first_node).uid1 = uid1;
   ((CGNGConnection *)m_first_node).uid2 = uid2;
   m_last_node = m_first_node;
   m_curr_node = m_first_node;
   m_curr_idx=0;
  }
//+------------------------------------------------------------------+
//| check if there is connection between the set neurons             |
//| INPUT: uid1,uid2 - IDs of the neurons                            |
//| OUTPUT: pointer at the connection if there is one, or NULL       |
//+------------------------------------------------------------------+
CGNGConnection *CGNGConnectionList::Find(int uid1,int uid2)
  {
   if(!CheckPointer(GetFirstNode())) return(NULL);
   do
     {
      if((((CGNGConnection *)m_curr_node).uid1==uid1 && ((CGNGConnection *)m_curr_node).uid2==uid2)
         ||(((CGNGConnection *)m_curr_node).uid1==uid2 && ((CGNGConnection *)m_curr_node).uid2==uid1))
         return(m_curr_node);
     }
   while(CheckPointer(GetNextNode()));
   return(NULL);
  }
//+------------------------------------------------------------------+
//| search for the first topological neighbor of the set neuron      |
//| starting with the first element of the list                      |
//| INPUT: uid - ID of the neuron                                    |
//| OUTPUT: pointer at the connection if there is one, or NULL       |
//+------------------------------------------------------------------+
CGNGConnection *CGNGConnectionList::FindFirstConnection(int uid)
  {
   if(!CheckPointer(GetFirstNode())) return(NULL);
   while(true)
     {
      if(((CGNGConnection *)m_curr_node).uid1==uid || ((CGNGConnection *)m_curr_node).uid2==uid) break;
      if(!CheckPointer(GetNextNode())) return(NULL);
     }
   return(m_curr_node);
  }
//+------------------------------------------------------------------+
//| search for the first topological neighbor of the set neuron      |
//| starting with the list element next to the current one           |
//| INPUT: uid - ID of the neuron                                    |
//| OUTPUT: pointer at the connection if there is one, or NULL       |
//+------------------------------------------------------------------+
CGNGConnection   *CGNGConnectionList::FindNextConnection(int uid)
  {
   if(!CheckPointer(GetCurrentNode())) return(NULL);
   while(true)
     {
      if(!CheckPointer(GetNextNode())) return(NULL);
      if(((CGNGConnection *)m_curr_node).uid1==uid || ((CGNGConnection *)m_curr_node).uid2==uid) break;
     }
   return(m_curr_node);
  }

Metodi definiti della classe:

  • Append(). L'implementazione di questo metodo è simile a quella descritta nella classe precedente, tranne il tipo restituito (sfortunatamente, non ci sono modelli di classe in MQL5, quindi dobbiamo scrivere queste cose ogni volta).
  • Init(int uid1,int uid2) – l'algoritmo GNG richiede l'inizializzazione di una connessione all'inizio, che viene eseguita in questa funzione.
  • La funzione Find(int uid1,int uid2) è chiara.
  • La differenza tra i metodi FindFirstConnection(int uid) e FindNextConnection(int uid) è che il primo sta cercando una connessione con un vicino a partire dall'inizio dell'elenco, mentre il secondo inizia con il nodo accanto al corrente (m_curr_node).

Qui la descrizione delle strutture di dati è finita. È tempo di iniziare a programmare il nostro algoritmo.

4. La Classe dell'Algoritmo

Creare un nuovo file di intestazione GNG.mqh, inserirlo nella cartella Include\GNG.

//+------------------------------------------------------------------+
//|                                                          GNG.mqh |
//|                                             Copyright 2010, alsu |
//|                                                 alsufx@gmail.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2010, alsu"
#property link      "alsufx@gmail.com"

#include "Neurons.mqh"
//+------------------------------------------------------------------+
//| the main class representing the GNG algorithm                    |
//+------------------------------------------------------------------+
class CGNGAlgorithm
  {
public:
   //--- linked lists of object-neurons and connection between them
   CGNGNeuronList   *Neurons;
   CGNGConnectionList *Connections;
   //--- parameters of the algorithm
   int               input_dimension;
   int               iteration_number;
   int               lambda;
   int               age_max;
   double            alpha;
   double            beta;
   double            eps_w;
   double            eps_n;
   int               max_nodes;

                     CGNGAlgorithm();
                    ~CGNGAlgorithm();
   virtual void      Init(int __input_dimension,
                          double &v1[],
                          double &v2[],
                          int __lambda,
                          int __age_max,
                          double __alpha,
                          double __beta,
                          double __eps_w,
                          double __eps_n,
                          int __max_nodes);
   virtual bool      ProcessVector(double &in[],bool train=true);
   virtual bool      StoppingCriterion();
  };
//+------------------------------------------------------------------+
//| constructor                                                      |
//+------------------------------------------------------------------+
CGNGAlgorithm::CGNGAlgorithm(void)
  {
   Neurons=new CGNGNeuronList();
   Connections=new CGNGConnectionList();
   
   Neurons.FreeMode(true);
   Connections.FreeMode(true);
  }
//+------------------------------------------------------------------+
//| destructor                                                       |
//+------------------------------------------------------------------+
CGNGAlgorithm::~CGNGAlgorithm(void)
  {
   delete Neurons;
   delete Connections;
  }
//+------------------------------------------------------------------+
//| initializes the algorithm using two vectors of input data        |
//| INPUT: v1,v2 - input vectors                                     |
//|        __lambda - number of iterations after which a new         |
//|        neuron is inserted                                        |
//|        __age_max - maximum age of connection                     |
//|        __alpha, __beta - used for adapting errors                |
//|        __eps_w, __eps_n - used for adapting weights              |
//|        __max_nodes - limit on the network size                   |
//| OUTPUT: no                                                       |
//+------------------------------------------------------------------+
void CGNGAlgorithm::Init(int __input_dimension,
                         double &v1[],
                         double &v2[],
                         int __lambda,
                         int __age_max,
                         double __alpha,
                         double __beta,
                         double __eps_w,
                         double __eps_n,
                         int __max_nodes)
  {
   iteration_number=0;
   input_dimension=__input_dimension;
   lambda=__lambda;
   age_max=__age_max;
   alpha= __alpha;
   beta = __beta;
   eps_w = __eps_w;
   eps_n = __eps_n;
   max_nodes=__max_nodes;
   Neurons.Init(v1,v2);

   CGNGNeuron *tmp;
   tmp=Neurons.GetFirstNode();
   int uid1=tmp.uid;
   tmp=Neurons.GetLastNode();
   int uid2=tmp.uid;

   Connections.Init(uid1,uid2);
  }
//+------------------------------------------------------------------+
//| the main function of the algorithm                               |
//| INPUT: in - vector of input data                                 |
//|        train - if true, start learning, otherwise                |
//|        only calculate the input values of neurons                |
//| OUTPUT: true, if stop condition is fulfilled, otherwise false    |
//+------------------------------------------------------------------+
bool CGNGAlgorithm::ProcessVector(double &in[],bool train=true)
  {
   if(ArraySize(in)!=input_dimension) return(StoppingCriterion());

   int i;

   CGNGNeuron *tmp=Neurons.GetFirstNode();
   while(CheckPointer(tmp))
     {
      tmp.ProcessVector(in);
      tmp=Neurons.GetNextNode();
     }

   if(!train) return(false);

   iteration_number++;
//--- Find two neurons closest to in[], i.e. the nodes with weight vectors 
//--- Ws and Wt, so that ||Ws-in||^2 is minimal and ||Wt-in||^2 -    
//--- is second minimal value of distance of all the nodes.        
//--- Under ||*|| we mean Euclidean norm                
   CGNGNeuron *Winner,*SecondWinner;
   Neurons.FindWinners(Winner,SecondWinner);

//--- Update the local error of the winner                     
   Winner.E+=Winner.error;

//--- Shift the winner and all its topological neighbors (i.e.
//--- all neurons connected with the winner) in the direction of the input
//--- vector by distances equal to fractions eps_w and eps_n of the full.    
   double delta[],weights[];

   Winner.Weights(weights);
   ArrayResize(delta,input_dimension);

   for(i=0;i<input_dimension;i++) delta[i]=eps_w*(in[i]-weights[i]);
   Winner.AdaptWeights(delta);

//--- Increment the age of all connections emanating from the winner by 1. 
   CGNGConnection *tmpc=Connections.FindFirstConnection(Winner.uid);
   while(CheckPointer(tmpc))
     {
      if(tmpc.uid1==Winner.uid) tmp = Neurons.Find(tmpc.uid2);
      if(tmpc.uid2==Winner.uid) tmp = Neurons.Find(tmpc.uid1);

      tmp.Weights(weights);
      for(i=0;i<input_dimension;i++) delta[i]=eps_n*(in[i]-weights[i]);
      tmp.AdaptWeights(delta);

      tmpc.age++;

      tmpc=Connections.FindNextConnection(Winner.uid);
     }

//--- If two best neurons are connected, reset the age of the connection.    
//--- Otherwise create a connection between them.                     
   tmpc=Connections.Find(Winner.uid,SecondWinner.uid);
   if(tmpc) tmpc.age=0;
   else
     {
      Connections.Append();
      tmpc=Connections.GetLastNode();
      tmpc.uid1 = Winner.uid;
      tmpc.uid2 = SecondWinner.uid;
      tmpc.age=0;
     }

//--- Delete all the connections with an age larger than age_max.       
//--- If this results in neurons having no connections with other    
//--- nodes, remove those neurons.                                     
   tmpc=Connections.GetFirstNode();
   while(CheckPointer(tmpc))
     {
      if(tmpc.age>age_max)
        {
         Connections.DeleteCurrent();
         tmpc=Connections.GetCurrentNode();
        }
      else tmpc=Connections.GetNextNode();
     }

   tmp=Neurons.GetFirstNode();
   while(CheckPointer(tmp))
     {
      if(!Connections.FindFirstConnection(tmp.uid))
        {
         Neurons.DeleteCurrent();
         tmp=Neurons.GetCurrentNode();
        }
      else tmp=Neurons.GetNextNode();
     }

//--- If the number of the current iteration is multiple of lambda, and the network   
//--- hasn't been reached yet, create a new neuron r according to the following rules  
   CGNGNeuron *u,*v;
   if(iteration_number%lambda==0 && Neurons.Total()<max_nodes)
     {
      //--- 1.Find neuron u with the maximum local error.               
      tmp=Neurons.GetFirstNode();
      u=tmp;
      while(CheckPointer(tmp=Neurons.GetNextNode()))
        {
         if(tmp.E>u.E)
            u=tmp;
        }

      //--- 2.determin among the neighbors of u the node u with the maximum local error. 
      tmpc=Connections.FindFirstConnection(u.uid);
      if(tmpc.uid1==u.uid) v=Neurons.Find(tmpc.uid2);
      else v=Neurons.Find(tmpc.uid1);
      while(CheckPointer(tmpc=Connections.FindNextConnection(u.uid)))
        {
         if(tmpc.uid1==u.uid) tmp=Neurons.Find(tmpc.uid2);
         else tmp=Neurons.Find(tmpc.uid1);
         if(tmp.E>v.E)
            v=tmp;
        }

      //--- 3.Create a node r "in the middle" between u and v.                      
      double wr[],wu[],wv[];

      u.Weights(wu);
      v.Weights(wv);
      ArrayResize(wr,input_dimension);
      for(i=0;i<input_dimension;i++) wr[i]=(wu[i]+wv[i])/2;

      CGNGNeuron *r=Neurons.Append();
      r.Init(wr);
      //--- 4.Replace the connection between u and v by a connection between u and r, v and r       
      tmpc=Connections.Append();
      tmpc.uid1=u.uid;
      tmpc.uid2=r.uid;

      tmpc=Connections.Append();
      tmpc.uid1=v.uid;
      tmpc.uid2=r.uid;

      Connections.Find(u.uid,v.uid);
      Connections.DeleteCurrent();

      //--- 5.Decrease the errors of neurons u and v, set the value of the error of  
      //---   neuron r the same as of u.                                 

      u.E*=alpha;
      v.E*=alpha;
      r.E = u.E;
     }

//--- Decrease the errors of all neurons by the fraction beta                     
   tmp=Neurons.GetFirstNode();
   while(CheckPointer(tmp))
     {
      tmp.E*=(1-beta);
      tmp=Neurons.GetNextNode();
     }

//--- Check the stopping criterion                                      
   return(StoppingCriterion());
  }
//+------------------------------------------------------------------+
//| Stopping criterion. In this version of file makes no             |
//| actions, always returns false.                                   |
//| INPUT: no                                                        |
//| OUTPUT: true, if the criterion is fulfilled, otherwise false     |
//+------------------------------------------------------------------+
bool CGNGAlgorithm::StoppingCriterion()
  {
   return(false);
  }

La classe CGNGAlgorithm ha due campi importanti: puntatori alle liste collegate di neuroni Neuronse connessioni tra loro Connessioni. Saranno il mezzo fisico della struttura della nostra rete neurale. I campi rimanenti sono i parametri dell'algoritmo definiti dall'esterno.

Tra i metodi di classe ausiliarie sceglierei Init(...) che passa i parametri esterni a un'istanza dell'algoritmo e inizializza le strutture dei dati e il criterio di arresto StoppingCriterion() che, come abbiamo concordato in precedenza, non fa nulla, restituendo semprefalse.

La funzione ProcessVector(...) che è la funzione principale dell'algoritmo che elabora il vettore di dati specificato, non contiene sottigliezze: abbiamo organizzato i dati e i metodi di lavoro con loro in modo che quando si tratta dell'algoritmo, abbiamo solo bisogno di passare meccanicamente attraverso tutti i suoi passaggi. La loro posizione nel codice è indicata dai commenti appropriati.

5. Utilizzo nel Lavoro

Dimostriamo il lavoro dell'algoritmo sui dati reali del terminale MetaTrader 5.

Qui non siamo finalizzati a creare un Expert Advisor funzionante basato su GNG (questo è un po’ troppo per un articolo), vogliamo solo vedere come funziona il growing neural gas, quella che viene chiamata presentazione "live".

Per eseguire magnificamente il rendering dei dati, creare una finestra vuota ridimensionata lungo l'asse dei prezzi nell'intervallo da 0 a 100. A tale scopo, utilizziamo un indicatore "vuoto" Dummy.mq5 (non ha altre funzioni):

//+------------------------------------------------------------------+
//|                                                        Dummy.mq5 |
//|                                             Copyright 2010, alsu |
//|                                                 alsufx@gmail.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2010, alsu"
#property link      "alsufx@gmail.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_minimum 0
#property indicator_maximum 100
#property indicator_buffers 1
#property indicator_plots   1
//--- plot Label1
#property indicator_type1   DRAW_LINE
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- indicator buffers
double         DummyBuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,DummyBuffer,INDICATOR_DATA);
   IndicatorSetString(INDICATOR_SHORTNAME,"GNG_dummy");
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime& time[],
                const double& open[],
                const double& high[],
                const double& low[],
                const double& close[],
                const long& tick_volume[],
                const long& volume[],
                const int& spread[])
  {
//--- an empty buffer
   ArrayInitialize(DummyBuffer,EMPTY_VALUE);

//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

Nel MetaEditor creare uno script chiamato GNG.mq5 - visualizzerà la rete nella finestra dell'indicatore Dummy.

Parametri esterni - il numero di vettori di dati per l'apprendimento e i parametri dell'algoritmo:

//--- the number of input vectors used for learning
input int      samples=1000;

//--- parameters of the algorithm
input int lambda=20;
input int age_max=15;
input double alpha=0.5;
input double beta=0.0005;
input double eps_w=0.05;
input double eps_n=0.0006;
input int max_nodes=100;

Dichiarare le variabili globali:

//---global variables
CGNGAlgorithm *GNGAlgorithm;
int window;
int rsi_handle;
int input_dimension;
int _samples;
double RSI_buffer[];
datetime time[];

Iniziare a scrivere la funzione OnStart(). Innanzitutto, troviamo la finestra necessaria:

void OnStart()
  {
   int i,j;
   int window=ChartWindowFind(0,"GNG_dummy");

Per i dati di input utilizziamo i valori dell'indicatore RSI - è conveniente perché i suoi valori sono normalizzati nell'intervallo da 0 a 100, quindi non sarà necessario condurre la pre-elaborazione.

Per un vettore di input della rete neurale assumiamo la coppia (input_dimension=2) che consiste di due valori RSI – sulla barra corrente e precedente (il cui nome scientifico è "immersione di una time serie in uno spazio di caratteristiche bidimensionale"). È più facile visualizzare vettori bidimensionali su un grafico piatto.

Quindi, preparare prima i dati per inizializzare e creare un'istanza dell'oggetto algoritmo:

//--- to have CopyBuffer() work correctly, the number of the vectors 
//--- must be within the number of bars with a reserve left for the vector length 
   _samples=samples+input_dimension+10;
   if(_samples>Bars(_Symbol,_Period)) _samples=Bars(_Symbol,_Period);

//--- receive input data for the algorithm
   rsi_handle=iRSI(NULL,0,8,PRICE_CLOSE);
   CopyBuffer(rsi_handle,0,1,_samples,RSI_buffer);

//--- return the user-defined value
   _samples=_samples-input_dimension-10;

//--- remember open time of the first 100 bars
   CopyTime(_Symbol,_Period,0,100,time);

//--- create an instance of the algorithm and set the size of input data
   GNGAlgorithm=new CGNGAlgorithm;
   input_dimension=2;

//--- data vectors
   double v[],v1[],v2[];
   ArrayResize(v,input_dimension);
   ArrayResize(v1,input_dimension);
   ArrayResize(v2,input_dimension);

   for(i=0;i<input_dimension;i++)
     {
      v1[i] = RSI_buffer[i];
      v2[i] = RSI_buffer[i+3];
     }

Ora inizializza l'algoritmo:

//--- initialization
   GNGAlgorithm.Init(input_dimension,v1,v2,lambda,age_max,alpha,beta,eps_w,eps_n,max_nodes);

Disegna una casella rettangolare e le etichette informative (per vedere visivamente quante iterazioni dell'algoritmo sono state elaborate e quanti neuroni sono "cresciuti" nella rete):

//-- draw a rectangular box and information labels
   ObjectCreate(0,"GNG_rect",OBJ_RECTANGLE,window,time[0],0,time[99],100);
   ObjectSetInteger(0,"GNG_rect",OBJPROP_BACK,true);
   ObjectSetInteger(0,"GNG_rect",OBJPROP_COLOR,DarkGray);
   ObjectSetInteger(0,"GNG_rect",OBJPROP_BGCOLOR,DarkGray);

   ObjectCreate(0,"Label_samples",OBJ_LABEL,window,0,0);
   ObjectSetInteger(0,"Label_samples",OBJPROP_ANCHOR,ANCHOR_RIGHT_UPPER);
   ObjectSetInteger(0,"Label_samples",OBJPROP_CORNER,CORNER_RIGHT_UPPER);
   ObjectSetInteger(0,"Label_samples",OBJPROP_XDISTANCE,10);
   ObjectSetInteger(0,"Label_samples",OBJPROP_YDISTANCE,10);
   ObjectSetInteger(0,"Label_samples",OBJPROP_COLOR,Red);
   ObjectSetString(0,"Label_samples",OBJPROP_TEXT,"Total samples: 2");

   ObjectCreate(0,"Label_neurons",OBJ_LABEL,window,0,0);
   ObjectSetInteger(0,"Label_neurons",OBJPROP_ANCHOR,ANCHOR_RIGHT_UPPER);
   ObjectSetInteger(0,"Label_neurons",OBJPROP_CORNER,CORNER_RIGHT_UPPER);
   ObjectSetInteger(0,"Label_neurons",OBJPROP_XDISTANCE,10);
   ObjectSetInteger(0,"Label_neurons",OBJPROP_YDISTANCE,25);
   ObjectSetInteger(0,"Label_neurons",OBJPROP_COLOR,Red);
   ObjectSetString(0,"Label_neurons",OBJPROP_TEXT,"Total neurons: 2");

Nel ciclo principale, prepara un vettore per l'input dell'algoritmo mostralo sul grafico come un punto blu:

//--- start the main loop of the algorithm with i=2 because 2 were used already
   for(i=2;i<_samples;i++)
     {
      //--- fill out the data vector (for clarity, get samples separated
      //--- by 3 bars - they are less correlated)
      for(j=0;j<input_dimension;j++)
         v[j]=RSI_buffer[i+j*3];

      //--- show the vector on the chart
      ObjectCreate(0,"Sample_"+i,OBJ_ARROW,window,time[v[0]],v[1]);
      ObjectSetInteger(0,"Sample_"+i,OBJPROP_ARROWCODE,158);
      ObjectSetInteger(0,"Sample_"+i,OBJPROP_COLOR,Blue);
      ObjectSetInteger(0,"Sample_"+i,OBJPROP_BACK,true);

      //--- change the information label
      ObjectSetString(0,"Label_samples",OBJPROP_TEXT,"Total samples: "+string(i+1));

Passa il vettore all'algoritmo (solo una funzione - questo è il vantaggio dell'approccio orientato agli oggetti!):

//--- pass the input vector to the algorithm for calculation
      GNGAlgorithm.ProcessVector(v);

Rimuovi i vecchi neuroni dal grafico e disegnane di nuovi (cerchi rossi) e connessioni (linee tratteggiate gialle), evidenzia il vincitore e il secondo miglior neurone con i colori Lime e Verde:

      //--- we need to remove old neurons an connections from the chart to draw new ones then
      for(j=ObjectsTotal(0)-1;j>=0;j--)
        {
         string name=ObjectName(0,j);
         if(StringFind(name,"Neuron_")>=0)
           {
            ObjectDelete(0,name);
           }
         else if(StringFind(name,"Connection_")>=0)
           {
            ObjectDelete(0,name);
           }
        }
      double weights[];
      CGNGNeuron *tmp,*W1,*W2;
      CGNGConnection *tmpc;

      GNGAlgorithm.Neurons.FindWinners(W1,W2);

      //--- drawing the neurons
      tmp=GNGAlgorithm.Neurons.GetFirstNode();
      while(CheckPointer(tmp))
        {
         tmp.Weights(weights);

         ObjectCreate(0,"Neuron_"+tmp.uid,OBJ_ARROW,window,time[weights[0]],weights[1]);
         ObjectSetInteger(0,"Neuron_"+tmp.uid,OBJPROP_ARROWCODE,159);

         //--- the winner is colored Lime, second best - Green, others - Red
         if(tmp==W1) ObjectSetInteger(0,"Neuron_"+tmp.uid,OBJPROP_COLOR,Lime);
         else if(tmp==W2) ObjectSetInteger(0,"Neuron_"+tmp.uid,OBJPROP_COLOR,Green);
         else ObjectSetInteger(0,"Neuron_"+tmp.uid,OBJPROP_COLOR,Red);

         ObjectSetInteger(0,"Neuron_"+tmp.uid,OBJPROP_BACK,false);

         tmp=GNGAlgorithm.Neurons.GetNextNode();
        }
      ObjectSetString(0,"Label_neurons",OBJPROP_TEXT,"Total neurons: "+string(GNGAlgorithm.Neurons.Total()));

      //--- drawing connections
      tmpc=GNGAlgorithm.Connections.GetFirstNode();
      while(CheckPointer(tmpc))
        {
         int x1,x2,y1,y2;

         tmp=GNGAlgorithm.Neurons.Find(tmpc.uid1);
         tmp.Weights(weights);
         x1=weights[0];y1=weights[1];

         tmp=GNGAlgorithm.Neurons.Find(tmpc.uid2);
         tmp.Weights(weights);
         x2=weights[0];y2=weights[1];

         ObjectCreate(0,"Connection_"+tmpc.uid1+"_"+tmpc.uid2,OBJ_TREND,window,time[x1],y1,time[x2],y2);
         ObjectSetInteger(0,"Connection_"+tmpc.uid1+"_"+tmpc.uid2,OBJPROP_WIDTH,1);
         ObjectSetInteger(0,"Connection_"+tmpc.uid1+"_"+tmpc.uid2,OBJPROP_STYLE,STYLE_DOT);
         ObjectSetInteger(0,"Connection_"+tmpc.uid1+"_"+tmpc.uid2,OBJPROP_COLOR,Yellow);
         ObjectSetInteger(0,"Connection_"+tmpc.uid1+"_"+tmpc.uid2,OBJPROP_BACK,false);

         tmpc=GNGAlgorithm.Connections.GetNextNode();
        }

      ChartRedraw();
     }
     
     //--- delete the instance of the algorithm from the memory
     delete GNGAlgorithm;
     
     //--- a pause before clearing the chart
     while(!IsStopped());
     
     //--- remove all the drawings from the chart
     ObjectsDeleteAll(0,window);
  }

Compilare il codice, avviare l'indicatore Dummy e quindi eseguire lo script GNG sullo stesso grafico. Un'immagine come la seguente dovrebbe apparire sul grafico:


Vedete, l'algoritmo funziona davvero: la griglia si adatta gradualmente ai nuovi dati in arrivo cercando di coprire il loro spazio in base alla densità di stand dei punti blu.

Il video mostra solo l'inizio del processo di apprendimento (solo 1000 iterazioni, mentre il numero reale dei vettori richiesti per l'apprendimento di GNG può essere fino a decine di migliaia); tuttavia questo ci dà già una comprensione abbastanza decente del processo.

6. Problemi Noti

Come già accennato, il problema principale di GNG è la sua incapacità di tracciare serie non stazionarie con caratteristiche in rapida evoluzione. Tali distribuzioni "jumping" dei segnali di input possono portare a che gran parte dei neuroni dello strato GNG, avendo già acquisito una certa struttura topologica, si ritrovano improvvisamente fuori dal mercato.

Inoltre, poiché i segnali di ingresso non cadono nella regione della loro posizione, l'età delle connessioni tra questi neuroni non aumenta, quindi, la parte "morta" della rete, che "ricorda" le vecchie caratteristiche del segnale, non fa un lavoro utile, ma consuma solo risorse di calcolo (vedi. Fig. 2).

Nel caso di distribuzioni a deriva lenta, questo effetto negativo non viene osservato: se la velocità di deriva è paragonabile alla "velocità di movimento" dei neuroni nell'adattamento dei pesi, GNG è in grado di tracciare questi cambiamenti.

Figura 2. La reazione del growing neural gas sulla distribuzione "jumping"

Nodi inattivi (morti) separati possono anche apparire sulla rete se una frequenza molto elevata di inserimento di nuovi neuroni (il parametro λ) viene data sull'input dell'algoritmo.

Il suo valore troppo basso porta al fatto che la rete inizia a tracciare le emissioni statisticamente insignificanti di distribuzione dei segnali di ingresso, la cui probabilità di recidiva è molto piccola. Se un neurone GNG viene inserito in questo luogo, quasi certamente rimarrà inattivo dopo questo per molto tempo.

Inoltre, come dimostrato dalla ricerca empirica, il basso valore di inserimento, sebbene contribuisca alla rapida diminuzione dell'errore medio di rete all'inizio del processo di apprendimento, a seguito della formazione fornisce i valori peggiori di questo indicatore: una tale rete raggruppa i dati in modo più crudo.

7. Modifica dell'Algoritmo

Il problema della distribuzione "jumping" può essere risolto modificando l'algoritmo in un certo modo. La modifica ampiamente accettata è quella che introduce il cosiddetto fattore dell'utilità dei neuroni (GNG con fattore di utilità o GNG-U). Le modifiche nel pseudo codice in questo caso sono minime e sono le seguenti:

  • ad ogni neurone viene impostata una variabile chiamata "fattore di utilità" (quella variabile U nell'elenco dei campi della classe CGNGNeuron);
  • nel passaggio 4, dopo aver adattato i pesi del neurone-vincitore, cambiamo il suo fattore di utilità di una quantità pari alla differenza tra un errore del secondo miglior neurone e il vincitore:



    Fisicamente, questo additivo è la quantità di cui l'errore di rete totale sarebbe cambiato se non ci fosse stato un vincitore in esso (quindi il secondo miglior vincitore sarebbe diventato il vincitore), cioè in realtà caratterizza l'utilità del neurone per ridurre l'errore complessivo.

  • i neuroni vengono rimossi nel passaggio 8 su un principio diverso: viene rimosso solo un nodo con un valore minimo di utilità e solo se il valore di errore massimo nello strato supera il suo fattore di utilità di più di volte:


  • quando si aggiunge un nuovo nodo nella fase 9, il suo fattore di utilità viene calcolato come media aritmetica tra le utilità dei neuroni vicini:


  • nella fase 10 il fattore di utilità di tutti i neuroni viene diminuito nello stesso modo e nello stesso ordine delle variabili degli errori:


La costante qui è fondamentale per la capacità di tracciare la non stazionarietà: il suo valore troppo grande porta alla rimozione non solo di "poca utilità", ma anche di altri neuroni abbastanza utilizzabili; un valore troppo piccolo porta a rimozioni rare e, di conseguenza, al ridotto tasso di adattamento.

Nel file GNG.mqh l'algoritmo GNG-U è descritto come una classe derivata da CGNGAlgorithm. I lettori possono tenere traccia in modo indipendente delle modifiche e provare a utilizzare l'algoritmo.

Conclusione

Creando una rete neurale, abbiamo esaminato le principali caratteristiche della programmazione orientata agli oggetti integrata nel linguaggio MQL5. Sembra un fatto piuttosto ovvio che in assenza di tali opportunità (per le quali sono grato agli sviluppatori) sarebbe molto più complicato scrivere programmi complessi per il trading automatizzato.

Per quanto riguarda gli algoritmi analizzati, va notato che, naturalmente, possono essere migliorati. In particolare, il primo candidato per l'aggiornamento è il numero di parametri esterni. Sono piuttosto numerosi, e questo significa che potrebbero esserci delle modifiche, in cui questi parametri diventerebbero variabili interne e sarebbero selezionati in base alle caratteristiche dei dati di input e allo stato dell'algoritmo.

L'autore dell'articolo augura buona fortuna a tutti nello studio della neuro informatica e nell'usarla nel trading!

Tradotto dal russo da MetaQuotes Ltd.
Articolo originale: https://www.mql5.com/ru/articles/163

File allegati |
gng_en.zip (8.86 KB)

Altri articoli di questo autore

MQL5 Wizard: Creazione di Expert Advisor senza Programmazione MQL5 Wizard: Creazione di Expert Advisor senza Programmazione
Vuoi provare una strategia di trading senza perdere tempo per la programmazione? In MQL5 Wizard puoi semplicemente selezionare il tipo di segnali di trading, aggiungere moduli di posizioni trailing e gestione del denaro - e il tuo lavoro è fatto! Crea le tue implementazioni di moduli o ordinale tramite il servizio Jobs e combina i tuoi nuovi moduli con quelli esistenti.
L'Handler dell'Evento "Nuova Barra" L'Handler dell'Evento "Nuova Barra"
Il linguaggio di programmazione MQL5 è in grado di risolvere i problemi a un livello completamente nuovo. Anche quei compiti, che hanno già tali soluzioni, grazie alla programmazione orientata agli oggetti possono salire ad un livello superiore. In questo articolo prendiamo un esempio particolarmente semplice di controllo della nuova barra su un grafico, che è stato trasformato in uno strumento piuttosto potente e versatile. Quale strumento? Scoprilo in questo articolo.
Il Semplice Esempio di Creazione di un Indicatore Utilizzando la Logica Fuzzy Il Semplice Esempio di Creazione di un Indicatore Utilizzando la Logica Fuzzy
L'articolo è dedicato all'applicazione pratica del concetto di logica fuzzy per l'analisi dei mercati finanziari. Proponiamo l'esempio dell'indicatore che genera segnali basati su due regole fuzzy basate sull'indicatore Envelopes. L'indicatore sviluppato utilizza diversi buffer di indicatori: 7 buffer per i calcoli, 5 buffer per la visualizzazione dei grafici e 2 buffer colore.
Simulink: una Guida per gli Sviluppatori di Expert Advisor Simulink: una Guida per gli Sviluppatori di Expert Advisor
Non sono un programmatore professionista. E così, il principio di "passare dal semplice al complesso" è di primaria importanza per me quando lavoro allo sviluppo del sistema di trading. Cosa esattamente è semplice per me? Prima di tutto, è la visualizzazione del processo di creazione del sistema e la logica del suo lavoro. Inoltre, è un minimo di codice scritto a mano. In questo articolo, tenterò di creare e testare il sistema di trading basato su un pacchetto Matlab, e quindi scrivere un Expert Advisor per MetaTrader 5. I dati storici di MetaTrader 5 verranno utilizzati per il processo del test.