
Growing Neural Gas: MQL5への実装
はじめに
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の疑似コードは以下のようなものです。
- 初期化:入力ベクトルの分布によって許可されており、ローカルエラーがゼロ値の ウェイトのベクトルでノードを2つ作成します。 そのエイジを0に設定することで ノードを関連付けます。
- 神経ネットワークにベクトルを入力します
。
の一番近くに
と
の2つのニューロンをみつけます。いわゆるその
が最小のウェイトベクトル
と
のノード で、また
はすべてのノードの中で距離が二番目に一番小さい値となっています。
ベクトル
と
間の距離の二乗を足すことで勝者ニューロン
ローカルエラーを更新します。
勝者ニューロン
とインプットベクトル方向にあるそれの位相的な近隣者(いわゆる勝者に関連性のあるニューロンすべて)
をシェアに等しい距離で
また
フルのものから移動します。
- 勝者から出る結合のエイジをすべて
1増やします。
- もっともすぐれた2つのニューロン
と
が結合されたら、その結合の世代をゼロに設定します。 そうでなければ、それらの間の結合を作成します。
- ~より大きい年齢の結合を削除します
。ニューロンがそれ以上発生するエッジを持たない結果が出たら、それらニューロンも削除します。
現在の反復数が倍数
でネットワークの限界サイズに到達していなければ、新規ニューロン
を追加します。以下のように行います。
- 最大ローカルエラーのニューロン
を決定します。
- 最大エラー
を持つニューロン
をを近隣から決定します。
と
の間で『真ん中に』ノード
を作成します。
と
、
と
の間のエッジによってエッジを
と
の間に移動します。
- ニューロン
と
のエラーを減らし、ニューロンのエラー値を
に設定します。
- 最大ローカルエラーのニューロン
すべてのニューロン
のエラーを分数
分減らします。
- 停止基準に満たなければ、ステップ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_node、m_curr_node、m_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]); } }
ご覧のとおり、この違いはフィールドの存在にあります。
- エラー – インプットベクトルからニューロンウェイトベクトルへの現在の距離の二乗
- E – ローカルエラーとユニークIDを集積する変数
- uid – のちにニューロンを結合によってペアにつなぎ合わせることができるように要求されます。(CListクラスにあるシンプルなインデックスは十分ではありません。なぜならニューロンを追加し削除する必要があるからです。それは番号をつける上で混乱を招きます。)
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クラス内で、疑似ランダム数のジェネレータは初期化されます。それはリストのユニーク識別子のエレメントを割り当てるためのものです。
クラスメソッドの意味を明確にしましょう。
- Append()メソッドはCListクラス機能性への追加です。それを呼ぶときは、ノードがリストの最後に付け足される、またはラストが空の場合は最初のノードが作成されます。
- Init(double &v1[],double &v2[])関数は その存在をGNGアルゴリズムに負います。ネットワークの成長は2つのニューロンから始まります。よって、このはわれわれにとってもっとも便利なものでしょう。関数本体では、m_curr_node、 m_first_node、 m_last_nodeなどのIDを使う場合、このクラス(指定の変数はCListから引き継がれます。よって通常それらはCObjectを指します。)の機能性を使うのであれば、CGNGNeuron*タイプに変換する必要があります。
- Find(int uid)関数は、名前のとおり、そのIDでニューロンを検索し、見つけたエレメントまたは見つからなければ NULLを返します。
- FindWinners(CGNGNeuron *&Winner,CGNGNeuron *&SecondWinner) – アルゴリズムの一部です。ニューロンリストで勝者を検索する必要が出てきます。そして、インプットベクトルへの近さと言う意味でその勝者の隣のニューロンがこの関数を使う対象です。パラメータは参照によってこの関数に渡されます。のにちわれわれはそこに返す値(*&は「ポインター参照」と意味します。これは正しい表現です。逆は&*で、「参照ポインター」を意味します。がこれは禁じられています。この場合はコンパイラがエラーを発生させます。)を書くことができます。
次のクラスはニューロン間の結合リストです。
//+------------------------------------------------------------------+ //| 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); }
クラスの定義済みメソッド
- Append()。このメソッドの実装は前のクラス記述に似ています。異なる点は、返す値のタイプです。(残念ながら、MQL5にはクラスのテンプレートがありません。よってこういったものは毎回各必要があります。)
- Init(int uid1,int uid2) – GNGアルゴリズムは最初に結合の初期化を要求します。それはこの関数内で行われます。
- Find(int uid1,int uid2)関数はわかりやすいですね。
- FindFirstConnection(int uid)メソッドとFindNextConnection(int uid)メソッドの違いは、全社はリストの最初から始め、近隣ニューロンとの結合を探しますが、後者は現在のノード (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)を導入したものです。この場合、疑似コード内の変更が最小限です。以下です。
- それぞれのニューロンに対して
『ユーティリティ要因』と呼ばれる変数
(CGNGNeuronクラスのフィールドのリストにある変数 U)を一致させます。
-
ステップ4で、勝者ニューロンのウェイトに順応したら、そのユーティリティ要因を二番手ニューロンと勝者ニューロンの誤りの差に等しい量で変更します。
物理的にこの追加は、その中に勝者がいなければ(そのときは二番手が勝者となります。)ネットワークエラーのトータルが変わった量です。すなわち実際全体的エラーを減らすニューロンの有用性を特徴づけます。 ニューロンはステップ8で削除されます。:最小のユーティリティ要因を伴う階層内の最大エラー値がそのユーティリティ要因を
倍以上超えた時だけノードのみが削除されます。
ステップ9で新規ノードを追加する際、そのユーティリティ要因は算術平均として近隣のニューロンのユーティリティの間で計算されます。
ステップ10では、全ニューロンのユーティリティ要因はエラー変数と同様に同じ順番で減少します。
ここでの定数 は、を監視する機能にとって重要です。:その値が大きすぎると『ほとんど有用性のない』ニューロンだけでなくその他のきわめて活用度の高いニューロンも削除してしまいます。またその値が小さすぎると、ほとんど削除せず、結果順応度を下げます。
GNG.mqh the GNG-UファイルでCGNGAlgorithmから派生したクラスとしてアルゴリズムが述べられています。みなさんは個別に変更を追跡してアルゴリズムを使ってみることができます。
おわりに
ニューロンネットワークの作成により、MQL5 言語で構築されるオブジェクトに基づいたプログラミングの主要な機能を見ました。そのような機会がなければ(開発者の方に感謝している部分です)、自動化トレーディングの複雑なプログラムを書くのはもっとずっと煩雑なことだろうというのは明らかだと思われます。
アルゴリズムの分析については、当然今後の改善が可能であることを付け加える必要があります。特に、グレードアップの第一候補は外部パラメータの数です。それらはかなり膨大で、これはこれらパラメータが内部変数となり、インプットデータの特性とアルゴリズムの状況に応じ選択されるようになるであろう変更です。
みなさんが神経情報科学を学ばれ、それをトレーニングに使っていくことができますように!
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/163





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索