神经网络变得轻松(第五十部分):软性扮演者-评价者(模型优化)(基础篇)
◍ 把软性扮演者-评价者塞进 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 内核,匹配激活后的选定值来分布梯度,避免复用父类覆盖保存的梯度。 外汇与贵金属属高风险品种,任何模型探索机制都只影响概率,不保证盈利。
__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 模块仅作信号参考,实盘须严格风控。
__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 推理模块,延迟抖动可能放大滑点,属高风险操作。
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; }