ニューラルネットワークが簡単に(第74回):適応による軌道予測
はじめに
取引戦略を構築することを、市場の状況を分析し、金融商品の最も可能性の高い動きを予測することから切り離すことはできません。この動きは、しばしば他の金融資産やマクロ経済指標と相関関係にあります。これは、各車両がそれぞれの目的地に向かう、交通の動きと比較することができます。道路上でのそれらの行動はある程度相互に関連しており、交通規則によって厳しく規制されています。また、道路状況に対するドライバーの個人的な認識により、道路には確率的な要素が残っています。
同様に、金融の世界でも、価格形成には一定のルールがありますが、市場参加者が生み出す需給の確率性は、価格の確率性につながります。ナビゲーションの分野で使われている多くの軌跡予測法が、将来の値動きを予測するのに適しているのはこのためかもしれません。
本稿では、自律走行車のナビゲーション分野の問題を解決するために提案された、重みの動的学習ADAPTを用いて、シーン上の全エージェントの軌道を効率的に共同予測する手法を紹介したいとおもいます。この手法は「ADAPT:Efficient Multi-Agent Trajectory Prediction with Adaptation」で初めて紹介されました。
1.ADAPTアルゴリズム
ADAPT法は、シーンマップ内のすべてのエージェントの過去の軌跡を分析し、将来の軌跡を予測します。ベクトル化されたシーン表現は、エージェントとマップ間の様々なタイプの相互作用をモデル化し、エージェントの最良の表現を得ます。ゴール設定アプローチと同様に、アルゴリズムはまず、可能性のあるエンドポイントのセットを予測します。各エンドポイントは、シーン内でのエージェントの変位を考慮して改良されます。その後、エンドポイントで決定された全軌跡が予測されます。
この手法の著者は、勾配停止によってエンドポイント予測と軌道予測を分離することで、モデル学習を安定させています。著者が発表したモデルは、モデルの複雑さを低く抑えるために、エンドポイントと軌道の予測に小さな多層パーセプトロンを使用しています。
著者が提案する手法では、ベクトル化表現を用いてマップとエージェントを構造的に符号化します。この表現は、エージェントの過去の軌跡とシーンマップが与えられれば、各シーン要素について独立に連結グラフを作成します。この手法の著者は、エージェントとマップオブジェクトに2つの別々のサブグラフを使用することを提案しています。
ADAPTでは、シーン要素間の様々なタイプの相互作用をシミュレートすることができます。著者は、エージェント-レーン(AL)、レーン-レーン(LL)、レーン-エージェント(LA)、エージェント-エージェント(AA)の4種類の関係をモデル化することを提案しました。
相互依存は、AutoBotと同様に、Multi-Head Attentionブロックを使って分析されます。ただし、Self-Attentionブロック(AA、LL)は、Cross-Attentionエンコーダーを使って、相互関係ブロック(AL、LA)で補完されます。各相互作用は順次モデル化され、そのプロセスはL回繰り返されます。
このようにして、各反復で中間的な特徴量を更新することができ、更新された特徴は次の反復でAttentionを計算するために使用されます。各シーン要素は、さまざまなタイプの相互作用によってL回情報を得ることができます。
エージェント中心の表現を使用する場合のエンドポイントの予測には、MLPを使用することが可能であり、単一エージェントの予測における利点から、MLPを使用することが望ましいと考えられます。しかし、シーン中心の表現を使用する場合、動的な重みを持つ適応的な頭部を使用することが推奨され、これは軌道エンドポイントのマルチエージェント予測においてより効果的です。
各エージェントのエンドポイントを受け取った後、アルゴリズムはMLPを用いてスタートポイントとエンドポイントの間の将来の座標を補間します。ここでは、完全な軌道予測のためのウェイト更新がエンドポイント予測から切り離されるように、エンドポイントを「切り離します」。同様に、非結合エンドポイントを用いて各軌跡の確率を予測します。
モデルを訓練するために、K個の軌跡を予測し、マルチモーダルな将来シナリオを捉えるために多様性損失を適用します。誤差勾配は、最も正確な軌道を通してのみバックプロパゲートされます。エンドポイントを条件として全軌跡を予測するため、全軌跡予測にはエンドポイント予測の精度が不可欠です。そのため、この手法の著者は、エンドポイント予測を改善するために別の損失関数を適用しています。元の損失関数の最後の要素は、軌道に割り当てられた確率を導くための分類損失です。
論文の著者によって発表された方法の原文()を以下に示します。
2.MQL5を使用した実装
上記は、ADAPT法のかなり凝縮された理論的説明ですが、これは今後の作業量の多さと論文形式の限界によるものです。いくつかの側面については、提案されたアプローチを実施する際に詳しく説明します。私たちの実装は、オリジナルの方法とは多くの点で異なることに注意してください。以下はその違いです。
まず、エージェントとポリラインのエンコードに別々のテンソルを使用しません。この場合のエージェントは、分析された特徴量です。各特徴量は、値と時間という2つのパラメータによって特徴づけられます。分析期間中、一定の軌道を描きます。各指標はそれぞれ値の範囲を持っていますが、実際にはシーンのマップは持っていません。ただし、すべてのエージェントがいる、ある時点のシーンのスナップショットを持っています。理論上、あるエンティティを別のエンティティに置き換えることができます。同じデータを別の次元で見るのだから、このために別のテンソルを作る必要はないようです。したがって、1つのテンソルに異なるアクセントをつけます。
2.1 相互関係ブロック
さらに、提案されたアプローチを実現する方法を考えてみると、相互関係ブロックの実装を持っていないことに気づきました。以前は自己回帰的なタスクが多くなっていました。このような課題では、Self-Attentionブロックの使用は極めて適切でした。今回は、さまざまな主体間の関係を分析する必要があります。そこで、新しいニューラル層CNeuronMH2AttentionOCLを実装します。クラスの実装アルゴリズムは、大部分がSelf-Attentionブロックから借用したものです。違いは、Query、Key、Valueの各エンティティが、ソースデータテンソルの異なる次元から形成されることです。そのため、大幅な修正が必要となりました。そこで、既存のクラスを最新化するのではなく、新しいクラスを作ることにしました。
class CNeuronMH2AttentionOCL : public CNeuronBaseOCL { protected: uint iHeads; ///< Number of heads uint iWindow; ///< Input window size uint iUnits; ///< Number of units uint iWindowKey; ///< Size of Key/Query window //--- CNeuronConvOCL Q_Embedding; CNeuronConvOCL KV_Embedding; CNeuronTransposeOCL Transpose; int ScoreIndex; CNeuronBaseOCL MHAttentionOut; CNeuronConvOCL W0; CNeuronBaseOCL AttentionOut; CNeuronConvOCL FF[2]; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL); virtual bool attentionOut(void); //--- virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); virtual bool AttentionInsideGradients(void); public: /** Constructor */ CNeuronMH2AttentionOCL(void); /** Destructor */~CNeuronMH2AttentionOCL(void) {}; virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint heads, uint units_count, ENUM_OPTIMIZATION optimization_type, uint batch); virtual bool calcInputGradients(CNeuronBaseOCL *prevLayer); //--- virtual int Type(void) const { return defNeuronMH2AttentionOCL; } //--- methods for working with files virtual bool Save(int const file_handle); virtual bool Load(int const file_handle); virtual CLayerDescription* GetLayerInfo(void); virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau); virtual void SetOpenCL(COpenCLMy *obj); };
クラスのコンストラクタでは、ローカル変数の初期値だけを設定します。
CNeuronMH2AttentionOCL::CNeuronMH2AttentionOCL(void) : iHeads(0), iWindow(0), iUnits(0), iWindowKey(0) { activation = None; }
クラスのデストラクタは空のままです。
CNeuronMH2AttentionOCLクラスオブジェクトの初期化はInitメソッドで実装されています。メソッドの冒頭で、親クラスの関連メソッドを呼び出し、そこで外部プログラムから受け取ったデータを確認し、継承したオブジェクトを初期化します。
bool CNeuronMH2AttentionOCL::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint heads, uint units_count, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch)) return false;
主要パラメータの値を保存します。
iWindow = fmax(window, 1); iWindowKey = fmax(window_key, 1); iUnits = fmax(units_count, 1); iHeads = fmax(heads, 1); activation = None;
ソースデータを異なる次元で分析するので、ソースデータのテンソルを転置する必要があります。
if(!Transpose.Init(0, 0, OpenCL, iUnits, iWindow, optimization_type, batch)) return false; Transpose.SetActivationFunction(None);
Query、Key、Valueエンティティの生成には畳み込み層を使用します。フィルタの数は、1つのエンティティのベクトルの次元に等しくなります。Queryは元のデータテンソルの1次元から生成され、KeyとValueは別の次元から生成されます。したがって、2つの層(各次元に1つずつ)を作成します。
if(!Q_Embedding.Init(0, 0, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, optimization_type, batch)) return false; Q_Embedding.SetActivationFunction(None); if(!KV_Embedding.Init(0, 0, OpenCL, iUnits, iUnits, 2 * iWindowKey * iHeads, iWindow, optimization_type, batch)) return false; KV_Embedding.SetActivationFunction(None);
必要なのは、OpenCLコンテキスト側の依存係数行列だけです。使用するリソースを節約するために、コンテキストの中だけにバッファを作成します。メインプログラムの側では、バッファへのポインタだけが保存されます。
ScoreIndex = OpenCL.AddBuffer(sizeof(float) * iUnits * iWindow * iHeads, CL_MEM_READ_WRITE); if(ScoreIndex == INVALID_HANDLE) return false;
次に来るのは、Self-Attentionブロックに似たものです。ここでは、Multi-Head Attention出力層を作成します。
//--- if(!MHAttentionOut.Init(0, 0, OpenCL, iWindowKey * iUnits * iHeads, optimization_type, batch)) return false; MHAttentionOut.SetActivationFunction(None);
次元削減層。
if(!W0.Init(0, 0, OpenCL, iWindowKey * iHeads, iWindowKey * iHeads, iWindow, iUnits, optimization_type, batch)) return false; W0.SetActivationFunction(None);
Attentionブロックの出力では、元のデータで得られた結果を別の層にまとめます。
if(!AttentionOut.Init(0, 0, OpenCL, iWindow * iUnits, optimization_type, batch)) return false; AttentionOut.SetActivationFunction(None);
続いて線形MLPのブロックが続きます。
if(!FF[0].Init(0, 0, OpenCL, iWindow, iWindow, 4 * iWindow, iUnits, optimization_type, batch)) return false; if(!FF[1].Init(0, 0, OpenCL, 4 * iWindow, 4 * iWindow, iWindow, iUnits, optimization_type, batch)) return false; for(int i = 0; i < 2; i++) FF[i].SetActivationFunction(None);
バックプロパゲーションパスの間、親クラスのバッファから内部層のバッファへの誤差勾配の不必要なコピーを避けるため、ポインタをオブジェクトに置き換えます。
Gradient.BufferFree(); delete Gradient; Gradient = FF[1].getGradient(); //--- return true; }
フィードフォワードパスの説明に移りますが、特定の機能を実装する内部層が多数あるにもかかわらず、その関係を直接分析する必要があることに注意してください。この機能は、数学的にはSelf-Attentionブロックと完全に同じですが、Queryエンティティの数がKeyエンティティおよびValueエンティティの数と異なる可能性が高いという事実に直面し、その結果、Score行列が長方形になり、以前に作成したカーネルのロジックに違反します。したがって、新しいカーネルを作ることになります。
フィードフォワードパスでは、MH2AttentionOutカーネルを作成します。カーネルは、データバッファへの4つのポインタと、1つのエンティティ要素のベクトル次元をパラメータとして受け取ります。すべてのエンティティの要素のサイズは同じです。
__kernel void MH2AttentionOut(__global float *q, ///<[in] Matrix of Querys __global float *kv, ///<[in] Matrix of Keys __global float *score, ///<[out] Matrix of Scores __global float *out, ///<[out] Matrix of Scores int dimension ///< Dimension of Key ) { //--- init const int q_id = get_global_id(0); const int k = get_global_id(1); const int h = get_global_id(2); const int qunits = get_global_size(0); const int kunits = get_global_size(1); const int heads = get_global_size(2);
Query、Key、Attentionヘッドという3次元の要素からなるタスク空間でカーネルを起動します。さらに、1つのQuery要素と1つのAttentionヘッド内のすべてのスレッドは、指定されたグループ内でSoftMax関数でScore行列を正規化する必要があるため、グループにまとめられます。
カーネル本体では、まず各スレッドを識別し、グローバルデータバッファ内のオフセットを決定します。
const int shift_q = dimension * (q_id + qunits * h); const int shift_k = dimension * (k + kunits * h); const int shift_v = dimension * (k + kunits * (heads + h)); const int shift_s = q_id * kunits * heads + h * kunits + k;
その他の定数も定義し、ローカル配列を宣言します。
const uint ls = min((uint)get_local_size(1), (uint)LOCAL_ARRAY_SIZE); float koef = sqrt((float)dimension); if(koef < 1) koef = 1; __local float temp[LOCAL_ARRAY_SIZE];
その後、従属係数行列を計算します。
//--- sum of exp uint count = 0; if(k < ls) do { if((count * ls) < (kunits - k)) { float sum = 0; for(int d = 0; d < dimension; d++) sum = q[shift_q + d] * kv[shift_k + d]; sum = exp(sum / koef); if(isnan(sum)) sum = 0; temp[k] = (count > 0 ? temp[k] : 0) + sum; } count++; } while((count * ls + k) < kunits); barrier(CLK_LOCAL_MEM_FENCE);
count = min(ls, (uint)kunits); //--- do { count = (count + 1) / 2; if(k < ls) temp[k] += (k < count && (k + count) < kunits ? temp[k + count] : 0); if(k + count < ls) temp[k + count] = 0; barrier(CLK_LOCAL_MEM_FENCE); } while(count > 1);
//--- score float sum = temp[0]; float sc = 0; if(sum != 0) { for(int d = 0; d < dimension; d++) sc = q[shift_q + d] * kv[shift_k + d]; sc = exp(sc / koef); if(isnan(sc)) sc = 0; } score[shift_s] = sc; barrier(CLK_LOCAL_MEM_FENCE);
また、ベクトルの各要素の依存係数を個別に考慮して、Queryエンティティの新しい値を計算します。
//--- out for(int d = 0; d < dimension; d++) { uint count = 0; if(k < ls) do { if((count * ls) < (kunits - k)) { float sum = q[shift_q + d] * kv[shift_v + d] * (count == 0 ? sc : score[shift_s + count * ls]); if(isnan(sum)) sum = 0; temp[k] = (count > 0 ? temp[k] : 0) + sum; } count++; } while((count * ls + k) < kunits); barrier(CLK_LOCAL_MEM_FENCE); //--- count = min(ls, (uint)kunits); do { count = (count + 1) / 2; if(k < ls) temp[k] += (k < count && (k + count) < kunits ? temp[k + count] : 0); if(k + count < ls) temp[k + count] = 0; barrier(CLK_LOCAL_MEM_FENCE); } while(count > 1); //--- out[shift_q + d] = temp[0]; } }
次に、バックプロパゲーション機能MH2AttentionInsideGradientsを実装するために新しいカーネルを作成します。また、このカーネルを3次元タスク空間で実行します。
カーネルパラメータには、データバッファへの6つのポインタを渡します。これらには、すべてのエンティティのエラーグラデーションバッファが含まれます。
__kernel void MH2AttentionInsideGradients(__global float *q, __global float *q_g, __global float *kv, __global float *kv_g, __global float *scores, __global float *gradient, int kunits) { //--- init const int q_id = get_global_id(0); const int d = get_global_id(1); const int h = get_global_id(2); const int qunits = get_global_size(0); const int dimension = get_global_size(1); const int heads = get_global_size(2);
カーネル本体では、いつものようにスレッドを識別し、必要な定数を作成します。
const int shift_q = dimension * (q_id + qunits * h) + d; const int shift_k = dimension * (q_id + kunits * h) + d; const int shift_v = dimension * (q_id + kunits * (heads + h)) + d; const int shift_s = q_id * kunits * heads + h * kunits; const int shift_g = h * qunits * dimension + d; float koef = sqrt((float)dimension); if(koef < 1) koef = 1;
まず、Valueエンティティの誤差勾配を計算します。これをおこなうには、Attentionブロックの出力から得られる誤差勾配のベクトルに、対応する依存係数を掛けるだけです。
//--- Calculating Value's gradients int step_score = q_id * kunits * heads; for(int v = q_id; v < kunits; v += qunits) { int shift_score = h * kunits + v; float grad = 0; for(int g = 0; g < qunits; g++) grad += gradient[shift_g + g * dimension] * scores[shift_score + g * step_score]; kv_g[shift_v + v * dimension]=grad; }
次に、Queryエンティティの誤差勾配を計算します。今回はまず、SoftMax関数の微分を考慮して、依存係数行列の要素に対する誤差勾配を計算する必要があります。そして、Keyテンソルの対応する要素を掛け合わせます。
//--- Calculating Query's gradients float grad = 0; float out_g = gradient[shift_g + q_id * dimension]; int shift_val = (heads + h) * kunits * dimension + d; int shift_key = h * kunits * dimension + d; for(int k = 0; k < kunits; k++) { float sc_g = 0; float sc = scores[shift_s + k]; for(int v = 0; v < kunits; v++) sc_g += scores[shift_s + v] * out_g * kv[shift_val + v * dimension] * ((float)(k == v) - sc); grad += sc_g * kv[shift_key + k * dimension]; } q_g[shift_q] = grad / koef;
同様に、Keyエンティティの誤差勾配を計算します。しかし今回は、対応するテンソル列に沿って依存係数の誤差勾配を計算します。
//--- Calculating Key's gradients for(int k = q_id; k < kunits; k += qunits) { int shift_score = h * kunits + k; int shift_val = (heads + h) * kunits * dimension + d; grad = 0; float val = kv[shift_v]; for(int scr = 0; scr < qunits; scr++) { float sc_g = 0; int shift_sc = scr * kunits * heads; float sc = scores[shift_sc + k]; for(int v = 0; v < kunits; v++) sc_g += scores[shift_sc + v] * gradient[shift_g + scr * dimension] * val * ((float)(k == v) - sc); grad += sc_g * q[shift_q + scr * dimension]; } kv_g[shift_k + k * dimension] = grad / koef; } }
OpenCLコンテキスト側でアルゴリズムを構築した後、メインプログラム側の処理を整理するためにクラスに戻ります。まず、feedForwardメソッドを見てみましょう。他のニューラル層に関連するメソッドと同様、パラメータには、ソースデータを提供する前のニューラル層へのポインタを受け取ります。
bool CNeuronMH2AttentionOCL::feedForward(CNeuronBaseOCL *NeuronOCL) { //--- if(!Q_Embedding.FeedForward(NeuronOCL)) return false;
ただし、受信したポインタの関連性は確認しません。代わりに、Q_Embeddingの内部層のフィードフォワードメソッドを呼び出して、Queryエンティティのテンソルを作成し、その結果のポインタを渡します。指定されたメソッドの本体では、必要なコントロールはすべてすでに実装されているので、改めて実装する必要はありません。
次に、KeyとValueエンティティを生成します。前述したように、これらには元データのテンソルの異なる次元を使用します。そこで、まずソースデータ行列を転置し、対応する内部層のフィードフォワードメソッドを呼び出します。
if(!Transpose.FeedForward(NeuronOCL) || !KV_Embedding.FeedForward(NeuronOCL)) return false;
MH2AttentionOutカーネルコールは、別のメソッドattentionOutで実装されます。
if(!attentionOut()) return false;
Multi-Head Attention結果テンソルを元のデータサイズに圧縮します。
if(!W0.FeedForward(GetPointer(MHAttentionOut))) return false;
次に、得られた値を元のデータに加え、正規化します。SumAndNormilizeメソッドは親クラスから継承されています。
//--- if(!SumAndNormilize(W0.getOutput(), NeuronOCL.getOutput(), AttentionOut.getOutput(), iWindow)) return false;
Attentionブロックの最後に、MLPにデータを通す。
if(!FF[0].FeedForward(GetPointer(AttentionOut))) return false; if(!FF[1].FeedForward(GetPointer(FF[0]))) return false;
Повторно суммируем и нормализуем.
if(!SumAndNormilize(FF[1].getOutput(), AttentionOut.getOutput(), Output, iWindow)) return false; //--- return true; }
フィードフォワードアルゴリズムのイメージを完成させるために、attentionOutメソッドを考えてみましょう。このメソッドはパラメータを受け取らず、内部クラスのオブジェクトに対してのみ機能します。したがって、メソッド本体では、OpenCLコンテキストへのポインタの関連性のみを確認します。
bool CNeuronMH2AttentionOCL::attentionOut(void) { if(!OpenCL) return false;
次に、タスクスペースとオフセット配列を作成します。カーネルを構築するときに説明したように、2次元目に沿った局所群を持つ3次元の問題空間を作成します。
uint global_work_offset[3] = {0}; uint global_work_size[3] = {iUnits, iWindow, iHeads}; uint local_work_size[3] = {1, iWindow, 1};
必要なパラメータをカーネルに渡します。
ResetLastError(); if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_q, Q_Embedding.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_kv, KV_Embedding.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_score, ScoreIndex)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_out, MHAttentionOut.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgument(def_k_MH2AttentionOut, def_k_mh2ao_dimension, (int)iWindowKey)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; }
次に、カーネルを実行キューに入れます。
if(!OpenCL.Execute(def_k_MH2AttentionOut, 3, global_work_offset, global_work_size, local_work_size)) { printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError()); return false; } //--- return true; }
メインプログラム側とOpenCLコンテキスト側の両方でフィードフォワードパス処理を実装しました。次に、バックプロパゲーションのプロセスをアレンジする必要があります。OpenCLコンテキスト側でアルゴリズムを実装するために、すでにMH2AttentionInsideGradientsカーネルを作成しています。次に、このカーネルを呼び出すためのAttentionInsideGradientsメソッドを作成する必要があります。関連するフィードフォワードメソッドと同様に、メソッドへのパラメータには何も渡しません。
bool CNeuronMH2AttentionOCL::AttentionInsideGradients(void) { if(!OpenCL) return false;
メソッド本体では、OpenCLコンテキストへのポインタの妥当性を確認します。その後、タスク空間の次元とオフセットを示す配列を作成します。
uint global_work_offset[3] = {0}; uint global_work_size[3] = {iUnits, iWindowKey, iHeads};
カーネルに必要なパラメータを渡します。
ResetLastError(); if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionInsideGradients, def_k_mh2aig_q, Q_Embedding.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionInsideGradients, def_k_mh2aig_qg, Q_Embedding.getGradientIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionInsideGradients, def_k_mh2aig_kv, KV_Embedding.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionInsideGradients, def_k_mh2aig_kvg, KV_Embedding.getGradientIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionInsideGradients, def_k_mh2aig_score, ScoreIndex)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionInsideGradients, def_k_mh2aig_outg, MHAttentionOut.getGradientIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgument(def_k_MH2AttentionInsideGradients, def_k_mh2aig_kunits, (int)iWindow)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; }
カーネルを実行キューに入れます。
if(!OpenCL.Execute(def_k_MH2AttentionInsideGradients, 3, global_work_offset, global_work_size)) { printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError()); return false; } //--- return true; }
一般的に、これはこのようなタスクの標準的なアルゴリズムです。そして、誤差勾配を層内部に分配するアルゴリズム全体は、calcInputGradientsメソッドによって記述されます。パラメータとして、このメソッドは、誤差勾配を渡さなければならない前の層のオブジェクトへのポインタを受け取ります。
bool CNeuronMH2AttentionOCL::calcInputGradients(CNeuronBaseOCL *prevLayer) { if(!FF[1].calcInputGradients(GetPointer(FF[0]))) return false;
メソッドの本体では、ブロック出力から前の層に誤差勾配を交互に伝播させます。覚えているように、クラスを初期化するときに、ポインタを誤差勾配バッファに置き換えました。そして後続の層は、内部MLPの最終層に直接誤差勾配を書き込みました。そこから誤差勾配をAttentionブロックの出力レベルに伝播させます。
if(!FF[0].calcInputGradients(GetPointer(AttentionOut))) return false;
このレベルでは、Attentionブロックの結果を初期データに加えました。同様に、2方向からグラデーションを集めます。
if(!SumAndNormilize(FF[1].getGradient(), AttentionOut.getGradient(), W0.getGradient(), iWindow, false)) return false;
次に、誤差勾配を注意の頭全体に伝播させます。
if(!W0.calcInputGradients(GetPointer(MHAttentionOut))) return false;
誤差勾配をエンティティに伝播します。
if(!AttentionInsideGradients()) return false;
KeyとValueから転置層に誤差勾配を伝播させます。フィードフォワードパスでは、ソースデータの行列を転置しました。誤差勾配では、逆の操作をしなければなりません。
if(!KV_Embedding.calcInputGradients(GetPointer(Transpose))) return false;
次に、すべてのエンティティからの誤差勾配を前の層に移さなければなりません。
if(!Q_Embedding.calcInputGradients(prevLayer)) return false;
ここで、誤差の勾配が4スレッドから前の層に行くことに注意してください。
- Query
- Key
- Value
- Attentionブロックを回避する
しかし、私たちの内層のメソッドは、誤差勾配を渡すときに、以前に記録されたデータを削除してしまいます。したがって、Queryから誤差勾配を受け取った後、それを内層バッファのAttentionブロックの出力の誤差勾配に加えます。
if(!SumAndNormilize(prevLayer.getGradient(), W0.getGradient(), AttentionOut.getGradient(), iWindow, false)) return false;
そして、KeyとValueからデータを受け取った後、すべてのスレッドを合計します。
if(!Transpose.calcInputGradients(prevLayer)) return false; if(!SumAndNormilize(prevLayer.getGradient(), AttentionOut.getGradient(), prevLayer.getGradient(), iWindow, false)) return false; //--- return true; }
重みの更新方法はいたってシンプルです。内部層の関連メソッドを呼び出すだけです。
bool CNeuronMH2AttentionOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(!Q_Embedding.UpdateInputWeights(NeuronOCL)) return false; if(!KV_Embedding.UpdateInputWeights(GetPointer(Transpose))) return false; if(!W0.UpdateInputWeights(GetPointer(MHAttentionOut))) return false; if(!FF[0].UpdateInputWeights(GetPointer(AttentionOut))) return false; if(!FF[1].UpdateInputWeights(GetPointer(FF[0]))) return false; //--- return true; }
これで、相互関係のプロセスを整理する方法についての考察を終えます。クラスとそのメソッドの完全なコードは添付ファイルにあります。モデルの訓練とテストのためのエキスパートアドバイザーの構築に進みます。
2.2 モデルアーキテクチャ
ADAPT法の理論的説明からわかるように、提案されたアプローチはかなり複雑な階層構造を持っています。私たちにとって、これは多数の訓練されたモデルを意味します。ここでは、そのアーキテクチャを2つの方法に分けて説明します。まず、エンドポイント予測プロセスに関連する3つのモデルを作成します。
bool CreateTrajNetDescriptions(CArrayObj *encoder, CArrayObj *endpoints, CArrayObj *probability) { //--- CLayerDescription *descr; //--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) return false; } if(!endpoints) { endpoints = new CArrayObj(); if(!endpoints) return false; } if(!probability) { probability = new CArrayObj(); if(!probability) return false; }
環境状態エンコーダーは、1つの状態を表す生の入力データを受け取ります。
//--- Encoder encoder.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; int prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
いつものように、受信データを正規化します。
//--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = MathMax(1000, GPTBars); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
また、埋め込みを生成し、それを過去のシーケンス蓄積バッファに追加します。
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronEmbeddingOCL; { int temp[] = {prev_count}; ArrayCopy(descr.windows, temp); } prev_count = descr.count = GPTBars; int prev_wout = descr.window_out = EmbeddingSize; if(!encoder.Add(descr)) { delete descr; return false; }
次に位置符号化を紹介します。
//--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronPEOCL; descr.count = prev_count; descr.window = prev_wout; if(!encoder.Add(descr)) { delete descr; return false; }
続いて、総合的な注目のブロックが続きます。モデルアーキテクチャを管理する便宜上、ブロックの反復回数に基づいてループを作成します。
for(int l = 0; l < Lenc; l++) { //--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronTransposeOCL; descr.count = prev_count; descr.window = prev_wout; if(!encoder.Add(descr)) { delete descr; return false; }
ADAPT法の著者が提案したアルゴリズムに従って、まずポリライン(この場合は状態)とエージェントの関係を確認します。この方向で相互関係ブロックを使用する前に、結果の情報量を転置する必要があります。そして新しい層を追加します。
//--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMH2AttentionOCL; descr.count = prev_wout; descr.window = prev_count; descr.step = 8; descr.window_out = 16; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
そして、軌道Self-Attentionブロックがやってきます。
//--- layer 6 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMLMHAttentionOCL; descr.count = prev_wout; descr.window = prev_count; descr.step = 8; descr.window_out = 16; descr.layers = 1; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
次に、この関係を別の平面で分析します。このため、データを移調し、Attentionブロックを繰り返します。
//--- layer 7 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronTransposeOCL; descr.count = prev_wout; descr.window = prev_count; if(!encoder.Add(descr)) { delete descr; return false; }
//--- layer 8 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMH2AttentionOCL; descr.count = prev_count; descr.window = prev_wout; descr.step = 8; descr.window_out = 16; descr.layers = 1; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
//--- layer 9 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMLMHAttentionOCL; descr.count = prev_count; descr.window = prev_wout; descr.step = 8; descr.window_out = 16; descr.layers = 1; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; } }
上述したように、エンコーダーブロックをループに入れました。ループの反復回数は定数で与えられます。
#define Lenc 3 //Number ADAPT Encoder blocks
このように、定数を1つ変えるだけで、エンコーダーのAttentionブロックの数を素早く変えることができます。
エンコーダーの結果は、複数のエンドポイントのセットを予測するために使用されます。このようなセットの数はNForecast定数によって決定されます。
#define NForecast 5 //Number of forecast
エンドポイント予測モデルには単純なMLPを使用します。このモデルでは、エンコーダーから受信したデータは 全結合層を通過します。
//--- Endpoints endpoints.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = (prev_count * prev_wout); descr.activation = None; descr.optimization = ADAM; if(!endpoints.Add(descr)) { delete descr; return false; }
//--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = SIGMOID; descr.optimization = ADAM; if(!endpoints.Add(descr)) { delete descr; return false; }
潜在状態はSoftMax関数によって正規化されます。
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronSoftMaxOCL; descr.count = LatentCount; descr.step = 1; descr.activation = None; descr.optimization = ADAM; if(!endpoints.Add(descr)) { delete descr; return false; }
次に、 全結合層のエンドポイントを生成します。
//--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = 3 * NForecast; descr.activation = None; descr.optimization = ADAM; if(!endpoints.Add(descr)) { delete descr; return false; }
軌道を選択する確率を予測するモデルも、エンコーダーの結果を入力データとして使用します。
//--- Probability probability.Clear(); //--- Input layer if(!probability.Add(endpoints.At(0))) return false;
しかしその中では、予測されるエンドポイントを考慮して分析されます。
//--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConcatenate; descr.count = LatentCount; descr.window = prev_count; descr.step = 3 * NForecast; descr.optimization = ADAM; descr.activation = SIGMOID; if(!probability.Add(descr)) { delete descr; return false; }
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = LReLU; descr.optimization = ADAM; if(!probability.Add(descr)) { delete descr; return false; }
確率的な量を使った操作では、モデルの出力でSoftMax層を使用することができます。
//--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = NForecast; descr.activation = None; descr.optimization = ADAM; if(!probability.Add(descr)) { delete descr; return false; }
//--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronSoftMaxOCL; descr.count = NForecast; descr.step = 1; descr.activation = None; descr.optimization = ADAM; if(!probability.Add(descr)) { delete descr; return false; } //--- return true; }
さて、ADAPTメソッドのアルゴリズムに根本的な変更を加えたところまで来ました。私たちの変更は、金融市場の特殊性によって求められています。しかし、私見によれば、それらはこの手法の著者が提案したアプローチと絶対に矛盾するものではありません。
著者は、自律走行車のナビゲーションに関する問題を解くための独自のアルゴリズムを提案しました。ここで重要なのは、軌道予測の質です。なぜなら、軌道のどの部分でも2台以上の車両が衝突すると、重大な結果につながる可能性があるからです。
金融市場取引の場合、コントロールポイントにより多くの注意が払われます。値動きの軌跡や、一般的なトレンドの範囲内での小さな変動にはそれほど関心がありません。私たちにとってより重要なのは、この動きの枠組みの中で可能な最大の利益とドローダウンの両極端です。
そこで、軌跡予測ブロックを除外し、トランザクションのパラメータを生成するActorモデルに置き換えました。同時に、モデルを訓練する一般的なアプローチも維持しました。これは少し後で説明します。
私たちのActorは4つのデータソースを用いて決断を下します。
- 状態の埋め込み
- 口座ステータスの説明
- 予測されるエンドポイントセット
- 予測される各エンドポイントの確率
以前、2つの情報ストリームだけを組み合わせる仕組みを作りました。4つのストリームを組み合わせるために、モデルのカスケードを構築します。
bool CreateDescriptions(CArrayObj *actor, CArrayObj *end_encoder, CArrayObj *state_encoder) { //--- CLayerDescription *descr; //--- if(!actor) { actor = new CArrayObj(); if(!actor) return false; } if(!end_encoder) { end_encoder = new CArrayObj(); if(!end_encoder) return false; } if(!state_encoder) { state_encoder = new CArrayObj(); if(!state_encoder) return false; }
予測されたエンドポイントとその確率のセットをエンドポイント埋め込みに結合します。
//--- Endpoints Encoder end_encoder.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; int prev_count = descr.count = 3 * NForecast; descr.activation = None; descr.optimization = ADAM; if(!end_encoder.Add(descr)) { delete descr; return false; } //--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConcatenate; descr.count = LatentCount; descr.window = prev_count; descr.step = NForecast; descr.optimization = ADAM; descr.activation = LReLU; if(!end_encoder.Add(descr)) { delete descr; return false; }
環境状態の埋め込みと、残高およびポジションのパラメータを組み合わせます。
//--- State Encoder state_encoder.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = GPTBars * EmbeddingSize; descr.activation = None; descr.optimization = ADAM; if(!state_encoder.Add(descr)) { delete descr; return false; } //--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConcatenate; descr.count = LatentCount; descr.window = prev_count; descr.step = AccountDescr; descr.optimization = ADAM; descr.activation = SIGMOID; if(!state_encoder.Add(descr)) { delete descr; return false; }
2つの指定されたモデルの作業結果を、意思決定のためにActorに渡します。
//--- Actor actor.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = LatentCount; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; } //--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConcatenate; descr.count = LatentCount; descr.window = prev_count; descr.step = LatentCount; descr.optimization = ADAM; descr.activation = LReLU; if(!actor.Add(descr)) { delete descr; return false; }
Actor内部では、 全結合層を使用しています。
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = SIGMOID; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
その確率的挙動を生成します。
//--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = 2 * NActions; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; } //--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronVAEOCL; descr.count = NActions; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; } //--- return true; }
ご覧の通り、私たちは可能な限りシンプルなモデルアーキテクチャを使用する予定です。これはADAPT法の利点の1つです。
この記事では、環境との相互作用のためのEAの詳細な説明にこだわらないことにしました。収集したデータの構造や環境との相互作用の方法は変わっていません。もちろん、意思決定のためにモデルを呼び出す順序にも変更が加えられています。その順序を確認するために、コードを勉強することをお勧めします。完全なEAコードは添付ファイルにあります。ただし、モデル訓練EAにはいくつかのユニークな点があります。
2.3 モデルの訓練
今回は前回までとは異なり、1つのEA「...\Experts\ADAPT\Study.mq5」内ですべてのモデルを学習させます。これは、ほとんどすべてのモデルの誤差勾配を環境エンコーダーに転送する必要があるためです。
EAの初期化メソッドは、標準的なスキームに従って構築されています。まず、訓練データセットを読み込みます。
int OnInit() { //--- ResetLastError(); if(!LoadTotalBase()) { PrintFormat("Error of load study data: %d", GetLastError()); return INIT_FAILED; }
その後、2段階で、以前に作成したモデルを読み込み、必要に応じて新しいモデルを作成します。
//--- load models float temp; if(!ADAPTEncoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true) || !ADAPTEndpoints.Load(FileName + "Endp.nnw", temp, temp, temp, dtStudied, true) || !ADAPTProbability.Load(FileName + "Prob.nnw", temp, temp, temp, dtStudied, true) ) { CArrayObj *encoder = new CArrayObj(); CArrayObj *endpoint = new CArrayObj(); CArrayObj *prob = new CArrayObj(); if(!CreateTrajNetDescriptions(encoder, endpoint, prob)) { delete endpoint; delete prob; delete encoder; return INIT_FAILED; } if(!ADAPTEncoder.Create(encoder) || !ADAPTEndpoints.Create(endpoint) || !ADAPTProbability.Create(prob)) { delete endpoint; delete prob; delete encoder; return INIT_FAILED; } delete endpoint; delete prob; delete encoder; }
if(!StateEncoder.Load(FileName + "StEnc.nnw", temp, temp, temp, dtStudied, true) || !EndpointEncoder.Load(FileName + "EndEnc.nnw", temp, temp, temp, dtStudied, true) || !Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true)) { CArrayObj *actor = new CArrayObj(); CArrayObj *endpoint = new CArrayObj(); CArrayObj *encoder = new CArrayObj(); if(!CreateDescriptions(actor, endpoint, encoder)) { delete actor; delete endpoint; delete encoder; return INIT_FAILED; } if(!Actor.Create(actor) || !StateEncoder.Create(encoder) || !EndpointEncoder.Create(endpoint)) { delete actor; delete endpoint; delete encoder; return INIT_FAILED; } delete actor; delete endpoint; delete encoder; //--- }
すべてのモデルを1つのOpenCLコンテキストに転送します。
OpenCL = Actor.GetOpenCL(); StateEncoder.SetOpenCL(OpenCL); EndpointEncoder.SetOpenCL(OpenCL); ADAPTEncoder.SetOpenCL(OpenCL); ADAPTEndpoints.SetOpenCL(OpenCL); ADAPTProbability.SetOpenCL(OpenCL);
モデルアーキテクチャを制御します。
Actor.getResults(Result); if(Result.Total() != NActions) { PrintFormat("The scope of the actor does not match the actions count (%d <> %d)", NActions, Result.Total()); return INIT_FAILED; } //--- ADAPTEndpoints.getResults(Result); if(Result.Total() != 3 * NForecast) { PrintFormat("The scope of the Endpoints does not match forecast endpoints (%d <> %d)", 3 * NForecast, Result.Total()); return INIT_FAILED; } //--- ADAPTEncoder.GetLayerOutput(0, Result); if(Result.Total() != (HistoryBars * BarDescr)) { PrintFormat("Input size of Encoder doesn't match state description (%d <> %d)", Result.Total(), (HistoryBars * BarDescr)); return INIT_FAILED; }
補助バッファを作成します。
if(!bGradient.BufferInit(MathMax(AccountDescr, NForecast), 0) || !bGradient.BufferCreate(OpenCL)) { PrintFormat("Error of create buffers: %d", GetLastError()); return INIT_FAILED; }
モデル訓練開始のカスタムイベントを生成します。
if(!EventChartCustom(ChartID(), 1, 0, 0, "Init")) { PrintFormat("Error of create study event: %d", GetLastError()); return INIT_FAILED; } //--- return(INIT_SUCCEEDED); }
訓練のプロセス自体は、Trainメソッドを用いて構成されます。
void Train(void) { //--- vector<float> probability = GetProbTrajectories(Buffer, 0.9);
メソッドの本体では、まず経験再生バッファから軌道を選択する確率のベクトルを作成します。次に、必要なローカル変数を作成します。
vector<float> result, target; matrix<float> targets, temp_m; bool Stop = false; //--- uint ticks = GetTickCount();
訓練は通常通り、入れ子になったループのシステムで実施されます。外側ループの本体では、軌跡とその上の学習状態のパケットをサンプリングします。
for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter ++) { int tr = SampleTrajectory(probability); int batch = GPTBars + 48; int state = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2 - PrecoderBars - batch)); if(state <= 0) { iter--; continue; } ADAPTEncoder.Clear(); int end = MathMin(state + batch, Buffer[tr].Total - PrecoderBars);
一連の履歴データに対するモデルの学習プロセスは、入れ子になったループの中で構築されます。
for(int i = state; i < end; i++) { bState.AssignArray(Buffer[tr].States[i].state);
環境状態を1つ取り出し、それをエンコーダーに渡します。
//--- Trajectory if(!ADAPTEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
そして、予測されるエンドポイントとその確率のセットを生成します。
if(!ADAPTEndpoints.feedForward((CNet*)GetPointer(ADAPTEncoder), -1, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
if(!ADAPTProbability.feedForward((CNet*)GetPointer(ADAPTEncoder), -1, (CNet*)GetPointer(ADAPTEndpoints))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
次に、エンドポイント訓練のプロセスを整理するために、目標値を生成する必要があります。経験再生バッファから所定の計画深度までの後続の状態を取ります。
targets = matrix<float>::Zeros(PrecoderBars, 3); for(int t = 0; t < PrecoderBars; t++) { target.Assign(Buffer[tr].States[i + 1 + t].state); if(target.Size() > BarDescr) { matrix<float> temp(1, target.Size()); temp.Row(target, 0); temp.Reshape(target.Size() / BarDescr, BarDescr); temp.Resize(temp.Rows(), 3); target = temp.Row(temp.Rows() - 1); } targets.Row(target, t); }
しかし、エンドポイントの定義から想像されるように、最後の状態は使用しません。その代わりに、最も近い極値を探します。まず、各ローソク足の終値の分析状態からの乖離の累計を計算します。そして得られた値に、各バーの高値と安値までの間隔を加えます。計算結果を行列に保存します。
target = targets.Col(0).CumSum(); targets.Col(target, 0); targets.Col(target + targets.Col(1), 1); targets.Col(target + targets.Col(2), 2);
得られた行列から、最も近い極値を見つけます。
int extr = 1; if(target[0] == 0) target[0] = target[1]; int direct = (target[0] > 0 ? 1 : -1); for(int i = 1; i < PrecoderBars; i++) { if((target[i]*direct) < 0) break; extr++; }
見つかった最近接極値からベクトルを形成します。
targets.Resize(extr, 3); if(direct >= 0) { target = targets.Max(AXIS_HORZ); target[2] = targets.Col(2).Min(); } else { target = targets.Min(AXIS_HORZ); target[1] = targets.Col(1).Max(); }
予測されたエンドポイントの集合の中から、偏差が最小のベクトルを決定し、目標値に置き換えます。
ADAPTEndpoints.getResults(result); targets.Reshape(1, result.Size()); targets.Row(result, 0); targets.Reshape(NForecast, 3); temp_m = targets; for(int i = 0; i < 3; i++) temp_m.Col(temp_m.Col(i) - target[i], i); temp_m = MathPow(temp_m, 2.0f); ulong pos = temp_m.Sum(AXIS_VERT).ArgMin(); targets.Row(target, pos);
得られた行列を使って、目標点を予測するモデルを訓練します。
Result.AssignArray(targets); //--- if(!ADAPTEndpoints.backProp(Result, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
誤差勾配をエンコーダーモデルに伝播し、そのパラメータを更新します。
if(!ADAPTEncoder.backPropGradient((CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
ここでは、軌道確率を予測するモデルも訓練します。しかし、その誤差勾配は他のモデルには伝わりません。
bProbs.AssignArray(vector<float>::Zeros(NForecast)); bProbs.Update((int)pos, 1); bProbs.BufferWrite(); if(!ADAPTProbability.backProp(GetPointer(bProbs), GetPointer(ADAPTEndpoints))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
エンドポイント予測モデルのパラメータを更新した後、Actorの方策の訓練に移ります。この段階でActorのフィードフォワード操作を実行するために必要なのは、口座とポジションの状態を記述するテンソルだけです。このテンソルを形成してみましょう。
//--- Policy float PrevBalance = Buffer[tr].States[MathMax(i - 1, 0)].account[0]; float PrevEquity = Buffer[tr].States[MathMax(i - 1, 0)].account[1]; bAccount.Clear(); bAccount.Add((Buffer[tr].States[i].account[0] - PrevBalance) / PrevBalance); bAccount.Add(Buffer[tr].States[i].account[1] / PrevBalance); bAccount.Add((Buffer[tr].States[i].account[1] - PrevEquity) / PrevEquity); bAccount.Add(Buffer[tr].States[i].account[2]); bAccount.Add(Buffer[tr].States[i].account[3]); bAccount.Add(Buffer[tr].States[i].account[4] / PrevBalance); bAccount.Add(Buffer[tr].States[i].account[5] / PrevBalance); bAccount.Add(Buffer[tr].States[i].account[6] / PrevBalance); double time = (double)Buffer[tr].States[i].account[7]; double x = time / (double)(D'2024.01.01' - D'2023.01.01'); bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0)); x = time / (double)PeriodSeconds(PERIOD_MN1); bAccount.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0)); x = time / (double)PeriodSeconds(PERIOD_W1); bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0)); x = time / (double)PeriodSeconds(PERIOD_D1); bAccount.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0)); if(bAccount.GetIndex() >= 0) bAccount.BufferWrite();
次に、Actorモデルのカスケードのフィードフォワードメソッドを順次呼び出します。
//--- State embedding if(!StateEncoder.feedForward((CNet *)GetPointer(ADAPTEncoder), -1, (CBufferFloat*)GetPointer(bAccount))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
ここで注意しなければならないのは、エンドポイントセットの予測値とその確率の代わりに、目標値のテンソルを使用することです。
//--- Endpoint embedding if(!EndpointEncoder.feedForward(Result, -1, false, (CBufferFloat*)GetPointer(bProbs))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
//--- Actor if(!Actor.feedForward((CNet *)GetPointer(StateEncoder), -1, (CNet*)GetPointer(EndpointEncoder))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
フィードフォワードパスの後、モデルのパラメータを更新する必要があります。そのためには目標値が必要です。ADAPT法によれば、モデルは経験再生バッファからの実データで軌道を予測するように訓練されなければなりません。以前と同じように、経験再生バッファからエージェントのアクションを取ることができます。しかしこの場合、そのような行動を評価し、優先順位をつけるメカニズムがありません。
このような状況で、私は違うアプローチを取ることにしました。訓練データセットから、その後の値動きの実データに基づく目標終値がすでにあるのだから、それを使って分析条件下での「最適」な取引を生成してはどうでしょう。「最適」な取引の方向性と取引レベルを決定します。取引ごとに資本の1%のリスクを考慮してポジション量を決定します。
result = vector<float>::Zeros(NActions); double value = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE_LOSS); double risk = AccountInfoDouble(ACCOUNT_EQUITY) * 0.01; if(direct > 0) { float tp = float(target[1] / _Point / MaxTP); result[1] = tp; int sl = int(MathMax(MathMax(target[1] / 3, -target[2]) / _Point, MaxSL/10)); result[2] = float(sl) / MaxSL; result[0] = float(MathMax(risk / (value * sl), 0.01))+FLT_EPSILON; } else { float tp = float((-target[2]) / _Point / MaxTP); result[4] = tp; int sl = int(MathMax(MathMax((-target[2]) / 3, target[1]) / _Point, MaxSL/10)); result[5] = float(sl) / MaxSL; result[3] = float(MathMax(risk / (value * sl), 0.01))+FLT_EPSILON; }
ポジション量を計算する際には、取引時に口座にすでにポジションがある場合があり、その利益(損失)は口座残高に考慮されないため、エクイティを使用します。
こうして生成された「最適」なポジションは、Actorモデルの訓練に使用されます。
Result.AssignArray(result); if(!Actor.backProp(Result, (CNet *)GetPointer(EndpointEncoder)) || !StateEncoder.backPropGradient(GetPointer(bAccount), (CBufferFloat *)GetPointer(bGradient)) || !EndpointEncoder.backPropGradient(GetPointer(bProbs), (CBufferFloat *)GetPointer(bGradient)) ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
Actorrモデルの訓練による誤差勾配を利用して、Encoderパラメータを更新します。
if(!ADAPTEncoder.backPropGradient((CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; }
なお、現段階ではエンドポイント予測モデルのパラメータは更新していません。この制限はADAPT法の著者によって導入されたもので、モデル学習の安定性を高めるために設計されています。
すべてのモデルのパラメータを更新した後、必要なのは訓練の進捗状況をユーザーに知らせ、ループシステムの次の反復に移ることだけです。
//--- if(GetTickCount() - ticks > 500) { double percent = (double(i - state) / ((end - state)) + iter) * 100.0 / (Iterations); string str = StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Actor", percent, Actor.getRecentAverageError()); str += StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Endpoints", percent, ADAPTEndpoints.getRecentAverageError()); str += StringFormat("%-14s %6.2f%% -> Error %15.8f\n", "Probability", percent, ADAPTProbability.getRecentAverageError()); Comment(str); ticks = GetTickCount(); } } }
メソッドの最後に、チャートのコメントフィールドを消去します。モデルの訓練結果を操作ログに記録します。それからEAを終了させます。
Comment(""); //--- PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Actor", Actor.getRecentAverageError()); PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Endpoints", ADAPTEndpoints.getRecentAverageError()); PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Probability", ADAPTProbability.getRecentAverageError()); ExpertRemove(); //--- }
以上で、私たちの考えるアルゴリズムのMQL5実装の説明を終えます。この記事で使用されているすべてのプログラムの完全なコードは添付ファイルにあります。
3.テスト
MQL5を使ってADAPTメソッドを実装するために、かなり多くの作業をおこないました。私たちの実装は、オリジナルのアルゴリズムとはかけ離れています。とはいえ、これは提案されたアプローチの精神に則ったものであり、分析されたシーンのオブジェクト間の関係の包括的な分析に関連する独自のアイデアを利用したものです。それではいよいよ、ストラテジーテスターで実際履歴データを使い、作業結果をテストしてみましょう。
モデルはEURUSDのH1、2023年の最初の7ヶ月間の履歴データを使用して訓練されました。すべての指標はデフォルトのパラメータで使用されています。
訓練されたモデルは、訓練パラメータを完全に遵守してテストされました。履歴データの期間を変えただけです。この段階では、2023年8月からの履歴データを使用しました。
環境との相互作用の過程で収集されるデータの構造は変わらないので、実験では新たな訓練データは収集しませんでした。モデルの訓練には、以前のモデルを訓練したときに収集したパスを使用します。さらに、「最適」の取引を計算する提案されたアプローチにより、訓練データ空間を洗練し補足する追加パスの計算を避けることができます。
ここで、モデルを訓練するには1回のパスで十分だと思われるかもしれません。しかし、訓練の過程では、口座の状態やポジションに関する情報など、できるだけ多様な情報をモデルに提供する必要があります。
テストの結果に基づいて、検討した方法の有効性について結論を下すことができます。モデルが単純であるため、モデルを迅速に訓練することができます。提案されたアプローチの有効性は、訓練済みモデルの結果によって確認され、訓練データセットとテストデータセットの両方で利益を生み出す能力が示されました。
結論
本稿で取り上げるADAPT法は、様々な複雑なシナリオにおけるエージェントの軌道を予測する革新的なアプローチです。このアプローチは効率的で、少量の計算資源で済み、シーン内の各エージェントに対して高品質の予測を提供します。
ADAPT法の改良点としては、サイズを大きくすることなくモデルの容量を増やす適応型ヘッドや、各エージェントの個々の状況によりよく適応するための重みの動的学習の使用などがあります。これらの技術革新は、効果的な軌道予測に大きく貢献しています。
本稿の実用的な部分では、MQL5を使用して提案されたアプローチのビジョンを実装しました。実際の履歴データを使ってモデルを訓練し、テストしました。得られた結果に基づき、ADAPT法の有効性と、そのバリエーションを用いてモデルを構築し、金融市場で運用する可能性について結論を下すことができます。
しかし、この記事で紹介されているプログラムは、あくまでも技術のデモンストレーションを目的としたものであり、実際の金融取引に使用できるものではないことをお断りしておきます。
参照文献
記事で使用されているプログラム
# | ファイル名 | 種類 | 詳細 |
---|---|---|---|
1 | Research.mq5 | EA | コレクションEAの例 |
2 | ResearchRealORL.mq5 | EA | Real-ORL法による事例収集のためのEA |
3 | Study.mq5 | EA | モデル訓練EA |
4 | Test.mq5 | EA | モデルをテストするEA |
5 | Trajectory.mqh | クラスライブラリ | システム状態記述の構造体 |
6 | NeuroNet.mqh | クラスライブラリ | ニューラルネットワークを作成するためのクラスのライブラリ |
7 | NeuroNet.cl | コードベース | OpenCLプログラムコードライブラリ |
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/14143
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索