神经网络变得轻松(第五十一部分):行为-指引的扮演者-评论者(BAC)(基础篇)
用 BAC 框架给 MT5 策略注入行为指引
在 MT5 里跑强化学习,传统 Actor-Critic 容易陷入无意义探索,样本效率偏低。BAC(Behavior-Guided Actor-Critic)把先验行为策略作为指引信号,让评论者多评估一步「偏离行为基线的代价」,actor 更新时受约束,收敛更稳。 外汇与贵金属杠杆高、滑点跳空频繁,这类模型只降低无效探索概率,不保证胜率,实盘前必须用历史tick复盘。 Dmitriy Gizlyk 在 2024-04-04 发布的示例里,单篇帖文 24 小时内获 743 次查看、4 条评论,说明社区对可落地的 NN 交易模块需求明确。开 MT5 新建 EA,把行为策略先写成普通信号函数,再接 RL 更新循环,是验证 BAC 的最低成本路径。
「从熵正则到分位数:连续动作空间的两条弯路」
前面两篇专门拆了软性 Actor-Critic(SAC)那套玩法。核心是在奖励函数里塞进一个熵项,用来在“探索环境”和“利用已知策略”之间找平衡。但熵项有个硬前提:你得知道动作被采样的概率密度,在连续动作空间里这事儿本身就很难精确估计。 我们之前试过分位数分布(quantile distribution)来绕开这个坑。做法是把动作空间离散成一堆分位数,每次推理时从学习到的分布里抽一个分位数,拿它的均值当实际动作。分位数够多、取值区间够窄时,近似连续空间的效果还行——但代价是模型复杂度直线上升,训练和推理成本都更贵,而且对网络结构本身也多了不少约束。
- 年 4 月提出的 BAC(Behavior-Guided Actor-Critic)给出了另一条替代路线,后面会具体说它怎么用行为指引避开前面这些限制。
◍ 用自动编码器替代熵正则来做探索调控
在软性 Actor-Critic 里,熵正则化虽然能逼着模型去踩低概率动作、从而顺带探索后续状态,但副作用很明显:一旦某条路径变优,其它动作概率下降、熵反而升高,模型会被拖去无意义地乱试,只有强正回报才能把它拉回主线。若新路线不行,动作概率降、熵涨得更凶,得靠重罚才压得住。温度系数权重没调好,研究和交易(开发)就会失衡。 BAC 算法换了个思路:把奖励函数里的熵项换成「状态-动作配对」的掌握度度量。具体用自动编码器当尺子——在见过的一批 (S,A) 上训好的自编码器,对已知配对能几乎无误差地重建;一旦喂入没见过的配对,重建误差会跳升。这个误差就直接当成「这一下我懂不懂」的信号。 比起熵正则,它有两个实在好处。一是确定性、随机性策略通吃,不挑 Actor 结构;二是刺激奖励随训练自然衰减到 0,模型会自己转入纯操作,而一旦出现神经网络泛化都没覆盖的新状态,探索立刻被激活,且和同状态下别的动作概率完全脱钩。 温度比率 α 不再手工拍脑袋,而是用公式算:σ 是 sigmoid,ω 固定为 10,Q 是评估动作质量的评论者网络。算出来 (1−α) 落在 0~0.5,动作质量评估越高、复制误差越趋 0,比值越靠近 0.5,逼模型跳出局部最优;误差大时分母涨、σ 趋 0,探索权重拉满。外汇与贵金属市场影响因素杂乱、连续动作空间难建模,这种按「懂没懂」动态切探索/开发的机制,比固定 ε 或熵权重更经得起 MT5 回测推敲。
用 MQL5 搭 BAC 模型架构与训练骨架
这一节把前面说的 BAC(行为感知评论者)思路落到 MT5 的 MQL5 代码里。模型主体沿用了上一版的扮演者-评论者框架,但把最后一个复杂神经层砍掉,随机扮演者政策改由变分自编码器的隐性层承载——输入张量大小正好是结果缓冲区的 2 倍,分别装分布均值和方差,连续动作空间里的随机性得以保留,计算量也降下来了。 评论者模型原样搬过来没动。自动编码器在这里充当“状态-动作”配对的访问计数器:它吃的是评论者某个隐藏状态(1 个源缓冲区),输出 1 个结果缓冲区,做到 1:1 比较,所以架构里只用全连接层。从第四层开始不再手写新层,而是反序复用前面描述,编码器任何改动(加层除外)会自动镜像到解码器。 样本采集仍由 ..\BAC\Research.mq5 这个 EA 负责,BAC 没改主采集逻辑,变动极小:往架构描述函数里加自编码器,OnInit 调用时传 3 个指针,但这个 EA 只用扮演者,所以评论者描述数组指针传两次凑数;奖励函数去掉了熵分量。 真正重活在 Study.mq5 训练 EA:新建自编码器动态数组、初始化全部模型、送进同一个 OpenCL 环境,并补上自编码器与评论者架构的一致性检查。温度比动态计算没单开模型,直接复用评论者,省一道训练工序。训练循环里每次按样本库和指定步长随机抽轨迹,跑扮演者+2 个目标评论者前向,取最小估值(已剔除熵项);评论者用同批样本同时训练,行为分量温度比依据两评论者误差的最小/最大/指数均值来算。 扮演者更新时,选平均误差最小的那个评论者,局部指针一存了事,避免反复双模型前向。自动编码器前向有个坑:它反向依赖评论者隐藏层,但评论者会切换,若层删除标志开着会误删指针,所以首次剥源数据层后必须关掉删除标志。自动编码器重建误差用向量变量批量算,行为分量损失直接并进去,扮演者反向分两步——先数据预处理单元分配梯度优卷积滤波,再决策模块逆向验算,顺序反了梯度会被覆盖。 下面这段是架构描述函数的开头,负责三个模型数组的懒初始化,注意 autoencoder 指针为空时也会 new 出来,但实际 Research EA 里可能只当占位用。
class="type">bool CreateDescriptions(CArrayObj *actor, CArrayObj *critic, CArrayObj *autoencoder) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!actor) { actor = new CArrayObj(); if(!actor) class="kw">return false; } if(!critic) { critic = new CArrayObj(); if(!critic) class="kw">return false; } if(!autoencoder) { autoencoder = new CArrayObj(); if(!autoencoder)
「Actor 网络的逐层堆叠细节」
在 MT5 用 OpenCL 跑强化学习策略时,Actor 网络的搭建靠 CLayerDescription 逐层 Add 完成。先清掉旧结构 actor.Clear(),再喂输入层:节点数 = HistoryBars * BarDescr,类型 defNeuronBaseOCL,激活函数 None,优化器 ADAM。 第一隐藏层用 BatchNorm(defNeuronBatchNormOCL),batch 设 1000,这步能缓解外汇小时线训练时的内部协变量偏移。第二层开始上卷积:defNeuronConvOCL,window=2、step=1、window_out=8,激活 LReLU,节点数随输入减 1。 第三层也是卷积,但 window=8、step=8,相当于对前层输出做 8 倍下采样,把局部形态压缩进 8 个通道。第四、五层退回全连接(defNeuronBaseOCL),节点数 256 → 128,均用 LReLU。 第六层用 defNeuronConcatenate 把账户状态(AccountDescr)拼进潜在向量 LatentCount,激活 SIGMOID 把输出压到 0~1,方便后续映射成仓位概率。任何一层 Add 失败就 delete descr 并 return false,避免显存泄漏。 实盘前建议把 batch=1000 在 MT5 策略测试器里降到 256 试跑,显存占用可能降 60% 以上,但收敛速度会偏慢,属正常概率现象。
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 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
◍ 策略网络与自编码器的层堆叠写法
在 MT5 里搭强化学习交易模型时,actor 网络的后几层基本是复制粘贴式堆叠。第 7、8 层都是 256 个 defNeuronBaseOCL 神经元,激活用 LReLU,优化用 ADAM;第 9 层扩到 2*NActions 个,第 10 层换成 defNeuronVAEOCL 做动作采样头,这种结构跑 EURUSD 15 分钟母样本时显存占用倾向平稳。 自编码器部分从 LatentCount 输入层开始,后面三层 count 依次除以 2,再硬写回 LatentCount,中间插一道固定 20 的写法有点反直觉,但能保证潜变量维度对齐。 开 MT5 把下面这段直接塞进 CNet 派生类的 Create 函数,编译过说明你的 CLayerDescription 封装没漏接口;若 actor.Add 返回 false,优先查 descr 是不是没 new 出来就提前 return 了。
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">//--- Autoencoder autoencoder.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(!autoencoder.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 = defNeuronBaseOCL; prev_count = descr.count = prev_count / class="num">2; descr.optimization = ADAM; descr.activation = LReLU; if(!autoencoder.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 = defNeuronBaseOCL; prev_count = descr.count = prev_count / class="num">2; descr.activation = LReLU; descr.optimization = ADAM; if(!autoencoder.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 = defNeuronBaseOCL; prev_count = descr.count = class="num">20; descr.count = LatentCount; descr.activation = LReLU; descr.optimization = ADAM; if(!autoencoder.Add(descr)) { class="kw">delete descr; class="kw">return false; }