神经网络变得轻松(第五十四部分):利用随机编码器(RE3)进行高效研究(基础篇)
📘

神经网络变得轻松(第五十四部分):利用随机编码器(RE3)进行高效研究(基础篇)

第 1/3 篇

随机编码器怎么给强化学习降维

在 MetaTrader 5 里做强化学习研究,状态空间一大,采样效率就会塌方。RE3(Random Encoders for Efficient Exploration)的思路很直接:用一堆随机初始化的编码器把原始观测压到低维表征,再拿表征的相似性去驱动探索奖励,而不是靠环境回报硬碰。 原文作者 Dmitriy Gizlyk 在 2024 年 4 月 19 日发布的这一节里提到,该方法在 MT5 的 EA 回测框架下能把无效探索步数明显压下来,同一套网格搜索里候选策略的通过率有可见提升。外汇与贵金属品种波动跳变频繁,这类随机表征对噪声敏感,实盘前务必在策略测试器里跑多品种验证,存在滑点与断层导致表征失稳的高风险。 落地动作很简单:开 MT5 → 策略测试器 → 载入你现有的 RL 决策 EA,把观测先过一层随机投影再进策略网络,对比关 / 开 RE3 的回测样本效率,大概率能看出探索曲线更早收敛。

「探索困境与随机编码器的切入点」

强化学习在交易环境里最头疼的是有效探索——模型总爱待在已踩过的位置,不愿碰未涉足的状态路径。常见做法是挂一套内部奖励去哄它探索,但这会带来额外模型和算法复杂度,而且“动作新颖”未必等于把环境摸全。 基于动作熵和状态熵的评估看起来最干净,可它要求你先知道执行动作和跳转状态的概率分布;在连续动作/状态空间下,直接算熵几乎不可行。 RE3(随机编码器高效探索)那篇思路值得在 MT5 上验证:用随机编码器把状态压进隐空间,再估隐空间熵来驱动探索,绕开了连续空间概率建模的坑。外汇与贵金属波动连续、样本贵,这类方法高风险,但值得跑通后再谈实盘适配。

◍ 用随机编码器绕开额外训练成本

在连续动作和状态空间里做强化学习,真实案例常出现每个状态-动作对只在训练集里露一次脸,再撞见相同状态的几率近乎为 0。传统做法如 BAC 会另训一个自动编码器判新颖性,但这等于多引入一套超参与模型,品质波动会直接拖累主策略,资源也烧得凶。 RE3 的核心赌注是:能不能不训额外模型?作者点出卷积网络天生会抽对象的表里特征、压低维度,但重点在「已训练」这三个字——其实随机初始化、参数固定的卷积编码器,已经足够抓出两状态是否相近。原文用 k-最近态可视化比对过:随机编码器表述空间的距离,和真实状态空间距离能对上。 于是 RE3 只在固定随机编码器 f(θ) 的表征空间里最大化状态熵。内部奖励公式用 L2 距离范数 ‖y_i - y_j‖,加 1 保对数非负: r_i = log(1 + ‖y_i - y_j‖) 足够多近邻状态下该值倾向收敛到 0,且因编码器参数固定,状态对距离训练期不变,奖励比训着的编码器稳得多。 实操上,低维表示存进经验回放缓冲,交互时直接查缓冲算距离,不必每次更新都过一遍高维编码器,效率与估值稳定性都更高。β 控制内外奖励平衡(β≥0),作者建议指数衰减 β_t = β_0·p^t(p 为衰减率),让智能体后期更多吃外部奖励;无外部奖励时也能先拿 RE3 预探索高维空间。外汇/贵金属行情高维且高风险,这种探索法只解决「见得多」,不承诺收益。

用 MQL5 搭一套 RE3 与 SAC 糅合的训练骨架

这套实现没有照搬 RE3 原作者的完整算法,而是把主要思想和前几篇研究过的软性扮演者-评论者(SAC)揉在一起。卷积编码器被加进定义模型架构的方法里,代理者在连续动作空间里训练随机政策,扮演者架构复用了《行为指引扮演者-评论者》一文里的结构,因为 RE3 只负责估算奖励的熵分量,扮演者本身可以简化。 评论者直接吃扮演者的隐含状态作为输入,奖励做了分解,把原本每个动作 6 个单独熵分量元素压缩成 1 个内部奖励元素。编码器这边和原方法第一个区别在于:RE3 原版靠隐含状态表述间的距离算内部奖励,这里改用「状态-动作」对在编码器源数据层大小里的隐含表述。编码器没经过预训练,所以批量常规化层没用,但接了一个全连接层,再叠 3 个连续卷积层降维,最后再用全连接层把隐藏表述压到指定维度;除第一层外全用 LReLU,激活范围不限制,方便把对象分准。 经验回放缓冲区里同时存状态的隐含表述和 standard 数据集,只算一次隐含表述就能反复训练用。第一次跑 Research.mq5 收集数据时没有预训练模型,扮演者随机初始化没问题,但编码器若也随机生成,优化模式下并行多个 EA 验算会得到互相不可比的隐含表述,直接破坏 RE3 原则。我选了第二个方案:在 Study.mq5 主体里生成编码器和编码表述,不动 Research.mq5 的存储结构。 Study.mq5 里建 6 个模型对象只训 3 个,目标模型用软更新。初始化时加载训练集和预训练模型,缺了就建随机参数的新模型,并延长目标模型的使用时间先做预训练。所有模型丢进同一个 OpenCL 环境,训前校验架构。Train 过程先从各验算的 Total 变量累加出状态总量,再循环给所有状态-动作对建隐含表述:原始数据进缓冲区 → 调卷积编码器前向 → 结果写进嵌入矩阵对应行,外部奖励存同编号行,并打信息消息监控进度。 训练循环里随机抽验算和状态,过阈值就调目标评论者前向;先跑扮演者前向,再调两种目标评论者直接验算,按软 SAC 取后续状态估值最小值(实现里用奖励元素简单求和,也可换权重向量积)。target_reward 向量存的是评论者预测减实际奖励再乘折扣因子后的方差,它驱动扮演者政策随状态值偏移。评论者训练用分解奖励,借 CAGrad 校正误差梯度:先造目标值减预测得偏差,CAGrad 校正完再加回预测值,两个评论者各算一遍。 RE3 的引入点在政策更新后:把分析状态、账户状态、更新后动作收进缓冲区,跑一次扮演者直接验算、一次选定评论者直接验算、一次编码器直接验算拿嵌入,再调 KNNReward 函数结合 CAGrad 和 RE3 造目标值。KNNReward 接收邻居数 k、状态嵌入、训练集嵌入矩阵、奖励矩阵,算出嵌入间距离后只找 k 个最小距离而不全排序——沿距离向量验一次,把前 k 行拷进结果矩阵并维护最大距离位置,循环扫完即得 k-最近邻。内部奖励定义为 k-最近邻奖励的距离加权平均值,熵分量字段写 RE3 距离的对数。 这套实现里卷积编码器架构的差异和 KNNReward 的不全排序搜邻,是和原版 RE3 最实打实的两处分叉。外汇与贵金属行情下用这类 RL 模型做策略探索属高风险,隐含表述不可比或 k 值设错都会让奖励估计失真,建议直接开 MT5 把 Study.mq5 的卷积层数和 LReLU 范围调一遍验证收敛。

MQL5 / C++
class="type">bool CreateDescriptions(CArrayObj *actor, CArrayObj *critic, CArrayObj *convolution)
  {
class=class="str">"cmt">//---
   CLayerDescription *descr;
class=class="str">"cmt">//---
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)

「Actor 网络逐层堆叠的写法」

在构建强化学习的 Actor 部分时,先要把 critic、convolution 两个容器做空指针保护:若未实例化就 new 一个 CArrayObj,失败直接 return false,避免后续往空对象里 Add 导致崩溃。 actor.Clear() 之后开始填输入层。输入层用 defNeuronBaseOCL,神经元数量 = HistoryBars * BarDescr(例如 HistoryBars=200、BarDescr=5 时就是 1000 个输入),激活函数 None,优化器 ADAM。 第一隐藏层接 BatchNorm(defNeuronBatchNormOCL),batch 写死 1000,激活仍留 None;第二层进卷积 defNeuronConvOCL,窗口 2、步长 1、输出窗口 8,激活 LReLU,此时 prev_count 减 1。第三层继续卷积,窗口 8、步长 8、输出窗口 8。 后面三层回到全连接:layer4 是 256 个神经元、layer5 是 128 个,都用 LReLU + ADAM。每一层都先 new CLayerDescription,Add 失败就 delete 并返回 false,这段骨架直接拷进 MT5 的 AI 模板就能跑通前向结构。外汇与贵金属波动剧烈,这类模型仅作辅助研判,实盘杠杆风险极高。

MQL5 / C++
   if(!critic)
   {
      critic = new CArrayObj();
      if(!critic)
         class="kw">return false;
   }
   if(!convolution)
   {
      convolution = new CArrayObj();
      if(!convolution)
         class="kw">return false;
   }
class=class="str">"cmt">//--- Actor
   actor.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   class="type">int prev_count = descr.count = (HistoryBars * BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
   {
      class="kw">delete descr;
      class="kw">return false;
   }
class=class="str">"cmt">//--- layer class="num">1
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = class="num">1000;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
   {
      class="kw">delete descr;
      class="kw">return false;
   }
class=class="str">"cmt">//--- layer class="num">2
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = prev_count - class="num">1;
   descr.window = class="num">2;
   descr.step = class="num">1;
   descr.window_out = class="num">8;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
   {
      class="kw">delete descr;
      class="kw">return false;
   }
class=class="str">"cmt">//--- layer class="num">3
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = prev_count;
   descr.window = class="num">8;
   descr.step = class="num">8;
   descr.window_out = class="num">8;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
   {
      class="kw">delete descr;
      class="kw">return false;
   }
class=class="str">"cmt">//--- layer class="num">4
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = class="num">256;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!actor.Add(descr))
   {
      class="kw">delete descr;
      class="kw">return false;
   }
class=class="str">"cmt">//--- layer class="num">5
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = class="num">128;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
   {
      class="kw">delete descr;
      class="kw">return false;
   }
class=class="str">"cmt">//--- layer class="num">6

◍ 拼装网络尾段与奖励结构

Actor 网络从第六层往后继续堆叠:先放一个 Concatenate 层把潜变量和账户描述拼起来,window 取上一层的 prev_count、step 绑定 AccountDescr,优化器统一用 ADAM、激活走 SIGMOID。 第七到九层都是 BaseOCL 全连接,第七、八层节点数固定 256、激活 LReLU;第九层节点数写成 2 * NActions,给后续动作均值方差留双路输出。第十层切到 VAEOCL、节点数等于 NActions,负责把分布重参数化。 奖励信号一共挂了四项:0 号是余额增量 Delta Balance,1 号是净值增量(回撤为负、盈利为正),2 号对空仓施罚,3 号记平均持仓距离。Critic 侧先 Clear 再建输入层,type 用 BaseOCL、count 回写 LatentCount 到 prev_count、激活设 None。外汇与贵金属波动剧烈,这类结构只是建模骨架,实盘前务必在 MT5 策略测试器跑多品种回测。

MQL5 / C++
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = prev_count;
   descr.step = AccountDescr;
   descr.optimization = ADAM;
   descr.activation = SIGMOID;
   if(!actor.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">7
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = class="num">256;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">8
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = class="num">256;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">9
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = class="num">2 * NActions;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">10
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronVAEOCL;
   descr.count = NActions;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Rewards structure                                                 |
class=class="str">"cmt">//|  class="num">0   -  Delta Balance                                             |
class=class="str">"cmt">//|  class="num">1   -  Delta Equity( "-" Drawdown / "+" Profit)                 |
class=class="str">"cmt">//|  class="num">2   -  Penalty for no open positions                            |
class=class="str">"cmt">//|  class="num">3   -  Mean distance                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Critic
   critic.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = LatentCount;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!critic.Add(descr))
     {
      class="kw">delete descr;

常见问题

可用随机编码器把高维市场状态投影到低维空间,降维后再喂给策略网络,能明显缩短单轮训练耗时。
这是探索困境;随机编码器能度量状态的新颖度,用编码距离当探索奖励,逼智能体去碰没去过的行情区域。
小布可对接你的训练日志,自动标出编码维度坍塌或探索奖励异常,省去你盯曲线的重复劳动。
直接用随机初始化的固定编码器,不反向传播更新它,就能零额外训练成本拿到降维表征。
尾段用一层线性输出动作均值,配合固定标准差,把环境盈亏和探索奖励按权重相加作为总奖励回传即可。