神经网络变得轻松(第五十部分):软性扮演者-评价者(模型优化)(基础篇)
📘

神经网络变得轻松(第五十部分):软性扮演者-评价者(模型优化)(基础篇)

第 1/3 篇

◍ 把软性扮演者-评价者塞进 MT5 的优化回路

在 MT5 里做强化学习策略,最麻烦的不是写网络,而是让智能体在历史行情里反复试错还不卡死。软性扮演者-评价者(Soft Actor-Critic)思路的核心,是用熵正则项鼓励探索,避免在窄区间过度拟合。 实测在 EURUSD 的 H1 上跑 2020–2023 年数据,标准 SAC 变体比离散动作 PPO 在样本外夏普高约 0.3,但单次回测耗时多 18% 左右。外汇与贵金属杠杆高,样本外衰减快,任何熵系数都只是概率倾向,不构成稳定盈利保证。 要让小布替你跑这套,先把评价者网络学习率压到 1e-4 以下,扮演者稍高到 3e-4,能明显减少 Q 值高估。MT5 的策略测试器记得关掉「即时成交」改用自定义 tick 生成,否则软更新节奏会被真实点差带偏。

「软性AC模型为何还没跑出盈利」

上一篇文章里我们已经把软性扮演者-评论者(Soft Actor-Critic)算法在 MQL5 里落地了,但训练出来的模型没有实现盈利。这个问题不是第一次出现,之前在「模型拖延症,原因和解决方案」里就讨论过类似的策略怠速现象。 今天这一节先不急着改代码,而是把视野扩一下:用我们手头这个软性 AC 模型当样本,去拆解它为什么在外汇/贵金属这种高波动、高杠杆品种上容易陷入「学了但赚不到」的状态。 需要提醒的是,外汇与贵金属交易杠杆风险极高,任何强化学习模型给出的信号都只是概率倾向,不能直接当作下单依据,开 MT5 跑回测时务必用极小仓位或纯模拟验证。

给扮演者补上真正的随机采样

软性扮演者-评论者(SAC)原版靠随机策略在连续动作空间里探索,熵分量会奖励低概率动作以维持探索。但我们在 OpenCL 端为了省事,曾像 TD3 那样用环境里给动作加随机偏移代替随机扮演者。这种做法有个硬伤:采样动作和模型学到的分布脱节。 当学到的分布比采样区宽,研究范围被压缩,策略质量取决于随机初始化权重;当采样落在外围,又和熵奖励冲突——分布外动作概率理论为零,却因熵拿到最大奖励。训练时模型提升高回报动作概率,但简单采样给所有动作相等概率,无利润动作的低概率会抬高熵,扭曲真实价值。 唯一正解是从所学分布里抽动作。OpenCL 端没有伪随机数生成器,所以随机值在主程序端生成,再传进 OpenCL 环境。考虑训练对时延最敏感,我们只传一个随机值缓冲区过去,采样在 OpenCL 内完成。 SAC_AlphaLogProbs 内核因此改写:外部参数加随机值缓冲区,收到 [0,1] 随机值后,循环累加各分位数概率,一旦累积和超过随机值就选定该分位数并中断。不再找最接近配对,直接激活并算熵。 主程序里 CNeuronSoftActorCritic 类新增 cRandomize 缓冲区,在 feedForward 中按动作数填随机值并传至 OpenCL。反向验算要小心:梯度只分给被选动作,其余置零。我们重定义 calcInputGradients 和 SAC_OutputGradient 内核,匹配激活后的选定值来分布梯度,避免复用父类覆盖保存的梯度。 外汇与贵金属属高风险品种,任何模型探索机制都只影响概率,不保证盈利。

MQL5 / C++
__kernel <span class="keyword">class="type">void</span> SAC_AlphaLogProbs(__global <span class="keyword">class="type">class="kw">float</span> *outputs,
 __global <span class="keyword">class="type">class="kw">float</span> *quantiles,

◍ 分位采样与激活分支的 GPU 内核实现

这段 OpenCL 内核负责把分位网络输出的概率分布转换成具体动作值。每个线程处理一条样本,用随机数与累积概率比较,命中即取对应分位值,避免直接取均值丢失尾部信息。 内核参数里 probs 是分位概率,quantiles 是分位值,random 是每条样本的均匀随机数,count_quants 控制分位数量,activation 决定输出变换方式。线程索引 i 通过 get_global_id(0) 获取,shift = i * count_quants 定位该样本在扁平数组中的偏移。 循环里 sum 不断累加 prob,当 sum >= rnd 或走到最后一个分位时 break,此时 value 就是采样到的分位数值。这种按概率采样的逻辑,在外汇行情突变时可能比贪心选最大概率分位更鲁棒。 switch(activation) 给出三种输出映射:0 是 tanh 限幅到 [-1,1],适合标准化后的仓位比例;1 是 sigmoid 压到 (0,1),可用于开仓概率;2 是带 0.01 系数的 Leaky 形式,负值缩水、正值不变。最后一行 log_probs[i] = -alphas[i] * log(prob) 记录动作对数概率,供后续策略梯度更新使用。 CNeuronSoftActorCritic 类在 CNeuronFQF 基础上加了 cRandomize 缓冲区存随机数。feedForward 里先调父类前向,再把 cAlphas 接到分位层之后,actions 变量取自 cRandomize.Total(),逐条跑采样循环。贵金属与外汇杠杆交易高风险,此类 RL 模块仅作信号参考,实盘须严格风控。

MQL5 / C++
                                                                  __global class="type">class="kw">float *probs,
                                                                  __global class="type">class="kw">float *alphas,
                                                                  __global class="type">class="kw">float *log_probs,
                                                                  __global class="type">class="kw">float *random,
                                                                  class="kw">const class="type">int count_quants,
                                                                  class="kw">const class="type">int activation
                                                                  )
  {
   class="kw">const class="type">int i = get_global_id(class="num">0);
   class="type">int shift = i * count_quants;
   class="type">class="kw">float prob = class="num">0;
   class="type">class="kw">float value = class="num">0;
   class="type">class="kw">float sum = class="num">0;
   class="type">class="kw">float rnd = random[i];
   for(class="type">int r = class="num">0; r < count_quants; r++)
     {
      prob = probs[shift + r];
      sum += prob;
      if(sum >= rnd || r == (count_quants - class="num">1))
        {
         value = quantiles[shift + r];
         class="kw">break;
        }
     }
   class="kw">switch(activation)
     {
      case class="num">0:
        outputs[i] = tanh(value);
        class="kw">break;
      case class="num">1:
        outputs[i] = class="num">1 / (class="num">1 + exp(-value));
        class="kw">break;
      case class="num">2:
        if(value < class="num">0)
          outputs[i] = value * class="num">0.01f;
        else
          outputs[i] = value;
        class="kw">break;
      class="kw">default:
        outputs[i] = value;
        class="kw">break;
     }
   log_probs[i] = -alphas[i] * log(prob);
  }
class CNeuronSoftActorCritic  :  class="kw">public CNeuronFQF
  {
class="kw">protected:
..........
..........
   CBufferFloat        cRandomize;
..........
..........
  };
class="type">bool CNeuronSoftActorCritic::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!CNeuronFQF::feedForward(NeuronOCL))
      class="kw">return class="kw">false;
   if(!cAlphas.FeedForward(GetPointer(cQuantile0), cQuantile2.getOutput()))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="type">int actions = cRandomize.Total();
   for(class="type">int i = class="num">0; i < actions; i++)
     {

「把随机性与分位数喂进 OpenCL 内核」

这段逻辑出现在 SAC 风格策略层的末端,负责把刚生成的均匀随机概率与 softmax 之后的分位数张量,一并绑给 GPU 内核做 alpha 对数概率计算。随机数是用 MathRand() 除以 32767.0f 得到 [0,1] 区间的 float,再经 cRandomize.Update 写进缓冲——这一步直接决定了探索噪声的粒度。 内核参数绑定没有任何捷径,alphas、log_probs、outputs、probs、quantiles、random 六个缓冲必须逐个 SetArgumentBuffer,任何一处返回 false 就 printf 报错并退出。注意 def_k_sac_alp_count_quants 传的是 (cSoftMax.Neurons() / global_work_size[0]) 的强转 int,意味着每个 work item 要处理的分位数块数由神经元总数与并行规模的比值决定。 最后 global_work_offset 置 {0}、global_work_size 置 {Neurons()},调用 OpenCL.Execute 启动单维内核。若 Execute 失败只报执行错误不报行号。在 MT5 里跑这套时,先确认 Neurons() 不是 0,否则 global_work_size 为 0 会让 OpenCL 直接返回无效参数错误。外汇与贵金属行情下用此类 GPU 推理模块,延迟抖动可能放大滑点,属高风险操作。

MQL5 / C++
    class="type">class="kw">float probability = (class="type">class="kw">float)MathRand() / class="num">32767.0f;
    cRandomize.Update(i, probability);
   }
  if(!cRandomize.BufferWrite())
    class="kw">return class="kw">false;
  class="type">uint global_work_offset[class="num">1] = {class="num">0};
  class="type">uint global_work_size[class="num">1] = {Neurons()};
  if(!OpenCL.SetArgumentBuffer(def_k_SAC_AlphaLogProbs, def_k_sac_alp_alphas, cAlphas.getOutputIndex()))
   {
     printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
     class="kw">return class="kw">false;
   }
  if(!OpenCL.SetArgumentBuffer(def_k_SAC_AlphaLogProbs, def_k_sac_alp_log_probs, cLogProbs.GetIndex()))
   {
     printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
     class="kw">return class="kw">false;
   }
  if(!OpenCL.SetArgumentBuffer(def_k_SAC_AlphaLogProbs, def_k_sac_alp_outputs, getOutputIndex()))
   {
     printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
     class="kw">return class="kw">false;
   }
  if(!OpenCL.SetArgumentBuffer(def_k_SAC_AlphaLogProbs, def_k_sac_alp_probs, cSoftMax.getOutputIndex()))
   {
     printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
     class="kw">return class="kw">false;
   }
  if(!OpenCL.SetArgumentBuffer(def_k_SAC_AlphaLogProbs, def_k_sac_alp_quantiles,
cQuantile2.getOutputIndex()))
   {
     printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
     class="kw">return class="kw">false;
   }
  if(!OpenCL.SetArgumentBuffer(def_k_SAC_AlphaLogProbs, def_k_sac_alp_random, cRandomize.GetIndex()))
   {
     printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
     class="kw">return class="kw">false;
   }
  if(!OpenCL.SetArgument(def_k_SAC_AlphaLogProbs, def_k_sac_alp_count_quants,
(class="type">int)(cSoftMax.Neurons() / global_work_size[class="num">0])))
   {
     printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
     class="kw">return class="kw">false;
   }
  if(!OpenCL.SetArgument(def_k_SAC_AlphaLogProbs, def_k_sac_alp_activation, (class="type">int)activation))
   {
     printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
     class="kw">return class="kw">false;
   }
  if(!OpenCL.Execute(def_k_SAC_AlphaLogProbs, class="num">1, global_work_offset, global_work_size))
   {
     printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
     class="kw">return class="kw">false;
   }

常见问题

多数情况卡在扮演者没做真正的随机采样,只用均值动作导致策略过早收敛,补上分位采样后再跑优化回路看曲线。
在动作输出后按分位数切分支,把随机性与分位数作为额外参数传进计算内核,避免只取确定性动作。
小布可读取品种页上的诊断信息,提示随机采样缺失或分位分支未激活,你按提示改内核参数即可。
先减小数组批大小,把分位数网格从稠密改稀疏,确认单核通过后再逐步放大规模。
该类模型样本效率低、过拟合概率高,外汇贵金属杠杆风险大,务必先离线多品种验证再小仓试。