機械学習の限界を克服する(第3回):不可逆的誤りに関する新たな視点
本記事では、現行の機械学習モデルが持つ高度な制約について紹介します。これらの制約は、モデルを実運用に導入する前に、多くの場合、指導者からから明示的に教えられることはありません。機械学習の分野は数学的表記や文献が中心であり、学習者が参照する抽象度によってアプローチは異なります。たとえば、一部の実務者はscikit-learnのような高レベルライブラリを通じて学びます。この場合、モデルの利用は直感的で簡単ですが、内部にある数学的概念は抽象化され、ユーザーの目には見えません。
しかし、習熟度や求める制御の度合いによっては、この抽象化を外し、モデル内部で何が起こっているかを直接観察する必要があります。したがって、機械学習プロジェクトでは常に不可約誤差(Irreducible Error)が存在しますが、多くの場合明示的に言及されることはありません。
ここで単純線形回帰の公式を考えます。通常、目的変数Yは、観測可能な入力Xの関数fとして考えることができます。入力Xは一連の係数Bによって変換され、制御不能なランダムノイズeが加わることで目的変数Yが生成されます。本記事では、この誤差項eに注目します。すべての機械学習モデルにはこの誤差項が潜み、静かに予測に影響を与えています。本記事の目的は、この誤差項eが古典的な文献で述べられているように完全にランダムで不可約であるわけではないことを読者に示すことです。誤差項が完全にランダムで不可約であると考えることは、必ずしも正確ではありません。

図1:最小二乗法(OLS)回帰の数学的説明では、不可約誤差項およびその原因についてほとんど詳細が示されていない
本記事の唯一の目的は、読者に不可約誤差に対する新しい視点を提供することです。一般に「不可約誤差」と呼ばれる単一の量は、実際には複数の独立した誤差源に分解可能です。研究コミュニティで広く認識されている誤差源は次の2つです。
- 真の基礎プロセスが持つ固有の変動性または自然なランダム性
- モデル自体のバイアス
本記事ではさらに、あまり知られていない第三の不可約誤差の構成要素を紹介します。このポイントの重要な結論は、第三の誤差源に対してある程度の制御を行うことで、取引パフォーマンスを改善できる可能性がある、ということです。
機械学習モデルは複数の視点から理解できるため、すべてを完全に習得することは容易ではありません。私たちはしばしば統計的視点から学びますが、幾何学的視点から探求する読者は少なく、この第三の不可約誤差はまさに幾何学的視点の中に潜んでおり、高レベルの抽象に留まる実務者には見えません。
この第三の誤差は修正が困難であるため「不可約」と呼ばれます。それだけでなく、そもそも気づくことすら難しく、多くの場合、簡潔すぎる数学的表記の背後に隠れています。
本記事は、この誤差をゼロにできると主張するものではありません。むしろ、誤差の存在を認識したうえで、機械学習モデルをより賢明かつ適切に活用する方法を示すことが目的です。
幾何学的視点から見ると、機械学習モデルは入力から出力への関数を真に「学習」しているわけではありません。モデルは目的変数を生成する関数を直接近似しようとはしていません。
例として、人間の芸術家が紙に絵を描くことを考えてください。紙はキャンバスとして使用され、芸術家は対象を描写します。同様に、機械学習モデルは与えられた入力から新しい「キャンバス」、すなわち多様体空間を作成します。コインを持ち、その影をモデルが生成した多様体に落とすと、影が当たる点が、モデルの予測です。しかし、真の目的変数はコインそのものです。つまり、モデルは与えられた入力の組み合わせや多様体上に目的変数の「像」を埋め込むに過ぎません。
予測対象の目的変数は、入力から生成された多様体上に必ずしも存在するわけではなく、独自の多様体空間に存在します。したがって、入力から生成される多様体と、真の目的変数の多様体の間には常に不可約距離(irreducible distance)が存在します。 これが第一の誤差源です。さらに、プロセスの自然なランダム性(第二の誤差源)およびモデルのバイアス(第三の誤差源)を加えると、不可約誤差の全体像が見えてきます。

図2:「オレンジ色の平面」は、モデルが与えられた入力から生成した「キャンバス」、すなわち多様体を表している。一方、「黄色の三角形」は、そのキャンバスと真の目的変数との間に存在する不可約誤差を示している。
解析幾何学に精通している読者であれば、機械学習モデルに与えられた入力の広がり(スパン)が新しい座標系を定義することを理解できるでしょう。モデルはこの入力データから学習した新座標系を用いて目的変数を表現しようとします。しかし、目的変数は独自の座標系上に存在しており、入力による座標系とは独立しています。
この幾何学的視点に慣れた熟練読者にとっては自明かもしれませんが、まだ馴染みのない読者のために、残りの記事では丁寧に解説します。
重要なポイントは、取引活動において、機械学習モデルを意識的、慎重、知的に活用することで、不可約誤差に対してある程度の制御が可能であるということです。
本記事での議論は、前回のフィードバックコントローラーに関する検討で確立したベースラインパフォーマンスから始まります(リンクはこちら)。前回のコントローラーは初期状態と比べて大幅に改善されていましたが、本記事で提案する調整により、旧コントローラーのパフォーマンスをさらに上回ることができました。
今回の手法では、直接的な点対点比較をやめました。モデルに将来の価格水準を予測させ、それを現在価格と比較するのではなく、2つの時間区間における価格水準をモデル化し、予測される傾き(スロープ/トレンド)を基に取引をおこないました。つまり、予測価格と実際価格を直接比較するのではなく、2区間におけるモデル予測スロープを取引の根拠としたのです。
EUR/USDペアを対象に、戦略はすべて同一とし、モデルに価格をどのようにモデル化させるかのみを変更して、5年間のバックテストを実施しました。その結果、以下のパフォーマンス指標において成長が確認されました。
取引の収益性
総純利益は245ドルから253ドルに増加し、収益性は3%向上しました。同時にシャープレシオは0.68から0.84に上昇し、EUR/USDのように難易度の高い金融市場で取引する場合において、シャープレシオが23%も改善されたことは注目に値します。
さらに驚くべきことに、5年間のバックテストにおける総損失は838ドルから721ドルに減少しました。これは、取引アプリケーションが負った総リスクが13%削減されたことを示しています。加えて、累積取引回数は152回から139回に減少し、必要な取引総数は8%削減されました。つまり、このアプリケーションは、より少ないリスクでより大きな利益を実現したことになります。取引精度
最後に、利益の出る取引の割合は4%上昇し、従来のフィードバックコントローラーのベンチマークでは57.24%だったのが、私たちが提案した調整後には59.71%となりました。 これは、全体としてシステムがより収益性を高めつつ、リスクを低減できたことを意味しており、あらゆる取引アプリケーションにとって理想的な特性です。
これらの修正は、すべての実務者が真剣に検討すべき内容です。しかし、まず、前回の議論で実装した最初のフィードバックコントローラーによって確立された旧パフォーマンスレベルを振り返ることから始めましょう。
最初のバックテストは2020年1月1日から2025年5月1日までの期間で実施されました。今回の第2テストでも、この日付はそのまま維持します。

図3:最初の議論で確立したベースラインパフォーマンスの再確認
旧フィードバックコントローラーの結果は十分に受け入れ可能でしたが、まだ改善の余地はあります。ここでは旧結果を保持して、読者がこれから示す新しい結果と比較できるようにしています。そのため、下記の図4は旧フィードバックコントローラーから引用したもので、今日の改善結果と対比するためのベンチマークとなります。

図4:初期フィードバックコントローラーで確立された旧パフォーマンスレベル
旧フィードバックコントローラーによるエクイティカーブは有望でしたが、いくつかの戦略調整を加えることで、システム全体の収益性に顕著な影響を与え、より多くの利益を実現できました。下記の図5では、旧フィードバックコントローラーは2023年時点でようやく700ドルの利益水準に到達しています。しかし、後ほど示す通り、改良版フィードバックコントローラーは2021年にすでに700ドルの利益水準に達しています。もちろん、2021年以降に収益性に一時的なショックは発生しましたが、改善の効果は明らかです。

図5:旧ベンチマーク版取引戦略による利益とエクイティカーブ
MQL5の始め方
すべての取引アプリケーションと同様に、今回も最初の取引戦略バージョンから引き継がれた重要なシステム定義を、変更せずにまず設定します。これらの定義は、テストを公平に保ち、比較を一貫性のあるものにするために重要であることを思い出してください。//+------------------------------------------------------------------+ //| Closed Loop Feedback 1.2.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/ja/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/ja/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System definitions | //+------------------------------------------------------------------+ #define MA_PERIOD 10 #define FEATURES 12 #define TARGETS 15 #define HORIZON 10 #define OBSERVATIONS 90 #define ACCOUNT_STATES 3
次に、取引戦略で使用される重要なグローバル変数を定義します。これらの変数はテクニカル指標、取引口座の状態や状況、ストップロスの幅、そして戦略が予測をおこなわずに取引するか、先に予測をおこなってから取引するかを管理する役割を果たします。グローバル変数を用いることで、アプリケーションの挙動を予測可能かつ再現性のある形で定義し制御することが可能となります。
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int ma_h_handler,ma_l_handler,atr_handler,scenes,b_matrix_scenes; double ma_h[],ma_l[],atr[]; double padding; matrix snapshots,OB_SIGMA,OB_VT,OB_U,b_vector,b_matrix; vector S,prediction; vector account_state; bool predict,permission;
すべての取引アプリケーションには、同じ定型コードを書き直す手間を避けるための依存関係があります。そこで、重要なライブラリを読み込みます。具体的には、ポジションのオープンやクローズに用いる取引ライブラリに加え、本議論のために作成した2つのカスタムライブラリ、時間ライブラリと取引情報ライブラリです。時間ライブラリは新しいローソク足が形成されたかを判断するために使用し、取引情報ライブラリは最小取引ロットサイズや現在の買値と売値などの重要な情報を提供します。
//+------------------------------------------------------------------+ //| Dependencies | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> #include <VolatilityDoctor\Time\Time.mqh> #include <VolatilityDoctor\Trade\TradeInfo.mqh> CTrade Trade; Time *DailyTimeHandler; TradeInfo *TradeInfoHandler;
初期化時に、これまでに作成したすべてのカスタムクラスの新しいインスタンスを生成します。また、移動平均やATRなどのテクニカル指標のインスタンス、さらに必要となる重要な行列やベクトルも定義します。ブールフラグは初期化され、経過シーンのカウンタはアプリケーションが起動するたびにゼロにリセットされます。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- DailyTimeHandler = new Time(Symbol(),PERIOD_D1); TradeInfoHandler = new TradeInfo(Symbol(),PERIOD_D1); ma_h_handler = iMA(Symbol(),PERIOD_D1,MA_PERIOD,0,MODE_EMA,PRICE_HIGH); ma_l_handler = iMA(Symbol(),PERIOD_D1,MA_PERIOD,0,MODE_EMA,PRICE_LOW); atr_handler = iATR(Symbol(),PERIOD_D1,14); snapshots = matrix::Ones(FEATURES,OBSERVATIONS); scenes = 0; b_matrix_scenes = 0; account_state = vector::Zeros(3); b_matrix = matrix::Zeros(1,1); prediction = vector::Zeros(2); predict = false; permission = true; //--- return(INIT_SUCCEEDED); }
アプリケーションの使用が終了した際には、MQL5において適切なメモリ管理をおこなうことが推奨されます。したがって、カスタムオブジェクトのインスタンスを削除し、使用しなくなったテクニカル指標を解放します。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- delete DailyTimeHandler; delete TradeInfoHandler; IndicatorRelease(ma_h_handler); IndicatorRelease(ma_l_handler); IndicatorRelease(atr_handler); }
取引サーバーから新しい価格水準が受信されると、エキスパートアドバイザー(EA)はOnTick関数を呼び出します。この設定では、最初の確認として新しいローソク足が形成されたかどうかを判定します。これにより、バックテストの実行速度が向上し、各ローソク足ごとに一度だけ処理がおこなわれるようになります。
ローソク足の形成が確認されたら、テクニカル指標の値と、ストップロスの幅を示すページャント変数を更新します。その後、直近の確定価格を記録します。1つ以上の保有ポジションがある場合は、ポジションのチケットを取得し、利益が増加するにつれてトレーリングストップとなるようにストップロスを修正します。
トレーリングストップを設定するには、まず現在のストップロス値とテイクプロフィット値を取得します。これらの値と、更新が推奨される値を比較して、更新の可否を判断します。推奨値がより利益的であれば更新をおこない、そうでなければ変更は適用されません。重要な点として、この更新をおこなう前に、修正対象のポジションの種類を確認する必要があります。
保有ポジションがない場合は、口座状態ベクトルを初期化します。このベクトルは、開こうとしているポジションの種類を記録する役割があります。確定価格が高い移動平均を上回っている場合は取引を開始し、低い移動平均を下回っている場合は売り取引を開始します。
取引は、predictブールフラグがfalseであり、permissionブールフラグがtrueの場合に直接開始されます。それ以外の場合は、システムがすべての変数を記録し、取引を開始する前に予測をおこないます。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- if(DailyTimeHandler.NewCandle()) { CopyBuffer(ma_h_handler,0,0,1,ma_h); CopyBuffer(ma_l_handler,0,0,1,ma_l); CopyBuffer(atr_handler,0,0,1,atr); padding = atr[0]*2; double c = iClose(Symbol(),PERIOD_D1,0); if(PositionsTotal() > 0) { ulong ticket = PositionSelectByTicket(PositionGetTicket(0)); if(ticket) { double sl,tp; sl = PositionGetDouble(POSITION_SL); tp = PositionGetDouble(POSITION_TP); if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { double new_sl = TradeInfoHandler.GetBid()-padding; double new_tp = TradeInfoHandler.GetBid()+padding; if(new_sl > sl) Trade.PositionModify(ticket,new_sl,new_tp); } else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { double new_sl = TradeInfoHandler.GetAsk()+padding; double new_tp = TradeInfoHandler.GetAsk()-padding; if(new_sl < sl) Trade.PositionModify(ticket,new_sl,new_tp); } } } if(PositionsTotal() == 0) { account_state = vector::Zeros(ACCOUNT_STATES); if(c > ma_h[0]) { if(!predict) { if(permission) Trade.Buy(TradeInfoHandler.MinVolume(),Symbol(),TradeInfoHandler.GetAsk(),(TradeInfoHandler.GetBid()-(padding)),(TradeInfoHandler.GetBid()+(padding)),""); } account_state[0] = 1; } else if(c < ma_l[0]) { if(!predict) { if(permission) Trade.Sell(TradeInfoHandler.MinVolume(),Symbol(),TradeInfoHandler.GetBid(),(TradeInfoHandler.GetAsk()+(padding)),(TradeInfoHandler.GetAsk()-(padding)),""); } account_state[1] = 1; } else { account_state[2] = 1; } } if(scenes < OBSERVATIONS) { take_snapshots(); } else { matrix temp; temp.Assign(snapshots); snapshots = matrix::Ones(FEATURES,scenes+1); //--- The first row is the intercept and must be full of ones for(int i=0;i<FEATURES;i++) snapshots.Row(temp.Row(i),i); take_snapshots(); fit_snapshots(); predict = true; permission = false; } scenes++; } }
スナップショットを取得するメソッドは簡潔です。重要な市場情報をsnapshotsと呼ぶ行列に記録します。これには、始値、高値、安値、終値の価格に加え、口座の純資産も含まれます。以前の議論を追ってきた読者であれば、このコードは本連載の初めで使用したものと同じであることに気付くでしょう。。
//+------------------------------------------------------------------+ //| Record the current state of our system | //+------------------------------------------------------------------+ void take_snapshots(void) { snapshots[1,scenes] = iOpen(Symbol(),PERIOD_D1,1); snapshots[2,scenes] = iHigh(Symbol(),PERIOD_D1,1); snapshots[3,scenes] = iLow(Symbol(),PERIOD_D1,1); snapshots[4,scenes] = iClose(Symbol(),PERIOD_D1,1); snapshots[5,scenes] = AccountInfoDouble(ACCOUNT_BALANCE); snapshots[6,scenes] = AccountInfoDouble(ACCOUNT_EQUITY); snapshots[7,scenes] = ma_h[0]; snapshots[8,scenes] = ma_l[0]; snapshots[9,scenes] = account_state[0]; snapshots[10,scenes] = account_state[1]; snapshots[11,scenes] = account_state[2]; }
しかしここで、初期バージョンに対しておこなった改善が見え始めます。これまでの内容は、以前の読者には馴染みのあるものです。ここからは、スナップショットの変化をモデル化するシステムの入力と出力を準備します。スナップショットは、戦略が市場とどのように相互作用しているかの重要な情報を追跡します。例えば、口座残高や純資産が時間とともにどのように変化するかを記録します。
X行列は入力データを保持し、Y行列は出力データを保持します。Y行列を注意深く見ると、Yの4行目、5行目、6行目、7行目は、Xの5行目、6行目、7行目、8行目からコピーされていることがわかります。その後、同じXの行が再び複製され、未来にシフトされます。つまり、モデルには口座残高を1ステップ先だけでなく、10ステップ先まで予測させることになります。
これら2つの期間にわたる予測トレンドに応じて、取引をおこなうかどうかを判断します。擬似逆行列法(前述)で最適解が見つかると、モデルは2つの予測を出力します。
- 次のローソク足時点の予想残高(prediction[4])
- 10本先のローソク足時点の予想残高(prediction[8])
モデルはこれらの予測に基づいて取引判断をおこないます。口座残高の成長が見込まれる場合は取引が許可され、減少が予想される場合は許可されません。
これは以前の方法に比べて大幅な改善です。以前の議論では、モデルの予測口座残高を現在の実残高と直接比較しているかのように扱っていましたが、このアプローチではその誤りを避けています。
//+------------------------------------------------------------------+ //| Fit our linear model to our collected snapshots | //+------------------------------------------------------------------+ void fit_snapshots(void) { matrix X,y; X.Reshape(FEATURES,scenes); y.Reshape(TARGETS,scenes); for(int i=0;i<scenes-HORIZON;i++) { X[0,i] = snapshots[0,i]; X[1,i] = snapshots[1,i]; X[2,i] = snapshots[2,i]; X[3,i] = snapshots[3,i]; X[4,i] = snapshots[4,i]; X[5,i] = snapshots[5,i]; X[6,i] = snapshots[6,i]; X[7,i] = snapshots[7,i]; X[8,i] = snapshots[8,i]; X[9,i] = snapshots[9,i]; X[10,i] = snapshots[10,i]; X[11,i] = snapshots[11,i]; y[0,i] = snapshots[1,i+1]; y[1,i] = snapshots[2,i+1]; y[2,i] = snapshots[3,i+1]; y[3,i] = snapshots[4,i+1]; y[4,i] = snapshots[5,i+1]; y[5,i] = snapshots[6,i+1]; y[6,i] = snapshots[7,i+1]; y[7,i] = snapshots[8,i+1]; y[8,i] = snapshots[5,i+HORIZON]; y[9,i] = snapshots[6,i+HORIZON]; y[10,i] = snapshots[7,i+HORIZON]; y[11,i] = snapshots[8,i+HORIZON]; y[12,i] = snapshots[9,i+1]; y[13,i] = snapshots[10,i+1]; y[14,i] = snapshots[11,i+1]; } if(PositionsTotal() == 0) { //--- Find optimal solutions b_vector = y.MatMul(X.PInv()); Print("Day Number: ",scenes+1); Print("Snapshot"); Print(snapshots); Print("Input"); Print(X); Print("Target"); Print(y); Print("Coefficients"); Print(b_vector); Print("Prediciton"); prediction = b_vector.MatMul(snapshots.Col(scenes-1)); Print("Expected Balance at next candle: ",prediction[4],". Expected Balance after 10 candles: ",prediction[8]); permission = false; if(prediction[4] < prediction[8]) { Print("Account size expected to grow, permission granted"); permission = true; } else permission = false; if(permission) { if(PositionsTotal() == 0) { if(account_state[0] == 1) Trade.Buy(TradeInfoHandler.MinVolume(),Symbol(),TradeInfoHandler.GetAsk(),(TradeInfoHandler.GetBid()-(atr[0]*2)),(TradeInfoHandler.GetBid()+(atr[0]*2)),""); else if(account_state[1] == 1) Trade.Sell(TradeInfoHandler.MinVolume(),Symbol(),TradeInfoHandler.GetBid(),(TradeInfoHandler.GetAsk()+(atr[0]*2)),(TradeInfoHandler.GetAsk()-(atr[0]*2)),""); } } } } //+------------------------------------------------------------------+
前回の議論と同様に、テスト期間の日付は常に同じにする必要があります。これにより、常に公平な比較がおこなえるようになります。

図6:同一期間における改良版フィードバックコントローラーのバックテスト
詳細な統計は、初期バージョンの取引戦略に比べて明確かつ測定可能な成長を示しています。この改良版戦略は、初期バージョンよりも全体的に低リスクで取引をおこなうことができます。また、興味深い点として、利益が出る売り取引の割合が大幅に改善され、ほぼ70%に近づいていることが挙げられます。

図7:取引システムに加えた改善点の詳細分析
新しい戦略では、元の戦略と比べてドローダウン期間が短く、より安定したエクイティカーブを生成しています。この戦略は、ボラティリティが低く安定した成長を示しながらも、元のリスクの高い戦略よりも速い成長を実現しています。

図8:改良版取引戦略によるエクイティカーブは、元の戦略と比べて加速的な成長を示している
結論
腕を伸ばしてゴルフボールを空にかざし、AIモデルに月と目視で大きさを比較させることを想像してください。ある日にはモデルが月の方がわずかに大きいと判断し、別の日にはゴルフボールの方がわずかに大きいと判断するかもしれません。人間として、この行為は根本的に誤っており、ある程度滑稽であることも理解できます。しかし、そこで笑いは終わりです。
原理的には、私たちの機械学習モデルも、金融市場での取引において同様の誤りを静かに犯す可能性があります。この思考実験における誤りとは、モデルが月そのものを直接扱っているわけではなく、空に投影された月の像を比較している点にあります。
市場においても、私たちの機械学習モデルは「真の」将来価格を予測しているのではなく、むしろ入力特徴上に目的変数の像を描いているに過ぎません。したがって、モデルの予測を実際の価格と直接比較して同一視すべきではありません。むしろ、機械学習モデルは入力から学習した座標系を使って目的変数の像を描こうとしており、その像は常に現実から不可約距離によって隔たっています。その像はさまざまな要因によって歪む可能性があるため、直接的な予測に頼ることは避けるべきです。
この記事を読んだ後、読者は、このずれ誤差の影響を軽減するために複数の予測期間を活用すべき理由を理解し、しばしば見過ごされがちなこの誤差に対して、より意識的かつ適切に対処できるようになるでしょう。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/19371
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
平均足を使ったプロフェッショナルな取引システムの構築(第1回):カスタムインジケーターの開発
プライスアクション分析ツールキットの開発(第38回):ティックバッファVWAPと短期不均衡エンジン
プライスアクション分析ツールキットの開発(第39回):MQL5でBOSとChoCHの検出を自動化する
MQL5での取引戦略の自動化(第29回):プライスアクションに基づくガートレーハーモニックパターンシステムの作成
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索