神经网络变得轻松(第四十五部分):训练状态探索技能(基础篇)
◍ 给训练状态加一层可观测窗口
在 MQL5 里跑神经网络训练,最容易被忽略的不是模型结构,而是训练过程本身是否「看得见」。很多交易者直接把 EA 挂上就等结果,等到权益曲线走歪了才发现是学习率或批次设置早就在第 3 轮就发散了。 下面这段把训练状态通过 Comment 打到图表左上角,每轮 epoch 结束刷新一次损失与准确率。实盘前你至少能在策略测试器里复现:若 20 轮内 loss 不降反升,大概率是特征归一化没做对,而不是网络容量不够。 外汇与贵金属品种上做这类实验波动放大明显,小样本过拟合概率偏高,任何「训练好看」的曲线都只是概率倾向,不等于实盘能复现。
Comment("Epoch: ", epoch, "\nLoss: ", loss, "\nAccuracy: ", accuracy);把复杂任务拆成技能层来训
分层强化学习能啃下相当复杂的问题,核心做法是把总目标切成分层子任务,再逐一训练所谓“技能”,让代理在环境里尽可能多拿主动权。 之前跑过的 DIAYN 和 DADS 就是两条老路。DIAYN 专教行为多样性,逼代理穷举环境,但大量技能对当前任务根本没用;DADS 改成从“技能对环境动态的影响”来学,挑那些能让状态变化收益最大的技能。 这两种旧法有个硬伤:技能在训练时是作为输入、顺带做探索的,实际覆盖的状态空间很薄。结果就是练出来的技能,碰到没见过的环境状态就懵,互动效率掉链子。 本文要拆的 EDL(探索、发现和学习)换了个切口,不直接拿旧技能当输入凑探索,而是从发现未知状态入手,从而把状态覆盖的窟窿补上,代理行为也更柔性、更跟手。外汇与贵金属行情状态切换极快,这类覆盖不足的代理直接实盘等于高风险的裸奔。
「EDL 用变分自编码器倒推隐藏技能」
探索、发现和学习(EDL)算法 2020 年 8 月见诸论文,核心是把“先训技能再排调度”的路子反过来:先在毫无环境先验时铺一批均匀采样状态,覆盖尽可能多的环境行为,再用变分自动编码器(VAE)从状态样本里反推“状态—技能”的隐变量映射。 第二阶段不依赖任何教导员轨迹,VAE 输入输出都是环境状态,潜在空间里逼出当前状态对应的技能分布,解码器再做逆映射。把确定性的状态-技能对应变成概率分布,复杂随机环境里模型稳定性通常会更好。 最后阶段用强化学习训代理者,内部奖励结构脱胎于 DADS 但做了化简。DADS 原奖励含一项对所有技能都为常数的减数,优化时只留被减数即可,不损训练品质却降了计算量。奖励函数固定、不随策略行为漂移,这一点和早前方法不同,收敛更稳。 外汇与贵金属行情属高随机环境,直接搬 EDL 框架前建议在 MT5 用历史 tick 先跑通 VAE 潜变量收敛,再接 RL 代理,避免过拟合到样本内状态。
◍ EDL 模型在 MT5 里的搭建与训练链路
把探索-发现-学习(EDL)方法落到 MQL5,第一道关是模型架构描述。所有网络结构都写进 CreateDescriptions 函数,向它传两个动态数组指针,分别装调度器(scheduler)和代理者(actor)的层定义。注意 EDL 不建独立鉴别器——这个角色由调度器里变分自动编码器的解码器兼任。 调度器输入端吞下历史K线(HistoryBars * BarDescr)和账户状态(AccountDescr)拼接成的原始向量,先过批量归一化,再经卷积降维提取特征,随后三个全连接层逐级压缩。最后一层维度是技能数的 2 倍:变分自动编码器每个潜变量用均值和离散度两个参数表达,和传统自编码器不同。重参数化层按技能数抽样,解码器用三个无激活全连接层只复原账户状态,不还原行情——代理者对价格影响可忽略,但余额直接受策略牵动。 代理者输入层维度砍到技能数,因为只接自编码器潜状态,已含全部压缩信息,可省掉预处理模块。后面三个全连接层做决策,输出接完全参数化分位数函数,能细看奖励分布。这样调度器和代理者共用同一套架构描述,贯穿 EDL 全部阶段。 训练分三段 EA:Research.mq5 跑探索,给调度器喂历史、取潜状态送代理者决策;StudyModel.mq5 训调度器自编码器,目标不是还原输入而是预测下一根后账户状态,加三条限制(开仓最低利润、平仓最小波动、反向单全平后再开)塑形期望行为;StudyActor.mq5 训代理者,用自编码器解码器当判别器,奖励 = 预测状态与期望状态欧氏距离乘 -1,遍历所有可能动作填奖励向量,不减最小值归一化、故意不接 SoftMax,保住远离期望的惩罚结构。 外汇与贵金属杠杆高、跳空频繁,自编码器预测状态永远带近似误差;代理者最终表现上限取决于预测账户状态与真实交互状态的吻合度,实盘前务必用历史回放缓冲校验预测品质。
class="type">bool CreateDescriptions(CArrayObj *actor, CArrayObj *scheduler) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!actor) { actor = new CArrayObj(); if(!actor) class="kw">return class="kw">false; } class=class="str">"cmt">//--- if(!scheduler) { scheduler = new CArrayObj(); if(!scheduler) class="kw">return class="kw">false; } class=class="str">"cmt">//--- Scheduler scheduler.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; class="type">int prev_count = descr.count = (HistoryBars * BarDescr + AccountDescr); descr.window = class="num">0; descr.activation = None; descr.optimization = ADAM; if(!scheduler.Add(descr)) {
堆叠八层神经结构的描述符写法
在 MT5 的 OpenCL 神经调度器里,网络不是一口气建好的,而是一层一层往 scheduler 里 Add。每一层都先 new 一个 CLayerDescription,填完字段再提交,任何一步失败就 delete 并返 false,避免显存泄漏。 第一层是批归一化,type 设 defNeuronBatchNormOCL,count 沿用上层传进来的 prev_count,batch 写死 1000,激活函数 None,优化器 ADAM。这一层只做标准化,不引入非线性。 第二层卷积 defNeuronConvOCL 把 prev_count 减 1 作为输出通道数,窗口 2、步长 1、输出窗口 4,激活用 LReLU。第四层是同样配置的卷积,说明网络在中间插了两次「卷积+证明层」的对称结构。 第三、五层都是 defNeuronProofOCL,window 4、step 4,count 不变,作用偏向特征证明与降维。第六、七层退回全连接式 base 层,节点数分别 256(TANH)和 128(LReLU),都在 ADAM 下优化。 把这段代码直接贴进你的 EA 训练初始化函数,改 prev_count 初值就能控制前端通道规模;外汇与贵金属行情噪声大,这类深结构过拟合概率偏高,建议先在小样本上跑通再上实盘。
class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return class="kw">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 class="kw">false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return class="kw">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(!scheduler.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronProofOCL; prev_count = descr.count = prev_count; descr.window = class="num">4; descr.step = class="num">4; if(!scheduler.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return class="kw">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(!scheduler.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">5 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronProofOCL; prev_count = descr.count = prev_count; descr.window = class="num">4; descr.step = class="num">4; if(!scheduler.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">6 if(!(descr = new CLayerDescription())) class="kw">return class="kw">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 class="kw">false; } class=class="str">"cmt">//--- layer class="num">7 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; descr.count = class="num">128; descr.activation = LReLU; descr.optimization = ADAM; if(!scheduler.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">8