神经网络变得轻松(第五十一部分):行为-指引的扮演者-评论者(BAC)·综合运用
◍ 训练循环里怎么盯双评论网络误差
在 TD3 这类双评论家(twin critic)训练结构里,Critic1 与 Critic2 的权重需要分别向目标网络做软更新,代码里用 TargetCritic1.WeightsUpdate(GetPointer(Critic1), Tau) 与 TargetCritic2.WeightsUpdate(GetPointer(Critic2), Tau) 完成,Tau 控制更新步长,通常取远小于 1 的值。 每隔 500 毫秒(GetTickCount() - ticks > 500)把两个网络的近期平均误差刷到图表右上角 Comment,格式为百分比进度加 15.8f 精度误差值,方便肉眼判断收敛是否卡住。训练结束前用 PrintFormat 把 Critic1 / Critic2 的最终平均误差以 10.7f 精度打到日志,随后 ExpertRemove() 自卸载,避免 EA 继续吃资源。 外汇与贵金属行情噪声大,这类自编码+双评论家组合在 MT5 上回测误差下降可能偏慢,实盘前务必用历史数据跑通再上模拟盘。
PrintFormat("%s -> %d", __FUNCTION__, __LINE__); critic.TrainMode(true); class="kw">break; } critic.TrainMode(true); class=class="str">"cmt">//--- Autoencoder study Result.AssignArray(CriticResult); if(!Autoencoder.backProp(Result, critic, class="num">1)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); class="kw">break; } class=class="str">"cmt">//--- Update Target Nets TargetCritic1.WeightsUpdate(GetPointer(Critic1), Tau); TargetCritic2.WeightsUpdate(GetPointer(Critic2), Tau); class=class="str">"cmt">//--- 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">//--- }
「EURUSD H1 上跑通 BAC 模型的实测账」
模型先在 2023 年前 5 个月 EURUSD H1 上训练,指标全默认,本金 1 万美元。第一阶段用 Research.mq5 做 300 次随机验算,攒出 75 万组状态-行动-新状态-奖励样本;此时无预训练模型,每次验算都重建随机参数模型,结果天然发散,不卡盈利门槛。 随后 Study.mq5 跑 50 万次迭代做初始训练,扮演者策略的随机性仍很强,独立验算收益散布极宽。第二阶段切到优化模式再采 300 次,但只留回报≥0 的验算,实际仅进库 15–20 条;因为已用同一预训练模型,分散度只来自策略随机性。采完再训 50 万次,如此循环约 7 轮,样本库与训练交替刷新,直到无进展或达局部最小。 Trajectory.mqh 里加了 MaxReplayBuffer 常量,按验算条数而非文件大小封顶,满了删旧的。设备弱就调小它,别让样本文件拖垮回测机。约 7 轮后拿到训练期内能盈利的模型:5 个月净赚 16%,净值最大回撤 8.41%、余额回撤 6.68%,99 笔交易胜率 51.5%,平均盈利比平均亏损高 50% 以上,盈利系数 1.53。 更关键的是甩开训练集测 2023 年 6 月:月盈利略超 3%(折算训练样本月均约 3.2%),11 笔交易胜率降到 36.4%,但平均盈利是平均亏损的近 6 倍,盈利系数冲到 3.12。外汇与贵金属高杠杆品种,样本外仅 1 个月、11 笔,过拟合倾向仍不能排除,拿去 MT5 复跑前先控好仓位。
class="macro">#define MaxReplayBuffer class="num">500
从实现到实盘还差一层验证
前文用 MQL5 把这套替代软性 AC 的算法落了地,测试跑通说明实现路径本身没堵死,连续动作空间里随机和确定性模型都能训,模型结构也不受限。 但代码跑通和能扔进真实盘面完全是两件事。外汇和贵金属是高杠杆品种,这套 EA 目前只证明了「技术可行」,没经过样本外压力和滑点冲击,直接上实盘大概率会被微观结构教做人。 真要验证,先在 MT5 策略测试器里把历史区间切到训练集之外,再把点差和延迟参数按你经纪商真实值填进去跑一轮,看回测曲线是否塌方。塌了就别碰,不塌再谈下一步。
◍ 用深度强化学习重塑策略探索
传统策略梯度在外汇与贵金属高噪声行情里容易陷入局部最优,软性扮演者-评价者(SAC)引入最大熵框架,让策略在追求回报同时保留随机性,从而提升探索效率。该思路在 MT5 智脑信号模块中已可复现,但需警惕外汇/贵金属杠杆带来的高风险,回测盈利不预示实盘概率。 SAC 的模型优化版(第五十部分)进一步压缩了 Critic 估值误差,在 EURUSD 15 分钟样本上策略熵值平均提升约 18%,意味着 agent 更少重复同类无效动作。 这类行为导向的参与者-批评家结构,适合你拿 MT5 的 Python API 接一个轻量 RL 环境,先跑 3 个月 Tick 数据验证熵增是否伴随回撤收敛。
「随文附带的工程文件与网络搭建片段」
这套 LSTM 优化方案落地时附带了 6 个工程文件,直接决定了你能否在 MT5 里复现训练流程。Research.mq5、Study.mq5、Test.mq5 三个 EA 分别管样本收集、代理者训练和模型测试;Trajectory.mqh 定义系统状态结构,NeuroNet.mqh 与 NeuroNet.cl 则是神经网络类库和 OpenCL 内核,压缩包 MQL5.zip 体积约 2708.64 KB。 下面这段 CreateDescriptions 函数展示了演员网络前几层怎么用代码堆出来。输入层神经元数由 HistoryBars * BarDescr 决定,紧接一个 batch=1000 的批归一化层,再叠两组一维卷积(窗口 2→8、步长 1→8),激活全用 LReLU。 从调试打印能看出层数追踪靠 layerNumber 自增,每层先 new CLayerDescription 再设 type / count / activation / optimisation,任一层 Add 失败就 delete 并返回 false。把这套结构抄进你自己的 EA,最慢的瓶颈通常在 OpenCL 卷积层编译,建议先单卡跑通再上多设备。 外汇与贵金属行情受杠杆和跳空影响大,这类神经网络策略回测与实盘偏差可能显著,上真仓前务必用 Test.mq5 做样本外验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CreateDescriptions(CArrayObj *actor, CArrayObj *critic, CArrayObj *autoencoder) { class=class="str">"cmt">//--- 用于调试的智能打印语句 Print("Creating layer descriptions..."); CLayerDescription *descr; class="type">int layerNumber = class="num">0; class=class="str">"cmt">// 追踪层数 class=class="str">"cmt">//--- 如果输入数组为空,检查并初始化输入数组 if (!actor) { actor = new CArrayObj(); if (!actor) class="kw">return false; } if (!critic) { critic = new CArrayObj(); if (!critic) class="kw">return false; } if (!autoencoder) { autoencoder = new CArrayObj(); if (!autoencoder) class="kw">return false; } class=class="str">"cmt">//--- 演员 actor.Clear(); class=class="str">"cmt">//--- 第 class="num">1 层:输入层 layerNumber++; Print("Creating actor - Input Layer ", layerNumber, ": Input layer"); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; class="type">int prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimisation = ADAM; if (!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- 第 class="num">2 层 layerNumber++; Print("Creating actor - Batch Normalisation Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = class="num">1000; descr.activation = None; descr.optimisation = ADAM; if (!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- 第 class="num">3 层 layerNumber++; Print("Creating actor - Convolutional Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConvOCL; prev_count = descr.count = prev_count - class="num">1; descr.window = class="num">2; descr.step = class="num">1; descr.window_out = class="num">8; descr.activation = LReLU; descr.optimisation = ADAM; if (!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- 第 class="num">4 层 layerNumber++; Print("Creating actor - Convolutional Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConvOCL; prev_count = descr.count = prev_count; descr.window = class="num">8; descr.step = class="num">8; descr.window_out = class="num">8; descr.activation = LReLU; descr.optimisation = ADAM; if (!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- 第 class="num">5 层 layerNumber++; Print("Creating actor - Dense/Base Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = class="num">2048; descr.optimisation = ADAM; descr.activation = LReLU; if (!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- 第 class="num">6 层 layerNumber++; Print("Creating actor - Dense/Base Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = class="num">1024; descr.activation = LReLU; descr.optimisation = ADAM; if (!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- 第 class="num">7 层 layerNumber++; Print("Creating actor - SoftMax Layer ", layerNumber); if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronSoftMaxOCL; prev_count = descr.count = prev_count / class="num">16; descr.step = class="num">16; descr.optimisation = ADAM; descr.activation = None; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- 第 class="num">8 层多头关注层 layerNumber++; Print("Creating actor - Multilayer Multi-Head Attention Layer ", layerNumber); if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronMLMHAttentionOCL; descr.count = prev_count; descr.window = class="num">16; descr.window_out = class="num">8; descr.step = class="num">4; descr.layers = class="num">3; descr.optimisation = ADAM; descr.activation = None; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- 第 class="num">9 层 layerNumber++; Print("Creating actor - Concatenate Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConcatenate; descr.count = LatentCount; descr.window = prev_count; descr.step = AccountDescr; descr.optimisation = ADAM; descr.activation = SIGMOID; if (!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- 第 class="num">10 层 layerNumber++; Print("Creating actor - SoftMax Layer ", layerNumber); if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronSoftMaxOCL; prev_count = descr.count = prev_count / class="num">16; descr.step = class="num">16; descr.optimisation = ADAM; descr.activation = None; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- 第 class="num">11 层多头关注层 layerNumber++; Print("Creating actor - Multilayer Multi-Head Attention Layer ", layerNumber); if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronMLMHAttentionOCL; descr.count = prev_count; descr.window = class="num">16; descr.window_out = class="num">8; descr.step = class="num">4; descr.layers = class="num">3; descr.optimisation = ADAM; descr.activation = None; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- 第 class="num">12 层 layerNumber++; Print("Creating actor - Dense/Base Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = class="num">2048; descr.activation = LReLU; descr.optimisation = ADAM; if (!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- 第 class="num">13 层 layerNumber++; Print("Creating actor - Dense/Base Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = class="num">2048; descr.activation = LReLU; descr.optimisation = ADAM; if (!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- 第 class="num">14 层 layerNumber++; Print("Creating actor - Dense/Base Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = class="num">2 * NActions; descr.activation = LReLU; descr.optimisation = ADAM; if (!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- 第 class="num">15 层 layerNumber++; Print("Creating actor - VAE Output Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronVAEOCL;
把工具请下神坛
上面那段 C++ 风格的网络搭建代码,把 critic 堆到 5 层、autoencoder 镜像到 7 层,每层都用 ADAM 优化、LReLU 激活,看起来结构完整,但论坛里 2023 年 7 月的实测反馈很直白:有用户把 MinProfit 调到 -10000 仍进不了绿色区域,另有人在 50 万次迭代后测试曲线变成一条直线、交易根本不开。 这类基于行为 actor-critic 的模型不是接上数据就能出正收益的黑箱神器。初始参数随机时按理至少该有少数组合盈利,可多位严格照文章步骤训练的人连一次正结果都没稳定观察到,其中一个 critic 误差一度飙高再归零,说明训练动态本身就可能塌掉。 外汇与贵金属杠杆高、点差跳空频繁,拿这类未经验证的网络直接跑实盘属于高风险操作。开 MT5 把文中网络描述代码贴进 EA 跑 3~4 个「采集—训练—测试」周期,先盯 critic 误差是否异常衰减,比盲目信回测曲线更实在。
descr.count = NActions; descr.optimise = ADAM; if (!actor.Add(descr)) { Delete descr; class="kw">return false; } class=class="str">"cmt">//--- critic critic.Clear(); class=class="str">"cmt">//--- Layer class="num">1: class="kw">input layerNumber++; Print("Creating critic - Input Layer ", layerNumber, ": Input layer"); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = LatentCount; descr.activation = None; descr.optimisation = ADAM; if (!critic.Add(descr)) { Delete descr; class="kw">return false; } class=class="str">"cmt">//--- Layer class="num">2 layerNumber++; Print("Creating critic - Concatenate Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConcatenate; descr.count = LatentCount; descr.window = prev_count; descr.step = NActions; descr.optimisation = ADAM; descr.activation = LReLU; if (!critic.Add(descr)) { Delete descr; class="kw">return false; } class=class="str">"cmt">//--- Layer class="num">3 layerNumber++; Print("Creating critic - Dense/Base Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = LReLU; descr.optimisation = ADAM; if (!critic.Add(descr)) { Delete descr; class="kw">return false; } class=class="str">"cmt">//--- Layer class="num">4 layerNumber++; Print("Creating critic - Dense/Base Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = LReLU; descr.optimisation = ADAM; if (!critic.Add(descr)) { Delete descr; class="kw">return false; } class=class="str">"cmt">//--- Layer class="num">5 layerNumber++; Print("Creating critic - Dense/Base Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = class="num">1; descr.optimisation = ADAM; descr.activation = None; if (!critic.Add(descr)) { Delete descr; class="kw">return false; } class=class="str">"cmt">//--- autoencoder autoencoder.Clear(); class=class="str">"cmt">//--- Layer class="num">1: class="kw">input layerNumber++; Print("Creating autoencoder - Dense/Base Layer ", layerNumber, ": Input layer"); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = LatentCount; descr.activation = None; descr.optimisation = ADAM; if (!autoencoder.Add(descr)) { Delete descr; class="kw">return false; } class=class="str">"cmt">//--- Layer class="num">2 layerNumber++; Print("Creating autoencoder - Dense/Base Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = prev_count / class="num">2; descr.optimisation = ADAM; descr.activation = LReLU; if (!autoencoder.Add(descr)) { Delete descr; class="kw">return false; } class=class="str">"cmt">//--- Layer class="num">3 layerNumber++; Print("Creating autoencoder - Dense/Base Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = prev_count / class="num">2; descr.activation = LReLU; descr.optimisation = ADAM; if (!autoencoder.Add(descr)) { Delete descr; class="kw">return false; } class=class="str">"cmt">//--- Layer class="num">4 layerNumber++; Print("Creating autoencoder - Dense/Base Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = class="num">20; descr.count = LatentCount; descr.activation = LReLU; descr.optimisation = ADAM; if (!autoencoder.Add(descr)) { Delete descr; class="kw">return false; } class=class="str">"cmt">//--- Layer class="num">5 layerNumber++; Print("Creating autoencoder - Dense/Base Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; if (!(descr.Copy(autoencoder.At(class="num">2)))) { Delete descr; class="kw">return false; } if (!autoencoder.Add(descr)) { Delete descr; class="kw">return false; } class=class="str">"cmt">//--- Layer class="num">6 layerNumber++; Print("Creating autoencoder - Output Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; if (!(descr.Copy(autoencoder.At(class="num">1)))) { Delete descr; class="kw">return false; } if (!autoencoder.Add(descr)) { Delete descr; class="kw">return false; } class=class="str">"cmt">//--- Layer class="num">7 layerNumber++; Print("Creating autoencoder - Output Layer ", layerNumber); if (!(descr = new CLayerDescription())) class="kw">return false; if (!(descr.Copy(autoencoder.At(class="num">0)))) { Delete descr; class="kw">return false; } if (!autoencoder.Add(descr)) { Delete descr; class="kw">return false; } class=class="str">"cmt">//--- debug Print("Layer descriptions created successfully!"); class="kw">return true;