神经网络变得轻松(第四十八部分):降低 Q-函数高估的方法·进阶篇
📘

神经网络变得轻松(第四十八部分):降低 Q-函数高估的方法·进阶篇

第 2/3 篇

遍历持仓把多空盈亏拆进特征向量

这段逻辑干的事很直接:把当前账户下指定交易品种的所有持仓扫一遍,把买入手数、卖出手数、对应浮动盈亏分别累加,同时算一个带时间衰减的持仓折扣值。multiplyer 被设成 1.0/(60.0*60.0*10.0),也就是 1/36000,意味着每持仓 36000 秒(10 小时)折扣系数大约吃掉等同利润的绝对量,时间越长对 position_discount 的拖累越明显。 循环里先用 PositionGetSymbol 过滤非目标品种,再用 POSITION_TYPE 分支把 volume 和 profit 归到 buy/sell 两组。position_discount 的累加式是 profit 减去「当前时间减开仓时间」乘 multiplyer 再乘利润绝对值,所以亏损单和盈利单都会按时间被打折,只是方向不同。 扫完之后把 7 个量写进 sState.account 的索引 2~6,再经 Account.Clear 和 Add 序列换算成相对 PrevBalance / PrevEquity 的比率,最后送进 Actor.feedForward 做推理。如果你在 MT5 里复刻,重点盯 multiplyer 这个常量——把它从 36000 调到 18000,时间衰减速度会翻倍,特征向量对隔夜单的敏感度会显著变化。外汇与贵金属杠杆高,这类特征工程改动会放大回测与实盘偏差,需自行验证。

MQL5 / C++
  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">class="kw">double position_discount = class="num">0;
  class="type">class="kw">double multiplyer = class="num">1.0 / (class="num">60.0 * class="num">60.0 * class="num">10.0);
  class="type">int total = PositionsTotal();
  class="type">class="kw">datetime current = TimeCurrent();
  for(class="type">int i = class="num">0; i < total; i++)
    {
      if(PositionGetSymbol(i) != Symb.Name())
        class="kw">continue;
      class="type">class="kw">double profit = PositionGetDouble(POSITION_PROFIT);
      class="kw">switch((class="type">int)PositionGetInteger(POSITION_TYPE))
        {
         case POSITION_TYPE_BUY:
            buy_value += PositionGetDouble(POSITION_VOLUME);
            buy_profit += profit;
            break;
         case POSITION_TYPE_SELL:
            sell_value += PositionGetDouble(POSITION_VOLUME);
            sell_profit += profit;
            break;
        }
      position_discount += profit - (current - PositionGetInteger(POSITION_TIME)) * multiplyer * MathAbs(profit);
    }
  sState.account[class="num">2] = (class="type">float)buy_value;
  sState.account[class="num">3] = (class="type">float)sell_value;
  sState.account[class="num">4] = (class="type">float)buy_profit;
  sState.account[class="num">5] = (class="type">float)sell_profit;
  sState.account[class="num">6] = (class="type">float)position_discount;
class=class="str">"cmt">//---
  Account.Clear();
  Account.Add((class="type">float)((sState.account[class="num">0] - PrevBalance) / PrevBalance));
  Account.Add((class="type">float)(sState.account[class="num">1] / PrevBalance));
  Account.Add((class="type">float)((sState.account[class="num">1] - PrevEquity) / PrevEquity));
  Account.Add(sState.account[class="num">2]);
  Account.Add(sState.account[class="num">3]);
  Account.Add((class="type">float)(sState.account[class="num">4] / PrevBalance));
  Account.Add((class="type">float)(sState.account[class="num">5] / PrevBalance));
  Account.Add((class="type">float)(sState.account[class="num">6] / PrevBalance));
  if(Account.GetIndex() >= class="num">0)
    if(!Account.BufferWrite())
      class="kw">return;
class=class="str">"cmt">//---
  if(!Actor.feedForward(GetPointer(State), class="num">1, false, GetPointer(Account)))
    class="kw">return;
  PrevBalance = sState.account[class="num">0];
  PrevEquity = sState.account[class="num">1];
class=class="str">"cmt">//---
  vector<class="type">float> temp;
  Actor.getResults(temp);
  class="type">float delta = MathAbs(ActorResult - temp).Sum();
  ActorResult = temp;
  if(AddSmooth)
    {
      class="type">int err = class="num">0;
      for(class="type">ulong i = class="num">0; i < temp.Size(); i++)

◍ 多空双向的头寸与止损闸门

这段逻辑把买、卖两套控制完全对称地处理,核心是先拿经纪商限制卡一道门槛:最小手数、挂单步长、以及止损止盈距离不能低于 StopsLevel 换算出的 stops。以 XAUUSD 为例,若 StopsLevel 返回 10、Point 为 0.01,stops 就是 0.1 美元,任何低于该值的 TP/SL 会直接触发平仓而非开仓。 temp 数组前三个元素管买侧:temp[0] 是目标仓位占比,temp[1]、temp[2] 分别是 TP、SL 对应的 MaxTP/MaxSL 倍数。只要 temp[0] 算出来比 min_lot 还小,或 TP/SL 折算点数不超过 stops,现有多单会被 CloseByDirection 清掉;否则按 step_lot 圆整手数,用 Ask 加减点数算出 buy_tp、buy_sl,仓位不符就补单或 ClosePartial 减仓。 卖侧把 temp[3~5] 镜像用一遍,价格基准换成 Bid,SL 挂在 Bid 之上、TP 在 Bid 之下。最后 reward 以账户净值 Account[0] 起步,若多空总仓位为 0,再扣掉 atr 与 PrevBalance 的比值作为空载惩罚——这意味着横盘无持仓时,波动率越高对评分越不友好。 开 MT5 把这段贴进 EA 的 OnTick,把 MaxTP、MaxSL 先设成 50、30 跑回测,重点看 StopsLevel 在周末休市前后是否跳变导致异常平仓。外汇与贵金属杠杆高,stops 计算偏差可能瞬间放大滑点风险。

MQL5 / C++
   temp[i] += (class="type">float)(temp[i] * Math::MathRandomNormal(class="num">0, class="num">0.3, err));
   temp.Clip(class="num">0.0f, class="num">1.0f);
   }
   class="type">class="kw">double min_lot = Symb.LotsMin();
   class="type">class="kw">double step_lot = Symb.LotsStep();
   class="type">class="kw">double stops = MathMax(Symb.StopsLevel(), class="num">1) * Symb.Point();
class=class="str">"cmt">//--- buy control
   if(temp[class="num">0] < min_lot || (temp[class="num">1] * MaxTP * Symb.Point()) <= stops || (temp[class="num">2] * MaxSL * Symb.Point()) <= stops)
   {
      if(buy_value > class="num">0)
         CloseByDirection(POSITION_TYPE_BUY);
   }
   else
   {
      class="type">class="kw">double buy_lot = min_lot+MathRound((class="type">class="kw">double)(temp[class="num">0]-min_lot) / step_lot) * step_lot;
      class="type">class="kw">double buy_tp = NormalizeDouble(Symb.Ask() + temp[class="num">1] * MaxTP * Symb.Point(), Symb.Digits());
      class="type">class="kw">double buy_sl = NormalizeDouble(Symb.Ask() - temp[class="num">2] * MaxSL * Symb.Point(), Symb.Digits());
      if(buy_value > class="num">0)
         TrailPosition(POSITION_TYPE_BUY, buy_sl, buy_tp);
      if(buy_value != buy_lot)
      {
         if(buy_value > buy_lot)
            ClosePartial(POSITION_TYPE_BUY, buy_value - buy_lot);
         else
            Trade.Buy(buy_lot - buy_value, Symb.Name(), Symb.Ask(), buy_sl, buy_tp);
      }
   }
class=class="str">"cmt">//--- sell control
   if(temp[class="num">3] < min_lot || (temp[class="num">4] * MaxTP * Symb.Point()) <= stops || (temp[class="num">5] * MaxSL * Symb.Point()) <= stops)
   {
      if(sell_value > class="num">0)
         CloseByDirection(POSITION_TYPE_SELL);
   }
   else
   {
      class="type">class="kw">double sell_lot = min_lot+MathRound((class="type">class="kw">double)(temp[class="num">3]-min_lot) / step_lot) * step_lot;;
      class="type">class="kw">double sell_tp = NormalizeDouble(Symb.Bid() - temp[class="num">4] * MaxTP * Symb.Point(), Symb.Digits());
      class="type">class="kw">double sell_sl = NormalizeDouble(Symb.Bid() + temp[class="num">5] * MaxSL * Symb.Point(), Symb.Digits());
      if(sell_value > class="num">0)
         TrailPosition(POSITION_TYPE_SELL, sell_sl, sell_tp);
      if(sell_value != sell_lot)
      {
         if(sell_value > sell_lot)
            ClosePartial(POSITION_TYPE_SELL, sell_value - sell_lot);
         else
            Trade.Sell(sell_lot - sell_value, Symb.Name(), Symb.Bid(), sell_sl, sell_tp);
      }
   }
class=class="str">"cmt">//---
   class="type">float reward = Account[class="num">0];
   if((buy_value + sell_value) == class="num">0)
      reward -= (class="type">float)(atr / PrevBalance);

「TD3训练回路里的样本抽取与目标构造」

这段逻辑是双延迟DDPG(TD3)智能体在MT5里做离线训练的核心循环。先看一下相关输入参数与网络实例声明,它们决定了训练规模与软更新强度。 input int Iterations = 1000000; input int UpdatePolicy = 3; input int UpdateTargets = 100; input float Tau = 0.01f; CNet Actor; CNet Critic1; CNet Critic2; CNet TargetActor; CNet TargetCritic1; CNet TargetCritic2; Iterations 设成 1000000 意味着最多跑百万次梯度步;Tau=0.01f 表示目标网络每次只吸收主网络 1% 的权重,属于慢跟随。UpdatePolicy=3 与 UpdateTargets=100 控制策略网络与目标网络的更新频率。 在 OnDeinit 里,EA 卸载前把三个目标网络用 Tau 做软更新并落盘成 .nnw 权重文件,其中 Critic 还写入了近期平均误差。这样下次加载可续训,不必从随机初始化重来。 Train 函数开头取回放池大小 total_tr,并用 GetTickCount 记时。循环里先用 MathRand()/32767.0 均匀抽一条轨迹 tr,再用 MathRand()*MathRand()/32767^2 的平方分布抽该轨迹内步号 i——平方分布让早期样本被抽中的概率更低,倾向聚焦轨迹中段以后的转移。 构造目标时,把下一状态 Buffer[tr].States[i+1].state 填进 State,并以账户权益变化率等 6 个量写进 Account 向量:余额变化率、权益/余额比、权益变化率,以及另外三项账户特征。这些归一化特征让 Critic 估算的 Q 值对绝对资金规模不敏感,在外汇或贵金属这种高杠杆品种上可能更稳定,但实盘仍属高风险,需自行回测验证。

MQL5 / C++
input class="type">int                Iterations   = class="num">1000000;
input class="type">int                UpdatePolicy  = class="num">3;
input class="type">int                UpdateTargets = class="num">100;
input class="type">float              Tau           = class="num">0.01f;
CNet                     Actor;
CNet                     Critic1;
CNet                     Critic2;
CNet                     TargetActor;
CNet                     TargetCritic1;
CNet                     TargetCritic2;
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//---
   TargetActor.WeightsUpdate(GetPointer(Actor), Tau);
   TargetCritic1.WeightsUpdate(GetPointer(Critic1), Tau);
   TargetCritic2.WeightsUpdate(GetPointer(Critic2), Tau);
   TargetActor.Save(FileName + "Act.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true);
   TargetCritic1.Save(FileName + "Crt1.nnw", TargetCritic1.getRecentAverageError(), class="num">0, class="num">0, TimeCurrent(), true);
   TargetCritic1.Save(FileName + "Crt2.nnw", TargetCritic2.getRecentAverageError(), class="num">0, class="num">0, TimeCurrent(), true);
   class="kw">delete Result;
  }
class="type">void Train(class="type">void)
  {
   class="type">int total_tr = ArraySize(Buffer);
   class="type">uint ticks = GetTickCount();
   for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped()); iter ++)
     {
      class="type">int tr = (class="type">int)((MathRand() / class="num">32767.0) * (total_tr - class="num">1));
      class="type">int i = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (Buffer[tr].Total - class="num">2));
      class=class="str">"cmt">//--- Target
      State.AssignArray(Buffer[tr].States[i + class="num">1].state);
      class="type">float PrevBalance = Buffer[tr].States[i].account[class="num">0];
      class="type">float PrevEquity = Buffer[tr].States[i].account[class="num">1];
      Account.Clear();
      Account.Add((Buffer[tr].States[i + class="num">1].account[class="num">0] - PrevBalance) / PrevBalance);
      Account.Add(Buffer[tr].States[i + class="num">1].account[class="num">1] / PrevBalance);
      Account.Add((Buffer[tr].States[i + class="num">1].account[class="num">1] - PrevEquity) / PrevEquity);
      Account.Add(Buffer[tr].States[i + class="num">1].account[class="num">2]);
      Account.Add(Buffer[tr].States[i + class="num">1].account[class="num">3]);
      Account.Add(Buffer[tr].States[i + class="num">1].account[class="num">4] / PrevBalance);

双评论员网络下的奖励回传写法

这段逻辑跑在经验回放的训练循环里,先把下一状态(i+1)的账户特征按 PrevBalance 归一化后塞进 Account 缓冲,索引非负就落盘。随后 TargetActor 做前向推理,若失败直接 ExpertRemove 并 break,避免脏数据继续污染网络。 目标侧用两个 Critic 网络(TargetCritic1/2)对 Actor 输出做评估,任一 feedForward 失败同样终止。取两者结果的最小值乘以折扣因子 DiscFactor,再叠上相邻两步奖励差(Buffer[tr].Revards[i] - Buffer[tr].Revards[i+1]),构成保守 Q 值更新的 reward,降低外汇与贵金属行情突变时的过估计倾向。 当前状态 i 的特征重建时,PrevBalance / PrevEquity 取 i-1 与 0 的较大下标,防止越界;Account 清零后重新装入 7 个归一化字段(含余额差比、权益比、持仓数等)。实盘验证时可在 MT5 策略测试器把 DiscFactor 从 0.99 调到 0.95,观察奖励曲线波动是否收敛更快。 主网络 Actor 与 Critic1/2 的前向调用若返回 false,打印函数名与行号后移除 EA。这种双评论员 + 目标网络的代码结构,在 EURUSD 15 分钟复盘里可能比单 Critic 的回撤更浅,但仍属高风险验证,需自行跑样本外数据。

MQL5 / C++
Account.Add(Buffer[tr].States[i + class="num">1].account[class="num">5] / PrevBalance);
Account.Add(Buffer[tr].States[i + class="num">1].account[class="num">6] / PrevBalance);
if(Account.GetIndex() >= class="num">0)
   Account.BufferWrite();
if(!TargetActor.feedForward(GetPointer(State), class="num">1, false, GetPointer(Account)))
   {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    ExpertRemove();
    break;
   }
if(!TargetCritic1.feedForward(GetPointer(TargetActor), LatentLayer, GetPointer(TargetActor)) ||
   !TargetCritic2.feedForward(GetPointer(TargetActor), LatentLayer, GetPointer(TargetActor)))
   {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    ExpertRemove();
    break;
   }
TargetCritic1.getResults(Result);
class="type">float reward = Result[class="num">0];
TargetCritic2.getResults(Result);
reward = DiscFactor * MathMin(reward, Result[class="num">0]) + (Buffer[tr].Revards[i] - Buffer[tr].Revards[i + class="num">1]);
class=class="str">"cmt">//--- Q-function study
State.AssignArray(Buffer[tr].States[i].state);
PrevBalance = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">0];
PrevEquity = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">1];
Account.Clear();
Account.Add((Buffer[tr].States[i].account[class="num">0] - PrevBalance) / PrevBalance);
Account.Add(Buffer[tr].States[i].account[class="num">1] / PrevBalance);
Account.Add((Buffer[tr].States[i].account[class="num">1] - PrevEquity) / PrevEquity);
Account.Add(Buffer[tr].States[i].account[class="num">2]);
Account.Add(Buffer[tr].States[i].account[class="num">3]);
Account.Add(Buffer[tr].States[i].account[class="num">4] / PrevBalance);
Account.Add(Buffer[tr].States[i].account[class="num">5] / PrevBalance);
Account.Add(Buffer[tr].States[i].account[class="num">6] / PrevBalance);
class=class="str">"cmt">//---
if(Account.GetIndex() >= class="num">0)
   Account.BufferWrite();
class=class="str">"cmt">//---
if(!Actor.feedForward(GetPointer(State), class="num">1, false, GetPointer(Account)))
   {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    ExpertRemove();
    break;
   }
if(!Critic1.feedForward(Result,class="num">1,false, GetPointer(Actions)) ||
   !Critic2.feedForward(Result,class="num">1,false, GetPointer(Actions)))
   {

◍ 策略网络在回放缓冲里的更新节奏

这段逻辑跑在强化学习训练循环里,核心是在满足迭代间隔时才做策略(Actor)与评论家(Critic)的前向推演。用 MathRand() 除以 32767.0 取均匀随机轨迹索引 tr,再用两次 MathRand() 乘积除以 32767 的平方做二级随机,把采样点 i 压到单条轨迹可用长度减 2 以内,避免越界读状态。 账户特征做了相对化:当前权益与上一帧权益的比值差、余额差值除上一余额等 8 个量写入 Account 向量,再喂给 Actor.feedForward。任何一步 feedForward 或 backProp 返回失败,就 PrintFormat 打出函数名与行号并 ExpertRemove 退出 EA——实盘里这等于训练线程直接自杀,需盯日志定位哪层网络崩了。 Critic1 的前向把 Actor 的输出当作输入,LatentLayer 做隐层桥接,若返回 false 同样强制移除专家。外汇与贵金属这类高杠杆品种下,这类自退出逻辑可能让策略在波动尖峰静默停摆,复盘时优先查 __LINE__ 报出的层。

MQL5 / C++
PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
ExpertRemove();
break;
}
Result.Clear();
Result.Add(reward);
if(!Critic1.backProp(Result, GetPointer(Actions), GetPointer(Gradient)) ||
   !Critic2.backProp(Result, GetPointer(Actions), GetPointer(Gradient)))
   {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   ExpertRemove();
   break;
   }
class=class="str">"cmt">//--- Policy study
if(iter > class="num">0 && (iter % UpdatePolicy) == class="num">0)
   {
   tr = (class="type">int)((MathRand() / class="num">32767.0) * (total_tr - class="num">1));
   i = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (Buffer[tr].Total - class="num">2));
   State.AssignArray(Buffer[tr].States[i].state);
   PrevBalance = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">0];
   PrevEquity = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">1];
   Account.Clear();
   Account.Add((Buffer[tr].States[i].account[class="num">0] - PrevBalance) / PrevBalance);
   Account.Add(Buffer[tr].States[i].account[class="num">1] / PrevBalance);
   Account.Add((Buffer[tr].States[i].account[class="num">1] - PrevEquity) / PrevEquity);
   Account.Add(Buffer[tr].States[i].account[class="num">2]);
   Account.Add(Buffer[tr].States[i].account[class="num">3]);
   Account.Add(Buffer[tr].States[i].account[class="num">4] / PrevBalance);
   Account.Add(Buffer[tr].States[i].account[class="num">5] / PrevBalance);
   Account.Add(Buffer[tr].States[i].account[class="num">6] / PrevBalance);
   if(Account.GetIndex() >= class="num">0)
      Account.BufferWrite();
   class=class="str">"cmt">//---
   if(!Actor.feedForward(GetPointer(State), class="num">1, false, GetPointer(Account)))
      {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      break;
      }
   if(!Critic1.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actor)))
      {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      break;
      }

「目标网络同步与训练中断的兜底」

这段收尾逻辑跑在双评论家(Critic1/Critic2)的迭代训练循环末尾,核心做两件事:按节奏把在线网络的权重软更新到目标网络,以及在反向传播失败时安全退出。 UpdateTargets 控制同步频率,代码里用 iter % UpdateTargets == 0 触发 WeightsUpdate(..., Tau),Tau 是软更新系数,值越小目标网络跟得越慢,训练可能更稳但收敛偏懒。 每过 500 毫秒(GetTickCount() - ticks > 500)就把两个 Critic 的近期平均误差刷到图表 Comment,格式是 Critic1 12.34% -> Error 0.00012345,你能直接盯着这个数字看是否还在降。 如果 backPropbackPropGradient 任一返回失败,立即切回训练模式、打印函数与行号、ExpertRemove()break——EA 会自我卸载,不会在坏状态下硬跑。最后循环外再各打一行最终平均误差并移除 EA,整个离线训练会话干净收摊。 外汇与贵金属品种上跑这套强化学习训练,历史回测不代表实盘概率,杠杆品种风险高,参数没调明白前别直接挂真仓。

MQL5 / C++
  Critic1.getResults(Result);
  class="type">float forecast = Result[class="num">0];
  Result.Update(class="num">0, (forecast > class="num">0 ? forecast + PoliticAdjust : PoliticAdjust));
  Critic1.TrainMode(false);
  if(!Critic1.backProp(Result, GetPointer(Actor)) ||
     !Actor.backPropGradient(GetPointer(Account), GetPointer(Gradient)))
     {
     Critic1.TrainMode(true);
     PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
     ExpertRemove();
     break;
     }
  Critic1.TrainMode(true);
  }
  class=class="str">"cmt">//--- Update Target Nets
  if(iter > class="num">0 && (iter % UpdateTargets) == class="num">0)
   {
   TargetActor.WeightsUpdate(GetPointer(Actor), Tau);
   TargetCritic1.WeightsUpdate(GetPointer(Critic1), Tau);
   TargetCritic2.WeightsUpdate(GetPointer(Critic2), Tau);
   }
  if(GetTickCount() - ticks > class="num">500)
   {
    class="type">class="kw">string str = StringFormat("%-15s %class="num">5.2f%% -> Error %class="num">15.8f\n", "Critic1", iter * class="num">100.0 / (class="type">class="kw">double)(Iterations), Critic1.getRecentAverageError());
    str += StringFormat("%-15s %class="num">5.2f%% -> Error %class="num">15.8f\n", "Critic2", iter * class="num">100.0 / (class="type">class="kw">double)(Iterations), Critic2.getRecentAverageError());
    Comment(str);
    ticks = GetTickCount();
    }
   }
  Comment("");
class=class="str">"cmt">//---
  PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Critic1", Critic1.getRecentAverageError());
  PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Critic2", Critic2.getRecentAverageError());
  ExpertRemove();
class=class="str">"cmt">//---
  }

常见问题

遍历持仓列表,按订单方向累加 profit 与止损距离,分别写入特征向量的多、空通道,避免混合成单一净值掩盖方向暴露。
取两个评论员 Q 值的最小值作为目标,再算 TD 误差回传,能有效缓解单网络高估,代码里用 MathMin 选小值即可。
可以,小布能读取你的策略脚本日志,自动比对样本抽取节奏与目标网络同步间隔,标记异常并给出修正建议。
会,建议每若干步才更新一次策略网络,并配合目标网络软同步,单步狂更容易震荡,回测里步均收益波动会明显放大。
落地时存目标网络权重与步数,重启后若超阈值未同步则强制软同步一次,并在中断点续抽样本,避免从头冷启。