Growing Neural Gas: MQL5への実装

Alexey Subbotin | 6 10月, 2015

はじめに

1990年代、人工神経ネットワークの研究者はこういった計算メカニズムの新しいクラスを開発する必要がある、という結論に至りました。その将来はネットワーク層の固定化されたトポロジーの不在です。これは、特徴空間で人工ニューロンの数と組織化が指定済みではなく、インプットデータに応じてそのようなモデルを学ぶ過程で計算され、自律的に調整をするということを意味しています。

そのような考えの出現理由は、発話や画像認識、分類、パターン不在の認識などの入力パラメータの圧縮の妨げとベクトル量子化に関する数々の現実的問題です。

それ以来、自己組織化マップおよび ヘビアン学習はすでに知られています。(特に層の『フレームワーク』を形成しながらネットワークのトポロジー化を行い、言い換えるとニューロン間の接続セットを作成 するアルゴリズム)また、『ソフト』競合学習 へのアプローチでは(『勝者』のニューロンだけでなく、その『近隣』にもウェイト順応手続きが起こります。)これらを組み合わせるという理論的な手順に取り組んできました。それは1995年にドイツの科学者Bernd Fritzkeによって行われました。彼は今は一般的なアルゴリズム "Growing neural gas"(,GNG)を産み出しました。

その手法はきわめて成功であると証明されました。その亜種がいくつも出現しました。その一つは教師あり学習への適応(Supervised-GNG)です。著者によって知らされたようにS-GNGは、分類が難しいインプットスペース領域におけるトポロジーの最適化能力のおかげで、 radial basis functionsのネットワークよりたとえばデータ分類において偉大な効果を示しました。疑いなくGNGは "K-means" クラスタよりも優れています。

ドイツ株式市場(Deutsche Bӧrse)に職を得得たあと、2001年FritzkeはRuhr大学(Bochum, Germany) での科学者としてのキャリアを閉じたことはよく知られています。この事実は本稿を書くための基として彼のアルゴリズムを選んだもうひとつの理由です。


1. Growing Neural Gas

GNGは適用可能なインプットデータのクラスタリング を可能にする、言い換えると、スペースをクラスタに分割するだけでなく、データの性質に基づいてまたその要求される番号を決定するアルゴリズムです。

2つのニューロンだけ取り上げて始めます。コンペティティブなヘビアン学習のアプローチを使って インプットベクトルに最もよく応答するニューロン間の関連を作成しているあいだ、 アルゴリズムは常にその数を変更(ほとんどは増加)します。それぞれのニューロンは、いわゆる『ローカルエラー』を集積する内部変数を持ちます。ノード間の関連づけは『エイジ』と呼ばれる変数によって性格づけられます。

GNGの疑似コードは以下のようなものです。

  1. 初期化:入力ベクトルの分布によって許可されており、ローカルエラーがゼロ値の ウェイトのベクトルでノードを2つ作成します。 そのエイジを0に設定することで ノードを関連付けます。
  2. 神経ネットワークにベクトルを入力します
  3. の一番近くにの2つのニューロンをみつけます。いわゆるそのが最小のウェイトベクトルのノード で、また はすべてのノードの中で距離が二番目に一番小さい値となっています。
  4. ベクトル間の距離の二乗を足すことで勝者ニューロン ローカルエラーを更新します。

  5. 勝者ニューロン とインプットベクトル方向にあるそれの位相的な近隣者(いわゆる勝者に関連性のあるニューロンすべて)をシェアに等しい距離でまた フルのものから移動します。

  6. 勝者から出る結合のエイジをすべて 1増やします。
  7. もっともすぐれた2つのニューロンが結合されたら、その結合の世代をゼロに設定します。 そうでなければ、それらの間の結合を作成します。
  8. ~より大きい年齢の結合を削除します。ニューロンがそれ以上発生するエッジを持たない結果が出たら、それらニューロンも削除します。
  9. 現在の反復数が倍数でネットワークの限界サイズに到達していなければ、新規ニューロン を追加します。以下のように行います。

    • 最大ローカルエラーのニューロンを決定します。
    • 最大エラーを持つニューロンをを近隣から決定します。
    • の間で『真ん中に』ノード を作成します。

    • の間のエッジによってエッジを の間に移動します。
    • ニューロン のエラーを減らし、ニューロンのエラー値を に設定します。

  10. すべてのニューロン のエラーを分数分減らします。

  11. 停止基準に満たなければ、ステップ2を続けます。

GNGがインプットスペースの特性にどのように適用されるのか考察します。

まず、ステップ4での勝者のエラー変数が増加することに注意します。この手順は、もっとも頻繁に勝つノード、すなわちもっとも大きい数字のインプットシグナルが出現する近隣のノードが、最大のエラーを持ち、そのためこれらの領域は新ノード追加による「圧縮」の第一候補となることにつながります。

ステップ5でのインプットベクトル方向へのノードの移動は、勝者が近所にあるインプットシグナルの一つを平均化しようとしていることを意味します。この場合、一番強いニューロンがシグナル方向に近隣のニューロンがを少し「押す」ことになります(一般に が選ばれます)。

ステップ6~8でニューロン間のエッジ処理を説明しました。古い結合の加齢と削除の意味は、ネットワークのトポロジーはいわゆるドローネー三角分割法、言い換えると、とりわけ三角形分割における三角形のすべての角の最小角が最大化される ニューロンの三角形分割(三角形への再分割)に極限まで近い、ということです。

簡単に言いますと、ドローネー三角分割法は階層の最も「美しい」トポロジー化に相当する、ということです。トポロジー的ストラクチャは分かれたユニットであることを要求しませんが、ステップ8に挿入される新規ノードの一を決定するために使われるとき、それらは常にエッジの真ん中に配置されます。

ステップp は階層内の全ニューロンのエラー変数の修正です。これは、ネットワークが古いインプットベクトルと新しいインプットベクトルへのより良い応答を「忘れる」のを確実にするためです。このため、われわれは時間依存性の神経ネットワークを適用する、すなわち、ゆっくりとインプットシグナルの分布をドリフトする可能性を得るのです。しかしこれでインプットの特性における速い変化を追いかけることはできません。(詳細は以下の項を参照ください。そこでアルゴリズムのドローバックについて述べられています。)

おそらく停止基準については別途考察する必要があるでしょう。アルゴリズムは分析システム開発者に夢を見る余地を与えてくれます。可能な選択肢は:テスト設定におけるネットワークの有効性を確認する、ニューロンの平均的エラーの動きを分析する、ネットワークの複雑性を制約する、などです。

情報ということで、もっとも簡単な選択で作業します。なぜなら、本稿の目的はアルゴリズムそのものだけでなく、 MQL5という手段で実装する可能性も示すことだからです。手持ちのインプットがなくなるまで階層の学習は続けていきます。


2. データ整理手法の選択

アルゴリズムを作成するとき、明らかに『設定』と呼ばれるものを保存する必要性に取り組む必要があります。ここに設定が2つあります – ニューロンの設定とニューロン間のエッジ設定です。両ストラクチャがプログラムの中で進化するあいだ(そしてわれわれは両方にアイテムを加えたり取り除いたりする予定です)、またこのメカニズムを提供する必要があります。

もちろん オブジェクトの動的配列を使用することもできますが、 それにはデータのコピー、移動と大量の処理が必要となり、それは基本的にプログラムの処理スピードを落とします。 特定のプロパティの抽象概念に関わる適切な選択はプログラムグラフとそのもっともシンプルなバージョン、リンクリストです。

読者のみなさんにリンクリスト (図1)の原則で作業することを思い出していただきます。基本クラスのオブジェクトは、メンバの一つとしての同じオブジェクトに対するポインターを含みます。それにより、メモリ内の物理的順番は問わず線形のストラクチャに結合することができます。また、そこには『キャリッジ』クラスがあります。 それはリストを使って移動、追加、ノード挿入と削除、検索、比較、保存などの手順、また必要であればその他の手順を カプセル化します

図1 リニアにリンクしたリスト作成の図解

MetaQuotes Software Corp.の専門家はすでに 標準ライブラリCObjectクラスオブジェクトのリンクしたリストを持っています。対応するプログラミングコードは、MetaTrader 5の配信標準パックのMQL5\Include\Arrays内ヘッダファイルList.mqhにあります。

基本のデータストラクチャとして CObjectクラスおよび CList クラスを取り入れているMetaQuotesの立派なプログラマーの能力を信用し、一から作ることはしません。ここでは、オブジェクトを基にしたアプローチ – 継承のメカニズムの柱の一つを利用していきます。


3. プログラムモデル

まず、『人工ニューロン』のコンセプトのソフトウェア形式を決めます。

OOPアプリケーションを作成するときのきまりのひとつは、常にもっとも一般的なデータストラクチャでプログラムを始めることです。自分のためにプログラムする場合でも、とりわけ他のプログラマーが使う前提でプログラムする場合は、将来、開発者が開発やプログラムロジックの変更について異なる考えを持つかもしれない、そして、どこに修正がくわえられるか前もってわかるはずはない、ということを頭に入れておく必要があります。

OOPの基本的性質は他の開発者がもともとクラスを作した人のものを検証しない、代わりに階層の正しい位置で使用できるデータからデータストラクチャを引継ぐというものです。最初に書かれたクラスはできるだけ抽象的であるべきです。また、特定の条件は下位レベルで追加されるべきです。

われわれの問題にあてはめると、これはCCustomNeuronクラス(「なんらかのニューロン」)の定義からプログラムを書き始めることを意味します。すべての人口ニューロン特定のシナプス数(インプットウェイト)とアウトプット値を持つように、です。初期化(ウェイトに値を割り当てます)、アウトプットでシグナルの値の計算、指定した値でそのウェイトを変化させることが可能となります。

それ以上の抽象概念は達成が難しそうです。(最大限一般化された CObjectからクラスを受けついている事実を考慮して) - すべてのニューロンは特定の処理ができる必要があります。

データ記述をするためヘッダファイルNeurons.mqhを作成し、 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;
  }

クラスで定義される関数はたいへんシンプルです。よってここでは詳しく述べることはしません。インプットデータ処理関数はvirtualモディファイアを伴ってProcessVector(double &in[])(ここでのアウトプット値は通常のパーセプトロン値のように計算されます。)と定義しました。

これは、メソッドが派生クラスによって再定義される場合、実行時に実際のオブジェクトクラスによって動的に適切なプロシージャが選ばれることを意味しています。それは、ユーザー相互交流という意味も含め柔軟性を高め、プログラミングの労働コストを下げます。

リンクリストでニューロンを作成するにあたり、われわれはなにもしていないように見えるにもかかわらず、実はすでにCObjectからの新規クラスを指摘した瞬間にそれは起こっているのです。よって、ここで、われわれのクラスのプライベートメンバは m_first_nodem_curr_nodem_last_nodeで、それらは『CObjectのポインター』タイプのもので、それぞれリストの最初、現在、最終エレメントを指します。また、リストを使用するために要求されるすべての関数も入手済みです。

それでは、CGNGNeuronクラスを定義することでGNG階層のニューロンがその他のニューロンと異なる点について概説していきます。

//+------------------------------------------------------------------+
//| 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]);
     }
  }

ご覧のとおり、この違いはフィールドの存在にあります。

ProcessVector(...)関数は変化しています。 – ここでそれはエラーフィールドの値を計算します。

U フィールドには今のところ注意をする必要はありません。その意味は後に『アルゴリズムの変更』項で説明します。

次のステップは2つのニューロン間の結合を表すクラスを書くことです。

//+------------------------------------------------------------------+
//| 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;
  }

何も難しいことはありません。 – エッジには両端があります。(識別子uid1 uid2で指定されたニューロン)そして年齢は初期値ではゼロです。

リンクリストの『キャリッジ』クラスを連携します。これはGNGアルゴリズムを実装するのに要求される可能性があります。

First of all inherit a class of neurons list from 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;
  }

In the class constructorクラス内で、疑似ランダム数のジェネレータは初期化されます。それはリストのユニーク識別子のエレメントを割り当てるためのものです。

クラスメソッドの意味を明確にしましょう。

次のクラスはニューロン間の結合リストです。

//+------------------------------------------------------------------+
//| 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);
  }

クラスの定義済みメソッド

これでストラクチャについての説明は終わりです。われわれ自身のアルゴリズムをプログラムする時がやってきました。


4. アルゴリズムのクラス

新規ヘッダファイルGNG.mqhをnclude\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);
  }

CGNGAlgorithmクラスには2つ重要なフィールドがあります。 - ニューロンのリンクリストのポインターNeurons とその両者間の結合Connectionsです。それらはニューロンネットワークのストラクチャの物理メディアです。のこりのフィールドは外部から定義されるアルゴリズムのパラメータです。

予備クラスメソッドとして、アルゴリズムののインスタンスに外部パラメータを渡し、データストラクチャと停止基準StoppingCriterion()(先に確認したようにそれは常にfalseを返すだけで何もしません)を初期化するInit(...) を選びたいと思います。

ProcessVector(…)関数は指定されたデータベクトルを処理し、細かい部分は含まないアルゴリズムの主要な関数です。:データとメソッドの連携を作成してきましたが、アルゴリズムに関しては、全ステップを機械的に作業していくだけです。コード内の位置は適切なコメントで表示されます。


5. 作業での使用

MetaTrader 5ターミナルの実データでアルゴリズムにを使った作業を見ます。

ここでの狙いはGNG(一つの記事で扱うには大きすぎる話題です)を基にした使えるExpert Advisorを作成することではありません。GNGがどのように動作するか見たいだけです。それはいわゆる『実演』というものです。

データを美しくレンダーリングするために、空のウィンドウを価格軸にそって0-100の範囲の大きさにします。そのため、『空の』インディケータDummy.mq5(関数は持ちません) を使います。

//+------------------------------------------------------------------+
//|                                                        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);
  }
//+------------------------------------------------------------------+

MetaEditorにGNG.mq5と呼ばれるスクリプトを作成します。 - Dummyインディケータのウィンドウにネットワークを表示します。

外部パラメータ - 学習のためのデータベクトルの数とアルゴリズムのパラメータ

//--- 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;

グローバル変数を宣言します。

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

OnStart()関数を書き始めます。まず、必要なウィンドウを見つけます。

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

インプットデータには、RSIインディケータの値を使います。 - 値が0~100の範囲に標準化されているので処理を作成する必要がなく、便利です。

ニューロンネットワークのインプットベクトルには、2つのRSI値で構成されるペア(input_dimension=2) を想定します。– 現在バーおよび前回バーで(科学的名称は『二次元特徴空間における時系列のイマージョン』です。)平面チャートに二次元ベクトルを表示するのはたやすいことです。

まず、初期化をするデータを準備し、アルゴリズムオブジェクトのインスタンスを作成します。

//--- 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];
     }

アルゴリズムを初期化します。

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

長方形と情報ラベル(アルゴリズム処理がいくつ行われたか、またネットワークにいくつのニューロンが『育った』か視覚的に確認するため)を 描きます

//-- 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");

メインのループでは、アルゴリズムのインプット用ベクトルを準備し、ブルーのどっととしてチャート上に表示します。

//--- 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));

アルゴリズムにベクトルを渡します。(関数はただ一つ - それがオブジェクトに基づくアプローチのメリットです!)

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

チャートから古いニューロンを削除し、新しいニューロン(赤い丸)と連結(黄色の点線)を描きます。そして優勝者を強調し、二番てのニューロンをライムとグリーンで色づけします。

//--- 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);
  }

コードをコンパイルし、ダミーのインディケータを開始します。それから同じチャート上でGNGスクリプトを実行します。チャートには以下のような絵が表示されるはずです。


おわかりですね。アルゴリズムはほんとうに動作するのです。グリッドは次第にブルーのドットの密度に応じてそのスペースを埋めようとする新しい入信データに応じて変化します。

ビデオでは学習プロセスのほんの初期しか見ることができません。(たった1000の反復です。実際GNGの学習に要求されるベクトルは数万にのぼります。) ただ、これでもプロセスはそこそこ理解できます。


6. よくある問題

すでに述べたように、GNGの主な問題はすばやく変化する特性の非定常系を追跡できないことです。そのようなインプットシグナルの『とびとび』の分布は、GNG階層のニューロンの多くが、すでになんらかの位相的ストラクチャを得ているので、突然自身が仕事をしていないと気づくことにつながります。

それ以上に、インプットシグナルはその位置の領域に集まらないので、これらニューロン間の結合年齢は 増加しません。それゆえ、ネットワークの『死んだ』部分、それはシグナルの古い特性を「憶えて」いて、有用な仕事はせず、計算リソースを消費するだけです。(図2参照)

ゆっくりとした分布については、これの悪影響は認められません。ただようスピードがウェイトに順応するニューロンの『動きのスピード』と比較できるなら、GNGはこれら変化を追跡可能です。

図2 『跳びあがる』分布へのGNGの反応

アルゴリズムの入力時、非常に高振動の新しいニューロン(パラメータλ)の挿入が起こると、結合していない活動的でない(死んだ)ノードもネットワークに出現するかもしれません。

その低すぎる値はネットワークがインプットシグナルの分布の統計的に取るに足らない排出を監視し始めることにつながります。GNGニューロンがこの場所に挿入されたら、それはほとんど確実にこのあと長期にわたって不活性のままとなります。

また、研究で示されるように、挿入の低い値は学習の初期段階で平均的ネットワークエラーを速く減らすことに貢献していても、トレーニングの結果としてはこのインディケータ:もっと雑にそのようなネットワーククラスタデータ、の値は最低と出ます。


7. アルゴリズムの変更

『飛び跳ねる』分布の問題はアルゴリズムを特定の方法で変更することで解決が可能です。広く使われている変更はいわゆるニューロンのユーティリティ要因(ユーティリティ要因を伴うGNG、またはGNG-U)を導入したものです。この場合、疑似コード内の変更が最小限です。以下です。

ここでの定数 は、を監視する機能にとって重要です。:その値が大きすぎると『ほとんど有用性のない』ニューロンだけでなくその他のきわめて活用度の高いニューロンも削除してしまいます。またその値が小さすぎると、ほとんど削除せず、結果順応度を下げます。

GNG.mqh the GNG-UファイルでCGNGAlgorithmから派生したクラスとしてアルゴリズムが述べられています。みなさんは個別に変更を追跡してアルゴリズムを使ってみることができます。


おわりに

ニューロンネットワークの作成により、MQL5 言語で構築されるオブジェクトに基づいたプログラミングの主要な機能を見ました。そのような機会がなければ(開発者の方に感謝している部分です)、自動化トレーディングの複雑なプログラムを書くのはもっとずっと煩雑なことだろうというのは明らかだと思われます。

アルゴリズムの分析については、当然今後の改善が可能であることを付け加える必要があります。特に、グレードアップの第一候補は外部パラメータの数です。それらはかなり膨大で、これはこれらパラメータが内部変数となり、インプットデータの特性とアルゴリズムの状況に応じ選択されるようになるであろう変更です。

みなさんが神経情報科学を学ばれ、それをトレーニングに使っていくことができますように!