取引におけるニューラルネットワーク:適応型モード分解を用いた時系列予測(ACEFormer)
はじめに
金融市場は、数多くの要因が複雑に相互作用することで形成される、非常に複雑かつ動的なシステムです。そこには、マクロ経済情報の流れ、企業固有のニュース、投資家心理の変化、さらにはアルゴリズム取引戦略の冷徹な計算まで、ほぼあらゆる要素が反映されています。この膨大なシグナル、ノイズ、そして歪みが混在する環境の中から、有益な情報を抽出し、本質的な市場トレンドを識別することは、単なる興味深い研究課題ではなく、戦略的に重要な課題となっています。
市場の方向性を正確に予測する能力は、持続的な競争優位性をもたらす可能性があります。その中でも最大の課題の1つが情報ノイズです。これは、短期的な取引、ニュース速報、またはランダムなアルゴリズム取引によって発生する、頻繁かつ意味を持たない価格の微細な変動を指します。これらの変動は、分析モデルが本質的なトレンドを正しく捉えることを妨げる要因となります。
予測モデルの開発は、20世紀後半から始まりました。初期のニューラルネットワークアーキテクチャは、市場変動を予測するモデルを学習できる可能性を示しました。しかし、これらの手法には長期間にわたる情報保持能力が不足しており、わずかに過去の出来事であっても、その影響を急速に失ってしまうという問題がありました。
LSTMネットワークの登場により、この状況は改善されました。メモリ機構を備えたLSTMモデルは、長期間にわたって重要なパターンを保持することが可能となり、時系列予測分野で広く利用されるようになりました。しかし、問題はそれほど単純ではありません。金融時系列は、一般的なシーケンシャルデータとは大きく異なります。ティック間の間隔が不均一で、不規則に観測されることが多くなり、また、市場トレンドの本質とは関係の薄い、一時的な急激なスパイクが多数含まれています。
特に高頻度取引は、非常に大きな課題をもたらします。高頻度取引によって発生する、極めて短い時間間隔で繰り返される価格変動は、一般的にマーケットノイズと呼ばれます。これらの変動は、本来存在するトレンドを覆い隠し、データの不安定性を高め、予測モデルに重要性の低いイベントを大量に処理させる原因となります。その結果、高度なニューラルネットワークであっても、本当に重要な情報ではなく、不要な短期変動へ過剰に注目してしまう可能性があります。
このような課題に対応するため、論文「An End-to-End Structure with Novel Position Mechanism and Improved EMD for Stock Forecasting」では、ACEFormerフレームワークが提案されています。これは、高頻度取引環境における金融時系列解析を目的として設計された統合型アーキテクチャです。ACEFormerは単一の予測モデルではなく、複数の補完的なコンポーネントを組み合わせた構造を持っています。それぞれのコンポーネントは、ノイズ除去、不規則な時間間隔のモデリング、そして最も情報量の多い市場変化への選択的な注目という、異なる課題を担当します。
ACEFormerアーキテクチャの第1段階では、データのノイズを除去します。ここでは、改良版ACEEMD (Alias Complete Ensemble Empirical Mode Decomposition with Adaptive Noise)アルゴリズムが使用されます。この手法は経験的モード分解(EMD)を基盤としながら、複数の改良を加えたものです。これにより、従来のEMDが抱えていた主な問題である端点効果とモード混合を軽減します。高周波振動の大部分を含む第1の固有モード関数 (IMF, Intrinsic Mode Function)を除去することで、ACEEMDは市場ノイズを効果的に抑制しながら、基礎的なトレンドを特徴付ける重要な転換点を保持します。
この事前フィルタリング処理の後、ノイズ除去されたデータは時間認識モジュールへ入力されます。金融市場のイベントは不規則な時間間隔で発生するため、従来のAttention機構だけでは十分に適切なモデル化が困難です。この問題に対処するため、著者らはTime-Awareモジュールを導入しました。このモジュールは、特徴量の値を処理する際に、観測間の経過時間を明示的に考慮します。これにより、モデルはイベント系列をより正確に捉え、イベント間の因果的な関係を理解しやすくなります。
その後、抽出された特徴量は強化型Attentionブロックによって処理されます。標準的なAttention機構とは異なり、このモジュールは、重要な変化点を識別しながら意味のない変動を無視する必要がある金融データ向けに設計されています。時系列内の情報価値の高い領域をより強調することで、モデルはノイズ要素に注意を分散させることなく、将来予測に関連する可能性の高い情報へ集中できます。
最終段階では、全結合ニューラルネットワークが使用されます。このネットワークは、抽出された特徴量を統合し、将来の価格方向に関する最終予測を生成します。このように、ACEFormerアーキテクチャは、ノイズ除去、時間的モデリング、Attentionによる特徴抽出、そして最終的な予測生成まで、金融時系列予測に必要な一連の処理パイプライン全体を包含しています。
ACEFormerアルゴリズム
ACEFormerアルゴリズムは、金融市場における価格変動を高精度に予測することを目的とした、多段階の時系列データ処理フレームワークです。その基本的な考え方は、市場ノイズを段階的かつ適応的に抑制した後、情報価値の高い特徴量を抽出し、長期的な市場トレンドを考慮した予測を生成することにあります。このアプローチは、重要なシグナルが多数のランダムな変動や市場ノイズによって覆い隠される高頻度取引環境において、特に有効です。
処理は、入力時系列𝑆={𝑠1,𝑠2,…,𝑠𝑛}から開始されます。ここで、各ベクトル𝑠𝑖は、時刻(i)に観測された価格、取引量、およびその他の市場指標を含みます。モデル学習用のデータを準備するため、系列の末尾には長さ(p)のゼロパディング領域が追加されます。これにより、入力データ内に未来情報が明示的に存在しない状態でも、モデルは将来の(p)ステップを予測できるようになります。生成される入力データは、𝐷=[𝑠1,𝑠2,…,𝑠𝑛,0,0,…,0] ∈ 𝑅(𝑛+𝑝)×𝑑として表されます。ここで、𝑑は特徴量数を示します。このゼロ領域は、系列構造を維持しながら、モデルが将来値を予測するための基盤となります。
次の段階では、畳み込みフィルタを用いた信号平滑化処理が実行されます。2つの畳み込みフィルタ𝑓および𝑔を順番に適用することで、ランダムな変動を低減し、入力系列を安定化します。この前処理により、一時的な価格スパイクの影響が抑制され、その後のモデル処理に入力されるデータ品質が向上します。
初期平滑化処理の後、高周波ノイズを抑制するために、適応型のACEEMDアルゴリズムが適用されます。この処理では、まず入力時系列の各要素に対してガウスノイズ𝑛𝑖(𝑡)を加算および減算します。その結果、𝑝𝑒𝑖(𝑡)=𝑥(𝑡)+𝑛𝑖(𝑡)と𝑝𝑚𝑖(𝑡)=𝑥(𝑡)−𝑛𝑖(𝑡)の2つの新しい系列が生成されます。
生成されたそれぞれの系列に対して経験的モード分解(EMD)を実行し、第1の固有モード関数(IMF)を抽出します。
両方の分解結果から得られたIMF成分は加算され、その成分を元の信号から減算します。これにより、ノイズ除去後の時系列𝑟1(𝑡)=𝑥(𝑡)−IMF1(𝑡)が得られます。このノイズ除去済み時系列は、その後の処理段階へ送られます。
次に、モデルがイベントの時間的順序を理解できるようにするため、入力表現へ位置エンコーディングを追加します。これにより、系列内における各データの位置情報が保持されます。その後、データに対して線形射影処理が実行されます。
ACEFormerアーキテクチャの特徴的な要素の1つが、Probabilistic Attentionモジュールです。この機構は、モデルの汎化性能向上において中心的な役割を果たします。Probabilistic Attentionは、従来のSelf-Attention機構を計算効率化した方式であり、重要性の低いAttention接続を排除します。系列全体に対してAttention計算をおこなうのではなく、情報価値の高い時間ステップのみへ処理対象を限定します。このため、まず各位置に対する重要度スコアを推定します。ACEFormerでは、このスコアは、各QueryをランダムにサンプリングされたKey集合へ投影した際の最大値として定義されます。その後、正規化し、情報量の高い位置が選択されます。そしてSelf-Attention計算は、この選択された部分集合に対してのみ実行されます。結果として、Attention処理は系列全体ではなく、有益な時間的イベントを含む可能性が高い、より小規模な部分集合に対して適用されます。
ACEFormerにおけるProbabilistic Attentionモジュールは、単なる計算量削減のための技術的最適化ではありません。これは、時間とともに重要な依存関係が変化する動的な市場環境へ、モデルをより効果的に適応させるための戦略的な設計です。この方式により、ノイズが多くボラティリティの高い金融データに対して、より堅牢で信頼性の高い予測を生成できます。
その結果、Probabilistic AttentionによってACEFormerモデルは、本当に重要なパターンへ集中しながら、不要な依存関係やランダムな市場変動を効果的に除外できます。これにより、有意義な関係性の抽出能力が向上し、特に金融市場における将来の価格方向予測において、高い精度を実現できます。
Probabilistic Attention処理の後、生成された特徴表現は畳み込み層および最大プーリング処理によってさらに加工されます。これらの処理により、局所的な特徴抽出能力が向上し、重要なパターンをより効果的に表現できるようになります。畳み込み処理は、時系列内に存在する予測に有用なシグナル領域を強調します。
最終的なデータ処理段階では、通常のSelf-Attention機構が適用されます。このモジュールにより、系列内の各要素はグローバルな文脈情報へアクセスできるようになり、長い時間間隔で隔てられたイベント間の依存関係を学習できます。
指定された予測期間に対する予測値を取得するために、全結合ネットワークが使用されます。
総合すると、ACEFormerアルゴリズムは、ノイズ除去から始まり、最終的な高精度予測生成に至るまで、複数の処理段階で構成されています。各段階がそれぞれ役割を果たすことで、モデルはノイズが多く変動性の高い金融時系列を処理し、長期的な市場トレンドを識別し、将来の価格変動を高精度に予測する能力を獲得します。
著者らが作成したACEFormerフレームワークの可視化を以下に示します。

MQL5での実装
ACEFormerフレームワークの理論的基盤を検討したので、次にMQL5での実際の実装に進みます。まず、Probabilistic Attentionモジュールから実装を開始します。このモジュールはアーキテクチャの中核を構成する要素の1つであり、データ表現の品質を維持しながら高い計算効率を実現します。
実装の詳細に入る前に、Probabilistic Attentionの概念的な利点について改めて確認しておきます。この機構は、予測精度と計算効率のバランスをとる手法です。系列全体を処理する従来のAttentionとは異なり、Probabilistic Attentionでは情報価値の高い要素のみを選択的に処理対象とします。この戦略により、特に長大な系列を処理する場合において、モデル品質を維持したままメモリ使用量と計算コストを大幅に削減できます。
本記事で紹介する実装では、処理を3つのカーネルに分割しています。これらのカーネルは順番に実行され、それぞれ異なる役割を担当します。具体的には、重要度の推定、重要なQueryの選択、そして最終的な文脈表現の計算までを段階的に処理します。それでは、この処理パイプラインを順番に確認していきます。
第1段階では、各Queryの重要度を推定します。この処理はProbAttentionQueryImpカーネルによって実行されます。このカーネルは、以下の入力を受け取ります。
- Query行列(querys)
- KeyとValueの結合行列(keys_values)
- 各Queryに対応するサンプリング済みKeyのインデックスを格納するインデックス配列index_keys
ここでいうKeyとは、 Queryの重要度を推定するためにランダムにサンプリングされた要素を指します。このサンプリング処理は、最終的なAttentionスコアの計算ではなく、統計的評価のみを目的としています。つまり、代表的なKey集合に対して各Queryがどの程度強く反応するかを測定するために使用されます。
__kernel void ProbAttentionQeuryImp(__global const float* querys, __global const float2* __attribute__((aligned(8))) keys_values, __global const float* index_keys, __global float* querys_imp, const int dimension ) { const size_t id_q = get_global_id(0); const size_t total_q = get_global_size(0); const size_t ind_k = get_local_id(1); const size_t total_ind = get_local_size(1); const size_t id_h = get_global_id(2); const size_t total_h = get_global_size(2);
このカーネルは3次元のタスク空間上で実行されます。それぞれの次元は、並列計算を構成する上で異なる役割を持っています。第1次元はQuery系列全体に対応します。第2次元は、各Queryに関連付けられたサンプリング済みKeyの数を表します。この数は、モデルの設定や解析の深さに応じて変化します。第3次元はAttention Headを表します。Attention Headは独立した処理単位であり、それぞれが入力系列の異なる側面を同時に解析します。
特に注目すべき点は、Attention Headの動作方式です。各ヘッドは、それぞれ独自にサンプリングされたKeyの部分集合を使用して処理します。この設計により、同一の系列に対して複数の補完的な視点を得ることが可能になります。その結果、各ヘッドは異なる関係性や構造的パターンを発見できます。これにより、アーキテクチャ全体の頑健性が向上します。あるヘッドが系列内の重要な領域を十分に評価できなかった場合でも、別のヘッドがその情報を捕捉できる可能性があります。複数のAttention Headが協調して処理することで、元のシグナルに対してより豊かで表現力の高い表現が生成され、Attention機構の品質および得られるコンテキスト情報の有用性が大きく向上します。
実行スレッドは、第2次元に沿ってワークグループに編成されます。各ワークグループ内で並列スレッド間のデータを効率的に共有するため、OpenCLデバイスのローカルメモリ上に共有配列を確保します。
__local float temp[LOCAL_ARRAY_SIZE][2]; const int ls = min((int)total_ind, (int)LOCAL_ARRAY_SIZE);
次の段階では、現在の実行スレッドに対応する入力バッファ内のオフセットを計算します。Queryバッファへのオフセットは、第1次元のスレッド識別子から直接決定されます。一方、Keyバッファへのオフセットは、まずインデックスバッファからサンプリングされたKeyのインデックスを取得し、その後、そのインデックスを対応するバッファオフセットへ変換することで計算されます。
const int shift_q = dimension * (id_q * total_h + id_h); const int id_k = index_keys[total_ind * id_q * total_h + ind_k * total_h + id_h]; const int shift_k = dimension * (id_k * total_h + id_h);
各Query–Keyペアに対して、カーネルは両者の内積を計算します。この内積は、それらの類似度を評価する指標となります。この処理では、要素ごとの乗算を実行した後、その結果得られた積を累積します。
float sum = 0; #pragma unroll for(int d = 0; d < dimension; d++) sum += IsNaNOrInf(querys[shift_q + d] * keys_values[shift_k + d].s0, 0);
次に、共有ローカルメモリ配列を利用して、各ワークグループ内のスレッドが協調的にこれらの内積値の合計値および最大値を計算します。この並列リダクション処理により、各サンプリング済み部分集合に対する集約統計量を効率的に取得できます。
int id_t = ind_k % ls; #pragma unroll for(int i = 0; i < total_ind; i += ls) { if(i <= ind_k || (i + ls) > ind_k) { temp[id_t][0] = IsNaNOrInf((i == 0 ? 0 : temp[id_t][0]) + sum, 0); temp[id_t][1] = (i == 0 ? IsNaNOrInf(sum, MIN_VALUE) : fmax(temp[id_t][1], IsNaNOrInf(sum, MIN_VALUE))); barrier(CLK_LOCAL_MEM_FENCE); } } int count = ls; #pragma unroll do { count = (count + 1) / 2; if(ind_k < count && (ind_k + count) < ls) { temp[ind_k][0] += temp[ind_k + count][0]; temp[ind_k + count][0] = 0; temp[ind_k][1] = fmax(temp[ind_k + count][1], temp[ind_k][1]); } barrier(CLK_LOCAL_MEM_FENCE); } while(count > 1);
その後、現在のQueryに対する重要度スコアを、最大内積値と平均内積値の差として計算します。この値が大きいほど、そのQueryはより多くの有益な情報を含むと判断されます。計算された重要度スコアは、出力バッファquerys_impに保存されます。
if(ind_k == 0) querys_imp[id_q * total_h + id_h] = IsNaNOrInf(temp[0][1] - temp[0][0] / total_ind, MIN_VALUE); }
次の段階では、情報量の高いQueryを選択します。この処理はTopKImportanceToIndexカーネルによって実行されます。計算コストの高いソートアルゴリズムを使用する代わりに、本実装ではシンプルでありながら堅牢なランキング方式を採用しています。
各Queryについて、カーネルは並列処理によって、自身より高い重要度スコアを持つQueryの数をカウントします。この数が指定された閾値top_kより小さい場合、現在のQueryは最終的なインデックスリストへ追加されます。この方法は単純な構造であるにもかかわらず、GPU実行に非常に適しています。これは、最小限の同期処理しか必要とせず、補助的なデータ構造にも依存しないためです。
__kernel void TopKImportanceToIndex(__global const float* importance, __global float* indexes, const int top_k ) { const size_t id_q = get_global_id(0); const size_t total_q = get_global_size(0); const size_t id_h = get_global_id(1); const size_t total_h = get_global_size(1); //--- float imp = importance[id_q * total_h + id_h]; int pos = 0; #pragma unroll for(int i = 0; i < total_q; i++) { if(i == id_q) continue; float val = importance[i * total_h + id_h]; if(val > imp || (i < id_q && val >= imp)) pos++; if(pos >= top_k) break; } //--- if(pos < top_k) indexes[pos * total_h + id_h] = (float)id_q; }
第3段階となる最終処理では、実際のAttention計算を実行します。この処理はQIndexAttentionカーネルによって実装されます。その目的は、選択された各Queryに対して最終的なコンテキスト表現を生成することです。
このカーネルは、Query、Key、Valueの完全な集合を入力として受け取ります。前述したように、本実装における重要な設計方針の1つは、選択されたQuery部分集合の追加コピーを作成しないことです。これは、メモリ使用量を削減し、計算効率を向上させるために重要です。その代わりに、カーネルは前段階で識別された情報量の高いQueryへの参照を格納したインデックスバッファを利用して処理します。
また、KeyとValueのトークンは、単一のデータバッファ内にまとめて格納されています。この配置により、メモリアクセスパターンが簡素化され、キャッシュ利用効率が向上します。具体的には、float2ベクトル型を使用しており、第1要素にはKey、第2要素には対応するValueが格納されます。各Key–Valueペアを1つの論理的な単位として扱うことで、メモリアクセスに伴うオーバーヘッドを削減し、よりコンパクトで効率的な実装を実現しています。
__kernel void QIndexAttention(__global const float *q,
__global const float2* kv,
__global float *scores,
__global const float *indexes,
__global float *out,
const int dimension,
const int heads_kv
)
{
//--- init
const int ind_q = get_global_id(0);
const int k = get_local_id(1);
const int h = get_global_id(2);
const int total_q = get_global_size(0);
const int total_k = get_local_size(1);
const int heads = get_global_size(2);
このカーネルも、3次元のタスク空間上で実行されます。ただし、第1次元のQueryは、選択された情報量の高いトークンの部分集合のみを処理対象とします。第2次元のKeyは、系列全体を対象とします。これまでと同様に、実行スレッドは第2次元に沿ってワークグループへ編成されます。
カーネル内部では、まずタスク空間のすべての次元における現在の実行スレッドを識別します。取得した識別子を使用して、入力バッファ内の対応するオフセットを計算します。
const int h_kv = h % heads_kv; const int q_id = (int)(indexes[ind_q * heads + h] + 0.001f); const int shift_q = dimension * (q_id * heads + h); const int shift_kv = dimension * (heads_kv * k + h_kv); const int shift_s = total_k * (ind_q * heads + h) + k;
ここで注目すべき点は、 Queryバッファへのオフセットを計算する前に、カーネルがまず、選択された重要度の高い要素を格納したバッファから対応するQueryインデックスを取得していることです。
その後、同一ワークグループに属するスレッド間でデータを共有できるように、ローカルメモリ上へ共有配列を確保します。
__local float temp[LOCAL_ARRAY_SIZE]; const uint ls = min((uint)total_k, (uint)LOCAL_ARRAY_SIZE);
最初の計算段階では、QueryベクトルとKeyベクトル間の内積を計算し、中間値の配列を生成します。これらの値は一般的にRaw Score(生スコア)と呼ばれます。これらのスコアは、各Query–Keyペア間の関連性を定量的に評価するものであり、その後に実行されるAttention計算の基礎となります。
//--- Score float score = 0; if(q_id >= 0) { #pragma unroll for(int d = 0; d < dimension; d++) score += IsNaNOrInf(q[shift_q + d] * kv[shift_kv + d].s0, 0); }
計算の安定性を向上させ、数値的な不安定性を防ぐためにSoftmax正規化は修正版として実装されています。まず、各ワークグループ内において、すべてのScoreの中から最大値を探索します。
//--- max of score #pragma unroll for(int i = 0; i < total_k; i += ls) { if(k >= i && k < (i + ls)) temp[k % ls] = (i == 0 ? score : fmax(temp[k % ls], score)); barrier(CLK_LOCAL_MEM_FENCE); } //--- uint count = ls; #pragma unroll do { count = (count + 1) / 2; if(k < count && (k + count) < ls) temp[k] = fmax(temp[k + count], temp[k]); barrier(CLK_LOCAL_MEM_FENCE); } while(count > 1);
次に、各Scoreからこの最大値を減算することで、値のシフト処理をおこないます。この変換により、すべての指数値が0以下になるため、指数関数計算におけるオーバーフローを防止できます。その結果、指数値は0から1の範囲内に収まります。
score = IsNaNOrInf(exp(score - temp[0]), 0);
その後、指数値の合計を計算し、各値をその合計値で除算します。これにより、正規化されていないRaw Scoreは、最終的なAttention重みに変換されます。
//--- sum of exp #pragma unroll for(int i = 0; i < total_k; i += ls) { if(k >= i && k < (i + ls)) temp[k % ls] = (i == 0 ? 0 : temp[k % ls]) + score; barrier(CLK_LOCAL_MEM_FENCE); } //--- count = ls; #pragma unroll do { count = (count + 1) / 2; if(k < count && (k + count) < ls) { temp[k] += temp[k + count]; temp[k + count] = 0; } barrier(CLK_LOCAL_MEM_FENCE); } while(count > 1); //--- score if(temp[0] > 0) score /= temp[0]; scores[shift_s] = score;
最後に、これらのAttention重みをValueテンソルへ適用します。重み付けされたValueベクトルは累積処理され、単一のコンテキストベクトルが生成されます。このコンテキストベクトルは、現在のQueryの視点から見た入力系列の意味表現を保持します。
//--- out #pragma unroll for(int d = 0; d < dimension; d++) { float val = kv[shift_kv + d].s1 * score; #pragma unroll for(int i = 0; i < total_k; i += ls) { if(k >= i && k < (i + ls)) temp[k % ls] = (i == 0 ? 0 : temp[k % ls]) + val; barrier(CLK_LOCAL_MEM_FENCE); } //--- uint count = ls; #pragma unroll do { count = (count + 1) / 2; if(k < count && (k + count) < ls) { temp[k] += temp[k + count]; temp[k + count] = 0; } barrier(CLK_LOCAL_MEM_FENCE); } while(count > 1); //--- if(k == 0) out[dimension * (ind_q * heads + h) + d] = temp[0]; barrier(CLK_LOCAL_MEM_FENCE); } }
これまで説明した一連の処理は、一貫性があり、かつ高効率なProbabilistic Attention機構を実現しています。処理は、 Queryの重要度を高速かつ近似的に推定する段階から始まり、情報価値の高いQueryを選択し、最後に限定された、しかし有益な部分集合に対してのみ完全なAttention計算を実行します。このアプローチにより、長大な系列の処理速度を向上させるだけでなく、高い予測精度も維持できます。同時に、中間データ量およびグローバルメモリへのアクセス回数を大幅に削減できます。
ただし、ここまで説明した処理はフォワードパス、つまり入力データからモデルが予測を生成する段階のみを対象としています。モデルを学習可能にするためには、さらにバックプロパゲーションを実装する必要があります。これにより、各コンポーネントの学習可能なパラメータを、モデルの最終出力への貢献度に応じて更新できるようになります。
本実装では、アーキテクチャ上の判断として、勾配伝播をAttention機構のみに限定し、Query選択段階はバックプロパゲーションの対象から除外しました。一見すると単純化のように見えますが、これは基礎となる計算構造を考慮した上で慎重に選択した設計方針です。
前述した2つの段階、すなわち情報価値の高いQueryの選択と、実際のAttention計算は、どちらも同じ基本操作に基づいています。それは、QueryトークンとKeyトークンの対応関係を評価する処理です。Query選択段階では、サンプリングされたKeyの部分集合を用いて、全Query との対応関係を評価し、それぞれの要素が示す反応に基づいて重要度を推定します。一方、Attention段階では視点が逆転し、先に選択されたQueryを用いて完全なKey系列との対応関係を評価します。つまり、両方の段階は同一の対象を扱いながら、異なる視点から処理をおこなっています。この対称性により、冗長な計算を排除し、単一の計算経路を通じた効率的な勾配伝播を実現できます。
この設計には、いくつかの重要な利点があります。まず、勾配が単一の情報経路のみを通過するため、計算コストを削減できます。次に、2つの並列的な勾配経路間で発生する可能性のある競合を排除することで、数値的安定性が向上します。さらに、依存関係の少ない、より整理されたアーキテクチャとなるため、実装およびテストの簡略化にもつながります。最も重要な点は、重要度に関する情報がすでに勾配信号の中に含まれていることです。そのため、1回のパラメータ更新によって、Attention機構とQuery選択処理の両方が同時に改善され、学習中に得られた情報を効率的に再利用できます。
QIndexAttentionGradientsカーネルは、Attention機構を通じた誤差逆伝播を実装します。このカーネルは、3つの主要な要素、すなわちQuery、Key、Valueへ正確に勾配を分配する役割を担います。計算領域は以下の3つの次元で構成されます。
- 最も重要なQuery
- トークン次元
- Attention Head
__kernel void QIndexAttentionGradients(__global const float* q, __global float* q_g, __global const float2* kv, __global float2* kv_g, __global const float* indexes, __global const float* scores, __global const float* gradient, const int kunits, const int heads_kv ) { //--- init const int ind_q = 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);
実行開始時に、各スレッドはタスク空間内で自身の座標を決定します。実際のQueryインデックスはindexes配列から取得され、グローバルメモリに格納された対応する要素との正しい対応関係が確立されます。その後、必要となるすべてのメモリオフセットが計算されます。
const int h_kv = h % heads_kv; const int q_id = (int)(indexes[ind_q * heads + h] + 0.001f); const int shift_q = dimension * (q_id * heads + h) + d; const int shift_s = (ind_q * heads + h) * kunits; const int shift_g = h * dimension + d;
最初の段階では、Valueベクトルに対する勾配を計算します。本実装では、Queryを処理するすべてのAttentionヘッド数に対して、KeyおよびValueで使用するAttention Head数(heads_kv)を少なく設定する構成をサポートしています。この方式により、アーキテクチャの柔軟性を維持しながら、メモリ使用量および計算コストを削減できます。ただし、その一方で、バックプロパゲーション時には特別な処理が必要になります。
Valueベクトルは複数のAttention Head間で共有される可能性があります。そのため、これらのValueに依存するすべてのヘッドからの勾配寄与を集約する必要があります。これにより、複数のAttention経路で使用されるValueへ、誤差情報を正しく伝播できます。
各Value位置に対して、アルゴリズムはそのValueを参照する可能性のあるすべてのAttention Headを順番に処理します。このループ内では、各ヘッドによる重み付き寄与を計算します。具体的には、フォワードパス時に取得された正規化済みAttention重み(score)と出力勾配の積として求められます。これらの寄与は累積され、最終的に勾配バッファkv_g内のfloat2構造体の第2要素へ保存されます。
この処理により、KeyおよびValueのヘッド数がQueryヘッド数と異なる場合でも、一貫性があり正確な勾配伝播が保証されます。その結果、Attentionコンポーネント間に構造的な非対称性が存在する場合でも、モデルを正しく学習できます。
//--- Calculating Value's gradients int step_score = kunits * heads; if(h < heads_kv) { #pragma unroll for(int v = ind_q; v < kunits; v += qunits) { float grad = 0; for(int hq = h; hq < heads; hq += heads_kv) { int shift_score = hq * kunits + v; for(int g = 0; g < qunits; g++) grad += IsNaNOrInf(gradient[shift_g + dimension * (hq - h + g * heads)], 0) * scores[shift_score + g * step_score]; } int shift_v = dimension * (heads_kv * v + h) + d; kv_g[shift_v].s1 = IsNaNOrInf(grad, 0); } }
次の段階では、Queryに対する勾配を計算します。この処理は、Softmax関数の微分を評価する必要があるため、やや複雑になります。各Queryについて、現在の位置に対応する出力勾配を取得します。その後、アルゴリズムはKeyに対して2重ループ処理を実行します。最初のループでは、各Attention重みが持つ寄与を計算します。続く2番目のループでは、正規化された値を通じて、各Keyが与える影響を考慮します。この処理により、Softmax分布を通じて誤差信号を正確に伝播しながら、Attention機構が持つ確率的構造を維持できます。最後に、累積されたQuery勾配は、事前に計算されたオフセット位置に対応するq_gバッファへ書き込まれます。
//--- Calculating Query's gradients float grad = 0; float out_g = IsNaNOrInf(gradient[shift_g + ind_q * dimension], 0); int shift_kv = h_kv * dimension + d; #pragma unroll for(int k = 0; (k < kunits && out_g != 0); k++) { float sc_g = 0; float sc = scores[shift_s + k]; if(sc == 0) continue; for(int v = 0; v < kunits; v++) sc_g += scores[shift_s + v] * out_g * kv[shift_kv + v * heads_kv * dimension].s1 * ((float)(k == v) - sc); grad += sc_g * kv[shift_kv + k * heads_kv * dimension].s0; } q_g[shift_q] = grad;
次の段階では、Keyに対する勾配を計算します。これは、バックプロパゲーション処理の中でも特に繊細な部分の一つです。ここでの目的は、Attention機構を通じて各Keyがモデルの最終的な予測にどのように寄与したのかを正確に求めることです。
前述したように、この実装ではQueryとKeyに対して異なる数のAttention Headを使用する場合があります。そのため、各Keyの勾配は、そのKeyが関与したすべてのAttention Headからの寄与を累積する必要があります。
Feed-forwardパスでは、各Query–KeyペアがスカラーのAttention Scoreを生成し、それがSoftmax関数によって正規化されます。正規化後の値はscoresバッファに保存されます。しかし、これらの値だけでは勾配を計算するには不十分です。Softmaxは非線形変換であるため、バックプロパゲーションではその導関数を評価する必要があります。Softmaxの出力値はすでに計算され保存されていますが、各入力Logitに対する関数全体の感度を求める必要があります。この導関数には、Softmaxの式における対角成分と非対角成分の両方が含まれます。そのため、特定のKeyに対する勾配を計算する際には、そのKeyに関連付けられたすべてのQueryを反復処理し、それぞれの寄与を累積する必要があります。
この処理において重要な点は、選択されたQueryインデックスを使用して、正しい依存関係の連鎖を再構築していることです。これらのインデックスが存在しない場合、勾配の分配は正しくおこなわれません。
アルゴリズムは、関連するすべてのペアを順番に処理し、必要なSoftmax微分項を評価します。その後、それらを対応する出力誤差勾配と乗算します。計算された結果は、現在のKeyに対する勾配として累積され、KeyおよびValueの両方の勾配を格納するkv_gバッファの第1要素へ書き込まれます。
//--- Calculating Key's gradients if(h < heads_kv) { #pragma unroll for(int k = ind_q; k < kunits; k += qunits) { int shift_k = dimension * (heads_kv * k + h_kv) + d; grad = 0; for(int hq = h; hq < heads; hq++) { int shift_score = hq * kunits + k; float val = kv[shift_k + heads_kv * dimension].s1; for(int scr = 0; scr < qunits; scr++) { float sc_g = 0; int shift_sc = scr * kunits * heads; float sc = scores[shift_sc + k]; if(sc == 0) continue; for(int v = 0; v < kunits; v++) sc_g += scores[shift_sc + v] * gradient[shift_g + scr * dimension] * val * ((float)(k == v) - sc); grad += IsNaNOrInf(sc_g * q[(hq + (int)(indexes[scr * heads + hq] + 0.001f) * heads) * dimension + d], 0); } } kv_g[shift_k].s0 = IsNaNOrInf(grad, 0); } } }
これにより、OpenCLプログラム内に実装されたProbabilistic Attentionアルゴリズムについての説明を終えます。本記事では、Queryの重要度推定と情報量の高い要素の選択から始まり、Attention計算およびバックプロパゲーション処理の実装に至るまで、主要な各段階を順番に確認しました。すべてのカーネルは、ACEFormerアーキテクチャに適合するよう慎重に設計され、GPUデバイス上で効率的に実行できるよう最適化されています。
上記で説明したすべてのカーネルのソースコードを含む完全な実装は、添付ファイルとして提供されています。
次の段階では、メインアプリケーション側へのProbabilistic Attentionアルゴリズムの実装に取り組みます。このレベルでは、OpenCLプログラムをモデルロジック、バッファ管理、計算同期処理と統合する必要があります。ただし、本記事の内容はすでに十分な範囲に達しているため、ここで一度区切り、続きは次回の記事で扱うことにします。
結論
本記事では、限られた計算リソース環境下で系列データを高効率に処理するために設計されたアーキテクチャ、ACEFormerフレームワークについて紹介しました。その主要な特徴であるモジュール性、適応性、計算効率は、本実装の基盤となる重要な要素です。
ACEFormerは、長大な系列に対してAttention機構を適用する際に発生するスケーリング問題に対して、効果的な解決策を提供します。入力系列全体を徹底的に処理する代わりに、確率的な選択機構を利用して情報量の高い要素を抽出します。これにより、ほぼ同等の品質を維持しながら計算負荷を大幅に削減できます。このような手法は、実行時間のわずかな差やメモリ使用量の違いが重要となる環境、例えばアルゴリズム取引プラットフォームなどにおいて、特に大きな価値を持ちます。
本記事の実践的な部分では、OpenCLプログラム内におけるProbabilistic Attentionの主要コンポーネントについて、詳細な実装方法を検討しました。次の段階では、メインアプリケーション側におけるProbabilistic Attentionアルゴリズムの実装へ進みます。ただし、本記事にすべての内容を詰め込むことを避けるため、今回はここで一区切りとします。次回の記事では、この実装をさらに進めていきます。そこでは、プロジェクトの中でも同様に興味深い新たな段階を扱う予定です。
参照文献
- An End-to-End Structure with Novel Position Mechanism and Improved EMD for Stock Forecasting
- 本連載の他の記事
記事で使用されたプログラム
| # | 名前 | 種類 | 詳細 |
|---|---|---|---|
| 1 | Research.mq5 | EA | サンプル収集用EA |
| 2 | ResearchRealORL.mq5 | EA | Real-ORL法を用いたサンプル収集用EA |
| 3 | Study.mq5 | EA | オンラインモデル学習用EA |
| 4 | StudyOnline.mq5 | EA | オンラインモデル学習用EA |
| 5 | Test.mq5 | EA | モデルテスト用EA |
| 6 | Trajectory.mqh | クラスライブラリ | システム状態とモデルアーキテクチャ記述構造 |
| 7 | NeuroNet.mqh | クラスライブラリ | ニューラルネットワークを作成するためのクラスのライブラリ |
| 8 | NeuroNet.cl | コードライブラリ | OpenCLプログラムコード |
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/18004
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
取引におけるニューラルネットワーク:適応型モード分解を用いた時系列予測(最終回)
取引におけるニューラルネットワーク:多変量時系列予測のためのLSTMの最適化(最終回)
取引におけるニューラルネットワーク:特定データへの依存なしに時系列を汎化する(Mamba4Cast)
取引におけるニューラルネットワーク:多変量時系列予測のためのLSTMの最適化(DA-CG-LSTM)
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索