神经网络变得轻松(第三十八部分):凭借分歧进行自我监督探索·综合运用
◍ 训练模式下给模型留点好奇心
这段逻辑出自一个把强化学习塞进 EA 的 C++ 类(CEVD),核心是在训练阶段给动作选择加不确定性扰动,避免策略过早收敛到局部最优。 先看主流程里的状态推进:前向网络跑完若失败直接释放 state 并返回 -1;随后用 AccountInfoDouble(ACCOUNT_BALANCE) 取账户余额,奖励 reward 定义为余额增量(首帧 dPrevBalance 为 0 时记 0)。动作范围被硬约束在 0~3,越界同样返回 -1,最后把 (inputVals, action, reward) 写入经验回放池 cReplay。 getAction 里才是“好奇心”的落点:训练模式且 state_size>0 时,把前向网络输出按状态数切分(forward.Reshape(forward.Cols()/state_size, state_size) 再 Hsplit),对各子矩阵求均值后算方差(MathPow(ensemble[i],2.0) 累加),把不确定性分数叠回原 model,再走 temp.AssignArray(model) 覆盖 softmax 前的 logits。最终返回 temp.Argmax(),也就是说探索倾向由模型离散度直接调制。 backProp 开头有个硬门槛:cReplay.Total() 小于 iMinBufferSize 或不在训练模式就直接 true 跳过,discount 默认 0.999000。外汇与贵金属杠杆高、滑点跳空频繁,这类自学习 EA 在实盘前务必用 MT5 策略测试器跑满回放缓冲再开训练,否则前几步梯度可能全是噪声。
if(!cForwardNet.feedForward(state, class="num">1, false)) { class="kw">delete state; class="kw">return -class="num">1; } class="type">class="kw">double balance = AccountInfoDouble(ACCOUNT_BALANCE); class="type">class="kw">double reward = (dPrevBalance == class="num">0 ? class="num">0 : balance - dPrevBalance); dPrevBalance = balance; action = getAction(state.Total()); class="kw">delete state; if(action < class="num">0 || action > class="num">3) class="kw">return -class="num">1; if(!cReplay.AddState(inputVals, action, reward)) class="kw">return -class="num">1; } else action = getAction(); class=class="str">"cmt">//--- class="kw">return action; } class="type">int CEVD::getAction(class="type">int state_size = class="num">0) { CBufferFloat *temp; class=class="str">"cmt">//--- get the result of the trained model. CNet::getResults(temp); if(!temp) class="kw">return -class="num">1; class=class="str">"cmt">//--- in training mode, make allowances for "curiosity" if(bTrainMode && state_size > class="num">0) { vector<class="type">float> model; matrix<class="type">float> forward; cForwardNet.getResults(model); forward.Init(class="num">1, model.Size()); forward.Row(model, class="num">0); temp.GetData(model); class=class="str">"cmt">//--- class="type">int actions = (class="type">int)model.Size(); forward.Reshape(forward.Cols() / state_size, state_size); matrix<class="type">float> ensemble[]; if(!forward.Hsplit(forward.Rows() / actions, ensemble)) class="kw">return -class="num">1; matrix<class="type">float> means = ensemble[class="num">0]; class="type">int total = ArraySize(ensemble); for(class="type">int i = class="num">1; i < total; i++) means += ensemble[i]; means = means / total; for(class="type">int i = class="num">0; i < total; i++) ensemble[i] -= means; means = MathPow(ensemble[class="num">0], class="num">2.0); for(class="type">int i = class="num">1 ; i < total; i++) means += MathPow(ensemble[i], class="num">2.0); model += means.Sum(class="num">1) / total; temp.AssignArray(model); } class=class="str">"cmt">//--- class="kw">return temp.Argmax(); } class="type">bool CEVD::backProp(class="type">int batch, class="type">float discount = class="num">0.999000f) { class=class="str">"cmt">//--- if(cReplay.Total() < (class="type">int)iMinBufferSize || !bTrainMode) class="kw">return true; class=class="str">"cmt">//--- CBufferFloat *state1, *state2, *targetVals = new CBufferFloat(); vector<class="type">float> target, actions, st1, st2, result; matrix<class="type">float> forward;
「经验回放里的双网训练循环」
这段训练循环跑在 batch 粒度上,每轮先从回放缓冲抽一条随机转移 (state1, action, reward, state2),再同时更新在线网络、目标网络和前向预测网络。 代码逐行看: for(int i=0;i<batch;i++) 控制训练批次,batch 就是单轮抽样的样本数。 if(!cReplay.GetRendomState(state1,action,reward,state2)) return false; 从回放池取随机样本,取不到直接退出。 if(!CNet::feedForward(state1,1,false)) return false; 用当前状态跑在线网络前向,getResults(target) 拿到输出。 if(!GetLayerOutput(iStateEmbedingLayer,state1)) return false; 卸出状态嵌入层输出。 if(!cTargetNet.feedForward(state2,1,false)) return false; 目标网对下一状态前向推理。 若开启目标网(bUseTargetNet),用 targetVals.Maximum() 乘折扣率 discount 加到 reward 上,这是标准 Q-learning 的 TD 目标构造:reward += discount * targetVals.Maximum(); 随后 target[action]=(float)reward 只改动作对应位。 前向网另算一路:用 state2 的嵌入做目标,按 ensemble = forward.Rows()/target.Size() 把目标状态铺进集成目标矩阵,再 backProp 更新 cForwardNet。 循环结束 delete state1/state2/targetVals 防内存泄漏,返回 true。外汇与贵金属波动剧烈、杠杆高风险,这类 RL 模块在 MT5 实盘前务必用历史 tick 回测验证收敛性。
class="type">class="kw">double reward; class="type">int action; class=class="str">"cmt">//--- training loop in the batch size for(class="type">int i = class="num">0; i < batch; i++) { class=class="str">"cmt">//--- get a random state and the buffer replay if(!cReplay.GetRendomState(state1, action, reward, state2)) class="kw">return false; class=class="str">"cmt">//--- feed forward pass of the training model("current" state) if(!CNet::feedForward(state1, class="num">1, false)) class="kw">return false; getResults(target); class=class="str">"cmt">//--- unload state embedding if(!GetLayerOutput(iStateEmbedingLayer, state1)) class="kw">return false; class=class="str">"cmt">//--- target net feed forward if(!cTargetNet.feedForward(state2, class="num">1, false)) class="kw">return false; class=class="str">"cmt">//--- reward adjustment if(bUseTargetNet) { cTargetNet.getResults(targetVals); reward += discount * targetVals.Maximum(); } target[action] = (class="type">float)reward; if(!targetVals.AssignArray(target)) class="kw">return false; class=class="str">"cmt">//--- backpropagation pass of the model being trained CNet::backProp(targetVals); class=class="str">"cmt">//--- forward net feed forward pass - next state prediction if(!cForwardNet.feedForward(state1, class="num">1, false)) class="kw">return false; class=class="str">"cmt">//--- download "future" state embedding if(!cTargetNet.GetLayerOutput(iStateEmbedingLayer, state2)) class="kw">return false; class=class="str">"cmt">//--- prepare targets for forward net cForwardNet.getResults(result); forward.Init(class="num">1, result.Size()); forward.Row(result, class="num">0); forward.Reshape(result.Size() / state2.Total(), state2.Total()); class="type">int ensemble = (class="type">int)(forward.Rows() / target.Size()); class=class="str">"cmt">//--- copy the target state to the ensemble goals matrix state2.GetData(st2); for(class="type">int r = class="num">0; r < ensemble; r++) forward.Row(st2, r * target.Size() + action); class=class="str">"cmt">//--- backpropagation pass of foward net targetVals.AssignArray(forward); cForwardNet.backProp(targetVals); } class=class="str">"cmt">//--- class="kw">delete state1; class="kw">delete state2; class="kw">delete targetVals; class=class="str">"cmt">//--- class="kw">return true; }
用多模型融汇替换好奇心模块跑实测
类和方法齐活之后,真正要验证的是换掉内在好奇心、改用分歧探索后模型还灵不灵。我们基于前篇的 EA 骨架改出 EVDRL-learning.mq5,不动训练主架构,只把模型类换成 CNeuronMultiModel 融汇,并删掉逆向模型描述、改正向模型结构。 原前向模型是单隐藏层感知器,现在改成「100 元素源数据层 + 1000 元素全连接隐藏层 + 多模型融汇层」的堆叠。源数据层 100 就是主模型压缩系统状态的大小,不拼动作向量,让模型自己吐全动作范围的预测。隐藏层 1000 实则是 5 个模型各分 200 神经元。 融汇层描述里几个硬参数:type 填 defNeuronMultiModels,count=400(每模型 100 元素描述 4 种动作状态),window=200 对应前层单模型神经元数,step=5 是融汇模型数,激活用 TANH 须与主模型嵌入层一致,优化只认 ADAM。 测试条件没动:EURUSD、H1、指标默认参数。现象上,训一组模型比单 Forward 更耗时,初期动作随机混乱,学习进程中随机性逐步下降。该模型在测试期有概率实现获利,外汇品种高杠杆下这仅是历史样本表现,不等于实盘倾向。 重复源数据进 OpenCL 关联内存很低效,每次拷贝多份系统状态会拖慢连接和显存传输;理想是让所有模型共享一个小状态副本,但当前前馈方法没留这个口子,先用全连接层当共享隐藏层顶着。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Includes | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "EVD.mqh" ........... ........... ........... ........... class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CEVD StudyNet; class="type">bool CreateDescriptions(CArrayObj *Description, CArrayObj *Forward) { class=class="str">"cmt">//--- ........... ........... class=class="str">"cmt">//--- if(!Forward) { Forward = new CArrayObj(); if(!Forward) class="kw">return false; } class=class="str">"cmt">//--- Model ........... ........... ........... ........... class=class="str">"cmt">//--- Forward Forward.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = class="num">100; descr.window = class="num">0; descr.activation = None; descr.optimization = ADAM; if(!Forward.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 = defNeuronBaseOCL; descr.count = class="num">1000; descr.activation = TANH; descr.optimization = ADAM; if(!Forward.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 = defNeuronMultiModels; descr.count = class="num">400; descr.window = class="num">200; descr.step = class="num">5; descr.activation = TANH; descr.optimization = ADAM; if(!Forward.Add(descr)) { class="kw">delete descr;
◍ 信号过滤的收口逻辑
上面这段收尾代码决定了指标信号最终是否对外放行。当任一前置条件不满足时,函数提前 return false,阻断后续绘制或报警;全部通过才 return true。 在 MT5 自定义指标里,这种「短路返回」写法能把无效 K 线挡在渲染之外,减少图表重绘开销。实盘加载后,若发现信号消失得比预期早,优先查这里的条件分支。 外汇与贵金属波动受杠杆放大,任何信号过滤都只是概率筛选,不等于方向锁定,请用小周期先验证再上实盘。
class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; }
「用分歧驱动探索的在线训练闭环」
训练强化模型时,从真实环境里持续学习仍是绕不开的坑。这篇给出的另一条路是“通过分歧进行探索”:代理边用策略优化攒交互数据,边在线更新自己的内部环境模型,每次和环境碰完就重算融汇,下一步的状态预测就能更贴近实际。 我们在 MT5 策略测试器里塞了真实 tick 数据跑这套,模型在测试窗口内产出了正收益;且从冷启动到测完耗时很短,说明朝这个方向的迭代成本可控。 不过测试样本有限,真要上实盘,得拿更长的历史段重训模型。外汇与贵金属杠杆高、滑点跳空频繁,纸面正收益不代表 live 环境能复现,先开 MT5 用自己品种回测一遍再谈下一步。
顺着分歧做自监督探索
把模型预测和环境真实反馈之间的落差当成信号,是近年 MQL5 社区里几篇「神经网络变得轻松」系列文章的核心思路。第三十五部分讲的内在好奇心模块(ICM),本质是用辅助网络预测下一状态,预测误差大就说明遇到了训练分布外的行情结构,这类样本值得优先回放。 第三十六部分的关系强化学习把多标的间的联动建模进奖励函数,第三十七部分的分散关注度则让多个注意力头各盯不同周期特征,降低策略对单一形态的过拟合。对外汇与贵金属这类高波动、高杠杆品种,这类自监督探索只能提高样本效率,不保证胜率,实盘前务必在 MT5 策略测试器用历史数据做walk-forward验证。
◍ 把这条线请下神坛
这套强化学习实验的载体就是随文附带的 MQL5.zip(206.95 KB),里面排了 EVDRL-learning.mq5 这个训练用 EA,以及 EVD.mqh、ICM.mqh、NeuroNet.mqh、NeuroNet.cl 这几层类库与 OpenCL 内核。想跑通,先把压缩包解进 MT5 的 MQL5 目录,编译 EA 再丢进策略测试器——有读者反馈新 EA 在导航器列表最底下,且测试器里不动,多半是库没替全或历史数据没加载。 外汇与贵金属市场高波动、高杠杆,这类 RL 智能系统只是概率型探索工具,不保证样本外稳定,实盘前请用历史回放反复验。 代码归作者所有、禁止整段转载,但你可以改参数、换分歧库自己试。线画得再神,也只是你验证假设的一根绳子,别供着。