神经网络变得轻松(第四十四部分):动态学习技能(基础篇)
📘

神经网络变得轻松(第四十四部分):动态学习技能(基础篇)

第 1/3 篇

◍ 给 EA 装上会进化的学习机制

在 MT5 里写 EA,多数人把参数写死,行情一变就失效。其实可以借助动态学习,让模型在运行时持续吸收新样本,而不是只在回测里训一次。 这套思路的核心,是把网络权重当作可变状态存进缓冲区,每根新 K 线闭合后做一次轻量更新。外汇与贵金属波动跳变频繁,这种机制能降低过拟合概率,但高频重训也可能放大噪声,属于高风险玩法。 实际落地时,建议先用 EURUSD 的 M15 跑 3 个月样本外观察权重漂移幅度,再决定更新频率。别把正态当圣经,样本外发散快说明市场结构已切换。

把难预测的技能拆成可预测的子任务

在外汇与贵金属这种高噪声随机环境里,直接训一个能泛化的总模型几乎不可能,把问题拆成子任务再分别建模,成功率会明显提升。之前我们用的 DIAYN 靠奖励不可预测行为来逼出多样技能,代价是技能本身难以预判,后续做状态规划和风控会很头疼。

  • 年提出的 DADS(技能的动态感知探索)正好补这个洞:它要求技能既保持行为多样,又让模型能依据当前状态预测下一步动作。对 MT5 上做 AIGC 辅助盯盘的人来说,可预测性直接决定你敢不敢把信号接进实盘。

贵金属与外汇杠杆高、滑点突变频繁,任何技能信号都只是概率倾向,实盘前务必在策略测试器用历史数据跑通再上。

「DADS 怎么用两套模型换出可预测技能」

DADS 和 DIAYN 一样靠两个模型打配合:技能模型(代理者)负责出动作,鉴别器负责基于初始状态和所用技能预测下一状态。区别在鉴别器干的是反向活——DIAYN 从新状态反推技能,DADS 拿初始状态加技能向量预测未来状态,类似自动编码器但解码端输出的是下一步状态而非重建原数据。 训练是迭代式的,先喂一批经验回放缓冲里的数据训鉴别器,用梯度下降逼它预测准;之后训代理者时,奖励来自鉴别器输出的概率比:分子是当前技能预测出的状态 q(s'|s,z),分母是全部 L 个技能的平均预测状态。这公式逼着代理者既贴近可预测结果、又偏离统计均值,从而在可预测性和多样性之间拉平衡。 两个模型共用同一个回放缓冲,但每次迭代随机抽两批独立数据分别训,避免互相带偏。由于代理者被训过后行为会偏离缓冲里的旧样本,作者建议加重要性系数——当前策略动作概率除以缓冲中该动作概率,把权重向新策略靠、摊平随机动作噪声。 原文实测这套迭代要反复跑多轮才能收敛,MT5 上若想复现,先把经验回放容量设足,否则鉴别器初始近似太糙会拖慢整个训练。外汇/贵金属环境噪声大,这种无监督技能学习在高波动品种上过拟合风险偏高,验证时建议先用低频周期跑小技能数(如 L=4)看预测误差是否收敛。

◍ DADS 的三模型架构与训练 EA 改造

DADS 实现沿用 DIAYN 的三角色划分:代理者(技能模型)、鉴别器(动态模型)、调度器。代理者输入为当前状态向量拼接技能独热向量,输出动作空间概率分布;其架构保持不动,以便和 DIAYN 做同构架对照实验。代理者内部用批量常规化接两层卷积+子采样提取形态趋势,再进全连接层与分位数 FQF 决策模块,FQF 输出能给出环境随机性下的奖励分布而非单点均值。 鉴别器是唯一动刀的模块。因本实现不做多步前瞻规划,且散户对价格历史零影响,故内部奖励只需预测账户状态指标,不必预测价格历史。为加速训练,鉴别器改成多头输出:一次前向返回所有技能的预测状态。源数据层与调度器类似,只描述系统状态不含技能 ID;输出层大小 = 技能数 × 账户状态元素数,且因账户相对值不可激活,输出用无激活线性层。全部模型定义收进函数库 Trajectory.mqh 的 CreateDescriptions,避免 EA 间手工拷贝。 训练侧用 3 个 EA,Research.mq5 仅改文件名与架构常量;Study.mq5 改动最大。新增 GetNewState 方法:基于账户前态、计划动作、回放缓冲区的后续价格盈亏,计算出(非预测)账户新态,绕开策略测试器重放,可显著扩充训练集。GetAgentReward 则据鉴别器预测、所选技能、前账户态算内部奖励——它从所有技能预测中隔离当前技能向量,用 Reshape(非 Resize,否则丢数据)重排矩阵,再按持仓变化方向判定奖励动作(双向减仓判平仓,否则取变化最大方向,相等则判等待以激励主动),奖励取预测值除均值后的绝对值对数,或欧氏距离备选。 训练主循环按阶段顺序迭代:阶段0鉴别器、阶段1代理者、阶段2调度器,迭代次数与各阶段包大小均由 EA 外部参数控。鉴别器阶段对所有技能循环采样代理者动作并算目标态;代理者阶段对单状态全技能枚举以强化技能辨识;调度器阶段把完整概率分布(非采样非贪婪)送代理者,代理者贪婪选动作,外部奖励用账户余额相对变化。每轮结束打印进度。外汇/贵金属训练涉及真实点差与滑点,回测过拟合风险高,上 MT5 策略测试器跑 Study.mq5 前务必核对外部参数与 CreateDescriptions 一致性。

Actor 网络的层堆叠与参数落点

除 EA 调度与训练模型测试逻辑保持不动外,整套程序的完整代码都随附件提供,解压后放进 MT5 的 MQL5\Experts\DADS 目录即可直接编译运行。 下面这段是 Actor 部分的前几层定义,用 OpenCL 后端跑神经网络。输入层节点数由 HistoryBars * BarDescr + AccountDescr + NSkills 算出来,优化器统一用 ADAM,激活函数先留 None。 第一层接 BatchNorm,batch 设成 1000,这一步在训练时做批量归一,能压住梯度发散的可能。第二层开始用卷积(defNeuronConvOCL),窗口 2、步长 1、输出窗口 4,激活换 LReLU,节点数会比上一层少 1。 往后 Proof 层做窗口 4、步长 4 的下采样,再叠一层卷积和两层全连接(256 节点,分别用 TANH 和 LReLU)。外汇与贵金属杠杆高,这类模型在实盘只是概率倾向,不等于稳定胜率,上 MT5 前先开策略测试器跑历史样本。

MQL5 / C++
class=class="str">"cmt">//--- Actor
  actor.Clear();
  CLayerDescription *descr;
class=class="str">"cmt">//--- Input layer
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronBaseOCL;
  class="type">int prev_count = descr.count = (class="type">int)(HistoryBars * BarDescr + AccountDescr + NSkills);
  descr.window = class="num">0;
  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">4;
  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 = defNeuronProofOCL;
  prev_count = descr.count = prev_count;
  descr.window = class="num">4;
  descr.step = class="num">4;
  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 = 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(!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;
  descr.count = class="num">256;
  descr.optimization = ADAM;
  descr.activation = TANH;
  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 = 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">7

「Actor与Scheduler的层定义落点」

把强化学习模型拆进 MT5 的 OpenCL 后端时,actor 与 scheduler 两张网必须各自独立堆层。下面这段直接给出两层网络的末端与输入侧定义,复制进 EA 的 CNet 初始化函数就能跑通编译。 actor 的第 7 层用 128 个基础神经元、LReLU 激活、ADAM 优化;第 8 层切到 FQF 分位数输出,window_out 写死 32,输出维度绑定 NActions。任何一层 Add 失败都要 delete descr 并 return false,否则显存对象会泄漏。 scheduler 的输入层神经元数由 (HistoryBars * BarDescr + AccountDescr) 动态算出,window 置 0 表示不吃时序滑窗;随后接一层 batch=1000 的批归一化,再叠两层 256 宽的全连接(TANH 与 LReLU 各一),末端用 FQF + SoftMax 双头给出 NSkills 维度的技能概率。外汇与贵金属行情噪声明,这类网络在 live 上过拟合概率偏高,上实盘前建议先跑两周 tick 回测。

MQL5 / C++
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   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">8
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronFQF;
   descr.count = NActions;
   descr.window_out = class="num">32;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
       class="kw">delete descr;
       class="kw">return false;
     }
class=class="str">"cmt">//--- Scheduler
   scheduler.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = (HistoryBars * BarDescr + AccountDescr);
   descr.window = class="num">0;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!scheduler.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(!scheduler.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;
   descr.count = class="num">256;
   descr.optimization = ADAM;
   descr.activation = TANH;
   if(!scheduler.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;
   descr.count = class="num">256;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!scheduler.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 = defNeuronFQF;
   descr.count = NSkills;
   descr.window_out = class="num">32;
   descr.optimization = ADAM;
   if(!scheduler.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 = defNeuronSoftMaxOCL;
   descr.count = NSkills;
   descr.step = class="num">1;
   descr.optimization = ADAM;
   if(!scheduler.Add(descr))
     {
       class="kw">delete descr;

常见问题

把学习机制拆成可预测的子任务,用动态学习技能框架让模型在线更新,而不是固定一套规则。
将难预测的完整技能分解为多个局部可预测的子目标,分别训练,降低单模型学习难度。
小布可以读取品种页上的策略诊断,提示学习曲线是否扁平或拟合,帮你决定是否重训。
用三模型架构:一套辨可预测性、两套分头学技能与调度,改 EA 时把对应网络层分开定义。
Actor 与 Scheduler 的层定义分开落点,参数集中到配置结构,便于单独加载和回测。