交易中的神经网络:双曲型潜在扩散模型(HypDiff)·进阶篇
(2/3)· 从双曲几何先验到两阶段训练,拆解 HypDiff 如何保住图结构拓扑
不少人在 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 被写回全局内存。
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 默认值改大十倍后聚类边界会明显发散。
__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 量级,再决定是否接通梯度核。
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 补上前一维度的连锁梯度;否则当前维度就是梯度起点。外汇与贵金属价格序列常带断点,这种兜底让模型在回测中更不容易整批失效,但高频噪声下仍属高风险信号,实盘须自测。
__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)