神经网络变得简单(第 57 部分):随机边际扮演者-评论者(SMAC)·综合运用
📘

神经网络变得简单(第 57 部分):随机边际扮演者-评论者(SMAC)·综合运用

第 3/3 篇

训练循环里的目标网同步与KNN奖励估计

在双评论家(Critic)架构的离线训练里,先把主网络置为训练态,再用软更新把目标网权重向当前 Critic1 / Critic2 靠拢,Tau 控制跟随速度。若每 500 毫秒(GetTickCount 差值 > 500)刷一次 Comment,就能在图表上看到类似「Critic1 37.50% -> Error 0.01234567」的进度与误差,方便判断收敛节奏。 下面这段 KNNReward 用嵌入向量做近邻回放:先校验 embedding 维度与状态矩阵列数一致,否则直接返回空向量;接着对所有历史状态算欧氏距离,取前 k 个最近邻(k 被 MathMin 截断到不超过样本数),奖励按 1 - log(distance+1) 加权——距离越近权重越接近 1。 近邻嵌入拼成 (k+1)×size 矩阵后做 SVD 分解,最终奖励取 k_rewards 按列均值。外汇与贵金属行情噪声大,这类嵌入奖励在实盘前务必用 MT5 策略测试器跑通维度检查,避免 Cols 不匹配导致静默返回空值。

MQL5 / C++
critic.TrainMode(true);
class=class="str">"cmt">//--- Update Target Nets
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">//---
 }
vector<class="type">class="kw">float> KNNReward(class="type">class="kw">ulong k,
                        vector<class="type">class="kw">float> &embedding,
                        matrix<class="type">class="kw">float> &state_embedding,
                        matrix<class="type">class="kw">float> &rewards
)
  {
  if(embedding.Size() != state_embedding.Cols())
   {
    PrintFormat("%s -> %d Inconsistent embedding size", __FUNCTION__, __LINE__);
    class="kw">return vector<class="type">class="kw">float>::Zeros(class="num">0);
   }
  class="type">class="kw">ulong size = embedding.Size();
  class="type">class="kw">ulong states = state_embedding.Rows();
  k = MathMin(k,states);
  class="type">class="kw">ulong rew_size = rewards.Cols();
  vector<class="type">class="kw">float> distance = vector<class="type">class="kw">float>::Zeros(states);
  matrix<class="type">class="kw">float> k_rewards = matrix<class="type">class="kw">float>::Zeros(k,rew_size);
  matrix<class="type">class="kw">float> k_embeding = matrix<class="type">class="kw">float>::Zeros(k + class="num">1,size);
  matrix<class="type">class="kw">float> U,V;
  vector<class="type">class="kw">float> S;
  for(class="type">class="kw">ulong i = class="num">0; i < size; i++)
    distance+=MathPow(state_embedding.Col(i) - embedding[i],class="num">2.0f);
  distance = MathSqrt(distance);
  for(class="type">class="kw">ulong i = class="num">0; i < k; i++)
   {
    class="type">class="kw">ulong pos = distance.ArgMin();
    k_rewards.Row(rewards.Row(pos) * (class="num">1 - MathLog(distance[pos] + class="num">1)),i);
    k_embeding.Row(state_embedding.Row(pos),i);
    distance[pos] = FLT_MAX;
   }
  k_embeding.Row(embedding,k);
  k_embeding.SVD(U,V,S);
  vector<class="type">class="kw">float> result = k_rewards.Mean(class="num">0);

◍ 奖励序列末端的归一与熵值回填

这段收尾逻辑把倒数第二个奖励项做了尺度压缩:用状态求和除以嵌入向量二范数与窗口长度的最大值之乘积的平方根,避免不同步长下量纲漂移。 具体看,k_embeding 先平方再求和得到二范数平方,MathMax(k+1,size) 兜住最小有效窗口,二者相乘开根后做分母,rew_size-2 位置拿到的是无量纲比值,横向对比多品种时更稳。 最后一个位置直接塞入 Actor 的潜状态熵 EntropyLatentState(Actor),把策略不确定性显式写进奖励尾端;外汇与贵金属波动跳变频繁,该熵值偏高往往意味着策略处于探索区,实盘前建议在 MT5 用历史Tick回放观察其分布再决定权重。

MQL5 / C++
  result[rew_size - class="num">2] = S.Sum() / (MathSqrt(MathPow(k_embeding,class="num">2.0f).Sum() * MathMax(k + class="num">1,size)));
  result[rew_size - class="num">1] = EntropyLatentState(Actor);
class=class="str">"cmt">//---
  class="kw">return (result);
  }

「EURUSD H1 上跑通的训练与测试账」

把随机边际扮演者-评论者方法并进 NNM 算法 EA 后,直接进测试环节。模型统一在 EURUSD H1 训练与验证,指标全用默认参数,训练期拉到 2023 年 7 个月,用当年 8 月历史数据做样本外测试。 训练沿用上一篇思路:不整体压缩经验回放缓冲区,改为逐步填充。首轮启动数据收集 EA 跑 100 次验算,在设定历史区间内攒出近 360K 个状态;之后每轮补 50 次验算的新状态,反复迭代直到扮演者政策达标。 样本外结果有可验证数字:策略测试器里训练后一个月产出 23.98% 盈利,共 263 笔操作,47% 胜率。单笔最大盈利接近最大亏损的 3 倍,平均盈利比平均亏损高 44%,盈利因子 1.28,余额曲线明显向上。外汇与贵金属这类品种杠杆高、回撤风险大,上述为历史样本表现,实盘迁移效果可能打折,须自行在 MT5 重跑验证。

潜变量政策落地的坑与边界

把随机边际扮演者-评论者(SMAC)塞进 NNM 架构后,训练出的扮演者政策在测试里跑出过月度最高 24% 的回测回报。这个数字只代表方法在演示环境里的可行性,不代表任何实盘期望。 潜变量让政策能显式建模观测与奖励里的随机性,表达力比普通确定性政策强,但带潜变量的政策训练本身难收敛,原作者给的边际化方案只是把坑填了一半。 必须点明:文中所有程序仅为方法演示,未接真实报价和风控,直接上真实账户做外汇或贵金属大概率爆仓。这类品种杠杆高、跳空频繁,高风险完全由操作者自担。 低成本改进探索的思路可以接着试——把潜在状态边际化当正则项挂进你已有的 Actor 网络,先在小周期 EURUSD 上跑离线回测看熵是否真的抬起来。

◍ 把工具请下神坛

这套 LSTM 优化方案落地到 MT5,实际由 6 个文件拼成:Research.mq5 负责采样、Study.mq5 跑智能体训练、Test.mq5 做模型推演,外加 Trajectory.mqh 定义状态、NeuroNet.mqh 与 NeuroNet.cl 提供神经网络和 OpenCL 底层。ZIP 包 530.73 KB,直接丢进 MT5 的 MQL5 目录就能编译,不用自己造轮子。 作者与 Chris 在评论区撕过一个点:Test.mq5 在 OnInit 第 99 行加载训练好的模型,但连续跑几次结果差异很大。根因在 Actor 用了 VAE——层 CNeuronVAEOCL 把前层输出当高斯分布的均值和 STD,再采样动作;初始权重随机,STD 不趋零前动作就是随机的,所以没训练也会飘。 下面这段是 Actor 网络尾部的两层定义,第 10 层铺 2*NActions 个基元神经元做 SIGMOID 激活,第 11 层接 VAE 输出 NActions 维动作,优化器都用 ADAM。 外汇与贵金属自带高杠杆风险,这类神经网络 EA 只是概率优势,不是印钞机。跑通之后你会明白:它不过是把人工盯盘的经验,压缩成可复用的几行描述文件——工具归工具,盘还是你自己盯。

MQL5 / C++
class=class="str">"cmt">//--- 第 class="num">10 层
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronBaseOCL;
   descr.count = class="num">2 * NActions;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return class="kw">false;
     }
class=class="str">"cmt">//--- 第 class="num">11 层
   if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
   descr.type = defNeuronVAEOCL;
   descr.count = NActions;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return class="kw">false;
     }

常见问题

通常在主网络更新若干步后软同步一次,比如每 100 步把目标网权重按 0.01 比例靠拢主网,硬同步则每 500 步整体拷贝,具体看回测波动。
末端不回填熵值会让后期策略过早收敛到低探索区,H1 回测中测试账可能少抓 10%~20% 的边界行情,建议末端按窗口均值归一后补熵。
可以,小布能载入你的 EURUSD H1 训练与测试账,标出潜变量政策落地的异常步和熵回填缺失点,省去手动翻日志。
最常见是潜变量方差塌缩导致动作采样退化,可在损失里加 KL 惩罚并把初始方差设高一点,H1 上用 0.5 起步较稳。
外汇贵金属高风险,训练账好看不等于 live 稳,先用迷你仓验证 KNN 奖励估计的偏移,再逐步放大,别直接全仓跟信号。