神经网络变得轻松(第四十五部分):训练状态探索技能(基础篇)
📘

神经网络变得轻松(第四十五部分):训练状态探索技能(基础篇)

第 1/3 篇

◍ 给训练状态加一层可观测窗口

在 MQL5 里跑神经网络训练,最容易被忽略的不是模型结构,而是训练过程本身是否「看得见」。很多交易者直接把 EA 挂上就等结果,等到权益曲线走歪了才发现是学习率或批次设置早就在第 3 轮就发散了。 下面这段把训练状态通过 Comment 打到图表左上角,每轮 epoch 结束刷新一次损失与准确率。实盘前你至少能在策略测试器里复现:若 20 轮内 loss 不降反升,大概率是特征归一化没做对,而不是网络容量不够。 外汇与贵金属品种上做这类实验波动放大明显,小样本过拟合概率偏高,任何「训练好看」的曲线都只是概率倾向,不等于实盘能复现。

MQL5 / C++
Comment("Epoch: ", epoch, "\nLoss: ", loss, "\nAccuracy: ", accuracy);

把复杂任务拆成技能层来训

分层强化学习能啃下相当复杂的问题,核心做法是把总目标切成分层子任务,再逐一训练所谓“技能”,让代理在环境里尽可能多拿主动权。 之前跑过的 DIAYN 和 DADS 就是两条老路。DIAYN 专教行为多样性,逼代理穷举环境,但大量技能对当前任务根本没用;DADS 改成从“技能对环境动态的影响”来学,挑那些能让状态变化收益最大的技能。 这两种旧法有个硬伤:技能在训练时是作为输入、顺带做探索的,实际覆盖的状态空间很薄。结果就是练出来的技能,碰到没见过的环境状态就懵,互动效率掉链子。 本文要拆的 EDL(探索、发现和学习)换了个切口,不直接拿旧技能当输入凑探索,而是从发现未知状态入手,从而把状态覆盖的窟窿补上,代理行为也更柔性、更跟手。外汇与贵金属行情状态切换极快,这类覆盖不足的代理直接实盘等于高风险的裸奔。

「EDL 用变分自编码器倒推隐藏技能」

探索、发现和学习(EDL)算法 2020 年 8 月见诸论文,核心是把“先训技能再排调度”的路子反过来:先在毫无环境先验时铺一批均匀采样状态,覆盖尽可能多的环境行为,再用变分自动编码器(VAE)从状态样本里反推“状态—技能”的隐变量映射。 第二阶段不依赖任何教导员轨迹,VAE 输入输出都是环境状态,潜在空间里逼出当前状态对应的技能分布,解码器再做逆映射。把确定性的状态-技能对应变成概率分布,复杂随机环境里模型稳定性通常会更好。 最后阶段用强化学习训代理者,内部奖励结构脱胎于 DADS 但做了化简。DADS 原奖励含一项对所有技能都为常数的减数,优化时只留被减数即可,不损训练品质却降了计算量。奖励函数固定、不随策略行为漂移,这一点和早前方法不同,收敛更稳。 外汇与贵金属行情属高随机环境,直接搬 EDL 框架前建议在 MT5 用历史 tick 先跑通 VAE 潜变量收敛,再接 RL 代理,避免过拟合到样本内状态。

◍ EDL 模型在 MT5 里的搭建与训练链路

把探索-发现-学习(EDL)方法落到 MQL5,第一道关是模型架构描述。所有网络结构都写进 CreateDescriptions 函数,向它传两个动态数组指针,分别装调度器(scheduler)和代理者(actor)的层定义。注意 EDL 不建独立鉴别器——这个角色由调度器里变分自动编码器的解码器兼任。 调度器输入端吞下历史K线(HistoryBars * BarDescr)和账户状态(AccountDescr)拼接成的原始向量,先过批量归一化,再经卷积降维提取特征,随后三个全连接层逐级压缩。最后一层维度是技能数的 2 倍:变分自动编码器每个潜变量用均值和离散度两个参数表达,和传统自编码器不同。重参数化层按技能数抽样,解码器用三个无激活全连接层只复原账户状态,不还原行情——代理者对价格影响可忽略,但余额直接受策略牵动。 代理者输入层维度砍到技能数,因为只接自编码器潜状态,已含全部压缩信息,可省掉预处理模块。后面三个全连接层做决策,输出接完全参数化分位数函数,能细看奖励分布。这样调度器和代理者共用同一套架构描述,贯穿 EDL 全部阶段。 训练分三段 EA:Research.mq5 跑探索,给调度器喂历史、取潜状态送代理者决策;StudyModel.mq5 训调度器自编码器,目标不是还原输入而是预测下一根后账户状态,加三条限制(开仓最低利润、平仓最小波动、反向单全平后再开)塑形期望行为;StudyActor.mq5 训代理者,用自编码器解码器当判别器,奖励 = 预测状态与期望状态欧氏距离乘 -1,遍历所有可能动作填奖励向量,不减最小值归一化、故意不接 SoftMax,保住远离期望的惩罚结构。 外汇与贵金属杠杆高、跳空频繁,自编码器预测状态永远带近似误差;代理者最终表现上限取决于预测账户状态与真实交互状态的吻合度,实盘前务必用历史回放缓冲校验预测品质。

MQL5 / C++
class="type">bool CreateDescriptions(CArrayObj *actor, CArrayObj *scheduler)
  {
class=class="str">"cmt">//---
   CLayerDescription *descr;
class=class="str">"cmt">//---
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         class="kw">return class="kw">false;
     }
class=class="str">"cmt">//---
   if(!scheduler)
     {
      scheduler = new CArrayObj();
      if(!scheduler)
         class="kw">return class="kw">false;
     }
class=class="str">"cmt">//--- Scheduler
   scheduler.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronBaseOCL;
   class="type">int prev_count = descr.count = (HistoryBars * BarDescr + AccountDescr);
   descr.window = class="num">0;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!scheduler.Add(descr))
     {

堆叠八层神经结构的描述符写法

在 MT5 的 OpenCL 神经调度器里,网络不是一口气建好的,而是一层一层往 scheduler 里 Add。每一层都先 new 一个 CLayerDescription,填完字段再提交,任何一步失败就 delete 并返 false,避免显存泄漏。 第一层是批归一化,type 设 defNeuronBatchNormOCL,count 沿用上层传进来的 prev_count,batch 写死 1000,激活函数 None,优化器 ADAM。这一层只做标准化,不引入非线性。 第二层卷积 defNeuronConvOCL 把 prev_count 减 1 作为输出通道数,窗口 2、步长 1、输出窗口 4,激活用 LReLU。第四层是同样配置的卷积,说明网络在中间插了两次「卷积+证明层」的对称结构。 第三、五层都是 defNeuronProofOCL,window 4、step 4,count 不变,作用偏向特征证明与降维。第六、七层退回全连接式 base 层,节点数分别 256(TANH)和 128(LReLU),都在 ADAM 下优化。 把这段代码直接贴进你的 EA 训练初始化函数,改 prev_count 初值就能控制前端通道规模;外汇与贵金属行情噪声大,这类深结构过拟合概率偏高,建议先在小样本上跑通再上实盘。

MQL5 / C++
   class="kw">delete descr;
   class="kw">return class="kw">false;
   }
class=class="str">"cmt">//--- layer class="num">1
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = class="num">1000;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!scheduler.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- layer class="num">2
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">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">4;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!scheduler.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- layer class="num">3
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronProofOCL;
   prev_count = descr.count = prev_count;
   descr.window = class="num">4;
   descr.step = class="num">4;
   if(!scheduler.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- layer class="num">4
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">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">4;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!scheduler.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- layer class="num">5
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronProofOCL;
   prev_count = descr.count = prev_count;
   descr.window = class="num">4;
   descr.step = class="num">4;
   if(!scheduler.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- layer class="num">6
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronBaseOCL;
   descr.count = class="num">256;
   descr.optimization = ADAM;
   descr.activation = TANH;
   if(!scheduler.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- layer class="num">7
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronBaseOCL;
   descr.count = class="num">128;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!scheduler.Add(descr))
      {
      class="kw">delete descr;
      class="kw">return class="kw">false;
      }
class=class="str">"cmt">//--- layer class="num">8

常见问题

给训练状态加一层可观测窗口,把损失、梯度、隐藏层激活按步打印出来,就能定位是数据问题还是结构问题。
把任务拆成技能层分别训练,先让每层学会子能力再堆叠,比端到端硬训更稳。
小布可接入你的训练日志,自动标出异常步并提示可能原因,省去手动盯盘式看数。
EDL 用 VAE 把观测状态编码后重构,从潜变量里反推未被显式标注的技能维度,再作描述符输入。
按层定义输入维、激活、 dropout,用数组顺序描述八层结构,每层输出接下层输入,末层出技能向量。