神经网络变得轻松(第五十三部分):奖励分解·进阶篇
🧠

神经网络变得轻松(第五十三部分):奖励分解·进阶篇

(2/3)· 复合奖励信号模棱两可,代理者行为跑偏时你却分不清是哪个分量在捣乱

案例拆解 第 2/3 篇
把红利惩罚一股脑塞进奖励函数就开训,是强化学习里最常见的自误。代理者没学会你要的行为,你却没法判断是探索激励不够还是不作为惩罚太重。不拆分量,调参全靠蒙。

◍ SAC-D目标网络与CAGrad的初始化落点

在 SAC-D 这类离线与在线混合策略里,目标网络(target critic / target nu)的权重同步不能省。下面这段初始化先把 cTargetCritic1、cTargetCritic2、cTargetNu 用 Create 建出来,失败就直接 return false 并打印错误码,避免在 MT5 里跑出空指针导致 EA 静默崩掉。 随后所有网络对象(actor、critic、zeta、nu 及三个 target)统一绑定 OpenCL 上下文,这一步决定你能否用显卡加速矩阵运算。若机器无可用 GPU,SetOpenCL 虽不报错但后续训练会明显偏慢,外汇与贵金属 tick 级样本下延迟可能放大数倍,属高风险计算环境。 权重更新用 WeightsUpdate(GetPointer(cCritic1), 1.0) 把在线 critic 整份拷给目标网络,系数 1.0 即硬拷贝;若想做软更新须改这里。之后 fLambda 初始化为全 1e-5、fLambda_m / fLambda_v 为零向量,尺寸由 cZeta.getResults(fZeta) 的返回长度决定。 CAGrad 函数是多任务梯度冲突的缓和器:先算 grad 的外积 GG,把 NaN 置 0,若 GG 全零直接返回原梯度。scale 用对角线加 1e-4 后开方取均值做归一,迭代 iCAGrad_Iters 次用 softmax 搜最优 w,目标含约束项 c = sqrt(gg+1e-4)*fCAGrad_C。开 MT5 把这段贴进类方法,调 fCAGrad_C 与迭代次数能直接看冲突梯度是否收敛。

MQL5 / C++
  if(!cTargetCritic1.Create(critic) || !cTargetCritic2.Create(critic) ||
      !cTargetNu.Create(nu))
     {
       PrintFormat("Error of create target models: %d", GetLastError());
       class="kw">return class="kw">false;
     }
class=class="str">"cmt">//---
   cActorExploer.SetOpenCL(opencl);
   cCritic1.SetOpenCL(opencl);
   cCritic2.SetOpenCL(opencl);
   cZeta.SetOpenCL(opencl);
   cNu.SetOpenCL(opencl);
   cTargetCritic1.SetOpenCL(opencl);
   cTargetCritic2.SetOpenCL(opencl);
   cTargetNu.SetOpenCL(opencl);
class=class="str">"cmt">//---
   if(!cTargetCritic1.WeightsUpdate(GetPointer(cCritic1), class="num">1.0) ||
      !cTargetCritic2.WeightsUpdate(GetPointer(cCritic2), class="num">1.0) ||
      !cTargetNu.WeightsUpdate(GetPointer(cNu), class="num">1.0))
     {
       PrintFormat("Error of update target models: %d", GetLastError());
       class="kw">return class="kw">false;
     }
class=class="str">"cmt">//---
   cZeta.getResults(fZeta);
   class="type">class="kw">ulong size = fZeta.Size();
   fLambda = vector<class="type">class="kw">float>::Full(size,class="num">1.0e-5f);
   fLambda_m = vector<class="type">class="kw">float>::Zeros(size);
   fLambda_v = vector<class="type">class="kw">float>::Zeros(size);
   fQWeights = vector<class="type">class="kw">float>::Ones(size);
   iLatentLayer = latent_layer;
class=class="str">"cmt">//---
   class="kw">return true;
  }
vector<class="type">class="kw">float> CNet_SAC_D_DICE::CAGrad(vector<class="type">class="kw">float> &grad)
  {
   matrix<class="type">class="kw">float> GG = grad.Outer(grad);
   GG.ReplaceNan(class="num">0);
   if(MathAbs(GG).Sum() == class="num">0)
      class="kw">return grad;
   class="type">class="kw">float scale = MathSqrt(GG.Diag() + class="num">1.0e-4f).Mean();
   GG = GG / MathPow(scale,class="num">2);
   vector<class="type">class="kw">float> Gg = GG.Mean(class="num">1);
   class="type">class="kw">float gg = Gg.Mean();
   vector<class="type">class="kw">float> w = vector<class="type">class="kw">float>::Zeros(grad.Size());
   class="type">class="kw">float c = MathSqrt(gg + class="num">1.0e-4f) * fCAGrad_C;
   vector<class="type">class="kw">float> w_best = w;
   class="type">class="kw">float obj_best = FLT_MAX;
   vector<class="type">class="kw">float> moment = vector<class="type">class="kw">float>::Zeros(w.Size());
   for(class="type">int i = class="num">0; i < iCAGrad_Iters; i++)
     {
       vector<class="type">class="kw">float> ww;
       w.Activation(ww,AF_SOFTMAX);
       class="type">class="kw">float obj = ww.Dot(Gg) + c * MathSqrt(ww.MatMul(GG).Dot(ww) + class="num">1.0e-4f);
       if(MathAbs(obj) < obj_best)
         {
          obj_best = MathAbs(obj);
          w_best = w;
         }
       if(i < (iCAGrad_Iters - class="num">1))
         {
          class="type">class="kw">float loss = -obj;
          vector<class="type">class="kw">float> derev = Gg + GG.MatMul(ww) * c / (MathSqrt(ww.MatMul(GG).Dot(ww) + class="num">1.0e-4f) * class="num">2) +

「梯度耦合里的权重归一与动量更新」

这段逻辑处在策略网络反向修正的核心段:先用 ww 与 Gram 矩阵 GG 做矩阵乘,再除以带 1e-4 稳定项的范数,得到缩放系数 c 作用下的方向向量。 delta = derev * loss 拿到基础梯度后,代码构造了 size 阶单位阵 ident 与全 1 向量 ones,通过 ones.Outer(ww) 生成softmax 雅可比的近似结构 sm_der,转置后乘 (ident - sm_der) 完成链式展开。 sm_der.MatMul(delta) 把梯度压回参数流形;若 delta 的点积范数(Ptp)非零就就地归一,避免量级爆炸。动量项 moment = delta * 0.8f + moment * 0.5f 显示历史冲量权重仅 0.5,当前梯度占 0.8,偏向快变环境——外汇与贵金属的高波动行情下这种设定可能更跟手,但过拟合风险也偏高。 w += moment 后再次按 Ptp 归一,保证策略向量始终在单位球附近。末段 w_best 走 softmax 激活,用 gw_norm 与常数 c 算 lmbda,最终 result 融合 grad 与 1/size 偏置,分母含 fCAGrad_C 平方——调这个 C 值会直接改输出幅度,开 MT5 把 fCAGrad_C 从默认改到 0.5 和 2.0 各跑一轮回测就能看出梯度裁剪松紧的差别。 Study 函数入口先判空 Actions 与 NextState,随后用 feedForward 把下一状态送进目标网络;cTargetCritic1/2 与 cTargetNu 都从 iLatentLayer 拉特征到末层,任意一条前向失败立即返回 false,说明这套 SAC-DICE 实现把目标网络同步当作训练不可跳过的闸门。

MQL5 / C++
ww.MatMul(GG) * c / (MathSqrt(ww.MatMul(GG).Dot(ww) + class="num">1.0e-4f) * class="num">2);
					 vector<class="type">class="kw">float> delta = derev * loss;
					 class="type">class="kw">ulong size = delta.Size();
					 matrix<class="type">class="kw">float> ident = matrix<class="type">class="kw">float>::Identity(size, size);
					 vector<class="type">class="kw">float> ones = vector<class="type">class="kw">float>::Ones(size);
					 matrix<class="type">class="kw">float> sm_der = ones.Outer(ww);
					 sm_der = sm_der.Transpose() * (ident - sm_der);
					 delta = sm_der.MatMul(delta);
					 if(delta.Ptp() != class="num">0)
						 delta = delta / delta.Ptp();
					 moment = delta * class="num">0.8f + moment * class="num">0.5f;
					 w += moment;
					 if(w.Ptp() != class="num">0)
						 w = w / w.Ptp();
				 }
		 }
	 w_best.Activation(w,AF_SOFTMAX);
	 class="type">class="kw">float gw_norm = MathSqrt(w.MatMul(GG).Dot(w) + class="num">1.0e-4f);
	 class="type">class="kw">float lmbda = c / (gw_norm + class="num">1.0e-4f);
	 vector<class="type">class="kw">float> result = ((w * lmbda + class="num">1.0f / (class="type">class="kw">float)grad.Size()) * grad) / (class="num">1 + MathPow(fCAGrad_C,class="num">2));
class=class="str">"cmt">//---
	 class="kw">return result;
	}
class="type">bool CNet_SAC_D_DICE::Study(CArrayFloat *State,
								CArrayFloat *SecondInput,
								CBufferFloat *Actions,
								vector<class="type">class="kw">float> &Rewards,
								CBufferFloat *NextState,
								CBufferFloat *NextSecondInput,
								class="type">class="kw">float discount,
								class="type">class="kw">float tau)
	{
class=class="str">"cmt">//---
	 if(!Actions)
		 class="kw">return class="kw">false;
	 if(!!NextState)
		 if(!CNet::feedForward(NextState, class="num">1, class="kw">false, NextSecondInput))
			 class="kw">return class="kw">false;
	 if(!cTargetCritic1.feedForward(GetPointer(this), iLatentLayer, GetPointer(this), layers.Total() - class="num">1) ||
		!cTargetCritic2.feedForward(GetPointer(this), iLatentLayer, GetPointer(this), layers.Total() - class="num">1))
		 class="kw">return class="kw">false;
class=class="str">"cmt">//---
	 if(!cTargetNu.feedForward(GetPointer(this), iLatentLayer, GetPointer(this), layers.Total() - class="num">1))
		 class="kw">return class="kw">false;

DualCVaR 里的 nu 与 zeta 梯度回传

这段逻辑跑在强化学习评判网络的反向更新里,核心是两个影子变量 nu 和 zeta 的梯度落地。nu 负责逼近状态价值,zeta 充当凸对偶变量,靠 fLambda 这个拉格朗日乘子把 CVaR 约束缝进策略梯度。 先看前向收尾:把策略输出拷进 output 缓冲,再依次喂给 cNu、cZeta 两个子网络拿回 nu 与 zeta 向量。若 NextState 为空,next_nu 直接置零向量,否则取目标网络的下一个 nu,这一步决定了贝尔曼残差是否带 bootstrap。 policy_ratio 用 log_prob 和 Rewards 算指数均值,再乘到 (next_nu*discount+Rewards) 上减 nu,得到 bellman_residuals。zeta_loss 和 nu_loss 都显式用了 MathAbs(bellman_residuals),说明梯度对残差符号不敏感,只在幅度上 penalize——这是 DualCVaR 跟普通 TD 最不一样的点。 fLambda 自身走了一套简化 Adam:m 和 v 分别用 b1、b2 做动量与二阶矩衰减,更新步长被 lr/sqrt(v+lr/100) 缩放。把 lr/100 写死进 epsilon 项,意味着学习率若调过 100 量级,分母稳定项会失效,MT5 里跑时应把 lr 压在 1e-3 附近才稳。 nu 的梯度回传要手动把 nu_grad 写进最后一层神经元梯度缓冲,再调 cNu.backPropGradient 往浅层传。代码里 last_layer 取 layers.Total()-1 后做了两次空指针 return false,漏掉任何一层对象都会导致整轮训练静默中断,开 MT5 调试时建议在这两处下断点看 layer/neuron 是否真的构建成功。

MQL5 / C++
  if(!CNet::feedForward(State, class="num">1, class="kw">false, SecondInput))
        class="kw">return class="kw">false;
   CBufferFloat *output = ((CNeuronBaseOCL*)((CLayer*)layers.At(layers.Total() - class="num">1)).At(class="num">0)).getOutput();
   output.AssignArray(Actions);
   output.BufferWrite();
   if(!cNu.feedForward(GetPointer(this), iLatentLayer, GetPointer(this)))
        class="kw">return class="kw">false;
   if(!cZeta.feedForward(GetPointer(this), iLatentLayer, GetPointer(this)))
        class="kw">return class="kw">false;
   vector<class="type">class="kw">float> nu, next_nu, zeta, ones;
   cNu.getResults(nu);
   cZeta.getResults(zeta);
   if(!!NextState)
        cTargetNu.getResults(next_nu);
   else
        next_nu = vector<class="type">class="kw">float>::Zeros(nu.Size());
   ones = vector<class="type">class="kw">float>::Ones(zeta.Size());
   vector<class="type">class="kw">float> log_prob = GetLogProbability(output);
   class="type">int shift = (class="type">int)(Rewards.Size() - log_prob.Size());
   if(shift < class="num">0)
        class="kw">return class="kw">false;
   class="type">class="kw">float policy_ratio = class="num">0;
   for(class="type">class="kw">ulong i = class="num">0; i < log_prob.Size(); i++)
        policy_ratio += log_prob[i] - Rewards[shift + i] / LogProbMultiplier;
   policy_ratio = MathExp(policy_ratio / log_prob.Size());
   vector<class="type">class="kw">float> bellman_residuals = (next_nu * discount + Rewards) * policy_ratio - nu;
   vector<class="type">class="kw">float> zeta_loss = MathPow(zeta, class="num">2.0f) / class="num">2.0f - zeta * (MathAbs(bellman_residuals) - fLambda) ;
   vector<class="type">class="kw">float> nu_loss = zeta * MathAbs(bellman_residuals) + MathPow(nu, class="num">2.0f) / class="num">2.0f;
   vector<class="type">class="kw">float> lambda_los = fLambda * (ones - zeta);
   vector<class="type">class="kw">float> grad_lambda = CAGrad((ones - zeta) * (lambda_los * (-class="num">1.0f)));
   fLambda_m = fLambda_m * b1 + grad_lambda * (class="num">1 - b1);
   fLambda_v = fLambda_v * b2 + MathPow(grad_lambda, class="num">2) * (class="num">1.0f - b2);
   fLambda += fLambda_m * lr / MathSqrt(fLambda_v + lr / class="num">100.0f);
   CBufferFloat temp;
   temp.BufferInit(MathMax(Actions.Total(), SecondInput.Total()), class="num">0);
   temp.BufferCreate(opencl);
class=class="str">"cmt">//--- update nu
   class="type">int last_layer = cNu.layers.Total() - class="num">1;
   CLayer *layer = cNu.layers.At(last_layer);
   if(!layer)
        class="kw">return class="kw">false;
   CNeuronBaseOCL *neuron = layer.At(class="num">0);
   if(!neuron)
        class="kw">return class="kw">false;
   CBufferFloat *buffer = neuron.getGradient();
   if(!buffer)
        class="kw">return class="kw">false;
   vector<class="type">class="kw">float> nu_grad = CAGrad(nu_loss * (zeta * bellman_residuals / MathAbs(bellman_residuals) - nu));
   if(!buffer.AssignArray(nu_grad) || !buffer.BufferWrite())
        class="kw">return class="kw">false;
   if(!cNu.backPropGradient(output, GetPointer(temp)))
        class="kw">return class="kw">false;
class=class="str">"cmt">//--- update zeta
   last_layer = cZeta.layers.Total() - class="num">1;
   layer = cZeta.layers.At(last_layer);

◍ 分布critic的梯度回传与zeta归一

这段逻辑跑在智能体反向更新里,先取最后一层神经元梯度缓冲,任何一层指针为空就直接 return false,避免空指针把EA拖崩。zeta梯度用 CAGrad 算,系数里带了 fLambda 和 bellman_residuals 的绝对值,符号翻负,说明是在做分布风险项的反向惩罚。 前向部分把 cCritic1 / cCritic2 都喂一遍,输出给后续策略网。fZeta 的更新很有意思:首次用 MathAbs(zeta) 直接赋值,之后走 0.9 与 0.1 的滑动平均,相当于给 zeta 做了个低通。归一化那行把 zeta 开三次方再除以 fZeta 三次方根的 10 倍,压缩了量纲差异,让不同品种(含外汇、贵金属)的梯度尺度倾向一致——但杠杆品种跳空时残差可能爆,仍属高风险。 critic 损失是 zeta 加权的二次误差,fLoss1 用 0.999 惯性平滑,初值为均方根,后续滚动更新。梯度 grad = CAGrad(loss * zeta * (target - result) * 2.0f),写进最后一层缓冲后触发 backPropGradient。你在 MT5 里改 fLambda 或把 0.9/0.1 换成 0.95/0.05,能直接观察回测曲线抖动变化。

MQL5 / C++
  if(!layer)
        class="kw">return class="kw">false;
  neuron = layer.At(class="num">0);
  if(!neuron)
        class="kw">return class="kw">false;
  buffer = neuron.getGradient();
  if(!buffer)
        class="kw">return class="kw">false;
  vector<class="type">class="kw">float> zeta_grad = CAGrad(zeta_loss * (zeta - MathAbs(bellman_residuals) + fLambda) * (-class="num">1.0f));
  if(!buffer.AssignArray(zeta_grad) || !buffer.BufferWrite())
        class="kw">return class="kw">false;
  if(!cZeta.backPropGradient(output, GetPointer(temp)))
        class="kw">return class="kw">false;
class=class="str">"cmt">//--- feed forward critics
  if(!cCritic1.feedForward(GetPointer(this), iLatentLayer, output) ||
     !cCritic2.feedForward(GetPointer(this), iLatentLayer, output))
        class="kw">return class="kw">false;
  vector<class="type">class="kw">float> result;
  if(fZeta.CompareByDigits(vector<class="type">class="kw">float>::Zeros(fZeta.Size()),class="num">8) == class="num">0)
     fZeta = MathAbs(zeta);
  else
     fZeta = fZeta * class="num">0.9f + MathAbs(zeta) * class="num">0.1f;
  zeta = MathPow(MathAbs(zeta), class="num">1.0f / class="num">3.0f) / (MathPow(fZeta, class="num">1.0f / class="num">3.0f) * class="num">10.0f);
  vector<class="type">class="kw">float> target = vector<class="type">class="kw">float>::Zeros(Rewards.Size());
  if(!!NextState)
     {
      cTargetCritic1.getResults(target);
      cTargetCritic2.getResults(result);
      if(fQWeights.Dot(result) < fQWeights.Dot(target))
         target = result;
     }
  target = (target * discount + Rewards);
  class="type">class="kw">ulong total = log_prob.Size();
  for(class="type">class="kw">ulong i = class="num">0; i < total; i++)
     target[shift + i] = log_prob[i] * LogProbMultiplier;
class=class="str">"cmt">//--- update critic1
  cCritic1.getResults(result);
  vector<class="type">class="kw">float> loss = zeta * MathPow(result - target, class="num">2.0f);
  if(fLoss1 == class="num">0)
     fLoss1 = MathSqrt(fQWeights.Dot(loss) / fQWeights.Sum());
  else
     fLoss1 = MathSqrt(class="num">0.999f * MathPow(fLoss1, class="num">2.0f) + class="num">0.001f * fQWeights.Dot(loss) / fQWeights.Sum());
  vector<class="type">class="kw">float> grad = CAGrad(loss * zeta * (target - result) * class="num">2.0f);
  last_layer = cCritic1.layers.Total() - class="num">1;
  layer = cCritic1.layers.At(last_layer);
  if(!layer)
        class="kw">return class="kw">false;
  neuron = layer.At(class="num">0);
  if(!neuron)
        class="kw">return class="kw">false;
  buffer = neuron.getGradient();
  if(!buffer)
        class="kw">return class="kw">false;
  if(!buffer.AssignArray(grad) || !buffer.BufferWrite())
        class="kw">return class="kw">false;
  if(!cCritic1.backPropGradient(output, GetPointer(temp)) ||
!backPropGradient(SecondInput, GetPointer(temp), iLatentLayer))
        class="kw">return class="kw">false;
class=class="str">"cmt">//--- update critic2

「双评论家网络下的梯度回传与探索策略更新」

这段逻辑跑在强化学习训练循环里,用两个评论家网络 cCritic1 与 cCritic2 交叉验证误差,再决定拿谁的结果去回传。loss 用 zeta 乘均方误差,fLoss2 首次直接取根号加权误差,之后按 0.999 与 0.001 做指数滑动平均,老值占 99.9% 新值占 0.1%,让训练曲线倾向平滑。 梯度写入前先抓最后一层神经元,取它的梯度缓冲并赋值为 CAGrad 算出的 grad,任何一步 AssignArray 或 BufferWrite 失败就直接 return false,避免脏梯度污染网络。 选评论家时比 fLoss1 与 fLoss2:若 fLoss1 更小,用 cCritic1 的 result 和 cCritic2 的 mean;反之对调。var 取两者差绝对值的一半,mean 取平均,target 先被写成 discount*log_prob*LogProbMultiplier 的序列,再叠一层 CAGrad(zeta*(target - var*2.5) - result) + result 的修正。 回传完立刻切到探索策略:cActorExploer 前向吃 State,把输出强行换成 Actions 再写回缓冲,target 改用 var*2.0 的正向偏移。外汇与贵金属行情跳空频繁,这类 OCL 网络在实盘前务必用 MT5 策略测试器以 Tick 级数据跑通回传,否则显存或缓冲区报错只会在 live 阶段暴露。

MQL5 / C++
  cCritic2.getResults(result);
  loss = zeta * MathPow(result - target, class="num">2.0f);
  if(fLoss2 == class="num">0)
      fLoss2 = MathSqrt(fQWeights.Dot(loss) / fQWeights.Sum());
  else
      fLoss2 = MathSqrt(class="num">0.999f * MathPow(fLoss2, class="num">2.0f) + class="num">0.001f * fQWeights.Dot(loss) / fQWeights.Sum());
  grad = CAGrad(loss * zeta * (target - result) * class="num">2.0f);
  last_layer = cCritic2.layers.Total() - class="num">1;
  layer = cCritic2.layers.At(last_layer);
  if(!layer)
      class="kw">return class="kw">false;
  neuron = layer.At(class="num">0);
  if(!neuron)
      class="kw">return class="kw">false;
  buffer = neuron.getGradient();
  if(!buffer)
      class="kw">return class="kw">false;
  if(!buffer.AssignArray(grad) || !buffer.BufferWrite())
      class="kw">return class="kw">false;
  if(!cCritic2.backPropGradient(output, GetPointer(temp)) ||
!backPropGradient(SecondInput, GetPointer(temp), iLatentLayer))
      class="kw">return class="kw">false;
  vector<class="type">class="kw">float> mean;
  CNet *critic = NULL;
  if(fLoss1 <= fLoss2)
   {
     cCritic1.getResults(result);
     cCritic2.getResults(mean);
     critic = GetPointer(cCritic1);
   }
  else
   {
     cCritic1.getResults(mean);
     cCritic2.getResults(result);
     critic = GetPointer(cCritic2);
   }
  vector<class="type">class="kw">float> var = MathAbs(mean - result) / class="num">2.0f;
  mean += result;
  mean /= class="num">2.0f;
  target = mean;
  for(class="type">class="kw">ulong i = class="num">0; i < log_prob.Size(); i++)
      target[shift + i] = discount * log_prob[i] * LogProbMultiplier;
  target = CAGrad(zeta * (target - var * class="num">2.5f) - result) + result;
  CBufferFloat bTarget;
  bTarget.AssignArray(target);
  critic.TrainMode(class="kw">false);
  if(!critic.backProp(GetPointer(bTarget), GetPointer(this)) ||
      !backPropGradient(SecondInput, GetPointer(temp)))
   {
     critic.TrainMode(true);
     class="kw">return class="kw">false;
   }
class=class="str">"cmt">//--- update exploration policy
  if(!cActorExploer.feedForward(State, class="num">1, class="kw">false, SecondInput))
   {
     critic.TrainMode(true);
     class="kw">return class="kw">false;
   }
  output = ((CNeuronBaseOCL*)((CLayer*)cActorExploer.layers.At(layers.Total() - class="num">1)).At(class="num">0)).getOutput();
  output.AssignArray(Actions);
  output.BufferWrite();
  cActorExploer.GetLogProbs(log_prob);
  target = mean;
  for(class="type">class="kw">ulong i = class="num">0; i < log_prob.Size(); i++)
      target[shift + i] = discount * log_prob[i] * LogProbMultiplier;
  target = CAGrad(zeta * (target + var * class="num">2.0f) - result) + result;
  bTarget.AssignArray(target);
  if(!critic.backProp(GetPointer(bTarget), GetPointer(cActorExploer)) ||
      !cActorExploer.backPropGradient(SecondInput, GetPointer(temp)))
   {
     critic.TrainMode(true);
     class="kw">return class="kw">false;
   }
  critic.TrainMode(true);

目标网络延迟更新与奖励向量定义

目标网络权重不是每帧都动。代码里用 iUpdateDelayCount 做递减闸门:当 NextState 有效且计数器大于 0 时,先自减并返回 true,跳过本次同步;归零后才把 iUpdateDelay 重新装入计数器,实现每隔 iUpdateDelay 步更新一次目标模型。这种延迟能缓和训练振荡,外汇与贵金属行情跳变频繁,过小的延迟值可能让策略对噪声过拟合。 若 cTargetCritic1 / cTargetCritic2 / cTargetNu 任一WeightsUpdate失败,立即打印错误码并返回 false,训练循环应捕获该信号停止。三个目标网络分别对应双评论家与 nu 网络,tau 为软更新系数。 奖励结构用宏写死:NActions=6 表示 6 种可执行动作,NRewards=3+NActions=9 即奖励向量长度。索引 0 为余额增量,1 为净值增减(负值回撤、正值盈利),2 为空仓惩罚,3 之后为各动作的对数概率。开 MT5 把 NActions 改成 4 试跑,能直接看到 NRewards 变为 7,影响 critic 第 4 层神经元数量。 CreateDescriptions 中 critic 末层 descr.count = NRewards、activation = None、optimization = ADAM,输出原始奖励估计值供后续计算。建好描述数组后返回 true,若 new CLayerDescription 失败则删对象并返 false,内存泄漏风险在此被截断。

MQL5 / C++
  if(!!NextState)
    {
      if(iUpdateDelayCount > class="num">0)
        {
         iUpdateDelayCount--;
         class="kw">return true;
        }
      iUpdateDelayCount = iUpdateDelay;
    }
  if(!cTargetCritic1.WeightsUpdate(GetPointer(cCritic1), tau) ||
     !cTargetCritic2.WeightsUpdate(GetPointer(cCritic2), tau) ||
     !cTargetNu.WeightsUpdate(GetPointer(cNu), tau))
    {
     PrintFormat("Error of update target models: %d", GetLastError());
     class="kw">return class="kw">false;
    }
class=class="str">"cmt">//---
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Rewards structure                                                |
class=class="str">"cmt">//|   class="num">0   -   Delta Balance                                          |
class=class="str">"cmt">//|   class="num">1   -   Delta Equity( "-" Drawdown / "+" Profit)               |
class=class="str">"cmt">//|   class="num">2   -   Penalty for no open positions                          |
class=class="str">"cmt">//|   class="num">3... -   LogProbs vector                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define                     NActions        class="num">6       class=class="str">"cmt">//Number of possible Actions
class="macro">#define                     NRewards        class="num">3+NActions  class=class="str">"cmt">//Number of rewards
class="type">bool CreateDescriptions(CArrayObj *actor, CArrayObj *critic)
  {
class=class="str">"cmt">//---
  CLayerDescription *descr;
class=class="str">"cmt">//---
  if(!actor)
    {
     actor = new CArrayObj();
     if(!actor)
       class="kw">return class="kw">false;
    }
  if(!critic)
    {
     critic = new CArrayObj();
     if(!critic)
       class="kw">return class="kw">false;
    }
class=class="str">"cmt">//--- Actor
........
........
class=class="str">"cmt">//--- Critic
  critic.Clear();
class=class="str">"cmt">//--- Input layer
........
........
class=class="str">"cmt">//--- layer class="num">4
  if(!(descr = new CLayerDescription()))
     class="kw">return class="kw">false;
  descr.type = defNeuronBaseOCL;
  descr.count = NRewards;
  descr.optimization = ADAM;
  descr.activation = None;
  if(!critic.Add(descr))
    {
     class="kw">delete descr;
     class="kw">return class="kw">false;
    }
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="kw">struct SState
  {
   class="type">class="kw">float          state[HistoryBars * BarDescr];
交给小布盯盘看训练诊断
这些分量层面的诊断逻辑小布盯盘的 AIGC 已内置,打开对应品种页即可看到代理者各奖励分量的响应曲线,你只管判断哪条该加权。

常见问题

原文作者实测迎头正对方案下多函数预测任务拖累了性能,需用改进形态重建分量重要性函数才可能追平或反超。
改动 Q-函数输出为每个分量留元素、再分别更新,该形态对离散和连续动作空间都适用。
倾向做法是把复合奖励拆成分量分别看重要性函数,哪条分量权重异常就能定位是信号歧义还是架构问题。
小布内置的 AIGC 诊断可呈现各分量影响曲线,但模型训练本身仍在你的 MQL5 环境,小布只替你做重复观测。
汇金市场高风险、噪声大,分量设计过细可能过拟合历史行情,建议先用简单分量验证再逐步加层。