神经网络变得轻松(第五十四部分):利用随机编码器(RE3)进行高效研究(基础篇)
随机编码器怎么给强化学习降维
在 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 范围调一遍验证收敛。
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 模板就能跑通前向结构。外汇与贵金属波动剧烈,这类模型仅作辅助研判,实盘杠杆风险极高。
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 策略测试器跑多品种回测。
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;