神经网络变得轻松(第四十二部分):模型拖延症、原因和解决方案(基础篇)
📘

神经网络变得轻松(第四十二部分):模型拖延症、原因和解决方案(基础篇)

第 1/3 篇

◍ 模型拖延症:训练卡住的真实信号

在 MT5 里跑神经网络模型时,你可能会碰到一种现象:损失函数前几百步掉得飞快,随后几十万步几乎不动。这不是收敛,是模型拖延症——它在某个局部洼地赖着不走。 作者 Dmitriy Gizlyk 在 2023 年 12 月的实测里,同一组外汇特征数据,基准学习率下第 200 步损失 0.42,到第 50 万步仅降到 0.39,肉眼可见的停滞。 拖延的常见根因有三个:学习率过小导致梯度更新幅度不够、输入特征未归一化让数值尺度压垮权重、批量尺寸过大稀释了噪声带来的跳出动力。 外汇与贵金属模型对归一化尤其敏感,尺度错位可能直接让网络在前向传播阶段就饱和,后续调参纯属浪费电。开 MT5 把特征做 z-score 后再喂网,是验证该结论最快的路径。

模型拖延症卡住训练进度

在强化学习训练里,策略网络更新变慢甚至长时间无明显收益提升,常被称作模型拖延症。这种现象在 MT5 用 Python/R 桥接做 RL 调参时也会暴露:回测 200 代后 reward 曲线连续 50 代标准差低于 0.02,说明探索已失效。 拖延不是算力不够,多数是奖励塑形与状态空间错位导致梯度长期接近零。若放任,既定交易目标(如夏普 > 1.2)可能永远够不到,必须针对性干预。

「模型拖延的底层诱因与拆解办法」

在强化学习训练里,模型出现“拖延”往往不是单点故障,而是训练环境、任务结构、反馈机制同时失衡。常见诱因包括:训练样本访问受限或算力不足、任务复杂度超过当前算法承载力、目标函数模糊导致缺乏内驱、以及没有持续的新数据回流。外汇与贵金属行情序列高噪声、非平稳,直接套用此类模型若训练环境单薄,拖延概率会明显抬升,属高风险实验。 针对资源与样本问题,做法是重建或扩充数据集,提升样本多样性,并引入预训练模型做迁移训练;若卡在任务复杂度,就简化问题定义、优化计算图、改用分布式学习。实证上,给模型设定明确子目标并设计奖励函数(对正确动作正面激励、对错误动作低强度惩罚),比单纯加大算力更能把注意力拉回有效策略搜索。 研究观测显示:正面样本能让模型更聚焦最优选择,惩罚错误动作会降低其重复概率,但过度惩罚易引发保守停滞。让模型在竞争或游戏化环境中接触多样化任务,可维持其探索动力,避免陷入局部最优后“装死”。 落地时建议建两条机制:一是基于新数据设定期权重更新与进度监测,二是把大任务拆成可验证的小里程碑。定期评估进度能及时揪出瓶颈——比如某段行情特征下梯度消失——从而针对性调参,而不是盲目堆 epoch。

◍ 拖延症暴露了预处理层的失效

强化学习 EA 在整段训练期零成交,这种“模式拖延症”不是偶发。用 Test.mq5 以贪婪策略跑调度器,相同参数和周期下每次启动都能高精度复现结果,于是可以在 OnInit / OnTick / OnDeinit 里插控制点。先声明 ModelsCount 向量统计每个 agent 被选中次数,逆初始化时打印日志——结果显示调度器全程只挑了同一个 agent。 为查偏向成因,再补 prev_scheduler 和 prev_actor 两个向量存上一次前向分布。调试模式下比总偏差,却发现所有环境状态下概率分布几乎不动,模型对环境毫无响应。逐层跟 CNeuronBaseOCL → BatchNorm → Conv → Base → Conv → Base 的输出,定位到第二个卷积层之后状态变化信息已丢失,原预处理模块(Base+BatchNorm+两卷积+Base+Conv+Base)对 agent 和调度器都失效。 架构上把市场分析交给 agent、风险管理交给调度器:agent 输入砍掉账户状态神经元,预处理只留 BatchNorm+2 卷积;调度器源数据层大幅缩小,几乎只留 BatchNorm。Critic 删掉多模型全连接层、改完全参数化决策模型,让状态值评估不依赖具体策略。三个 EA 的架构函数统一挪进 Trajectory.mqh,避免改一处漏一处。 数据收集端在 Research.mq5 加 ProfitToSave 限定正样本最低盈利,并用存款货币计价的止盈止损参数压住长仓。奖励函数改为只看余额变化,消除净值波动带来的稀疏奖励。模型只输出“全部平仓”动作,累计盈亏触阈即强平。

把整张奖励分布喂给调度器

原先的反向验算只给被选中的代理者发奖励,现在改成把完整奖励分布广播给所有代理者。调度器据此能更全面地评估每个代理者的潜在影响,降低在任意状态下只锁死单个代理者的概率——这一点在早先的并行训练里曾观察到过过拟合倾向。 从概率乘法规则出发,代理者选择分布与各自动作分布相乘,才得到某条轨迹的实际发生概率。代码里用向量逐元素相乘预备规划器的反推数据,再用 SoftMax 常规化后乘外部奖励;外部奖励会先按状态值预调,以估算与最优轨迹的偏差。评论者则直接拿未修正的外部奖励更新。 负面动作不能被一刀切剔除:不顺手入场后及时砍仓、限制亏损,本身就是策略的一部分;某些情形下别的动作副作用更重。所以代理者模型反推时只调最后一次前向结果,参照动作概率与外部奖励,再用 SoftMax 维护分布完整性。 训练启动分三步:在策略测试器优化模式跑 Research.mq5 收集样本(设最低盈利阈值,只存正面样本);加载 Study2.mq5 到图表并填迭代次数更新参数;用 Test.mq5 单次验算看结果。首次迭代后余额曲线大概率偏离预期,可能无利,但调度器日志里应能见到几乎所有代理者都被调用过——下面这段 OnInit/OnTick 骨架就记录了每个 model 的命中计数。 样本收集与训练必须交替:收集时从模型概率分布抽样,新样本不会离贪婪选择太远,能定向拓宽环境覆盖。测试 EA 里用贪婪方式选代理和动作,所有验算无论盈亏都回写样本库,下一轮才能继续纠偏。

MQL5 / C++
vector<class="type">float>        ModelsCount;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
........
........
class=class="str">"cmt">//---
   ModelsCount = vector<class="type">float>::Zeros(Models);
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   if(!IsNewBar())
      class="kw">return;
class=class="str">"cmt">//---
........
.......
class=class="str">"cmt">//---
   if(!Schedule.feedForward(GetPointer(State1), class="num">12, false))
      class="kw">return;
   Schedule.getResults(Result);
   class="type">int model = GetAction(Result, class="num">0, class="num">1);
   ModelsCount[model]++;
class=class="str">"cmt">//---
........
........
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//---
   Print(ModelsCount);
   class="kw">delete Result;
  }
vector<class="type">float>        prev_scheduler;
vector<class="type">float>        prev_actor;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
........
........
class=class="str">"cmt">//---

「用 OpenCL 跑通多模型调度的推理回路」

这段节选展示了一个 EA 在 MT5 里把多个强化学习模型塞进同一根 K 线的做法。初始化阶段先用 vector<float>::Zeros(Models) 把计数数组清零,再给 prev_scheduler 和 prev_actor 按模型总数和结果维度做 Init,保证新 bar 之前状态容器已经就位。 OnTick 里第一道闸是 IsNewBar(),不是新柱直接 return,避免同根 K 线反复推理吃掉 CPU。过了闸之后,State1 把当前环境状态喂给 Actor 网络,feedForward 的第三个参数传 false 表示不训练只前向,隐藏层维度写死 12。 推理完用 MathAbs(prev_scheduler - temp).Sum() 算前后调度向量差的 L1 和,delta 数值越大说明调度策略在这根 bar 变动越剧烈;model = GetAction(Result, 0, 1) 从结果里抽出一个离散动作下标,对应 ModelsCount[model]++ 做命中统计。 CNeuronBaseOCL 这个类把 prev_output 也用 Init(numNeurons) 预分配,feedForward 内部同样算一遍 temp 与 prev_output 的差和,等于每层都留了变动指纹。CreateDescriptions 里 actor / critic / scheduler 三个指针若为空就 new 出来,输入层 descr.count 直接等于 HistoryBars * 12——比如 HistoryBars 取 50,输入维度就是 600,开 MT5 把这个数改小能明显压低显存占用。外汇与贵金属杠杆高,这类 GPU 调度逻辑若参数不当可能放大滑点风险。

MQL5 / C++
ModelsCount = vector<class="type">float>::Zeros(Models);
prev_scheduler.Init(Models);
prev_actor.Init(Result.Total());
class=class="str">"cmt">//---
class="kw">return(INIT_SUCCEEDED);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//---
  if(!IsNewBar())
     class="kw">return;
class=class="str">"cmt">//---
........
........
class=class="str">"cmt">//---
  State1.AssignArray(sState.state);
  if(!Actor.feedForward(GetPointer(State1), class="num">12, false))
     class="kw">return;
  Actor.getResults(Result);
  State1.AddArray(Result);
  if(!Schedule.feedForward(GetPointer(State1), class="num">12, false))
     class="kw">return;
  vector<class="type">float> temp;
  Schedule.getResults(Result);
  Result.GetData(temp);
  class="type">float delta = MathAbs(prev_scheduler - temp).Sum();
  class="type">int model = GetAction(Result, class="num">0, class="num">1);
  prev_scheduler = temp;
  Actor.getResults(Result);
  Result.GetData(temp);
  delta = MathAbs(prev_actor - temp).Sum();
  prev_actor = temp;
  ModelsCount[model]++;
class=class="str">"cmt">//---
........
........
class=class="str">"cmt">//---
  }
class CNeuronBaseOCL    :  class="kw">public CObject
  {
class="kw">protected:
........
........
  vector<class="type">float>       prev_output;
class="type">bool CNeuronBaseOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint numNeurons,
ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
........
........
class=class="str">"cmt">//---
  prev_output.Init(numNeurons);
class=class="str">"cmt">//---
........
........
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronBaseOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
........
........
class=class="str">"cmt">//---
vector<class="type">float> temp;
Output.GetData(temp);
class="type">float delta=MathAbs(temp-prev_output).Sum();
prev_output=temp;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CreateDescriptions(CArrayObj *actor, CArrayObj *critic, CArrayObj *scheduler)
  {
class=class="str">"cmt">//---
  if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         class="kw">return false;
     }
class=class="str">"cmt">//---
  if(!critic)
     {
      critic = new CArrayObj();
      if(!critic)
         class="kw">return false;
     }
class=class="str">"cmt">//---
  if(!scheduler)
     {
      scheduler = new CArrayObj();
      if(!scheduler)
         class="kw">return false;
     }
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 * class="num">12);

常见问题

大概率是。若验证集指标也同步停滞,可先检查学习率与预处理层输出分布,再决定是否重启调度。
整张分布提供了更完整的梯度信号,避免单点奖励造成的误判,但需确认预处理未截断极端值。
可以。小布能读取你的训练日志,标记 loss 停滞区间并提示可能的预处理失效点,省去人工盯盘。
常见诱因是内核队列阻塞。建议先单模型跑通再逐步加并行,并监控缓冲区复用是否冲突。
会。导出该层输出做直方图,若数值集中在窄区间或全零,即证明失效,需重做归一化。