神经网络变得轻松(第三十八部分):凭借分歧进行自我监督探索(基础篇)
📘

神经网络变得轻松(第三十八部分):凭借分歧进行自我监督探索(基础篇)

第 1/3 篇

◍ 用分歧驱动的自监督探索思路

在 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 内核的骨架,演示二维问题空间怎么定位模型与神经元:

MQL5 / C++
__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 里改内核验证。

MQL5 / C++
__kernel <span class="keyword">class="type">void</span> FeedForwardMultiModels(__global <span class="keyword">class="type">float</span> *matrix_w,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;__global <span class="keyword">class="type">float</span> *matrix_i,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;__global <span class="keyword">class="type">float</span> *matrix_o,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span> inputs,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span> activation
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">class="type">int</span> i = get_global_id(<span class="number">class="num">0</span>);
&nbsp;&nbsp; <span class="keyword">class="type">int</span> outputs = get_global_size(<span class="number">class="num">0</span>);
&nbsp;&nbsp; <span class="keyword">class="type">int</span> m = get_global_id(<span class="number">class="num">1</span>);
&nbsp;&nbsp; <span class="keyword">class="type">int</span> models = get_global_size(<span class="number">class="num">1</span>);
&nbsp;&nbsp; <span class="keyword">class="type">float</span> sum = <span class="number">class="num">0</span>;
&nbsp;&nbsp; float4 inp, weight;
&nbsp;&nbsp; <span class="keyword">class="type">int</span> shift = (inputs + <span class="number">class="num">1</span>) * (i + outputs * m);
&nbsp;&nbsp; <span class="keyword">class="type">int</span> shift_in = inputs * m;
&nbsp;&nbsp; <span class="keyword">class="type">int</span> shift_out = outputs * m;
&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> k = <span class="number">class="num">0</span>; k &lt;= inputs; k = k + <span class="number">class="num">4</span>)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">switch</span>(inputs - k)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">case</span> <span class="number">class="num">0</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">break</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">case</span> <span class="number">class="num">1</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">break</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">case</span> <span class="number">class="num">2</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">break</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">case</span> <span class="number">class="num">3</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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>]);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">break</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <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 参数对比隐藏层输出分布,是验证自己网络结构是否合理的直接办法。外汇与贵金属杠杆高,内核算错一层梯度就可能让信号反转,实盘前务必在策略测试器跑满历史段。

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

常见问题

并行跑多个动态预测模型,把预测结果之间的分歧度当作内在奖励,分歧越大说明越陌生,驱动智能体往高分歧区域探索。
不用简单平均,用融汇模型并行输出的目标值做替换,保留各模型隔离的预测差异,避免早期就把分歧信号压成零。
可以,小布能按品种页把多模型预测分歧可视化,你只看高分歧时段对应的盘口变化,不用自己搭并行推理。
只要在梯度核里做多模型隔离的激活分支,各模型反向传播路径不共享,就不会串扰,隔离是硬性前提。
基础篇里靠模型分歧当内在奖励即可自驱,不需要外部标签;但环境本身仍需有可观测状态,纯噪声状态分歧无意义。