
知っておくべきMQL5ウィザードのテクニック(第59回):移動平均とストキャスティクスのパターンを用いた強化学習(DDPG)
はじめに
前回の記事では、強化学習アルゴリズムの一つであるDDPGを紹介し、Pythonで実装した3つの重要なクラス、すなわちリプレイバッファクラス、Actorネットワーククラス、Criticネットワーククラスを見てきました。しかし、DDPGエージェントクラスの解説や、MetaTrader 5の価格データをPythonに取り込む方法、移動平均やストキャスティクスの関数、2つの指標からデータをまとめてバイナリ入力ベクトルに変換するパターン取得関数(これは以前のMQL5による教師あり学習の記事で実装済み)、そしてActorとCriticネットワークの訓練に使う環境シミュレーションループについては触れていませんでした。
これらはすべて強化学習(RL)の要素であり、私たちはこれを教師あり学習(SL)から推論学習(IL)(または非教師あり学習)への橋渡しとして捉えています。いずれの学習モードも単独でモデルの訓練と使用が可能ですが、本連載ではこれらを組み合わせることで、より興味深い成果を目指せることを示そうとしています。今回は、非常に重要なDDPGエージェントクラスに焦点を当てて、RLのさらなる解説を進めていきます。
DDPGエージェント
このクラスのコアとなる構造と初期化は、以下のように定義できます。
def __init__(self, state_dim, action_dim): # Actor networks self.actor = Actor(state_dim, action_dim, HIDDEN_DIM).to(device) self.actor_target = Actor(state_dim, action_dim, HIDDEN_DIM).to(device) self.actor_target.load_state_dict(self.actor.state_dict()) self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=LR_ACTOR) # Critic networks self.critic = Critic(state_dim, action_dim, HIDDEN_DIM).to(device) self.critic_target = Critic(state_dim, action_dim, HIDDEN_DIM).to(device) self.critic_target.load_state_dict(self.critic.state_dict()) self.critic_optimizer = optim.Adam(self.critic.parameters(), lr=LR_CRITIC) self.replay_buffer = ReplayBuffer(BUFFER_SIZE)
ここで重要な構成要素は、二重ネットワークアーキテクチャ、オプティマイザーの設定、そして経験管理です。二重アーキテクチャでは、メインの方策ネットワーク(Actor)とバリューネットワーク(Critic)とは別に、ターゲットネットワークとしてのActorとCriticをそれぞれ保持します。これにより、学習の安定性が確保されます。それぞれのターゲットネットワークの初期化は、メインネットワークと同じ重みでおこなわれます。
オプティマイザー設定では、ActorとCriticそれぞれに対して別々のAdamオプティマイザーを用います。また、通常のケースと同様に、方策ネットワークとバリューネットワークで異なる学習率を設定しています。最後に、経験管理ではリプレイバッファに遷移データを蓄積し、オフポリシー学習を可能にします。バッファサイズを固定することでメモリ使用量が無制限に増えるのを防いでいます。行動の選択は、以下のように探索を組み込んでおこないます。
def select_action(self, state, noise_scale=0.1): state = torch.FloatTensor(state).unsqueeze(0).to(device) action = self.actor(state).cpu().data.numpy().flatten() action += noise_scale * np.random.randn(self.action_dim) return np.clip(action, -1, 1)
ここでの重要な仕組みは、状態処理、探索戦略、およびデバイス管理です。状態処理では、NumPy配列を適切なテンソル形式に変換し、バッチ次元を追加(unsqueezeで拡張)し、さらに計算が適切なデバイス上でおこなわれることを保証します。
探索戦略では、決定的な方策の出力にガウス雑音を加えます。ノイズのスケールは探索の大きさを制御し、クリッピングにより有効な行動範囲が維持されます。デバイス管理は、GPUとCPU間の効率的な切り替えをおこないます。また、環境との互換性のため、最終的な出力はNumPy配列として返されます。学習の更新メカニズムは以下のとおりです。
def update(self): if len(self.replay_buffer) < BATCH_SIZE: return
このif文は更新ゲートの役割を果たしており、バッチサイズ分の十分な経験が集まるまで更新処理をスキップします。これにより、有意義なバッチ統計が確保されます。2つのCriticネットワークの更新は以下のようにおこなわれます。
# Sample batch states, actions, rewards, next_states, dones = self.replay_buffer.sample(BATCH_SIZE) # Target Q calculation next_actions = self.actor_target(next_states) target_q = self.critic_target(next_states, next_actions) target_q = rewards + (1 - dones) * GAMMA * target_q # Current Q estimation current_q = self.critic(states, actions) # Loss computation and backpropagation critic_loss = nn.MSELoss()(current_q, target_q.detach()) self.critic_optimizer.zero_grad() critic_loss.backward() self.critic_optimizer.step()
このコードが扱う主な処理は、ターゲット値の計算、損失の算出、そして勾配の管理です。ターゲット値の計算では、安定したターゲットQ値を得るためにターゲットネットワークを使用します。経験パラメータのdonesで示される終了状態を考慮したベルマン方程式を実装しており、割引率GAMMAで将来の報酬の重要度を制御しています。
損失の計算では、現在のQ値とターゲットQ値の平均二乗誤差(MSE)を算出します。detachメソッドはターゲット値の勾配伝播を防ぎ(テンソルの転送可能性を維持)、標準的な時間差分学習を適用しています。勾配の管理では、すべての勾配をゼロにリセットし、Criticネットワークの最適化は別のステップでおこないます。Actorネットワークの更新は以下のように実施されます。
actor_loss = -self.critic(states, self.actor(states)).mean() self.actor_optimizer.zero_grad() actor_loss.backward() self.actor_optimizer.step()
ここで扱う方策勾配の具体的な内容は、負のQ値の最小化によるQ値の最大化、ActorおよびCriticネットワークの両方を通じた微分、そして対数確率を用いない純粋な方策勾配手法(決定的方策)を適用することです。ターゲットネットワークの更新は次のとおりです。
for target, param in zip(self.actor_target.parameters(), self.actor.parameters()): target.data.copy_(TAU * param.data + (1 - TAU) * target.data) for target, param in zip(self.critic_target.parameters(), self.critic.parameters()): target.data.copy_(TAU * param.data + (1 - TAU) * target.data)
このソフトアップデート機構は、通常1未満のTAU値を用いたPolyak averagingを特徴としています。ネットワークの重みはゆっくりと追跡され、これは定期的なハードアップデートの代替手段となります。このプロセス全体を通じて、学習の安定性を維持しつつ進めることができます。モデルは永続的である必要があり、訓練前に保存済みのネットワーク重みを読み込み、訓練後にはそれらを保存できるようにします。これを以下の方法で実現しています。
def save(self, filename): torch.save({ 'actor': self.actor.state_dict(), 'critic': self.critic.state_dict(), 'actor_target': self.actor_target.state_dict(), 'critic_target': self.critic_target.state_dict(), }, filename) def load(self, filename): checkpoint = torch.load(filename) self.actor.load_state_dict(checkpoint['actor']) self.critic.load_state_dict(checkpoint['critic']) self.actor_target.load_state_dict(checkpoint['actor_target']) self.critic_target.load_state_dict(checkpoint['critic_target'])
上記のコードの主な特徴は、ネットワークの全状態を保存・読み込みできること、ターゲットネットワークの整合性を維持していること、訓練の継続を可能にしていること、そしてモデルの評価に対応していることです。つまり、DDPGエージェントを実装する際には、いくつかの重要な設計上の選択が必要であり、これは大きく以下の3つのカテゴリに分けられます。これらは、大きく分けて、DDPG特有のコンポーネントの選択、実装の強みを活かす工夫、今後の潜在的な改善点の3つのカテゴリに分類されます。
まず、DDPGの構成要素として主に使われているのは、ターゲットネットワーク、決定的方策、および別々の学習率です。ターゲットネットワークは、連続的な行動空間を扱う際の報酬の安定学習(Q学習)に非常に重要です。連続空間を扱うことがこれをさらに重要にしています。決定的方策では、外部からのノイズを用いた探索が必須であり、これによって堅牢性を高めることができます。また、学習率を分けるのも典型的な応用例であり、方策(Actorネットワーク)の学習率はバリューネットワーク(Critic)よりも遅く設定されます。
この実装が比較的強力である理由は、明確な「関心の分離」がなされており、行動選択やネットワークの更新に対してきちんとしたメソッドが用意されていることです。さらに、GPUとCPUの切り替えを一貫して処理するためのデバイス管理もおこなわれています。バッチ処理によってテンソル演算の効率化も実現しており、テンソルの次元安全性も複数箇所でチェックされています。
将来的な改善点としては、勾配クリッピングによる勾配爆発の防止、学習率スケジュールの導入による学習制御の高度化、優先経験再生(priority replay)による効率的なサンプリング(これは前回の記事で触れたリプレイバッファに関連)、そして複数のActorを並列に動かしてデータ収集を加速する並列探索などが挙げられます。
また、いくつか注目すべき訓練のダイナミクスもあります。特に更新の順序とハイパーパラメータの設定です。更新の順序では、Criticネットワークを先に更新します。これはより正確なQ値が方策改善の指針となるためです。安定性を高めるために方策更新を遅延させる手法も導入可能です。さらに頻繁なターゲット更新によって、学習済みのパラメータ(ネットワーク重み)をゆっくりと追従させます。
ハイパーパラメータでは、TAUの重要性が特に高いです。TAUはターゲットネットワークの追従速度を制御し、学習全体の安定性の鍵となります。また、ノイズスケールは時間経過で減衰させる設計が望ましく、バッファサイズは学習効率に影響し、バッチサイズは更新のばらつきに影響を与えます。
MAと確率関数
これら2つの関数は、強化学習(RL)用にPythonで実装されています。これは、教師あり学習に関する記事でMQL5を使ってネットワークの入力データをPythonにエクスポートして学習させた方法とは異なります。ここでの実装では、MetaTraderのMetaTrader 5 Pythonモジュールを使用して、起動中の端末インスタンスに接続し、価格データを取得しています。これをおこなう方法については、こちらのドキュメントにガイドがあります。以下のインジケーター関数は、生の価格データをテクニカル指標データに変換するもので、これは教師あり学習モデルの入力として使用されます。データは変換および正規化され、バイナリパターンベクトルへと変換されます。
教師ありモデルの出力は、実質的に価格変動の予測であるため、私たちはこれらを「状態」として扱います。これらの状態が、DDPG強化学習エージェントへの入力として使用されます。MA関数は、MetaTrader 5 Pythonモジュールから取得した価格のpandasデータフレームを入力として受け取ります。このデータフレームは、以下のように検証および準備する必要があります。
p = np.asarray(p).flatten() # Convert to 1D array if not already if len(p) < window: raise ValueError("Window size cannot be larger than the number of prices.")
ここでおこなっているのは、配列を標準化して、入力の形状に関係なく一貫した1次元形式で扱えるようにすることです。また、計算エラーを引き起こす無効なウィンドウサイズを回避するためのエラーハンドリングも実装されています。データの整合性を保つことで、処理パイプライン全体においてクリーンなデータフローが維持されます。計算の仕組みは次のとおりです。
return np.convolve(p, np.ones(window), 'valid') / window
この実装では、平均値の計算を効率的にロール処理するために畳み込みを使用しています。validパラメータを使用することで、完全に計算可能なウィンドウのみが返されるようにしています。また、ウィンドウサイズで正規化をおこなうことで、正確な平均値を生成しています。全体の処理はベクトル化されており、パフォーマンスの最適化が図られています。この処理の金融的な意義は、価格データを平滑化することでトレンドを識別しやすくする点にあります。また、使用するウィンドウサイズ(つまり平均化期間)は、価格変動に対する感度を決定する重要な要素です。次に、ストキャスティック関数は、以下のようにその入力を検証します。
p = np.asarray(p).flatten() if len(p) < k_window: raise ValueError("Window size for %K cannot be larger than the number of prices.")
ここでの設計上の考慮事項は、MA関数と一貫した入力フォーマットを保つことです。%Kの計算ウィンドウに対しては、別途検証をおこなう必要があり、無効なパラメータが指定された場合には、早期にエラーを発生させることで処理を中断します。%Kの計算は次のとおりです。
for i in range(k_window - 1, len(p)): current_close = p[i] lowest_low = min(p[i - k_window + 1:i + 1]) highest_high = max(p[i - k_window + 1:i + 1]) K = ((current_close - lowest_low) / (highest_high - lowest_low)) * 100 K_values.append(K)
ここで重要な構成要素は、ロールウィンドウ分析、マーケットコンテキスト、そして全体の実装です。ロールウィンドウ分析では、一定の過去期間における価格レンジを検査する必要があります。これにより、現在の終値が相対的にどの位置にあるかを特定することができ、標準的なスケーリング(0〜100)が適用されます。マーケットコンテキストは、買われすぎ/売られすぎの状況を評価するのに役立ちます。100に近い値は下降方向への反転の可能性を示唆し、0に近い値は上昇への転換を示唆します。全体の実装では、明確さを保つために明示的なループを使用しており、エッジケースに対応できるよう適切なウィンドウインデックス処理をおこない、結果の時間的順序も保持しています。%Dの計算は次のとおりです。
D_values = MA(K_values, d_window)
本質的には、これはシグナルの洗練化であり、%Kの移動平均を滑らかにしたバージョンを使用します。この平滑化期間として一般的に使用される値は3であり、本実装でもそれを採用しています。この追加のバッファによって%Kの変動に対する確認が得られ、これは生の%Kから発生する騙しのシグナルを減らすのに役立ちます。
パターン取得関数
この関数は、上記で説明した2つのインジケーターバッファ(MAとSTO)からデータを統合し、学習パイプラインに組み込むために使用されます。これは特徴量エンジニアリングの役割を果たしています。その理由は大きく3つあります。まず、パターン取得関数は次元削減に貢献します。これは、生の価格データをより意味のあるシグナルへと変換することによって、モデルが処理しやすい形にするためです。次に、定常性の向上が期待できます。一般的にインジケーターの出力は生の価格データに比べて変動が安定しており、学習アルゴリズムにとって扱いやすいものとなります。そして、時間的文脈の保持という利点もあります。ウィンドウ計算によって時間依存性を保ちながら入力ベクトルを生成するため、たとえば[1, 0, 0, 1]といった出力が、その生成された時点の価格やインジケーターのように、時間情報と結びついて扱うことができます。
ただし、この関数の主な目的は教師あり学習のための前処理です。この関数が出力する0と1から成るバイナリベクトルは、次の価格変動を予測するモデルの訓練データとなります。ここでMAはトレンド情報を提供し、STO関数はモメンタムおよび反転の兆候を示す役割を果たします。これら2つのインジケーターから得られるパターンの組み合わせについては、第57回で詳しく説明しました。教師あり学習によって予測された価格の変化は、最終的に強化学習モデルにおける状態の表現として使われることになります。
つまり、教師ありモデルの出力がDDPGエージェントの状態入力となるということです。MAとSTOというインジケーターの組み合わせによって、市場のコンテキストを理解するための土台が形成され、生の価格履歴をそのまま状態として使う必要性が大きく減少します。
実装面でもこの関数は多くの利点を持っています。入力値の検証によって無言のエラーを防ぎ、配列の形状を一貫させることで、データの整合性を保っています。また、不適切な使い方があった場合には、明確なエラーメッセージを出すようになっています。パフォーマンス面でも、可能な限りベクトル演算を使い、ループを明示的に記述し、ストリーミング処理を意識したメモリ効率の良い設計が施されています。さらに重要なのは、この関数があくまでも実務的な取引の観点から有用であり、理論や技術だけに偏っていない点です。使用しているインジケーターは業界標準であり、トレンドとモメンタムという異なる側面を補完的に捉えており、また、出力される状態は正規化された値であるため、一貫性のある学習が可能になります。
将来的な改善点としては、%K計算の部分をベクトル化して処理速度を向上させること、NumbaのJITコンパイル機能を用いてSTO関数内のループを高速化すること、さらに中間計算結果をキャッシュすることで全体のパフォーマンスを最適化することが挙げられます。また、NaNや無限大(inf)といった特殊値への検証処理を追加することで、さらなる堅牢性も実現できるでしょう。この DDPG による強化学習のコードは全体としてかなりの規模があるため、その全体をここで詳細に解説することは控え、記事の最後に未公開部分のコードを添付する予定です。その中でも特に中核的な役割を果たすのが、このパターン取得関数になります。
テスト
教師あり学習の記事(第57回)でテストした10個のパターンのうち、1年間のウォークフォワードテストで収益を上げることができたのは7つだけでした。それぞれのパターンが独自のニューラルネットワークを形成していたため、強化学習ネットワークとその環境も各パターンごとに構築する必要があります。この際、私たちは第57回と同様の方法論に従っています。具体的には、通貨ペアEUR/USDを対象に、2023年の日足時間枠で学習をおこないます。このケースでは、2023年を「ライブ市場環境」としてシミュレーションすることで、強化学習ネットワークをトレーニングしています。前回および前々回の記事でも述べたように、強化学習は、すでに確立され訓練されたモデルをサポートし、その運用を保護するためのシステムです。私たちの場合、それは記事57で教師あり学習によりトレーニングされたネットワークを指します。
この強化学習は、履歴データに対してではなく、運用環境やライブ環境においてバックプロパゲーション(誤差逆伝播)をおこなうという点で特徴があります。しかし、MQL5からONNXネットワークへバックプロパゲーションをおこなうことは現実的ではないため、私たちは「ライブ環境をシミュレーション」するという形を取っています。その対象となるのが、やはり2023年のデータです。
教師あり学習で「次に価格がどう動くか?」という問いを立てていたのに対して、ここでは「この価格変動に対して、トレーダーはどのようなアクションを取るべきか?」という視点で問題に取り組みます。こうした前提のもと、2023年をシミュレートした訓練をおこない、続く2024年でフォワードウォークテストをおこないます。なお、このときのエントリー条件はわずかに変更されています。
つまり、私たちは単に「価格がどう動くか」に基づいてロングまたはショートのポジションを取るのではなく、「価格がどう動くかに照らして、自分たちはどのような行動を取るべきか?」という点を重視します。加えて、そのアクションによって得られる報酬が利益をもたらすかどうかも考慮に入れます。第57回でフォワードウォークを成功させた7つのパターンのうち、強化学習を用いた場合に意味のある成果を上げたのは、3つだけでした。10個のパターンは0から9までのインデックスで管理されていますが、このうち成功したのは1、2、5番のパターンです。これらのレポートは以下に掲載されています。
パターン1
パターン2
パターン5
テストされたEAは、いつものようにカスタムのシグナルクラスを使って構築されています。そのコードは以下に添付しています。この記事では、第57回で使用していたシグナルクラスのファイルに変更を加えています。具体的には、関数名IsPatternをSuperviseに変更しました。また、新しく「Reinforce」という関数も追加しています。これら両方のコードは以下に共有されています。
//+------------------------------------------------------------------+ //| Supervised Learning Model Forward Pass. | //+------------------------------------------------------------------+ double CSignal_DDPG::Supervise(int Index, ENUM_POSITION_TYPE T) { vectorf _x = Get(Index, m_time.GetData(X()), m_close, m_ma, m_ma_lag, m_sto); vectorf _y(1); _y.Fill(0.0); int _i=Index; if(_i==8) { _i -= 2; } ResetLastError(); if(!OnnxRun(m_handles[_i], ONNX_NO_CONVERSION, _x, _y)) { printf(__FUNCSIG__ + " failed to get y forecast, err: %i", GetLastError()); return(double(_y[0])); } if(T == POSITION_TYPE_BUY && _y[0] > 0.5f) { _y[0] = 2.0f * (_y[0] - 0.5f); } else if(T == POSITION_TYPE_SELL && _y[0] < 0.5f) { _y[0] = 2.0f * (0.5f - _y[0]); } return(double(_y[0])); } //+------------------------------------------------------------------+ //| Reinforcement Learning Model Forward Pass. | //+------------------------------------------------------------------+ double CSignal_DDPG::Reinforce(int Index, ENUM_POSITION_TYPE T, double State) { vectorf _x(1); _x.Fill(float(State)); vectorf _y(1); _y.Fill(0.0); vectorf _y_state(1); _y_state.Fill(float(State)); vectorf _y_action(1); _y_action.Fill(0.0); vectorf _z(1); _z.Fill(0.0); int _i=Index; if(_i==8) { _i -= 2; } ResetLastError(); if(!OnnxRun(m_handles_a[_i], ONNX_NO_CONVERSION, _x, _y)) { printf(__FUNCSIG__ + " failed to get y action forecast, err: %i", GetLastError()); } _y_action[0] = _y[0]; ResetLastError(); if(!OnnxRun(m_handles_c[_i], ONNX_NO_CONVERSION, _y_state, _y_action, _z)) { printf(__FUNCSIG__ + " failed to get z reward forecast, err: %i", GetLastError()); } //normalize action output & check for state-action alignment if(T == POSITION_TYPE_BUY && _y[0] > 0.5f) { _y[0] = 2.0f * (_y[0] - 0.5f); } else if(T == POSITION_TYPE_SELL && _y[0] < 0.5f) { _y[0] = 2.0f * (0.5f - _y[0]); } else { _y[0] = 0.0f; } return(double(_y[0]*_z[0])); }
このカスタムシグナルクラスのファイルは、MQL5ウィザードを通じてEAに組み込むことを前提としています。初心者の読者のために、その方法については、こちらとこちらにガイドがありますので、参考にしてください。
結論
本記事では、モデルが運用されている段階において強化学習を適用する意義について考察しました。使用した強化学習アルゴリズムはDeep Deterministic Policy Gradient (DDPG)であり、その実装にはリプレイバッファ、Actor、Critic、エージェントの各クラスが関わっています。これらについては、本記事および前回の記事で取り上げました。 運用段階における強化学習の役割は、教師あり学習フェーズで得られた知識を活用し続けること(探索)と、同時に未知の環境変化や市場条件を捉えて対応すること(活用)の両立にあります。これを正しく実行するためには、モデルを使用中にも誤差逆伝播を通じた学習をおこなう必要があります。
しかしながら、MQL5環境ではONNXモデルのトレーニングがサポートされていないため、代替として過去データを用いたライブ取引のシミュレーションをおこないました。このシミュレーション後、訓練された強化学習モデルを翌年(2024年)のデータでテストしたところ、7つのうち3つのみが意味のあるフォワードウォーク結果を出しました。ただし、取引結果はロングまたはショートの一方向に偏る傾向がありました。これについては、第57回でも述べたとおり、テスト期間が短すぎることが主な原因と考えられ、より長期間にわたるトレーニングとテストによって改善される可能性があります。次回は、推論に関する内容に進みます。
種類 | 詳細 |
---|---|
*.*onnxファイル | カスタムシグナルクラスファイルと並行したPythonサブフォルダ内にあるONNXモデルファイル |
*.*mqhファイル | 入力ネットワークデータを処理する関数を含むカスタムシグナルクラスファイル(57_X) |
*.*mq5ファイル | ヘッダーに使用されているファイルが表示されるウィザード組み立てのEA |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/17684
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。





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