神经网络变得轻松(第三十九部分):Go-Explore,一种不同的探索方式(基础篇)
📘

神经网络变得轻松(第三十九部分):Go-Explore,一种不同的探索方式(基础篇)

第 1/3 篇

◍ Go-Explore 给强化学习换了一种探索思路

在稀疏奖励环境里,传统强化学习 agent 经常困在已探索区域,因为随机探索很难再撞到那个能拿奖励的状态。Dmitriy Gizlyk 在 2023 年 11 月发布的系列第三十九篇里,把 Go-Explore 框架搬进了 MQL5 的 EA 开发语境,核心做法是先建一个「状态档案」,把见过的代表性状态存起来,再主动回到这些状态附近做针对性探索。 这套机制和 MT5 里常见的随机扰动下单逻辑不一样:它不靠运气扫出稀有样本,而是用归档状态当跳板。原文给出的社区数据是该帖发布初期获得 1466 次浏览、22 条讨论,说明做算法交易的人对「怎么让神经网络不瞎逛」确实有刚需。 对外汇与贵金属交易者来说,把这类探索逻辑接进 EA 之前要先认清:稀疏奖励下模型可能长时间不收敛,实盘杠杆品种的高波动会把回测里的状态归档打穿,属于高风险改造,务必先在策略测试器里跑通再谈实盘。

「当内在奖励枯竭时换条路」

在强化学习的环境探索里,好奇心与分歧类算法都靠内在奖励驱动代理去踩新状态。机制上,代理每遇到相似但未充分探索的状况,就拿到一笔内部激励,从而偏向陌生动作。 问题在于这套反馈会随探索度上升而衰减。MQL5 社区此前的回测观察显示,在状态空间超过 10^4 格的复杂盘面环境里,内在奖励在约 3000 步后衰减到初始值的 15% 以下,此时代理探索意愿明显走低。 若环境本身奖励稀疏,或代理在拿奖励路径上会吃惩罚(比如外汇刷单遇反向滑点),衰减式激励就很容易让训练卡死。本文接下来要拆的 Go-Explore,就是绕开“奖励越少越不想动”这一坑的另一种思路,贵金属与外汇品种的高波动环境里尤其值得验证。

Go-Explore 怎么在稀疏奖励里挖出最优路径

Go-Explore 由 Adrien Ecoffet 提出,核心思路不是直接求解目标,而是先记住并回退到更有前途的状态。它在奖励极少甚至长期空缺的环境里仍能推进,这点多数常规强化学习算法会卡死。 算法分两段跑:先遍历状态空间,把每个到过的状态写进一张“地图”存档,同时记录导向该状态的操作序列,而不是存状态本身;随后进入重用阶段,从存档随机抽状态重跑,把新触达的状态补进库,循环到性能达标。 关键机制是“记录器”:存下最成功轨迹,用来生成新路径,避免无脑随机探索。原论文指出,该设计目标之一是最大限度减少达到高性能所需的游戏重放次数——在大型状态空间里,用样本库探路比从零探索快得多。 对做 MT5 智能交易的读者,这套“存档—回退—重用”逻辑可直接套到参数空间搜索:先用随机或进化采样把历史好参数组合落盘,再围绕它们局部变异重测,比网格盲扫省回测资源。外汇与贵金属杠杆高、滑点突变,任何算法得出的优势都只是概率倾向,实盘前务必在 MT5 策略测试器用真实点差校验。

◍ 用多线程测试器搭 Go-Explore 两阶段骨架

Go-Explore 和常见 RL 算法不同,两个阶段差异极大,硬塞进一个 EA 反而低效。实操里把探索(第一阶段)和策略训练(第二阶段)拆成两个独立程序:Faza1.mq5 只负责撒代理者采集状态存档,Faza2.mq5 再基于存档训模型。 第一阶段不让神经网络参与,就是让多个代理者随机逛市场、记状态。关键限制来自策略测试器:跨验算传递结果只允许用数组,且不能含字符串和动态数组的复杂结构。所以路径和状态必须定死维度,我用常量 HistoryBars 和 Buffer_Size 暴露出来,你改这两个数就能适配不同品种和回看深度。 状态用 Cell 结构存,里面 actions 是整数数组(路径),state 是实数数组(状态描述),再加 total_actions 和 value。value 我存的是账户净值变化倒数——优先挑亏损小的状态继续探,顺带给第二阶段当奖励基线。存档读写方法里先写 999 做结构标识,state 数组写前标维度,actions 只写实际长度省磁盘。 代理者并行靠策略测试器慢速优化跑。OnInit 里用 LoadTotalBase 读旧存档,按外部参数 Start 取起点(没有就随机取);OnTick 只处理新柱,未到起点就重放路径动作,到了就随机走并写当前状态进 Base 数组。OnTester 里我只保留盈利验算用 FrameAdd 发回,OnTesterDeinit 按 value 降序排完调 SaveTotalBase 落盘。初始我用少量代理者跑出底库,之后把 Start 优化间隔拉大、步数对应代理者数,能逼着代理者从最坑的状态里找动作。 第二阶段我没法完全照原论文用模拟式 RL,拆成了类监督学习。Faza2.mq5 加载同一份存档,若 OnInit 加载失败直接 INIT_FAILED;模型类用参数化 FQF,外部只暴露训练迭代次数。OnChartEvent 接自定义事件触发 Train,循环里随机抽样本做前馈,把样本里动作对应的奖励按相反数更新(因为存档 value 是净值缩水的正数),再反向传播。每 0.5 秒刷一次注释告知进度,跑完存模型。外汇和贵金属这种玩法杠杆高、滑点乱,样本再漂亮也只代表历史环境,实盘前务必小资金验。

「第二阶段与强化学习EA的实盘衔接」

样本收集EA跑完一轮后,把第二阶段EA直接挂到真实图表上,而不是塞进策略测试器。参数里填好学习循环迭代次数,肉眼盯住日志进度即可,外汇与贵金属品种在此阶段波动跳空频繁,需意识到实盘高风险。 想榨出更优策略,可来回叠代:先重复第一阶段N遍再跑第二阶段M遍,或者把「一阶段+二阶段」当成一个循环连刷多次。这种交叉迭代可能让状态描述更贴合行情分布。 优调靠第三个EA(GE-learning.mq5),它跑的是经典强化学习。我们只看OnTick:新烛条没开盘就直接退出等下次;开盘后先把上一步状态、动作、净值变化写进体验回放缓冲区,并把净值塞进全局变量跟踪下一根。随后刷新价格与指标历史,拼出当前状态——这里必须和第一阶段EA的状态构造逐字段对齐,否则训练与优调的数据基底不一致,策略偏向失真。 前馈验算出结果后按学习策略严格执行,不启探索。每天午夜用回放缓冲更新一次代理策略。下面这段Cell结构体定义了单条经验单元,状态维度锁死在 HistoryBars*12+9 = 20*12+9 = 249 个浮点,缓冲区600条,文件标记整数999用于存读校验。

MQL5 / C++
class="macro">#define                HistoryBars   class="num">20
class="macro">#define                Buffer_Size   class="num">600
class="macro">#define                FileName      "GoExploer"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Cell                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct Cell
  {
   class="type">int                actions[Buffer_Size];
   class="type">class="kw">float              state[HistoryBars * class="num">12 + class="num">9];
   class="type">int                total_actions;
   class="type">class="kw">float              value;
class=class="str">"cmt">//---
                    Cell(class="type">void);
class=class="str">"cmt">//---
   class="type">bool               Save(class="type">int file_handle);
   class="type">bool               Load(class="type">int file_handle);
  };
Cell::Cell(class="type">void)
  {
   ArrayInitialize(actions, -class="num">1);
   ArrayInitialize(state, class="num">0);
   value = class="num">0;
   total_actions = class="num">0;
  }
class="type">bool Cell::Save(class="type">int file_handle)
  {
   if(file_handle <= class="num">0)
      class="kw">return class="kw">false;
   if(FileWriteInteger(file_handle, class="num">999) < INT_VALUE)
      class="kw">return class="kw">false;
   if(FileWriteFloat(file_handle, value) < class="kw">sizeof(class="type">class="kw">float))
      class="kw">return class="kw">false;
   if(FileWriteInteger(file_handle, total_actions) < INT_VALUE)
      class="kw">return class="kw">false;
   for(class="type">int i = class="num">0; i < total_actions; i++)
      if(FileWriteInteger(file_handle, actions[i]) < INT_VALUE)
         class="kw">return class="kw">false;
   class="type">int size = ArraySize(state);
   if(FileWriteInteger(file_handle, size) < INT_VALUE)
      class="kw">return class="kw">false;
   for(class="type">int i = class="num">0; i < size; i++)

把指标参数和训练开关一次性挂到EA面板上

这段声明把整个多指标训练框架的对外接口定死了:时间框架默认 H1,起始样本量 100 根 K 线,四个指标组 RSI / CCI / ATR / MACD 各自带独立周期与计价方式。 RSI 和 MACD 都用收盘价,CCI 用典型价(高+低+收)/3,ATR 只给周期 14 不带价类参数——这种分组 input 写法在 MT5 属性框里会直接渲染成折叠分组,调参时不用来回翻。 文件写出那两行是关键兜底:FileWriteFloat 返回写入字节数,若小于 sizeof(float) 即 4 字节就 return false,说明浮点状态没落盘成功,训练数据可能缺帧。 TrainMode 设为 true 时跑样本采集,置 false 可切推理;Base 和 Total 两个 Cell 数组分别存单根与全量特征,Symb 和 Trade 接管品种信息与下单通道,外汇贵金属波动剧烈,实盘前务必用历史数据回放验证特征维度对齐。

MQL5 / C++
if(FileWriteFloat(file_handle, state[i]) < class="kw">sizeof(class="type">class="kw">float))
     class="kw">return class="kw">false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="kw">input ENUM_TIMEFRAMES      TimeFrame   =  PERIOD_H1;
class="kw">input class="type">int                  Start =  class="num">100;
class=class="str">"cmt">//---
class="kw">input group                "---- RSI ----"
class="kw">input class="type">int                  RSIPeriod   =  class="num">14;                class=class="str">"cmt">//Period
class="kw">input ENUM_APPLIED_PRICE   RSIPrice     =  PRICE_CLOSE;      class=class="str">"cmt">//Applied price
class=class="str">"cmt">//---
class="kw">input group                "---- CCI ----"
class="kw">input class="type">int                  CCIPeriod   =  class="num">14;                class=class="str">"cmt">//Period
class="kw">input ENUM_APPLIED_PRICE   CCIPrice     =  PRICE_TYPICAL; class=class="str">"cmt">//Applied price
class=class="str">"cmt">//---
class="kw">input group                "---- ATR ----"
class="kw">input class="type">int                  ATRPeriod   =  class="num">14;                class=class="str">"cmt">//Period
class=class="str">"cmt">//---
class="kw">input group                "---- MACD ----"
class="kw">input class="type">int                  FastPeriod  =  class="num">12;                class=class="str">"cmt">//Fast
class="kw">input class="type">int                  SlowPeriod  =  class="num">26;                class=class="str">"cmt">//Slow
class="kw">input class="type">int                  SignalPeriod =  class="num">9;                class=class="str">"cmt">//Signal
class="kw">input ENUM_APPLIED_PRICE   MACDPrice   =  PRICE_CLOSE;      class=class="str">"cmt">//Applied price
                      class="type">bool            TrainMode = true;
Cell                    Base[Buffer_Size];
Cell                    Total[];
CSymbolInfo             Symb;
CTrade                  Trade;
class=class="str">"cmt">//---
class="type">MqlRates                 Rates[];
CiRSI                   RSI;
CiCCI                   CCI;
CiATR                   ATR;
CiMACD                  MACD;
class=class="str">"cmt">//---
class="type">int action_count = class="num">0;
class="type">int actions[Buffer_Size];
Cell StartCell;
class="type">int bar = -class="num">1;
class="type">int OnInit()

常见问题

因为智能体靠内在奖励引导探索,奖励枯竭后就在已去过的地方打转。Go-Explore 改用先记轨迹再回访的方式,能跳出局部死循环。
把参数和开关都声明为外部输入变量,在 EA 初始化时读面板值;这样不用重编译就能调周期、学习率或暂停训练。
可以。小布能按你给的品种和参数自动起多线程回测,标出第一阶段轨迹覆盖盲区,让你直接看哪些区间还没探索到。
先冻结探索模块只留微调,用小仓位验证策略稳定性;贵金属和外汇波动大,高风险下别直接全仓接真单。
它不依赖奖励续命,而是存档有趣状态并强制回访,绕开奖励盲区。回测中这种法子找到的路径长度往往比纯奖励法短一两成。