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

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

(2/3)·强化学习模型卡顿不是玄学,环境、目标与反馈三线失配才是主因

实战向进阶 第 2/3 篇
模型训练跑到一半突然不动了,多数人第一反应是加算力,结果钱烧了进度条仍不动。拖延症背后往往是环境样本贫乏、奖励函数错位或反馈周期断裂,先辨因再下药才省时间。

◍ Actor 与 Critic 的网络层堆叠

在 MT5 用 OpenCL 跑强化学习策略时,Actor 和 Critic 两套网络都靠 CLayerDescription 逐个堆层。下面这段就是两层网络的前向结构定义,外汇与贵金属行情噪声大,这种带批归一化的卷积栈对过拟合有一定抑制作用,但实盘仍属高风险。 Actor 输入层 window=0、无激活、优化器 ADAM;第一层 BatchNorm 的 batch 写死 1000,count 沿用上一层;第二层卷积把 prev_count 减 2 后作为通道数,window=3、step=1、window_out=2,激活用 LReLU;第三层卷积 count 取 (prev_count+1)/2,window=2、step=2、window_out=4,激活换 SIGMOID 收口。 Critic 清场后重搭:输入层 count 直接算成 HistoryBars*12+9,同样 window=0、ADAM;后面 BatchNorm 与两层卷积的参数和 Actor 几乎镜像,只是第三层在截图处截断。注意每层 new 失败或 Add 失败都会 delete descr 并 return false,内存泄漏点就藏在这些 early return 里。

MQL5 / C++
  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">2;
  descr.window = class="num">3;
  descr.step = class="num">1;
  descr.window_out = class="num">2;
  prev_count*=descr.window_out;
  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;
  descr.count = (prev_count+class="num">1)/class="num">2;
  descr.window = class="num">2;
  descr.step = class="num">2;
  descr.window_out = class="num">4;
  descr.activation = SIGMOID;
  descr.optimization = ADAM;
  if(!actor.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- Critic
  critic.Clear();
class=class="str">"cmt">//--- Input layer
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronBaseOCL;
  prev_count = descr.count = (class="type">int)(HistoryBars * class="num">12 + class="num">9);
  descr.window = class="num">0;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!critic.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(!critic.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">2;
  descr.window = class="num">3;
  descr.step = class="num">1;
  descr.window_out = class="num">2;
  prev_count*=descr.window_out;
  descr.activation = LReLU;
  descr.optimization = ADAM;
  if(!critic.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">3
  if(!(descr = new CLayerDescription()))

「判别器与调度器的层堆配置」

这段配置在 MT5 的 OpenCL 神经网络框架里分别给 critic(判别器)和 scheduler(调度器)堆层。critic 从第 3 层起用卷积层 defNeuronConvOCL,窗口 2、步长 2、输出窗口 4,激活统一 SIGMOID,优化用 ADAM;第 5、6 层退回到全连接 defNeuronBaseOCL,各 500 节点、TANH 激活。 第 7 层是分位数输出层 defNeuronFQF,count=4 且 window_out=32,意味着把状态压成 4 个分位数、每个展 32 维特征,给后续分布式决策用。任何一层 Add 失败都走 delete descr 并 return false,防止半吊子网络被训练。 scheduler 的输入层直接吃 9+40=49 维原始特征(prev_count=49),紧跟一层 defNeuronBatchNormOCL 做批量归一,batch 设 1000、激活 None。外汇与贵金属行情噪声大,这种归一前置能缓解不同品种量纲差异导致的梯度抖动,但高杠杆下模型误判仍可能迅速放大亏损。 开 MT5 把这段贴进 EA 的神经网络初始化函数,改 descr.count 或 batch 就能直观看到显存占用和回测稳定性的变化。

MQL5 / C++
   class="kw">return false;
   descr.type = defNeuronConvOCL;
   descr.count = (prev_count+class="num">1)/class="num">2;
   descr.window = class="num">2;
   descr.step = class="num">2;
   descr.window_out = class="num">4;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!critic.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;
   descr.count = class="num">150;
   descr.window = class="num">2;
   descr.step = class="num">2;
   descr.window_out = class="num">4;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!critic.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">500;
   descr.optimization = ADAM;
   descr.activation = TANH;
   if(!critic.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">500;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!critic.Add(descr))
      {
         class="kw">delete descr;
         class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">7
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronFQF;
   descr.count = class="num">4;
   descr.window_out = class="num">32;
   descr.optimization = ADAM;
   if(!critic.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 = (class="num">9 + class="num">40);
   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;

堆叠网络层与状态结构的写法

这段代码在调度器里连续登记了 5 层神经网络:第 3、4 层都是 256 个神经元的 base 层,激活函数用 TANH,优化器统一走 ADAM;第 4 层输出维度降到 10,且没设激活函数;第 5 层是 SoftMax 层,count 也是 10、step 为 1,同样挂 ADAM。每一层 new 出 CLayerDescription 后若 Add 失败就 delete 并 return false,能保证资源不漏。 SState 结构体把历史状态摊平成 float 数组:state 长度 = HistoryBars * 12,account 固定 9 维,并重载了 = 运算符用 ArrayCopy 直接拷两块内存,省掉逐元素循环。 外部输入参数给了三个双精度变量:ProfitToSave=10、MoneyTP=10、MoneySL=5,单位依上下文可能是点数或金额,外汇与贵金属品种波动剧烈,实盘套用前需在 MT5 策略测试器里把这组数值和压力测试跑通,参数不当可能放大回撤。 开 MT5 把这段贴进 EA 的层定义函数,重点看第 4 层少写 activation 后网络是否仍能编译通过,借此确认你用的 MT5 构建版本对默认激活的处理逻辑。

MQL5 / C++
  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 = TANH;
  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 = defNeuronBaseOCL;
  descr.count = class="num">10;
  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 = class="num">10;
  descr.step = class="num">1;
  descr.optimization = ADAM;
  if(!scheduler.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//---
  class="kw">return true;
  }

class="kw">struct SState
  {
   class="type">float            state[HistoryBars * class="num">12];
   class="type">float            account[class="num">9];
   class=class="str">"cmt">//---
                    SState(class="type">void);
   class=class="str">"cmt">//---
   class="type">bool             Save(class="type">int file_handle);
   class="type">bool             Load(class="type">int file_handle);
   class=class="str">"cmt">//--- overloading
   class="type">void             class="kw">operator=(const SState &obj)  { ArrayCopy(state, obj.state); ArrayCopy(account, obj.account); }
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Input parameters                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
input class="type">class="kw">double         ProfitToSave = class="num">10;
input class="type">class="kw">double         MoneyTP = class="num">10;
input class="type">class="kw">double         MoneySL = class="num">5;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//---

◍ 把K线与账户状态压进一个特征向量

做价格行为建模时,最麻烦的不是算指标,而是把不同维度的数据规整成模型能直接吃的连续数组。下面这段逻辑在每个新Bar触发时跑一次,先把历史K线拉下来,再逐根把涨跌、振幅、成交量、时间分量和技术指标塞进固定宽度的浮点数组。 特征宽度定死为12,意味着每根Bar在 sState.state 里占 12 个 float 位:0~2 是 close/high/low 相对开盘价的偏移,3 是 tick_volume 除以 1000,4~6 是小时/星期几/月份,7~11 依次是 RSI、CCI、ATR、MACD主线、MACD信号。这样 HistoryBars 根K线就线性铺成 b*12 的偏移,回测或推理时不用再反复查结构体。 账户侧另开 account[5],依次装余额、净值、空闲保证金、保证金水平百分比、浮动盈亏。外汇和贵金属杠杆高,保证金水平掉到阈值以下会被强平,把这个量送进特征能让模型感知当前风险暴露,但任何模型输出都只是概率倾向,不代表未来方向。 持仓扫描只统计当前品种,用 PositionsTotal 遍历并跳过非本符号仓位,为后续区分多空市值和浮盈留接口。开 MT5 把这段直接贴进 EA 的 OnTick 前面,改 HistoryBars 从 100 调到 300,能看到 state 数组长度从 1200 涨到 3600,特征密度立刻变厚。

MQL5 / C++
  if(!IsNewBar())
      class="kw">return;
class=class="str">"cmt">//---
  class="type">int bars = CopyRates(Symb.Name(), TimeFrame, iTime(Symb.Name(), TimeFrame, class="num">1), HistoryBars, Rates);
  if(!ArraySetAsSeries(Rates, true))
      class="kw">return;
class=class="str">"cmt">//---
  RSI.Refresh();
  CCI.Refresh();
  ATR.Refresh();
  MACD.Refresh();
class=class="str">"cmt">//---
  class="type">MqlDateTime sTime;
  for(class="type">int b = class="num">0; b < (class="type">int)HistoryBars; b++)
    {
      class="type">float open = (class="type">float)Rates[b].open;
      TimeToStruct(Rates[b].time, sTime);
      class="type">float rsi = (class="type">float)RSI.Main(b);
      class="type">float cci = (class="type">float)CCI.Main(b);
      class="type">float atr = (class="type">float)ATR.Main(b);
      class="type">float macd = (class="type">float)MACD.Main(b);
      class="type">float sign = (class="type">float)MACD.Signal(b);
      if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
         class="kw">continue;
      class=class="str">"cmt">//---
      sState.state[b * class="num">12] = (class="type">float)Rates[b].close - open;
      sState.state[b * class="num">12 + class="num">1] = (class="type">float)Rates[b].high - open;
      sState.state[b * class="num">12 + class="num">2] = (class="type">float)Rates[b].low - open;
      sState.state[b * class="num">12 + class="num">3] = (class="type">float)Rates[b].tick_volume / class="num">1000.0f;
      sState.state[b * class="num">12 + class="num">4] = (class="type">float)sTime.hour;
      sState.state[b * class="num">12 + class="num">5] = (class="type">float)sTime.day_of_week;
      sState.state[b * class="num">12 + class="num">6] = (class="type">float)sTime.mon;
      sState.state[b * class="num">12 + class="num">7] = rsi;
      sState.state[b * class="num">12 + class="num">8] = cci;
      sState.state[b * class="num">12 + class="num">9] = atr;
      sState.state[b * class="num">12 + class="num">10] = macd;
      sState.state[b * class="num">12 + class="num">11] = sign;
    }
class=class="str">"cmt">//---
  sState.account[class="num">0] = (class="type">float)AccountInfoDouble(ACCOUNT_BALANCE);
  sState.account[class="num">1] = (class="type">float)AccountInfoDouble(ACCOUNT_EQUITY);
  sState.account[class="num">2] = (class="type">float)AccountInfoDouble(ACCOUNT_MARGIN_FREE);
  sState.account[class="num">3] = (class="type">float)AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);
  sState.account[class="num">4] = (class="type">float)AccountInfoDouble(ACCOUNT_PROFIT);
class=class="str">"cmt">//---
  class="type">class="kw">double buy_value = class="num">0, sell_value = class="num">0, buy_profit = class="num">0, sell_profit = class="num">0;
  class="type">int total = PositionsTotal();
  for(class="type">int i = class="num">0; i < total; i++)
    {
      if(PositionGetSymbol(i) != Symb.Name())
         class="kw">continue;

「持仓分类与强化学习信号的分发逻辑」

这段逻辑先把当前账户里的多空持仓拆开统计。switch 按 POSITION_TYPE 分支,把每笔买单的量与浮动盈利累加进 buy_value / buy_profit,卖单同理进 sell_value / sell_profit,随后写进 sState.account 数组的第 5~8 位(索引从 0 计),供后续网络推理使用。 推理阶段把账户状态送进 Actor 网络(feedForward 隐藏层 12、训练标志 false),拿到 Result 后再把账户数组与 Result 拼接,喂给 Schedule 网络得到动作编号 act。若总浮盈 profit 越过 MoneyTP 或跌破 -MathAbs(MoneySL),则强制 act=2,相当于硬止损止盈覆盖模型输出。外汇与贵金属杠杆高,这种硬阈值若设得过窄,可能频繁截断模型原本的持仓倾向。 Train 函数里走的是带评论家的强化更新:Actor 与 Critic 出结果后,用 Buffer 里的奖励减 Critic 最大值再除 100 得到缩放 reward,按所记录 action 列做 Scheduler 输出的 softmax 加权,再乘 reward 反传。CriticResult 对应动作位被直接赋成当期奖励,然后分别 backProp 更新 Scheduler 与 Critic。 让小布替你跑这套 把 MoneyTP / MoneySL 先置成账户净值的 1% 与 0.8% 做离线回测,观察 act=2 触发次数;若占比超 30%,说明策略对硬止损依赖过强,可能削弱模型在趋势段的持仓能力。

MQL5 / C++
class="kw">switch((class="type">int)PositionGetInteger(POSITION_TYPE))
  {
   case POSITION_TYPE_BUY:
     buy_value += PositionGetDouble(POSITION_VOLUME);
     buy_profit += PositionGetDouble(POSITION_PROFIT);
     class="kw">break;
   case POSITION_TYPE_SELL:
     sell_value += PositionGetDouble(POSITION_VOLUME);
     sell_profit += PositionGetDouble(POSITION_PROFIT);
     class="kw">break;
   }
  }
 sState.account[class="num">5] = (class="type">float)buy_value;
 sState.account[class="num">6] = (class="type">float)sell_value;
 sState.account[class="num">7] = (class="type">float)buy_profit;
 sState.account[class="num">8] = (class="type">float)sell_profit;
 State1.AssignArray(sState.state);
 if(!Actor.feedForward(GetPointer(State1), class="num">12, false))
   class="kw">return;
 Actor.getResults(Result);
 State1.AssignArray(sState.account);
 State1.AddArray(Result);
 if(!Schedule.feedForward(GetPointer(State1), class="num">12, false))
   class="kw">return;
 class="type">int act = GetAction(Result, Schedule.getSample(), Models);
 class="type">class="kw">double profit = buy_profit + sell_profit;
 if(profit >= MoneyTP || profit <= -MathAbs(MoneySL))
   act = class="num">2;
class="type">void Train(class="type">void)
  {
........
........
   Actor.getResults(ActorResult);
   Critic.getResults(CriticResult);
   State1.AssignArray(Buffer[tr].States[i].account);
   State1.AddArray(ActorResult);
   if(!Scheduler.feedForward(GetPointer(State1), class="num">12, false))
     class="kw">return;
   Scheduler.getResults(SchedulerResult);
   class=class="str">"cmt">//---
   class="type">class="kw">ulong actions = ActorResult.Size() / Models;
   matrix<class="type">float> temp;
   temp.Init(class="num">1, ActorResult.Size());
   temp.Row(ActorResult, class="num">0);
   temp.Reshape(Models, actions);
   class="type">float reward=(Buffer[tr].Revards[i] - CriticResult.Max())/class="num">100;
   class="type">int action=Buffer[tr].Actions[i];
   SchedulerResult=SchedulerResult*temp.Col(action);
   SchedulerResult.Activation(SchedulerResult,AF_SOFTMAX);
   SchedulerResult = SchedulerResult  * reward;
   Result.AssignArray(SchedulerResult);
   class=class="str">"cmt">//---
   if(!Scheduler.backProp(GetPointer(Result)))
     class="kw">return;
   CriticResult[action] = Buffer[tr].Revards[i];
   Result.AssignArray(CriticResult);
   class=class="str">"cmt">//---
   if(!Critic.backProp(GetPointer(Result), class="num">0.0f, NULL))
     class="kw">return;
   class=class="str">"cmt">//---
   for(class="type">int r = class="num">0; r < Models; r++)

策略梯度回写时的行内权重修正

强化学习代理在更新 Actor 网络时,会把单条经验的动作价值直接叠加进对应动作的概率行。下面这段代码展示了从临时矩阵取一行、按 reward 缩放、再做 Softmax 归一化的过程。 [CODE] { vector<float> row = temp.Row(r); row[action] += row[action] * reward; row.Activation(row, AF_SOFTMAX); temp.Row(row, r); } [/CODE] 逐行看:先取出第 r 行的浮点向量;将该行中被执行动作的下标值乘以 reward 后累加,相当于局部提升或压低该动作权重;随后对整行做 Softmax 激活,把数值转回概率分布;写回原矩阵行。 temp.Reshape(1, ActorResult.Size()) 把矩阵压成单行,Result.AssignArray(temp.Row(0)) 提取为最终输出。最后若 backProp 失败直接 return,避免用脏梯度更新网络。 在 MT5 里跑这套时,reward 符号决定该动作下次被采样的倾向——正奖励抬高概率,负奖励压低,但外汇与贵金属杠杆高,策略过拟合单条经验会引发实盘漂移,验证时建议先关掉 live 交易只用历史 tick 回放。

MQL5 / C++
{
 vector<class="type">float> row = temp.Row(r);
 row[action] += row[action] * reward;
 row.Activation(row, AF_SOFTMAX);
 temp.Row(row, r);
}
temp.Reshape(class="num">1, ActorResult.Size());
Result.AssignArray(temp.Row(class="num">0));
class=class="str">"cmt">//---
if(!Actor.backProp(GetPointer(Result)))
 class="kw">return;
把停滞诊断交给小布盯盘
这些诊断维度小布盯盘的 AIGC 已内置,打开对应品种页即可看到模型训练与信号衰减的监测面板,你只管看策略是否还活着。

常见问题

看训练损失曲线是否长期平坦且验证集多样性低,若数据访问受限或资源吃紧,需扩充样本并引入预训练迁移。
目标要明确可量化,用正样本清晰标记正确动作,惩罚仅降错概率,过度惩罚反而让模型搜素保守停滞。
可能缓解,但先简化问题与算法更高效;分布式适合资源可切分的场景,否则通信开销会引入新瓶颈。
可以,小布盯盘的品种页用 AIGC 跟踪信号衰减与训练周期异常,你不必自己写日志解析脚本。
定期评估暴露瓶颈与资源问题,便于动态调整训练过程,否则模型会在无反馈中静默停滞。
指用清晰正奖励引导而非仅靠惩罚,外汇贵金属高风险下该机制提升策略针对性,但需防过拟合。