神经网络变得简单(第 94 部分):优化输入序列·进阶篇
📘

神经网络变得简单(第 94 部分):优化输入序列·进阶篇

第 2/3 篇

把 S3 层塞进 H1 编码器

新写的 CNeuronS3 类在 feedForward 里先准备分段优先级和乱序权重,初始数据是固定单位向量,不随输入变,只在学习的参数更新时才需要重算。梯度分派只在学习模式调用,把误差传给分段乱序层后,固定层那一级不用再往后传,只做可能的激活校正。调参就是调嵌套对象的更新方法,没什么弯弯绕。 模型架构上,我们把这个类接在源数据层后面,测试模型吃 H1 周期最后 120 根柱线,每根 9 个参数。连续叠了 3 个乱序层:第一层用 12 小时分段,第二层缩到 4 小时,第三层逐根乱序。后面的编码器和之前文章一致,交互训练程序没动。 内核里 FeedForwardS3 按概率排分段序号,再用两个权重 w1、w2 做输入加权和。注意 segments*window>total 时会少算一个分段,避免越界。外汇和贵金属行情高波动,这类结构只是特征重组,不预示方向。

MQL5 / C++
__kernel <span class="keyword">class="type">void</span> FeedForwardS3(__global <span class="keyword">class="type">float</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;__global <span class="keyword">class="type">float</span>* probability,
&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>* weights,
&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>* outputs,
&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>* positions,
&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">const</span> <span class="keyword">class="type">int</span> window,
&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">const</span> <span class="keyword">class="type">int</span> total
&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> pos = get_global_id(<span class="number">class="num">0</span>);
&nbsp;&nbsp; <span class="keyword">class="type">int</span> segments = get_global_size(<span class="number">class="num">0</span>);
&nbsp;&nbsp; <span class="keyword">if</span>((segments * window) &gt; total)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;segments--;
&nbsp;&nbsp; <span class="keyword">class="type">int</span> segment = <span class="number">class="num">0</span>;
&nbsp;&nbsp; <span class="keyword">if</span>(pos &lt; segments)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">const</span> <span class="keyword">class="type">float</span> prob = probability[pos];
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i &lt; pos; i++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(probability[i] &lt;= prob)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;segment++;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = pos + <span class="number">class="num">1</span>; i &lt; segments; i++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(probability[i] &lt; prob)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;segment++;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="keyword">else</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;segment = pos;
&nbsp;&nbsp; <span class="keyword">const</span> <span class="keyword">class="type">int</span> shift_in = segment * window;
&nbsp;&nbsp; <span class="keyword">const</span> <span class="keyword">class="type">int</span> shift_out = pos * window;
&nbsp;&nbsp; positions[pos] = (<span class="keyword">class="type">float</span>)segment;
&nbsp;&nbsp; <span class="keyword">const</span> <span class="keyword">class="type">float</span> w1 = weights[<span class="number">class="num">0</span>];
&nbsp;&nbsp; <span class="keyword">const</span> <span class="keyword">class="type">float</span> w2 = weights[<span class="number">class="num">1</span>];
&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i &lt; window; i++)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>((shift_in + i) &gt;= total || (shift_out + i) &gt;= total)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="kw">break</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;outputs[shift_out + i] = w1 * inputs[shift_in + i] + w2 * inputs[shift_out + i];
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;}
__kernel <span class="keyword">class="type">void</span> InsideGradientS3(__global <span class="keyword">class="type">float</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; __global <span class="keyword">class="type">float</span>* inputs_gr,

◍ 反向传播里的段偏移与权重梯度

这段 OpenCL 内核处理的是神经网络反向传播中对输入段与权重求梯度的并行逻辑。ProbabilityGradientS3 里每个线程先通过 get_global_id(0) 拿到样本序号,再用 positions[pos] 定位该样本属于哪个输入段,segment*window 就是段内偏移起点。 循环内做了两件事:一是累加 outputs_gr 经 w1 缩放后与 inputs 的乘积得到 grad;二是把 outputs_gr 经 w2 缩放的结果写回 inputs_gr,实现输入侧梯度回流。最后 probability_gr[segment] = grad / prob 完成段概率梯度归一,prob 若为 0 会触发除零,实盘跑前务必确认前向概率无零值。 WeightGradientS3 改用 local memory 做归约,temp[LOCAL_ARRAY_SIZE] 是核内共享缓冲,线程按 get_local_size(0) 步长遍历 total 个样本。外汇与贵金属行情高频跳变,这类 GPU 核在 EURUSD 的 M1 回测中单帧 10 万样本可能压到毫秒级,但显存越界或 local 数组超限会直接崩终端,建议先在策略测试器用小窗口验证。

MQL5 / C++
__kernel class="type">void ProbabilityGradientS3(__global class="type">float* inputs,
 __global class="type">float* inputs_gr,
 __global class="type">float* probability,
 __global class="type">float* probability_gr,
 __global class="type">float* weights,
 __global class="type">float* outputs_gr,
 __global class="type">float* positions,
 const class="type">int window,
 const class="type">int total
 )
{
 class="type">size_t pos = get_global_id(class="num">0);
 class="type">int segment = (class="type">int)positions[pos];
 class="type">float prob = probability[pos];
 const class="type">float w1 = weights[class="num">0];
 const class="type">float w2 = weights[class="num">1];
 const class="type">int shift_in = segment * window;
 const class="type">int shift_out = pos * window;
 class="type">float grad = class="num">0;
 class="type">float temp = class="num">0;
 for(class="type">int i = class="num">0; i < window; i++)
  {
   if((shift_out + i) >= total)
     class="kw">break;
   temp = outputs_gr[shift_out + i] * w1;
   grad += temp * inputs[shift_in + i];
   inputs_gr[shift_in + i] = temp + outputs_gr[shift_in + i] * w2;
  }
 probability_gr[segment] = grad / prob;
}
__kernel class="type">void WeightGradientS3(__global class="type">float *inputs,
 __global class="type">float *positions,
 __global class="type">float *outputs_gr,
 __global class="type">float *weights_gr,
 const class="type">int window,
 const class="type">int total
 )
{
 class="type">size_t l = get_local_id(class="num">0);
 class="type">size_t w = get_global_id(class="num">1);
 __local class="type">float temp[LOCAL_ARRAY_SIZE];
 class="type">size_t ls = min((class="type">uint)get_local_size(class="num">0), (class="type">uint)LOCAL_ARRAY_SIZE);
 if(l < ls)
  {
   class="type">float val = class="num">0;
   class=class="str">"cmt">//---
   for(class="type">int i = l; i < total; i += ls)
    {
     class="type">int shift_in = i;

「S3神经元的GPU归约与类骨架」

上面这段 OpenCL 内核片段干了两件事:先把卷积输出按窗口位置加权累加到局部内存 temp 数组,再用 do-while 循环做并行归约。循环里 t 从 ls 起步,每轮 t=(t+1)/2,当 l<t 且 l+t<ls 时把 temp[l+t] 加到 temp[l] 并清零,配合 barrier(CLK_LOCAL_MEM_FENCE) 保证线程同步,最终 l==0 的线程把 temp[0] 写回 weights_gr[w]。 归约的步长设计意味着局部数组长度 ls 最好是 2 的幂,否则最后几轮会出现 (l+t)>=ls 的线程空转,浪费一部分 GPU 算力。在 MT5 的 OpenCL 层跑这类算子时,窗口 window 与 segments 的取值会直接决定 ls 大小,调参时建议先用 64、128 这类值验证耗时。 CNeuronS3 类则把这套逻辑封装成标准神经元:保护成员里 iWindow、iSegments 控制分块,cShufle/cWeights 是两个卷积子层,cProbability 做 softmax,cPositions 存分段偏移。对外暴露的 Type() 返回 defNeuronS3,Init() 接收 window、numNeurons、optimization_type 和 batch,说明该层支持小批量训练。 想验证实现细节,可直接在 MT5 的 MQL5 源文件里搜 CNeuronS3::feedForwardS3,对比内核里的 shift_in 计算公式与 cPositions 缓冲区写入是否一致;外汇与贵金属行情的高波动会让这类特征提取层梯度不稳定,实盘前务必用历史数据回测。

MQL5 / C++
if(w == class="num">0)
  {
    class="type">int pos = i / window;
    shift_in = positions[pos] * window + i % window;
  }
val += outputs_gr[i] * inputs[shift_in];
}
temp[l] = val;
 }
 barrier(CLK_LOCAL_MEM_FENCE);
 class="type">int t = ls;
 do
  {
   t = (t + class="num">1) / class="num">2;
   if(l < t && (l + t) < ls)
     {
      temp[l] += temp[l + t];
      temp[l + t] = class="num">0;
     }
   barrier(CLK_LOCAL_MEM_FENCE);
  }
 while(t > class="num">1);
 if(l == class="num">0)
   weights_gr[w] = temp[class="num">0];
}
class CNeuronS3   :  class="kw">public CNeuronBaseOCL
  {
class="kw">protected:
   class="type">uint            iWindow;
   class="type">uint            iSegments;
   class=class="str">"cmt">//---
   CNeuronBaseOCL     cOne;
   CNeuronConvOCL     cShufle;
   CNeuronSoftMaxOCL cProbability;
   CNeuronConvOCL     cWeights;
   CBufferFloat       cPositions;
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL);
   class="kw">virtual class="type">bool      feedForwardS3(CNeuronBaseOCL *NeuronOCL);
   class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL);
   class="kw">virtual class="type">bool      calcInputGradientsS3(CNeuronBaseOCL *NeuronOCL);
   class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);
class="kw">public:
                    CNeuronS3(class="type">void)  {};
                   ~CNeuronS3(class="type">void)  {};
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool      Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                          class="type">uint window, class="type">uint numNeurons, ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
   class=class="str">"cmt">//---
   class="kw">virtual class="type">int       Type(class="type">void)  const   { class="kw">return defNeuronS3;  }
   class=class="str">"cmt">//--- methods for working with files
   class="kw">virtual class="type">bool      Save(class="type">int const file_handle);
   class="kw">virtual class="type">bool      Load(class="type">int const file_handle);
   class="kw">virtual CLayerDescription* GetLayerInfo(class="type">void);
   class="kw">virtual class="type">bool      WeightsUpdate(CNeuronBaseOCL *source, class="type">float tau);
   class="kw">virtual class="type">void      SetOpenCL(COpenCLMy *obj);
   };
class="type">bool CNeuronS3::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                     class="type">uint window, class="type">uint numNeurons,
ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {

S3 神经元的初始化与前向链路

CNeuronS3 这个类把一层 OpenCL 卷积拆成了多个子算子:cOne 做单点卷积,cShufle 负责段重排,cProbability 输出分段概率,cWeights 用 SIGMOID 压权重。Init 里先按 window 和 numNeurons 算 iSegments = (numNeurons + window - 1) / window,当 window=1 时 iSegments 直接等于神经元数,这个整除上界在调参时容易漏看。 初始化阶段每个子对象失败都直接 return false,其中 cPositions 用 BufferInit(iSegments, 0) 后还必须 BufferCreate(OpenCL),漏掉创建步骤在 MT5 终端会报空内核对象。cShufle 和 cProbability 都显式 SetActivationFunction(None),说明这两级只做线性变换,非线性全压在 cWeights 的 SIGMOID 上。 feedForward 里有个 bTrain 开关:训练态下先跑 cWeights→cShufle→cProbability 的前向,再调 feedForwardS3;推理态跳过前三步只跑 S3 主链路。反向的 calcInputGradients 先算 S3 梯度,再向 cShufle 回传,并对 cWeights 和传入 NeuronOCL 做 DeActivation——前提是它们 Activation() 不等于 None,否则跳过反激活。 updateInputWeights 只更新 cWeights 和 cShufle 面向 cOne 的入边,cProbability 没有可更新入权,说明它本质是无参重排后的映射头。外汇与贵金属市场波动剧烈、杠杆风险高,这类 GPU 算子若窗口或批次设错,回测与实盘偏差可能显著放大,上机前建议在 MT5 策略测试器单步跟一遍 Init 返回值。

MQL5 / C++
  if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, numNeurons, optimization_type, batch))
      class="kw">return false;
   iWindow = MathMax(window, class="num">1);
   iSegments = (numNeurons + window - class="num">1) / window;
   if(!cOne.Init(class="num">0, class="num">0, OpenCL, class="num">1, optimization, iBatch))
      class="kw">return false;
   CBufferFloat *buffer = cOne.getOutput();
   if(!buffer || !buffer.BufferInit(buffer.Total(),class="num">1))
      class="kw">return false;
   if(!buffer.BufferWrite())
     class="kw">return false;
   if(!cShufle.Init(class="num">0, class="num">1, OpenCL, class="num">1, class="num">1, iSegments, class="num">1, optimization, iBatch))
      class="kw">return false;
   cShufle.SetActivationFunction(None);
   if(!cProbability.Init(class="num">0, class="num">2, OpenCL, iSegments, optimization, iBatch))
      class="kw">return false;
   cProbability.SetActivationFunction(None);
   cProbability.SetHeads(class="num">1);
   if(!cWeights.Init(class="num">0, class="num">3, OpenCL, class="num">1, class="num">1, class="num">2, class="num">1, optimization, iBatch))
      class="kw">return false;
   cWeights.SetActivationFunction(SIGMOID);
   if(!cPositions.BufferInit(iSegments, class="num">0) || !cPositions.BufferCreate(OpenCL))
      class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronS3::feedForward(CNeuronBaseOCL *NeuronOCL)
   {
   if(bTrain)
     {
      if(!cWeights.FeedForward(cOne.AsObject()))
         class="kw">return false;
      if(!cShufle.FeedForward(cOne.AsObject()))
         class="kw">return false;
      if(!cProbability.FeedForward(cShufle.AsObject()))
         class="kw">return false;
     }
   if(!feedForwardS3(NeuronOCL))
      class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronS3::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
   {
   if(!calcInputGradientsS3(NeuronOCL))
      class="kw">return false;
   if(!cShufle.calcHiddenGradients(cProbability.AsObject()))
      class="kw">return false;
   if(cWeights.Activation() != None)
      if(!DeActivation(cWeights.getOutput(), cWeights.getGradient(), cWeights.getGradient(), cWeights.Activation()))
         class="kw">return false;
   if(NeuronOCL.Activation() != None)
      if(!DeActivation(NeuronOCL.getOutput(),NeuronOCL.getGradient(),NeuronOCL.getGradient(),NeuronOCL.Activation()))
         class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNeuronS3::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
   {
   if(!cWeights.UpdateInputWeights(cOne.AsObject()))
      class="kw">return false;
   if(!cShufle.UpdateInputWeights(cOne.AsObject()))
      class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return true;
   }

常见问题

先用小批量跑前向,观察 GPU 占用峰值;序列长度建议从 32 起按 8 的倍数递增,超过 128 大概率触发归约瓶颈。
会。段偏移错位会让梯度写到错误神经元,训练损失不降反升;核对偏移公式与张量维度对齐即可排查。
可以。小布能读取你的类骨架配置,比对初始化范围与前向维度,标出维度不匹配或归约缺失的位置。
在 4096 长度序列上,GPU 归约通常比 CPU 循环快 15–40 倍,但小规模序列因启动开销可能反而更慢。
编码器类结构倾向用标准差 0.02 左右的正态初始化,均匀易在浅层饱和;具体看反向传播梯度是否早期消失。