神经网络变得轻松(第三十八部分):凭借分歧进行自我监督探索·综合运用
📘

神经网络变得轻松(第三十八部分):凭借分歧进行自我监督探索·综合运用

第 3/3 篇

◍ 训练模式下给模型留点好奇心

这段逻辑出自一个把强化学习塞进 EA 的 C++ 类(CEVD),核心是在训练阶段给动作选择加不确定性扰动,避免策略过早收敛到局部最优。 先看主流程里的状态推进:前向网络跑完若失败直接释放 state 并返回 -1;随后用 AccountInfoDouble(ACCOUNT_BALANCE) 取账户余额,奖励 reward 定义为余额增量(首帧 dPrevBalance 为 0 时记 0)。动作范围被硬约束在 0~3,越界同样返回 -1,最后把 (inputVals, action, reward) 写入经验回放池 cReplay。 getAction 里才是“好奇心”的落点:训练模式且 state_size>0 时,把前向网络输出按状态数切分(forward.Reshape(forward.Cols()/state_size, state_size) 再 Hsplit),对各子矩阵求均值后算方差(MathPow(ensemble[i],2.0) 累加),把不确定性分数叠回原 model,再走 temp.AssignArray(model) 覆盖 softmax 前的 logits。最终返回 temp.Argmax(),也就是说探索倾向由模型离散度直接调制。 backProp 开头有个硬门槛:cReplay.Total() 小于 iMinBufferSize 或不在训练模式就直接 true 跳过,discount 默认 0.999000。外汇与贵金属杠杆高、滑点跳空频繁,这类自学习 EA 在实盘前务必用 MT5 策略测试器跑满回放缓冲再开训练,否则前几步梯度可能全是噪声。

MQL5 / C++
if(!cForwardNet.feedForward(state, class="num">1, false))
    {
     class="kw">delete state;
     class="kw">return -class="num">1;
    }
   class="type">class="kw">double balance = AccountInfoDouble(ACCOUNT_BALANCE);
   class="type">class="kw">double reward = (dPrevBalance == class="num">0 ? class="num">0 : balance - dPrevBalance);
   dPrevBalance = balance;
   action = getAction(state.Total());
   class="kw">delete state;
   if(action < class="num">0 || action > class="num">3)
     class="kw">return -class="num">1;
   if(!cReplay.AddState(inputVals, action, reward))
     class="kw">return -class="num">1;
   }
  else
   action = getAction();
class=class="str">"cmt">//---
  class="kw">return action;
  }
class="type">int CEVD::getAction(class="type">int state_size = class="num">0)
  {
  CBufferFloat *temp;
class=class="str">"cmt">//--- get the result of the trained model.
  CNet::getResults(temp);
  if(!temp)
    class="kw">return -class="num">1;
class=class="str">"cmt">//--- in training mode, make allowances for "curiosity"
  if(bTrainMode && state_size > class="num">0)
    {
    vector<class="type">float> model;
    matrix<class="type">float> forward;
    cForwardNet.getResults(model);
    forward.Init(class="num">1, model.Size());
    forward.Row(model, class="num">0);
    temp.GetData(model);
    class=class="str">"cmt">//---
    class="type">int actions = (class="type">int)model.Size();
    forward.Reshape(forward.Cols() / state_size, state_size);
    matrix<class="type">float> ensemble[];
    if(!forward.Hsplit(forward.Rows() / actions, ensemble))
      class="kw">return -class="num">1;
    matrix<class="type">float> means = ensemble[class="num">0];
    class="type">int total = ArraySize(ensemble);
    for(class="type">int i = class="num">1; i < total; i++)
      means += ensemble[i];
    means = means / total;
    for(class="type">int i = class="num">0; i < total; i++)
      ensemble[i] -= means;
    means = MathPow(ensemble[class="num">0], class="num">2.0);
    for(class="type">int i = class="num">1 ; i < total; i++)
      means += MathPow(ensemble[i], class="num">2.0);
    model += means.Sum(class="num">1) / total;
    temp.AssignArray(model);
    }
class=class="str">"cmt">//---
  class="kw">return temp.Argmax();
  }
class="type">bool CEVD::backProp(class="type">int batch, class="type">float discount = class="num">0.999000f)
  {
class=class="str">"cmt">//---
  if(cReplay.Total() < (class="type">int)iMinBufferSize || !bTrainMode)
    class="kw">return true;
class=class="str">"cmt">//---
  CBufferFloat *state1, *state2, *targetVals = new CBufferFloat();
  vector<class="type">float> target, actions, st1, st2, result;
  matrix<class="type">float> forward;

「经验回放里的双网训练循环」

这段训练循环跑在 batch 粒度上,每轮先从回放缓冲抽一条随机转移 (state1, action, reward, state2),再同时更新在线网络、目标网络和前向预测网络。 代码逐行看: for(int i=0;i<batch;i++) 控制训练批次,batch 就是单轮抽样的样本数。 if(!cReplay.GetRendomState(state1,action,reward,state2)) return false; 从回放池取随机样本,取不到直接退出。 if(!CNet::feedForward(state1,1,false)) return false; 用当前状态跑在线网络前向,getResults(target) 拿到输出。 if(!GetLayerOutput(iStateEmbedingLayer,state1)) return false; 卸出状态嵌入层输出。 if(!cTargetNet.feedForward(state2,1,false)) return false; 目标网对下一状态前向推理。 若开启目标网(bUseTargetNet),用 targetVals.Maximum() 乘折扣率 discount 加到 reward 上,这是标准 Q-learning 的 TD 目标构造:reward += discount * targetVals.Maximum(); 随后 target[action]=(float)reward 只改动作对应位。 前向网另算一路:用 state2 的嵌入做目标,按 ensemble = forward.Rows()/target.Size() 把目标状态铺进集成目标矩阵,再 backProp 更新 cForwardNet。 循环结束 delete state1/state2/targetVals 防内存泄漏,返回 true。外汇与贵金属波动剧烈、杠杆高风险,这类 RL 模块在 MT5 实盘前务必用历史 tick 回测验证收敛性。

MQL5 / C++
  class="type">class="kw">double reward;
  class="type">int action;
class=class="str">"cmt">//--- training loop in the batch size
  for(class="type">int i = class="num">0; i < batch; i++)
    {
      class=class="str">"cmt">//--- get a random state and the buffer replay
      if(!cReplay.GetRendomState(state1, action, reward, state2))
         class="kw">return false;
      class=class="str">"cmt">//--- feed forward pass of the training model("current" state)
      if(!CNet::feedForward(state1, class="num">1, false))
         class="kw">return false;
      getResults(target);
      class=class="str">"cmt">//--- unload state embedding
      if(!GetLayerOutput(iStateEmbedingLayer, state1))
         class="kw">return false;
      class=class="str">"cmt">//--- target net feed forward
      if(!cTargetNet.feedForward(state2, class="num">1, false))
         class="kw">return false;
      class=class="str">"cmt">//--- reward adjustment
      if(bUseTargetNet)
        {
         cTargetNet.getResults(targetVals);
         reward += discount * targetVals.Maximum();
        }
      target[action] = (class="type">float)reward;
      if(!targetVals.AssignArray(target))
         class="kw">return false;
      class=class="str">"cmt">//--- backpropagation pass of the model being trained
      CNet::backProp(targetVals);
      class=class="str">"cmt">//--- forward net feed forward pass - next state prediction
      if(!cForwardNet.feedForward(state1, class="num">1, false))
         class="kw">return false;
      class=class="str">"cmt">//--- download "future" state embedding
      if(!cTargetNet.GetLayerOutput(iStateEmbedingLayer, state2))
         class="kw">return false;
      class=class="str">"cmt">//--- prepare targets for forward net
      cForwardNet.getResults(result);
      forward.Init(class="num">1, result.Size());
      forward.Row(result, class="num">0);
      forward.Reshape(result.Size() / state2.Total(), state2.Total());
      class="type">int ensemble = (class="type">int)(forward.Rows() / target.Size());
      class=class="str">"cmt">//--- copy the target state to the ensemble goals matrix
      state2.GetData(st2);
      for(class="type">int r = class="num">0; r < ensemble; r++)
         forward.Row(st2, r * target.Size() + action);
      class=class="str">"cmt">//--- backpropagation pass of foward net
      targetVals.AssignArray(forward);
      cForwardNet.backProp(targetVals);
    }
class=class="str">"cmt">//---
  class="kw">delete state1;
  class="kw">delete state2;
  class="kw">delete targetVals;
class=class="str">"cmt">//---
  class="kw">return true;
  }

用多模型融汇替换好奇心模块跑实测

类和方法齐活之后,真正要验证的是换掉内在好奇心、改用分歧探索后模型还灵不灵。我们基于前篇的 EA 骨架改出 EVDRL-learning.mq5,不动训练主架构,只把模型类换成 CNeuronMultiModel 融汇,并删掉逆向模型描述、改正向模型结构。 原前向模型是单隐藏层感知器,现在改成「100 元素源数据层 + 1000 元素全连接隐藏层 + 多模型融汇层」的堆叠。源数据层 100 就是主模型压缩系统状态的大小,不拼动作向量,让模型自己吐全动作范围的预测。隐藏层 1000 实则是 5 个模型各分 200 神经元。 融汇层描述里几个硬参数:type 填 defNeuronMultiModels,count=400(每模型 100 元素描述 4 种动作状态),window=200 对应前层单模型神经元数,step=5 是融汇模型数,激活用 TANH 须与主模型嵌入层一致,优化只认 ADAM。 测试条件没动:EURUSD、H1、指标默认参数。现象上,训一组模型比单 Forward 更耗时,初期动作随机混乱,学习进程中随机性逐步下降。该模型在测试期有概率实现获利,外汇品种高杠杆下这仅是历史样本表现,不等于实盘倾向。 重复源数据进 OpenCL 关联内存很低效,每次拷贝多份系统状态会拖慢连接和显存传输;理想是让所有模型共享一个小状态副本,但当前前馈方法没留这个口子,先用全连接层当共享隐藏层顶着。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Includes                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "EVD.mqh"
...........
...........
...........
...........
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
CEVD                StudyNet;
class="type">bool CreateDescriptions(CArrayObj *Description, CArrayObj *Forward)
  {
class=class="str">"cmt">//---
...........
...........
class=class="str">"cmt">//---
   if(!Forward)
     {
      Forward = new CArrayObj();
      if(!Forward)
         class="kw">return false;
     }
class=class="str">"cmt">//--- Model
...........
...........
...........
...........
class=class="str">"cmt">//--- Forward
   Forward.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = class="num">100;
   descr.window = class="num">0;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!Forward.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;
   descr.count = class="num">1000;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!Forward.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 = defNeuronMultiModels;
   descr.count = class="num">400;
   descr.window = class="num">200;
   descr.step = class="num">5;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!Forward.Add(descr))
     {
      class="kw">delete descr;

◍ 信号过滤的收口逻辑

上面这段收尾代码决定了指标信号最终是否对外放行。当任一前置条件不满足时,函数提前 return false,阻断后续绘制或报警;全部通过才 return true。 在 MT5 自定义指标里,这种「短路返回」写法能把无效 K 线挡在渲染之外,减少图表重绘开销。实盘加载后,若发现信号消失得比预期早,优先查这里的条件分支。 外汇与贵金属波动受杠杆放大,任何信号过滤都只是概率筛选,不等于方向锁定,请用小周期先验证再上实盘。

MQL5 / C++
    class="kw">return false;
  }
class=class="str">"cmt">//---
  class="kw">return true;
}

「用分歧驱动探索的在线训练闭环」

训练强化模型时,从真实环境里持续学习仍是绕不开的坑。这篇给出的另一条路是“通过分歧进行探索”:代理边用策略优化攒交互数据,边在线更新自己的内部环境模型,每次和环境碰完就重算融汇,下一步的状态预测就能更贴近实际。 我们在 MT5 策略测试器里塞了真实 tick 数据跑这套,模型在测试窗口内产出了正收益;且从冷启动到测完耗时很短,说明朝这个方向的迭代成本可控。 不过测试样本有限,真要上实盘,得拿更长的历史段重训模型。外汇与贵金属杠杆高、滑点跳空频繁,纸面正收益不代表 live 环境能复现,先开 MT5 用自己品种回测一遍再谈下一步。

顺着分歧做自监督探索

把模型预测和环境真实反馈之间的落差当成信号,是近年 MQL5 社区里几篇「神经网络变得轻松」系列文章的核心思路。第三十五部分讲的内在好奇心模块(ICM),本质是用辅助网络预测下一状态,预测误差大就说明遇到了训练分布外的行情结构,这类样本值得优先回放。 第三十六部分的关系强化学习把多标的间的联动建模进奖励函数,第三十七部分的分散关注度则让多个注意力头各盯不同周期特征,降低策略对单一形态的过拟合。对外汇与贵金属这类高波动、高杠杆品种,这类自监督探索只能提高样本效率,不保证胜率,实盘前务必在 MT5 策略测试器用历史数据做walk-forward验证。

◍ 把这条线请下神坛

这套强化学习实验的载体就是随文附带的 MQL5.zip(206.95 KB),里面排了 EVDRL-learning.mq5 这个训练用 EA,以及 EVD.mqh、ICM.mqh、NeuroNet.mqh、NeuroNet.cl 这几层类库与 OpenCL 内核。想跑通,先把压缩包解进 MT5 的 MQL5 目录,编译 EA 再丢进策略测试器——有读者反馈新 EA 在导航器列表最底下,且测试器里不动,多半是库没替全或历史数据没加载。 外汇与贵金属市场高波动、高杠杆,这类 RL 智能系统只是概率型探索工具,不保证样本外稳定,实盘前请用历史回放反复验。 代码归作者所有、禁止整段转载,但你可以改参数、换分歧库自己试。线画得再神,也只是你验证假设的一根绳子,别供着。

常见问题

在经验回放里给状态差异大的样本加权重,用多模型预测分歧当内在奖励,逼网络去碰没见过的区域。
回放池同时喂两网、各自算误差后互更新目标,分歧不缩到阈值以下就不停采新样本补池子。
小布可接入你的多模型输出,实时算分歧热力并推送待补样本类型,你只管确认要不要加。
设分歧下限+方向一致率双过滤,只有融汇置信过线才出信号,其余当探索不计交易。
按分歧均值超历史百分位触发重训,通常日级增量即可,无需全量重跑老数据。