神经网络变得轻松(第四十四部分):动态学习技能(基础篇)
◍ 给 EA 装上会进化的学习机制
在 MT5 里写 EA,多数人把参数写死,行情一变就失效。其实可以借助动态学习,让模型在运行时持续吸收新样本,而不是只在回测里训一次。 这套思路的核心,是把网络权重当作可变状态存进缓冲区,每根新 K 线闭合后做一次轻量更新。外汇与贵金属波动跳变频繁,这种机制能降低过拟合概率,但高频重训也可能放大噪声,属于高风险玩法。 实际落地时,建议先用 EURUSD 的 M15 跑 3 个月样本外观察权重漂移幅度,再决定更新频率。别把正态当圣经,样本外发散快说明市场结构已切换。
把难预测的技能拆成可预测的子任务
在外汇与贵金属这种高噪声随机环境里,直接训一个能泛化的总模型几乎不可能,把问题拆成子任务再分别建模,成功率会明显提升。之前我们用的 DIAYN 靠奖励不可预测行为来逼出多样技能,代价是技能本身难以预判,后续做状态规划和风控会很头疼。
- 年提出的 DADS(技能的动态感知探索)正好补这个洞:它要求技能既保持行为多样,又让模型能依据当前状态预测下一步动作。对 MT5 上做 AIGC 辅助盯盘的人来说,可预测性直接决定你敢不敢把信号接进实盘。
贵金属与外汇杠杆高、滑点突变频繁,任何技能信号都只是概率倾向,实盘前务必在策略测试器用历史数据跑通再上。
「DADS 怎么用两套模型换出可预测技能」
DADS 和 DIAYN 一样靠两个模型打配合:技能模型(代理者)负责出动作,鉴别器负责基于初始状态和所用技能预测下一状态。区别在鉴别器干的是反向活——DIAYN 从新状态反推技能,DADS 拿初始状态加技能向量预测未来状态,类似自动编码器但解码端输出的是下一步状态而非重建原数据。 训练是迭代式的,先喂一批经验回放缓冲里的数据训鉴别器,用梯度下降逼它预测准;之后训代理者时,奖励来自鉴别器输出的概率比:分子是当前技能预测出的状态 q(s'|s,z),分母是全部 L 个技能的平均预测状态。这公式逼着代理者既贴近可预测结果、又偏离统计均值,从而在可预测性和多样性之间拉平衡。 两个模型共用同一个回放缓冲,但每次迭代随机抽两批独立数据分别训,避免互相带偏。由于代理者被训过后行为会偏离缓冲里的旧样本,作者建议加重要性系数——当前策略动作概率除以缓冲中该动作概率,把权重向新策略靠、摊平随机动作噪声。 原文实测这套迭代要反复跑多轮才能收敛,MT5 上若想复现,先把经验回放容量设足,否则鉴别器初始近似太糙会拖慢整个训练。外汇/贵金属环境噪声大,这种无监督技能学习在高波动品种上过拟合风险偏高,验证时建议先用低频周期跑小技能数(如 L=4)看预测误差是否收敛。
◍ DADS 的三模型架构与训练 EA 改造
DADS 实现沿用 DIAYN 的三角色划分:代理者(技能模型)、鉴别器(动态模型)、调度器。代理者输入为当前状态向量拼接技能独热向量,输出动作空间概率分布;其架构保持不动,以便和 DIAYN 做同构架对照实验。代理者内部用批量常规化接两层卷积+子采样提取形态趋势,再进全连接层与分位数 FQF 决策模块,FQF 输出能给出环境随机性下的奖励分布而非单点均值。 鉴别器是唯一动刀的模块。因本实现不做多步前瞻规划,且散户对价格历史零影响,故内部奖励只需预测账户状态指标,不必预测价格历史。为加速训练,鉴别器改成多头输出:一次前向返回所有技能的预测状态。源数据层与调度器类似,只描述系统状态不含技能 ID;输出层大小 = 技能数 × 账户状态元素数,且因账户相对值不可激活,输出用无激活线性层。全部模型定义收进函数库 Trajectory.mqh 的 CreateDescriptions,避免 EA 间手工拷贝。 训练侧用 3 个 EA,Research.mq5 仅改文件名与架构常量;Study.mq5 改动最大。新增 GetNewState 方法:基于账户前态、计划动作、回放缓冲区的后续价格盈亏,计算出(非预测)账户新态,绕开策略测试器重放,可显著扩充训练集。GetAgentReward 则据鉴别器预测、所选技能、前账户态算内部奖励——它从所有技能预测中隔离当前技能向量,用 Reshape(非 Resize,否则丢数据)重排矩阵,再按持仓变化方向判定奖励动作(双向减仓判平仓,否则取变化最大方向,相等则判等待以激励主动),奖励取预测值除均值后的绝对值对数,或欧氏距离备选。 训练主循环按阶段顺序迭代:阶段0鉴别器、阶段1代理者、阶段2调度器,迭代次数与各阶段包大小均由 EA 外部参数控。鉴别器阶段对所有技能循环采样代理者动作并算目标态;代理者阶段对单状态全技能枚举以强化技能辨识;调度器阶段把完整概率分布(非采样非贪婪)送代理者,代理者贪婪选动作,外部奖励用账户余额相对变化。每轮结束打印进度。外汇/贵金属训练涉及真实点差与滑点,回测过拟合风险高,上 MT5 策略测试器跑 Study.mq5 前务必核对外部参数与 CreateDescriptions 一致性。
Actor 网络的层堆叠与参数落点
除 EA 调度与训练模型测试逻辑保持不动外,整套程序的完整代码都随附件提供,解压后放进 MT5 的 MQL5\Experts\DADS 目录即可直接编译运行。 下面这段是 Actor 部分的前几层定义,用 OpenCL 后端跑神经网络。输入层节点数由 HistoryBars * BarDescr + AccountDescr + NSkills 算出来,优化器统一用 ADAM,激活函数先留 None。 第一层接 BatchNorm,batch 设成 1000,这一步在训练时做批量归一,能压住梯度发散的可能。第二层开始用卷积(defNeuronConvOCL),窗口 2、步长 1、输出窗口 4,激活换 LReLU,节点数会比上一层少 1。 往后 Proof 层做窗口 4、步长 4 的下采样,再叠一层卷积和两层全连接(256 节点,分别用 TANH 和 LReLU)。外汇与贵金属杠杆高,这类模型在实盘只是概率倾向,不等于稳定胜率,上 MT5 前先开策略测试器跑历史样本。
class=class="str">"cmt">//--- Actor actor.Clear(); CLayerDescription *descr; class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; class="type">int prev_count = descr.count = (class="type">int)(HistoryBars * BarDescr + AccountDescr + NSkills); descr.window = class="num">0; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = class="num">1000; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2 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">4; descr.activation = LReLU; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronProofOCL; prev_count = descr.count = prev_count; descr.window = class="num">4; descr.step = class="num">4; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">4 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">4; descr.activation = LReLU; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">5 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = class="num">256; descr.optimization = ADAM; descr.activation = TANH; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">6 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = class="num">256; descr.activation = LReLU; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">7
「Actor与Scheduler的层定义落点」
把强化学习模型拆进 MT5 的 OpenCL 后端时,actor 与 scheduler 两张网必须各自独立堆层。下面这段直接给出两层网络的末端与输入侧定义,复制进 EA 的 CNet 初始化函数就能跑通编译。 actor 的第 7 层用 128 个基础神经元、LReLU 激活、ADAM 优化;第 8 层切到 FQF 分位数输出,window_out 写死 32,输出维度绑定 NActions。任何一层 Add 失败都要 delete descr 并 return false,否则显存对象会泄漏。 scheduler 的输入层神经元数由 (HistoryBars * BarDescr + AccountDescr) 动态算出,window 置 0 表示不吃时序滑窗;随后接一层 batch=1000 的批归一化,再叠两层 256 宽的全连接(TANH 与 LReLU 各一),末端用 FQF + SoftMax 双头给出 NSkills 维度的技能概率。外汇与贵金属行情噪声明,这类网络在 live 上过拟合概率偏高,上实盘前建议先跑两周 tick 回测。
if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = class="num">128; descr.activation = LReLU; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">8 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronFQF; descr.count = NActions; descr.window_out = class="num">32; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- Scheduler scheduler.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = (HistoryBars * BarDescr + AccountDescr); descr.window = class="num">0; descr.activation = None; descr.optimization = ADAM; if(!scheduler.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = class="num">1000; descr.activation = None; descr.optimization = ADAM; if(!scheduler.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = class="num">256; descr.optimization = ADAM; descr.activation = TANH; if(!scheduler.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = class="num">256; descr.optimization = ADAM; descr.activation = LReLU; if(!scheduler.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronFQF; descr.count = NSkills; descr.window_out = class="num">32; descr.optimization = ADAM; if(!scheduler.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">5 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronSoftMaxOCL; descr.count = NSkills; descr.step = class="num">1; descr.optimization = ADAM; if(!scheduler.Add(descr)) { class="kw">delete descr;