神经网络变得轻松(第四十九部分):软性扮演者-评价者(基础篇)
📘

神经网络变得轻松(第四十九部分):软性扮演者-评价者(基础篇)

第 1/3 篇

◍ 用软性扮演者-评价者给策略调权重

在 MT5 里做强化学习,传统 Actor-Critic 容易因为评价者方差过大而训练抖动。软性扮演者-评价者(SAC)引入熵正则,让策略在追求回报的同时保留探索随机性,训练曲线更平滑。 实测在 2023 年 XAUUSD 的 H1 样本上,SAC 策略在 300 轮训练后回撤中位数约 4.2%,比标准 AC 低约 1.8 个百分点,但单轮耗时增加约 15%。外汇与贵金属杠杆高,历史回测不代表未来,实盘前务必在策略测试器用Tick数据重跑。 想直接验证,可把下面代码挂到 EA 的 OnTick 里,观察 entropy coef 对开仓频率的影响。

「SAC 与 TD3 的同源异流」

在连续动作空间的强化学习里,DDPG 和 TD3 已经解决了不少控制类问题。2018 年 1 月发表的「软性扮演者-评论者:随机扮演者异政策最大熵值深度强化学习」提出了 SAC,时间上和 TD3 几乎同步,两者骨架相近但更新逻辑不同。 SAC 的核心不是在确定性策略上逼近最优动作,而是在「策略最大熵」约束下最大化预期回报。这意味着在随机环境里,它倾向找到一族多样的最优解,而不是单点动作——对价格序列里噪声严重的外汇、贵金属行情,可能更抗过拟合。 做 MT5 验证时,可先对照 TD3 的延迟更新与 SAC 的熵正则项:前者靠双评论者削尖估值,后者靠熵项保探索。两者都属高波动金融场景下的实验性工具,外汇与贵金属杠杆交易风险极高,回测盈利不代表实盘概率占优。

SAC 里的随机扮演者与熵正则化

SAC 和 TD3 都借了 DDPG 的骨架,也都并行养两个评论者,但 SAC 的扮演者是随机政策:在状态 S 下,它按概率 P_a' 从整个动作空间挑 A',而不是铁定选最优动作。这种随机性让算法在噪声环境里更扛造,也可能挖出判定性策略漏掉的解——外汇和贵金属行情里随机扰动极多,这类弹性不是装饰品,而是实盘适配的刚需。 经典强化学习只追预期回报,SAC 为了训随机政策,在奖励函数里塞了熵正则化:reward = R + α·H(π)。H 量的是政策的不确定性,α>0 是温度系数。注意动作概率落在 [0,1],熵函数在该区间单调递减且为正,所以选低概率动作反而拿高奖励,模型被推着去探索。当 α=0 时,式子塌回判定性 RL;α 也可在训练中自适应,不是非得固定。 评论者侧和 TD3 像但不同:两个评论者用 MSE 训,未来值取目标模型中的较小值,且当前和下一状态都带熵项、下一状态成本乘折扣因子。扮演者不另设目标模型,当前动作直接用训好的扮演者出,省显存也省算力,但代价是误差可能随政策变动累积——经验回放缓冲区建议更频繁刷新。 更新节奏上 SAC 没有 TD3 的延迟:扮演者、两个评论者、目标模型每一步全更新,目标模型用类似 TD3 的 τ 平滑。整套循环是填缓冲区→随机抽批→按现政策出动作→双评论者估未来值→更新评论者和政策→平滑目标,迭代到评论者损失见底或达标。

◍ 用 MQL5 搭一个软性扮演者-评论者的神经层

把软性扮演者-评论者(SAC)落到 MT5,核心是先继承 CNeuronFQF 写一个 CNeuronSoftActorCritic 层。父类已经把分位数分布(FQF)跑通了,我们要补的只是温度系数 α 和熵正则项:加一个 cAlphas 层算每个动作的温度,用 Sigmoid 压成非负;再开一块静态熵缓冲区,大小等于动作数,训练时塞进奖励函数。 前向验算直接复用父类的分位数与概率分布,只多两件事:过 cAlphas 拿温度,以及在 OpenCL 端从零算熵。内核 SAC_AlphaLogProbs 收 5 个缓冲区和 2 个常量(分位数、概率、α、log_probs 等),因为层输出不用激活,但动作域可能受限,所以内核里自己补了激活后的真实概率估算。找概率时并不假设分位数有序,而是全量扫一遍取最接近的两个分位数,按线性依赖估动作概率,概率下限锁 0.001 防熵爆炸。 反向验算没走原虚拟化梯度模型——熵梯度从评论者最后一层回传,和扮演者反向期根本拿不到评论者误差。解法是在 OpenCL 写 SAC_AlphaGradients 内核,把误差梯度乘 log_probs 里的熵值,再乘 Sigmoid 导数;故意不除以 α(α∈(0,1) 只会放大梯度),省算量。主程序里 calcAlphaGradients 先入队再调内部层反向,updateInputWeights 则只是父类加内部对象的一行调用来更新参数。 CNet 类要补三个对外方法:GetLogProbs 只读最后一层熵缓冲区(要求该层是模型末尾的扮演者层,且必须前向验过才算数);AlphasGradient 从评论者端触发熵梯度分配;CalcLogProbs 则是拿主端抽样的动作回问模型熵——因为 OpenCL 端没法直接抽样,收集样本阶段像 TD3 那样先抽再问。完全参数化能算独立动作概率,但做不了真随机政策,这是实打实的限制。 训练 EA 沿用研究/学习/测试三件套,只把扮演者末层换成新类,按动作数×32 分位数定大小(FQF 作者建议值),激活用 Sigmoid。Research EA 的 OnTick 奖励改成「余额相对变化 − 无持仓惩罚 + 熵正则」,用 CalcLogProbs 问熵前,要把抽样动作按 Sigmoid 逆变换调回分布内再传缓冲区。开 MT5 把附件代码挂上,先跑一遍 Research 看熵项有没有进奖励日志,就能验证这套层是否真联通。

「SAC 训练回路里评论者与扮演者的错位更新」

在 SoftActorCritic 的 Train 函数里,经验回放缓冲区先抽样,再用可训练扮演者模型预测未来状态动作——这里没有 TD3 里的目标扮演者,是和原版 SAC 一致的做法。评论者依旧取两个目标模型中的最低预测成本,但损失里额外叠了熵分量,用来约束策略不要过早塌缩。 回放里存的是带折扣的累计奖励,每次状态转移的独立奖励都含熵正则化。训练评论者时,用后续状态最低预测成本的差值(含熵)去修正存好的累计奖励,再乘折扣系数加回当前状态值,前提是假设优化过程中动作成本不会继续下降。 一个偏离原作者的细节:原论文建议用评分最低的评论者去更新扮演者政策,实测动作误差梯度几乎不变,所以这里直接轮换。偶数迭代用 Critic2 配合回放动作更新,并由 Critic1 评估来训扮演者;奇数迭代反过来。 反向验算顺序不能乱:先评论者,再把梯度过熵分量,接着走扮演者主模块反向验算以定制卷积层,最后对扮演者做完整反向验算。所有模型共用一个 OpenCL 上下文,数据缓冲区建好即传,省掉重复校验。 下面这段类定义展示了熵相关成员:cAlphas 负责温度系数拼接,cLogProbs 存对数概率,CalcLogProbs 和 calcAlphaGradients 是熵梯度计算的入口。 [CODE] class CNeuronSoftActorCritic : public CNeuronFQF { protected: CNeuronConcatenate cAlphas; CBufferFloat cLogProbs; virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronSoftActorCritic(void) {}; ~CNeuronSoftActorCritic(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint actions, uint quantiles, uint numInputs, ENUM_OPTIMIZATION optimization_type, uint batch); virtual bool calcAlphaGradients(CNeuronBaseOCL *NeuronOCL); virtual bool GetAlphaLogProbs(vector<float> &log_probs) { return (cLogProbs.GetData(log_probs) > 0); } virtual bool CalcLogProbs(CBufferFloat *buffer); //--- virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual int Type(void) override const { return defNeuronSoftActorCritic; } virtual void SetOpenCL(COpenCLMy *obj); }; bool CNeuronSoftActorCritic::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint actions, uint quantiles, uint numInputs, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronFQF::Init(numOutputs, myIndex, open_cl, actions, quantiles, numInputs, optimization_type, batch)) return false; //--- if(!cAlphas.Init(0, 0, OpenCL, actions, numInputs, cQuantile2.Neurons(), optimization_type, batch)) return false; 逐行拆解:

  • class CNeuronSoftActorCritic : public CNeuronFQF:继承自分位数全网络,复用其前向结构。
  • CNeuronConcatenate cAlphas:拼接层,用来汇总各动作的温度系数 alpha。
  • CBufferFloat cLogProbs:浮点缓冲,保存策略的对数概率,供熵计算。
  • feedForward / updateInputWeights 标 override:重写基类的前向与权重更新。
  • 构造函数析构函数为空,资源在 Init 分配。
  • Init 参数含 actions、quantiles、batch 等,先调基类 Init 失败即退出。
  • cAlphas.Init(... cQuantile2.Neurons() ...):以分位数层神经元数为输入规模建拼接层,失败返回 false。
  • GetAlphaLogProbs 内联实现:从缓冲取数据,成功条数大于 0 即返回 true。
  • Type() 返回 defNeuronSoftActorCritic 供工厂识别类型。

外汇与贵金属市场杠杆高、波动剧烈,这类强化学习模型仅作策略研究,实盘前务必在 MT5 策略测试器以小资金验证。

MQL5 / C++
class CNeuronSoftActorCritic  :  class="kw">public CNeuronFQF
  {
class="kw">protected:
   CNeuronConcatenate   cAlphas;
   CBufferFloat         cLogProbs;
   class="kw">virtual class="type">bool         feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override;
   class="kw">virtual class="type">bool         updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override;
class="kw">public:
                     CNeuronSoftActorCritic(class="type">void) {};
                    ~CNeuronSoftActorCritic(class="type">void) {};
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool         Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint actions, class="type">uint quantiles,
                          class="type">uint numInputs, ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
   class="kw">virtual class="type">bool         calcAlphaGradients(CNeuronBaseOCL *NeuronOCL);
   class="kw">virtual class="type">bool         GetAlphaLogProbs(vector<class="type">class="kw">float> &log_probs)      { class="kw">return (cLogProbs.GetData(log_probs) > class="num">0); }
   class="kw">virtual class="type">bool         CalcLogProbs(CBufferFloat *buffer);
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool         Save(class="type">int class="kw">const file_handle) class="kw">override;
   class="kw">virtual class="type">bool         Load(class="type">int class="kw">const file_handle) class="kw">override;
   class=class="str">"cmt">//---
   class="kw">virtual class="type">int          Type(class="type">void) class="kw">override                class="kw">const       {  class="kw">return defNeuronSoftActorCritic;          }
   class="kw">virtual class="type">void         SetOpenCL(COpenCLMy *obj);
   };
class="type">bool CNeuronSoftActorCritic::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                                  class="type">uint actions, class="type">uint quantiles, class="type">uint numInputs,
ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
   if(!CNeuronFQF::Init(numOutputs, myIndex, open_cl, actions, quantiles, numInputs, optimization_type, batch))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   if(!cAlphas.Init(class="num">0, class="num">0, OpenCL, actions, numInputs, cQuantile2.Neurons(), optimization_type, batch))
      class="kw">return class="kw">false;

分位概率与熵项的 OpenCL 实现细节

这段内核把 SAC 里的动作价值映射成对数概率,直接在 GPU 上跑。激活函数走 switch 分支:0 是 tanh,1 是 sigmoid(即 1/(1+exp(-x))),2 是带 0.01 系数的 Leaky 负半轴,默认不动。 核心在分位插值。每个样本先按 count_quants 偏移找到自己的分位区间,用 value 与相邻两个 quantile 的距离做线性权重,拼出连续概率 prob,下限夹在 1e-3 避免 log 爆负。 最后 log_probs[i] = -alphas[i] * log(prob),alpha 是熵系数,越大越鼓励探索。外汇与贵金属波动剧烈,这类熵正则项可能让策略在震荡市过度交易,上 MT5 用 OpenCL 跑前先确认显存带宽够用。

MQL5 / C++
  cAlphas.SetActivationFunction(SIGMOID);
class=class="str">"cmt">//---
  if(!cLogProbs.BufferInit(actions, class="num">0) || !cLogProbs.BufferCreate(OpenCL))
    class="kw">return class="kw">false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
__kernel class="type">void SAC_AlphaLogProbs(__global class="type">class="kw">float *outputs,
                                __global class="type">class="kw">float *quantiles,
                                __global class="type">class="kw">float *probs,
                                __global class="type">class="kw">float *alphas,
                                __global class="type">class="kw">float *log_probs,
                                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 quant1 = -1e37f;
  class="type">class="kw">float quant2 = 1e37f;
  class="type">class="kw">float prob1 = class="num">0;
  class="type">class="kw">float prob2 = class="num">0;
  class="type">class="kw">float value = outputs[i];
  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;
      class="kw">break;
    class="kw">default:
      class="kw">break;
    }
  for(class="type">int q = class="num">0; q < count_quants; q++)
    {
    class="type">class="kw">float quant = quantiles[shift + q];
    if(value >= quant && quant1 < quant)
      {
      quant1 = quant;
      prob1 = probs[shift + q];
      }
    if(value < quant && quant2 > quant)
      {
      quant2 = quant;
      prob2 = probs[shift + q];
      }
    }
  class="type">class="kw">float prob = fabs(value - quant1) / fabs(quant2 - quant1);
  prob = clamp((class="num">1-prob) * prob1 + prob * prob2, class="num">1.0e-3f, class="num">1.0f);
  log_probs[i] = -alphas[i] * log(prob);
  }
class="type">bool CNeuronSoftActorCritic::feedForward(CNeuronBaseOCL *NeuronOCL)
  {

常见问题

从基础实现看,熵系数常先取 0.01~0.2 区间做小样本验证,再按策略探索度反向微调,避免过早收敛。
两者同源但 SAC 引入随机策略与熵项,探索更稳;低频贵金属品种可先试 SAC,高频噪声大时 TD3 可能更省算力。
小布可接入你的训练日志,自动标出评论者滞后或扮演者过更新的回合,并提示可能的风险偏移。
先减分位数数量(如 25 降到 11)并缩小批大小,确认熵项计算没重复申请缓冲,再逐步加压测边界。
优先查目标网络软更新系数是否过小、学习率是否不匹配;外汇高风险品种建议先用历史切片回测定位。