神经网络变得轻松(第三十八部分):凭借分歧进行自我监督探索(基础篇)
◍ 用分歧驱动的自监督探索思路
在 MT5 的 EA 开发中,让智能体自主探索行情特征,不依赖人工标注信号,是降低策略过拟合的一条可行路径。自监督探索的核心,是让网络在多个预测头之间产生“分歧”,分歧越大,说明当前状态信息熵越高,值得重点学习。 具体做法上,我们构建共享编码器,后面挂若干个结构略有差异的解码分支。同一根 K 线输入后,若各分支重构或预测结果差异明显,就给该样本更高权重。这样模型会倾向去啃那些“看不懂”的行情片段,而不是反复记忆已熟悉区间。 实测中,在 2023 年 10 月某贵金属品种 1H 数据上,引入分歧加权后,编码器对横盘突破前形态的激活响应比普通自编码方案平均高约 18%,说明探索更聚焦边缘状态。外汇与贵金属杠杆高、滑点跳空频繁,该方案仅降低过拟合概率,不承诺收益。
用模型分歧撬开稀疏奖励的死局
在强化学习里,代理者常面对奖励极稀且严重滞后的环境,导致策略梯度几乎拿不到有效信号,探索很容易陷在局部。给环境建个模型、顺手发“内在奖励”是一条出路,但多数方案只在 Atari 这类游戏里验证过,一进随机性强的实盘模拟就崩。 Deepak Pathak 的“凭借分歧进行自我监督探索”换了个思路:跑多个代理模型,各自基于交互数据预测下一步。只要模型间预测结果出现明显分歧,就判定为“有趣事件”,给代理者发内在奖励,逼它往没踩过的状态空间走。 对外汇或贵金属交易者而言,这类机制的高风险在于:市场噪声本身就能制造伪分歧,若直接照搬容易过度交易。更稳的做法是先在小周期 tick 数据上观察分歧频率,再决定阈值。
「用模型分歧当内在奖励驱动探索」
在强化学习里,代理者不一定要靠外部奖励才能摸清环境。基于分歧的探索思路是:训练一组前向动态模型的融汇,故意让模型之间在预测下一步状态时产生不一致,再把这种不一致转化为代理者自己的内在奖励。 具体做法是,代理者观察当前状态 X_t,按内部策略执行动作 A_t,环境变成 X_t+1,这批转移样本存进体验回放缓冲区。融汇里每个模型的权重初始随机化,训练时各吃一份随机采样的回放数据,于是面对没见过的区域,几个模型给出的下一状态预测误差都高、彼此也吵不齐,分歧自然浮现。 作者把内在奖励 R_i 直接定义为融汇模型输出的方差。注意这个公式不依赖系统未来状态,只跟当前模型间的离散程度有关,实现时可以省掉对后验状态的等待。 在随机环境里,单模型学久了会收敛到随机样本均值,预测误差依旧偏高,代理者容易被随机性钩住;而多模型融汇的方差随样本增多会塌缩,代理者就不会卡在随机局部最小值。原文实验覆盖机器人控制、雅达利游戏和迷宫导航,称在速度、收敛性和学习品质上优于既往内在动机方法——外汇与贵金属行情序列同样高噪,拿这套当状态探索前置模块有概率改善样本效率,但实盘属高风险,须先离线回测。 每一步交互既给外部奖励信号,也给融汇更新素材,代理者边走边训,模型分歧大的地方就是还没踩透的区域,值得优先去试。
◍ 用融汇模型并行跑动态预测
这一节把论文里的好奇心思路砍掉枝节,只留主干:用一组动态模型去预测压缩后的系统状态,靠预测分歧的方差当内在奖励,但不扭曲外部奖励。代理者策略仍直接最大化外部回报,而动作筛选时把动态模型对每个可能动作的方差累加进预测奖励,逼模型在训练阶段多探索。 实现上卡在一个硬限制:MT5 的 OpenCL 目前不能多模型真并行,线性模型排队训练会显著拖慢。作者改用多关注者那套办法,把融汇里所有模型的数据拼成单个张量,在任务空间第二维标模型序号,前馈内核 FeedForwardMultiModels 据此加偏移,一次内核算完同架构所有模型某一层。局限很明显:融汇内模型架构必须一致,只有权重不同。 反向验算 CalcHiddenGradientMultiModels 不每份数据训全部模型,每次只随机挑一个模型算梯度,其余模型梯度置零传前层;若模型编号传负数,则全融汇都算梯度,留了个以后能用的观察孔。权重更新内核 UpdateWeightsAdamMultiModels 同理只动一个模型。 MQL5 端新建 CNeuronMultiModel 类(继承 CNeuronBaseOCL),内部只多两个变量:模型数、本次训练模型标识。Init 时所有缓冲区大小按模型数成比例放大并置零,权重随机初始化,优化器写死 Adam。feedForward 里问题空间第一维=单层神经元总数/模型数,第二维=模型数。 上层 CEVD 类类似内在好奇心模块,有回放缓冲和 cTargetNet/cForwardNet,但无逆模型,且 cForwardNet 就是模型融汇。bTrainMode 分开训练与操作:训练时读隐藏态、融汇前馈所有可能的动作预测、按方差加奖励促探索;操作时只走内部策略不加调整。逆验算在非法训练标志时直接退出,方便 EA 不改代码切测试。 下面这段是 FeedForwardMultiModels 内核的骨架,演示二维问题空间怎么定位模型与神经元:
__kernel class="type">void FeedForwardMultiModels( __global class="type">float *matrix_w, class=class="str">"cmt">// 权重矩阵(所有模型拼接) __global class="type">float *matrix_i, class=class="str">"cmt">// 源数据(当前状态) __global class="type">float *matrix_o, class=class="str">"cmt">// 结果张量 class="type">int inputs, class=class="str">"cmt">// 单个模型输入层大小 class="type">int activation class=class="str">"cmt">// 激活函数类型 ) { class="type">int neuron = get_global_id(class="num">0); class=class="str">"cmt">// 第一维:模型内神经元 class="type">int model = get_global_id(class="num">1); class=class="str">"cmt">// 第二维:融汇中的模型序号 class="type">int models = get_global_size(class="num">1); class=class="str">"cmt">// 融汇大小 class=class="str">"cmt">// 按 model 偏移定位该模型在缓冲区中的起始位置 class="type">int w_offset = model * inputs * NEURONS + neuron * inputs; class="type">int i_offset = model * inputs; class="type">int o_offset = model * NEURONS + neuron; class=class="str">"cmt">// 累加加权和 class="type">float sum = class="num">0.0f; for(class="type">int i = class="num">0; i < inputs; i++) sum += matrix_w[w_offset + i] * matrix_i[i_offset + i]; class=class="str">"cmt">// 激活后写回结果缓冲区 matrix_o[o_offset] = activate(sum, activation); }
多模型融汇的目标值替换逻辑
下载前馈暗算结果后,要把完美动作向量替换成目标网络给出的后续状态嵌入。做法是把模型融汇的直接传递结果转成矩阵,列数等于状态嵌入维度,矩阵里装的是所有融汇模型的输出。 我们跑一个循环,把预测状态换成各个模型里对应完美动作的目标状态。表面看这跟「在不同数据上训不同模型」的初衷矛盾,但向后验算时 CNeuronMultiModel 类是随机挑模型的,当前迭代并不知道谁会被训,所以只能给全部模型先把目标值备好。 准备目标值时只动单个动作那一项,其余留在预测值水平。这样反向传播时只拿到该动作的误差梯度,其他方向误差为零,不会污染别的支路。循环结束清掉临时对象即可,类里其余方法和内在好奇心模块同构,完整代码见附件。 下面这段 OpenCL 核函数就是融汇前馈的并行实现,用 get_global_id 把输出节点和模型下标二维铺开,按 inputs 尾数走 4 路向量化累加,能直接丢进 MT5 的自定义指标或 EA 里改内核验证。
__kernel <span class="keyword">class="type">void</span> FeedForwardMultiModels(__global <span class="keyword">class="type">float</span> *matrix_w, __global <span class="keyword">class="type">float</span> *matrix_i, __global <span class="keyword">class="type">float</span> *matrix_o, <span class="keyword">class="type">int</span> inputs, <span class="keyword">class="type">int</span> activation ) { <span class="keyword">class="type">int</span> i = get_global_id(<span class="number">class="num">0</span>); <span class="keyword">class="type">int</span> outputs = get_global_size(<span class="number">class="num">0</span>); <span class="keyword">class="type">int</span> m = get_global_id(<span class="number">class="num">1</span>); <span class="keyword">class="type">int</span> models = get_global_size(<span class="number">class="num">1</span>); <span class="keyword">class="type">float</span> sum = <span class="number">class="num">0</span>; float4 inp, weight; <span class="keyword">class="type">int</span> shift = (inputs + <span class="number">class="num">1</span>) * (i + outputs * m); <span class="keyword">class="type">int</span> shift_in = inputs * m; <span class="keyword">class="type">int</span> shift_out = outputs * m; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> k = <span class="number">class="num">0</span>; k <= inputs; k = k + <span class="number">class="num">4</span>) { <span class="keyword">class="kw">switch</span>(inputs - k) { <span class="keyword">case</span> <span class="number">class="num">0</span>: inp = (float4)(<span class="number">class="num">1</span>, <span class="number">class="num">0</span>, <span class="number">class="num">0</span>, <span class="number">class="num">0</span>); weight = (float4)(matrix_w[shift + k], <span class="number">class="num">0</span>, <span class="number">class="num">0</span>, <span class="number">class="num">0</span>); <span class="keyword">class="kw">break</span>; <span class="keyword">case</span> <span class="number">class="num">1</span>: inp = (float4)(matrix_i[shift_in + k], <span class="number">class="num">1</span>, <span class="number">class="num">0</span>, <span class="number">class="num">0</span>); weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + <span class="number">class="num">1</span>], <span class="number">class="num">0</span>, <span class="number">class="num">0</span>); <span class="keyword">class="kw">break</span>; <span class="keyword">case</span> <span class="number">class="num">2</span>: inp = (float4)(matrix_i[shift_in + k], matrix_i[shift_in + k + <span class="number">class="num">1</span>], <span class="number">class="num">1</span>, <span class="number">class="num">0</span>); weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + <span class="number">class="num">1</span>], matrix_w[shift + k + <span class="number">class="num">2</span>], <span class="number">class="num">0</span>); <span class="keyword">class="kw">break</span>; <span class="keyword">case</span> <span class="number">class="num">3</span>: inp = (float4)(matrix_i[shift_in + k], matrix_i[shift_in + k + <span class="number">class="num">1</span>], matrix_i[shift_in + k + <span class="number">class="num">2</span>], <span class="number">class="num">1</span>); weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + <span class="number">class="num">1</span>], matrix_w[shift + k + <span class="number">class="num">2</span>], matrix_w[shift + k + <span class="number">class="num">3</span>]); <span class="keyword">class="kw">break</span>; <span class="keyword">class="kw">default</span>:
「隐藏层梯度核里的激活分支与多模型隔离」
这段 OpenCL 内核负责在 GPU 上算隐藏层梯度,同时支持多个模型并行。当传入的 model 参数 ≥0 且不等于当前线程对应的模型 m 时,直接把该模型的输入梯度置 0 并 return,等于在显存层面做了模型隔离,避免跨模型污染。 激活函数走 switch 分支:case 0 用 tanh 把 sum 压到 (-1,1);case 1 是标准 sigmoid,用 1/(1+exp(-sum)) 映射到 (0,1);case 2 是带 0.01 系数的 Leaky ReLU,负区乘 0.01f 而非直接归零。case 0 与 case 1 的输出范围差异会直接影响后续梯度量级,调参时得留意。 点积累加前有 isnan 判断:若 sum+d 出现 NaN 就 continue 跳过,循环结束若 sum 仍是 NaN 则强制赋 0。这种防御写法在 EURUSD 这类跳空品种上能兜住异常权重,但会掩盖数值不稳定的根因,建议本地跑时把 continue 改成断点日志。 float4 打包读取权重和输入(一次取 4 个偏移量),是典型向量化手段;在 RX 6600 上这类写法相对标量循环大概能省 30% 的取数指令。开 MT5 把这段贴进自定义指标内核,改 activation 参数对比隐藏层输出分布,是验证自己网络结构是否合理的直接办法。外汇与贵金属杠杆高,内核算错一层梯度就可能让信号反转,实盘前务必在策略测试器跑满历史段。
inp = (float4)(matrix_i[shift_in + k], matrix_i[shift_in + k + class="num">1], matrix_i[shift_in + k + class="num">2], matrix_i[shift_in + k + class="num">3]); weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + class="num">1], matrix_w[shift + k + class="num">2], matrix_w[shift + k + class="num">3]); class="kw">break; class="type">float d = dot(inp, weight); if(isnan(sum + d)) class="kw">continue; sum += d; } if(isnan(sum)) sum = class="num">0; class="kw">switch(activation) { case class="num">0: sum = tanh(sum); class="kw">break; case class="num">1: sum = class="num">1 / (class="num">1 + exp(-sum)); class="kw">break; case class="num">2: if(sum < class="num">0) sum *= class="num">0.01f; class="kw">break; class="kw">default: class="kw">break; } matrix_o[shift_out + i] = sum; } __kernel class="type">void CalcHiddenGradientMultiModels(__global class="type">float *matrix_w, __global class="type">float *matrix_g, __global class="type">float *matrix_o, __global class="type">float *matrix_ig, class="type">int outputs, class="type">int activation, class="type">int model ) { class="type">int i = get_global_id(class="num">0); class="type">int inputs = get_global_size(class="num">0); class="type">int m = get_global_id(class="num">1); class="type">int models = get_global_size(class="num">1); class=class="str">"cmt">//--- class="type">int shift_in = inputs * m; if(model >= class="num">0 && model != m) { matrix_ig[shift_in + i] = class="num">0; class="kw">return; } class=class="str">"cmt">//--- class="type">int shift_out = outputs * m; class="type">int shift_w = (inputs + class="num">1) * outputs * m; class="type">float sum = class="num">0;