神经网络变得轻松(第五十一部分):行为-指引的扮演者-评论者(BAC)·综合运用
📘

神经网络变得轻松(第五十一部分):行为-指引的扮演者-评论者(BAC)·综合运用

第 3/3 篇

◍ 训练循环里怎么盯双评论网络误差

在 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 上回测误差下降可能偏慢,实盘前务必用历史数据跑通再上模拟盘。

MQL5 / C++
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 复跑前先控好仓位。

MQL5 / C++
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 做样本外验证。

MQL5 / C++
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 误差是否异常衰减,比盲目信回测曲线更实在。

MQL5 / C++
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;

常见问题

实测中两评论网络误差比值持续超过 1.15 或单步突增超 30% 即偏异常,建议暂停训练检查学习率。
需补walk-forward滚动样本外验证与滑点/点差压力测试,仅看历史回测账容易过拟合。
可以,小布能定时拉取训练日志并标注双评论误差背离,异常时直接推提醒,省去人工刷屏。
大概率有关,奖励尺度不一或稀疏会导致探索崩坏,建议先归一化奖励再做退火探索。
结构可复用,但隐藏层维度与输入特征要按 XAUUSD 15M 重调,直接套参数胜率会掉。