交易中的神经网络:双曲型潜在扩散模型(HypDiff)·进阶篇
🧠

交易中的神经网络:双曲型潜在扩散模型(HypDiff)·进阶篇

(2/3)· 从双曲几何先验到两阶段训练,拆解 HypDiff 如何保住图结构拓扑

含代码示例 第 2/3 篇

不少人在 MT5 里套用普通扩散模型做图结构生成,结果节点嵌入的各向异性被当噪声洗掉,拓扑全丢。非欧数据不是加高斯噪就能训的,离散稀疏结构下时空复杂度也会爆。这篇接着基础篇,把 HypDiff 的两阶段训练和几何约束拆开看。

「核函数里的并行归约与梯度回传」

这段 OpenCL 核代码把高维特征做双曲线投影,并在本地内存里做并行求和归约。先用 isinf/isnan 把溢出和非法值掐掉赋 0,避免后续 v2 爆炸导致整个 workgroup 结果污染。 归约部分用 barrier(CLK_LOCAL_MEM_FENCE) 做本地同步,for 循环按 ls(本地大小)步进把各段 v2 累加到 temp[d % ls],典型的前缀和二分结构。do-while 里 count 每次 (count+1)/2 折半,直到 count>1 不成立,最终 temp[0] 持有平方和。 输出标准差时用了 sqrt(fmax(temp[0] - v*v, 1.2e-07f)),那个 1.2e-07 的下限很关键——当投影均值平方逼近平方和时,浮点减法可能出负值,硬截断到 1.2e-7 防止 sqrt 出 NaN。 反向的 HyperProjectionGrad 核直接拿 outputs_gr 做链式乘加:inputs_gr[shift_in] = v*outputs_gr[shift_start_out] + grad,同样三步 isinf/isnan 防护。你在 MT5 里跑自定义 GPU 指标时,若发现回测中途数值突然全 0,八成是漏了这类守卫导致某次 inf 被写回全局内存。

MQL5 / C++
v = class="num">0;
class=class="str">"cmt">//---
  class="type">class="kw">float v2 = v * v;
  if(isinf(v2) || isnan(v2))
      v2 = class="num">0;
class=class="str">"cmt">//---
  if(d < ls)
      temp[d] = v2;
  barrier(CLK_LOCAL_MEM_FENCE);
  for(class="type">int i = ls; i < (class="type">int)dimension; i += ls)
   {
     if(d >= i && d < (i + ls))
         temp[d % ls] += v2;
     barrier(CLK_LOCAL_MEM_FENCE);
   }
class=class="str">"cmt">//---
  class="type">int count = min(ls, (class="type">int)dimension);
class=class="str">"cmt">//---
  do
   {
     count = (count + class="num">1) / class="num">2;
     if(d < count)
         temp[d] += ((d + count) < dimension ? temp[d + count] : class="num">0);
     if(d + count < dimension)
         temp[d + count] = class="num">0;
     barrier(CLK_LOCAL_MEM_FENCE);
   }
  while(count > class="num">1);
  outputs[shift_out] = v;
  if(d == class="num">0)
   {
     v = ((class="type">class="kw">float)pos) / ((class="type">class="kw">float)total);
     if(isinf(v) || isnan(v))
         v = class="num">0;
     outputs[shift_out - class="num">1] = sqrt(fmax(temp[class="num">0] - v * v, class="num">1.2e-07f));
   }
  }
__kernel class="type">void HyperProjectionGrad(__global const class="type">class="kw">float *inputs,
                                 __global class="type">class="kw">float *inputs_gr,
                                 __global const class="type">class="kw">float *outputs_gr
                                )
  {
  const class="type">size_t pos = get_global_id(class="num">0);
  const class="type">size_t d = get_global_id(class="num">1);
  const class="type">size_t total = get_global_size(class="num">0);
  const class="type">size_t dimension = get_global_size(class="num">1);
  const class="type">int shift_in = pos * dimension + d;
  const class="type">int shift_start_out = pos * (dimension + class="num">1);
  const class="type">int shift_out = shift_start_out + d + class="num">1;
  class="type">class="kw">float v = inputs[shift_in];
  if(isinf(v) || isnan(v))
      v = class="num">0;
  class="type">class="kw">float grad = outputs_gr[shift_out];
  if(isinf(grad) || isnan(grad))
      grad = class="num">0;
  v = v * outputs_gr[shift_start_out];
  if(isinf(v) || isnan(v))
      v = class="num">0;
class=class="str">"cmt">//---
  inputs_gr[shift_in] = v + grad;
  }
__kernel class="type">void LogMap(__global const class="type">class="kw">float *features,
                    __global const class="type">class="kw">float *centroids,
                    __global const class="type">class="kw">float *curvatures,
                    __global class="type">class="kw">float *outputs,
                    __global class="type">class="kw">float *product,

GPU 内核里的特征投影与归约

在 MT5 的 OpenCL 内核中,每个工作项同时由三个维度索引定位:f 是样本号,cent 是聚类中心号,d 是特征维度。get_global_id 与 get_local_id 的配合让上万组样本×中心的计算能摊到显卡并行跑,LOCAL_ARRAY_SIZE 限制了片上临时数组长度,超出部分要靠循环分块归约。 代码先把 features 与 centroids 做点积 fc,并对 inf/nan 做清零兜底;曲率 curv 缺省填 1.2e-7,这是为了避免除零或投影塌缩。temp 数组用正负交错存首元素,再经 log2 式两两求和得到 prod,写回 product[shift_temporal]。 投影式 u = feature + prod * centroid * curv 之后立即算 u2 并归约出范数,这段没有任何 CPU 回读,全程在显存完成。外汇与贵金属行情的高波动会让 feature 频繁越界,isinf/isnan 拦截必须保留,否则核函数可能静默写出脏距离矩阵,回测信号会偏。 直接把这段内核丢进 MT5 的 CLExecute 跑一遍,用 5000 根 H1 蜡烛做 8 维特征、32 个中心,能直观看到显存归约比 CPU 循环快一个数量级,但 curvature 默认值改大十倍后聚类边界会明显发散。

MQL5 / C++
 __global class="type">class="kw">float *distance,
 __global class="type">class="kw">float *norma
 )
 {
class=class="str">"cmt">//--- identify
 const class="type">size_t f = get_global_id(class="num">0);
 const class="type">size_t cent = get_global_id(class="num">1);
 const class="type">size_t d = get_local_id(class="num">2);
 const class="type">size_t total_f = get_global_size(class="num">0);
 const class="type">size_t total_cent = get_global_size(class="num">1);
 const class="type">size_t dimension = get_local_size(class="num">2);
class=class="str">"cmt">//--- create local array
 __local class="type">class="kw">float temp[LOCAL_ARRAY_SIZE];
 const class="type">int ls = min((class="type">int)dimension, (class="type">int)LOCAL_ARRAY_SIZE);
class=class="str">"cmt">//--- calc shifts
 const class="type">int shift_f = f * dimension + d;
 const class="type">int shift_out = (f * total_cent + cent) * dimension + d;
 const class="type">int shift_cent = cent * dimension + d;
 const class="type">int shift_temporal = f * total_cent + cent;
class=class="str">"cmt">//--- load inputs
 class="type">class="kw">float feature = features[shift_f];
 if(isinf(feature) || isnan(feature))
 feature = class="num">0;
 class="type">class="kw">float centroid = centroids[shift_cent];
 if(isinf(centroid) || isnan(centroid))
 centroid = class="num">0;
 class="type">class="kw">float curv = curvatures[cent];
 if(isinf(curv) || isnan(curv))
 curv = class="num">1.2e-7;
class=class="str">"cmt">//--- dot(features, centroids)
 class="type">class="kw">float fc = feature * centroid;
 if(isnan(fc) || isinf(fc))
 fc = class="num">0;
class=class="str">"cmt">//---
 if(d < ls)
 temp[d] = (d > class="num">0 ? fc : -fc);
 barrier(CLK_LOCAL_MEM_FENCE);
 for(class="type">int i = ls; i < (class="type">int)dimension; i += ls)
 {
 if(d >= i && d < (i + ls))
 temp[d % ls] += fc;
 barrier(CLK_LOCAL_MEM_FENCE);
 }
class=class="str">"cmt">//---
 class="type">int count = min(ls, (class="type">int)dimension);
class=class="str">"cmt">//---
 do
 {
 count = (count + class="num">1) / class="num">2;
 if(d < count)
 temp[d] += ((d + count) < dimension ? temp[d + count] : class="num">0);
 if(d + count < dimension)
 temp[d + count] = class="num">0;
 barrier(CLK_LOCAL_MEM_FENCE);
 }
 while(count > class="num">1);
 class="type">class="kw">float prod = temp[class="num">0];
 if(isinf(prod) || isnan(prod))
 prod = class="num">0;
 product[shift_temporal] = prod;
class=class="str">"cmt">//--- project
 class="type">class="kw">float u = feature + prod * centroid * curv;
 if(isinf(u) || isnan(u))
 u = class="num">0;
class=class="str">"cmt">//--- norm(u)
 class="type">class="kw">float u2 = u * u;
 if(isinf(u2) || isnan(u2))
 u2 = class="num">0;
 if(d < ls)
 temp[d] = (d > class="num">0 ? u2 : -u2);
 barrier(CLK_LOCAL_MEM_FENCE);
 for(class="type">int i = ls; i < (class="type">int)dimension; i += ls)
 {
 if(d >= i && d < (i + ls))
 temp[d % ls] += u2;

◍ 双树归约下的双曲投影与梯度接口

这段 OpenCL 内核接着前面把局部内存里的归约结果收口。第一次 do-while 用 count=(count+1)/2 的折半方式把 temp 数组累加到 temp[0],得到范数平方 normu;若检测到 inf/nan 或小于等于 0,则兜底成 1e-7f,再开平方写进 norma[shift_temporal],这一步直接决定了后续距离度量的数值稳定性。 距离特征走的是双曲几何路线:theta = -prod * curv 先算缩放角,clamp 到 1.0f+1.2e-07f 下限防止 acosh 域外爆炸;dist 用 sqrt(pow(acosh(theta),2)/curv) 并夹在 0~50.0f 之间,写回 distance[shift_temporal]。外汇与贵金属行情的高波动下,curv 若设得过小,dist 可能频繁触顶 50.0f,倾向让特征饱和。 proj_u = dist * u / normu 算完投影幅值后,又用一次局部归约(同样的 ls 折半树)把各线程的 proj_u*centroid 加总,最后 d==0 的线程取出 temp[0]/centroid 做兜底,并把 outputs[shift_out] 写出。注意兜底里又用 fmax(u,1.2e-7f) 覆盖,可能掩盖了上游 u 为 0 的异常。 紧接的 LogMapGrad 内核声明了 features/centroids/curvatures/outputs 的正向量与对应 _gr 梯度缓冲,说明前面这套前向映射在训练时会走反向图:centroids_gr 与 curvatures_gr 意味着曲率和质心都是可微优化变量。开 MT5 接 Compute 插件时,可先单跑前向核看 norma 是否落在 1e-3~1e2 量级,再决定是否接通梯度核。

MQL5 / C++
  barrier(CLK_LOCAL_MEM_FENCE);
  }
class=class="str">"cmt">//---
  count = min(ls, (class="type">int)dimension);
class=class="str">"cmt">//---
  do
    {
      count = (count + class="num">1) / class="num">2;
      if(d < count)
        temp[d] += ((d + count) < dimension ? temp[d + count] : class="num">0);
      if(d + count < dimension)
        temp[d + count] = class="num">0;
      barrier(CLK_LOCAL_MEM_FENCE);
    }
  while(count > class="num">1);
  class="type">class="kw">float normu = temp[class="num">0];
  if(isinf(normu) || isnan(normu) || normu <= class="num">0)
    normu = class="num">1.0e-7f;
  normu = sqrt(normu);
  norma[shift_temporal] = normu;
class=class="str">"cmt">//--- distance features to centroid
  class="type">class="kw">float theta = -prod * curv;
  if(isinf(theta) || isnan(theta))
    theta = class="num">0;
  theta = fmax(theta, class="num">1.0f + class="num">1.2e-07f);
  class="type">class="kw">float dist = sqrt(clamp(pow(acosh(theta), class="num">2.0f) / curv, class="num">0.0f, class="num">50.0f));
  if(isinf(dist) || isnan(dist))
    dist = class="num">0;
  distance[shift_temporal] = dist;
  class="type">class="kw">float proj_u = dist * u / normu;
  if(d < ls)
    temp[d] = (d > class="num">0 ? proj_u * centroid : class="num">0);
  barrier(CLK_LOCAL_MEM_FENCE);
  for(class="type">int i = ls; i < (class="type">int)dimension; i += ls)
    {
      if(d >= i && d < (i + ls))
        temp[d % ls] += proj_u * centroid;
      barrier(CLK_LOCAL_MEM_FENCE);
    }
class=class="str">"cmt">//---
  count = min(ls, (class="type">int)dimension);
class=class="str">"cmt">//---
  do
    {
      count = (count + class="num">1) / class="num">2;
      if(d < count)
        temp[d] += ((d + count) < dimension ? temp[d + count] : class="num">0);
      if(d + count < dimension)
        temp[d + count] = class="num">0;
      barrier(CLK_LOCAL_MEM_FENCE);
    }
  while(count > class="num">1);
class=class="str">"cmt">//---
  if(d == class="num">0)
    {
      proj_u = temp[class="num">0] / centroid;
      if(isinf(proj_u) || isnan(proj_u))
        proj_u = class="num">0;
      proj_u = fmax(u, class="num">1.2e-7f);
    }
class=class="str">"cmt">//---
  outputs[shift_out] = proj_u;
  }
__kernel class="type">void LogMapGrad(__global const class="type">class="kw">float *features, __global class="type">class="kw">float *features_gr,
                        __global const class="type">class="kw">float *centroids, __global class="type">class="kw">float *centroids_gr,
                        __global const class="type">class="kw">float *curvatures, __global class="type">class="kw">float *curvatures_gr,
                        __global const class="type">class="kw">float *outputs, __global const class="type">class="kw">float *outputs_gr,

「反向传播里的梯度拆解」

这段 OpenCL 内核跑在 GPU 上,负责把输出梯度沿特征、质心、曲率三条线回传。本地线程用 get_local_id 拿到自己在特征维度和维度索引上的位置,get_global_id(1) 则对应当前质心编号,三者组合出读写偏移。 偏移计算是性能关键:shift_f 定位单样本特征,shift_out 定位该特征对输出的梯度,shift_cent 定位质心向量。当维度超过本地数组上限时,循环以 ls = min(dimension, LOCAL_ARRAY_SIZE) 为步长分块搬运,避免寄存器爆掉。 数值防御写得很死:inf 或 nan 的特征、质心直接归零;质心历史值 centroid0 若为零或非法,则垫一个 1.2e-7f 的小量,曲率同理。这一手能防止反向传播在极端行情样本(跳空、停牌)上直接 NaN 扩散。 proj_u_gr 的递推只在 d>0 时生效,用 grad0/centroid0*centroid 补上前一维度的连锁梯度;否则当前维度就是梯度起点。外汇与贵金属价格序列常带断点,这种兜底让模型在回测中更不容易整批失效,但高频噪声下仍属高风险信号,实盘须自测。

MQL5 / C++
 __global const class="type">class="kw">float *product,
 __global const class="type">class="kw">float *distance,
 __global const class="type">class="kw">float *norma
 )
{
class=class="str">"cmt">//--- identify
 const class="type">size_t f = get_local_id(class="num">0);
 const class="type">size_t cent = get_global_id(class="num">1);
 const class="type">size_t d = get_local_id(class="num">2);
 const class="type">size_t total_f = get_local_size(class="num">0);
 const class="type">size_t total_cent = get_global_size(class="num">1);
 const class="type">size_t dimension = get_local_size(class="num">2);
class=class="str">"cmt">//--- create local array
 __local class="type">class="kw">float temp[LOCAL_ARRAY_SIZE];
 const class="type">int ls = min((class="type">int)dimension, (class="type">int)LOCAL_ARRAY_SIZE);
class=class="str">"cmt">//--- calc shifts
 const class="type">int shift_f = f * dimension + d;
 const class="type">int shift_out = (f * total_cent + cent) * dimension + d;
 const class="type">int shift_cent = cent * dimension + d;
 const class="type">int shift_temporal = f * total_cent + cent;
class=class="str">"cmt">//--- load inputs
 class="type">class="kw">float feature = features[shift_f];
 if(isinf(feature) || isnan(feature))
    feature = class="num">0;
 class="type">class="kw">float centroid = centroids[shift_cent];
 if(isinf(centroid) || isnan(centroid))
    centroid = class="num">0;
 class="type">class="kw">float centroid0 = (d > class="num">0 ? centroids[shift_cent - d] : centroid);
 if(isinf(centroid0) || isnan(centroid0) || centroid0 == class="num">0)
    centroid0 = class="num">1.2e-7f;
 class="type">class="kw">float curv = curvatures[cent];
 if(isinf(curv) || isnan(curv))
    curv = class="num">1.2e-7;
 class="type">class="kw">float prod = product[shift_temporal];
 class="type">class="kw">float dist = distance[shift_temporal];
 class="type">class="kw">float normu = norma[shift_temporal];
 class="type">class="kw">float u = feature + prod * centroid * curv;
 if(isinf(u) || isnan(u))
    u = class="num">0;
 class="type">class="kw">float grad = outputs_gr[shift_out];
 if(isinf(grad) || isnan(grad))
    grad = class="num">0;
 class="type">class="kw">float grad0 = (d>class="num">0 ? outputs_gr[shift_out - d] : grad);
 if(isinf(grad0) || isnan(grad0))
    grad0 = class="num">0;
 class="type">class="kw">float feature_gr = class="num">0;
 class="type">class="kw">float centroid_gr = class="num">0;
 class="type">class="kw">float curv_gr = class="num">0;
 class="type">class="kw">float prod_gr = class="num">0;
 class="type">class="kw">float normu_gr = class="num">0;
 class="type">class="kw">float dist_gr = class="num">0;
 class="type">class="kw">float proj_u_gr = (d > class="num">0 ? grad + grad0 / centroid0 * centroid : class="num">0);
 for(class="type">int id = class="num">0; id < dimension; id += ls)
   {
    if(d >= id && d < (id + ls))
      {
       class="type">int t = d % ls;
       for(class="type">int ifeat = class="num">0; ifeat < total_f; ifeat++)
         {
          if(f == ifeat)
把非欧诊断交给小布
小布盯盘已内置双曲空间各向异性检测,打开对应品种页即可看到潜在嵌入的局部保留度,你只管调扩散步数。

常见问题

普通模型在欧式潜在空间做各向同性扩散,容易把非欧节点的各向异性当噪声丢;HypDiff 用双曲几何统一角度与径向测量,并加角度约束抑制各向异性噪声。
双曲流形天然贴合离散树状拓扑,节点嵌入全局各向同性、局部保留各向异性,能提供带物理语义的几何先验。
第一阶段学双曲潜在表征,第二阶段在径向量值扩散过程里做去噪,绕开双曲空间高斯可加性未定义的问题。
可以,小布盯盘的品种页内置了非欧嵌入各向异性保留度指标,省去你自己写双曲测地线计算。
径向约束定义双曲空间里的扩散路径,角度约束压制各向异性扰动,两者配合保住图的结构先验。