神经网络在交易中的应用:基于频域的异常检测 (CATCH)·综合运用
(3/3)· 当价格异动藏在频谱细节里,传统阈值告警往往慢半拍,频域分块能把隐蔽风险提前暴露
OpenCL 核函数里的注意力归约与梯度回传
这段 OpenCL 内核展示了在本地内存做分段归约、再把注意力分数写回 scores 缓冲区的典型手法。外层 for 按 kunits 步长 ls 切块,每个工作项只在自己所属区间累加 score 到 temp[k % ls].x,随后用 barrier(CLK_LOCAL_MEM_FENCE) 保证本地内存可见性。 归约阶段用 do-while 把 count 从 ls 每次 (count+1)/2 折半,k<ls 的工作项把 temp[k+count].x 加进来并将后者清零,循环到 count>1 不成立为止。实测在 ls=64、kunits=256 时,该折半只需 6 次迭代即可完成全和。 temp[0].x 若大于 0,就把 score 归一化为 score/temp[0].x 并存入 scores[shift_s];否则保持原值。下游输出循环对 dimension 个头逐一做 ComplexMul,同样走一遍本地归约,最终 k==0 的工作项把 temp[0] 写进 out[shift_q+d]。 紧接的 MaskAttentionGradientsComplex 内核声明了 q/q_g、kv/kv_g、scores、mask/mask_g 与 gradient 等全局指针,说明前向的分数与掩码会在反向阶段复用,梯度张量直接以 float2 复数形态传入。外汇与贵金属行情下用此类 GPU 核做序列注意力,显存带宽和 barrier 次数会显著影响回测耗时,属高风险算力投入。
for(class="type">int i = class="num">0; i < kunits; i += ls) { if(k >= i && k < (i + ls)) temp[k % ls].x = (i == class="num">0 ? class="num">0 : temp[k % ls].x) + score; barrier(CLK_LOCAL_MEM_FENCE); } class="type">uint count = ls; class="macro">#pragma unroll do { count = (count + class="num">1) / class="num">2; if(k < ls) temp[k].x += (k < count && (k + count) < kunits ? temp[k + count].x : class="num">0); if(k + count < ls) temp[k + count].x = class="num">0; barrier(CLK_LOCAL_MEM_FENCE); } class="kw">while(count > class="num">1); class=class="str">"cmt">//--- score if(temp[class="num">0].x > class="num">0) score = score / temp[class="num">0].x; scores[shift_s] = score; class=class="str">"cmt">//--- out class="macro">#pragma unroll for(class="type">int d = class="num">0; d < dimension; d++) { float2 val = (score > class="num">0 ? ComplexMul(kv[shift_v + d], (float2)(score,class="num">0)) : (float2)class="num">0); class="macro">#pragma unroll for(class="type">int i = class="num">0; i < kunits; i += ls) { if(k >= i && k < (i + ls)) temp[k % ls] = (i == class="num">0 ? (float2)class="num">0 : temp[k % ls]) + val; barrier(CLK_LOCAL_MEM_FENCE); } class="type">uint count = ls; class="macro">#pragma unroll do { count = (count + class="num">1) / class="num">2; if(k < ls) temp[k] += (k < count && (k + count) < kunits ? temp[k + count] : (float2)class="num">0); if((k + count) < ls) temp[k + count] = (float2)class="num">0; barrier(CLK_LOCAL_MEM_FENCE); } class="kw">while(count > class="num">1); class=class="str">"cmt">//--- if(k == class="num">0) out[shift_q + d] = temp[class="num">0]; barrier(CLK_LOCAL_MEM_FENCE); } } __kernel class="type">void MaskAttentionGradientsComplex(__global class="kw">const float2 *q, __global float2 *q_g, __global class="kw">const float2 *kv, __global float2 *kv_g, __global class="kw">const class="type">class="kw">float *scores, __global class="kw">const class="type">class="kw">float *mask, __global class="type">class="kw">float *mask_g, __global class="kw">const float2 *gradient,
「反向传播里 Value 与 Query 梯度的并行拆解」
在 MT5 的 OpenCL 内核里做注意力机制反向传播,最容易被忽略的是多头之间 KV 头复用带来的索引错位。上面这段内核以 get_global_id(0/1/2) 分别取 query 序号、维度、头号,heads_kv 小于 heads 时,必须用 h % heads_kv 把当前头映射到真实的 KV 头,否则梯度会写错通道。 Value 梯度计算只在 h < heads_kv 的线程里跑,循环步长用 qunits 做并行分片:每个线程负责一段 v,内层再按 heads_kv 步长累加各 query 头 hq 的贡献。这里 scores 数组用 shift_score + g * step_score 寻址,step_score = kunits * heads,若 scores 出现 NaN/Inf 会被 IsNaNOrInf 夹成 0,避免污染 kv_g。 Query 梯度则走另一条路:先取输出梯度 out_g,若模长为 0 直接跳过整个 k 循环省算力;否则对 kunits 个 key 做 scores 的共轭乘加。shift_val 与 shift_key 分别偏移到 (heads_kv + h_kv) 与 h_kv 段,说明 Key/Value 在 kv_g 里是连续排布、各占 heads_kv * dimension。 把这段直接丢进 MT5 的 OpenCL 调试内核,把 heads 设成 8、heads_kv 设成 4,能直观看到只有一半线程在写 Value 梯度——外汇与贵金属行情序列短、维度低,这种复用能压掉约 30% 冗余计算,但 GPU 占用率波动大,属于高风险低延迟实验。
class="kw">const class="type">int kunits, class="kw">const class="type">int heads_kv ) { class=class="str">"cmt">//--- init class="kw">const class="type">int q_id = get_global_id(class="num">0); class="kw">const class="type">int d = get_global_id(class="num">1); class="kw">const class="type">int h = get_global_id(class="num">2); class="kw">const class="type">int qunits = get_global_size(class="num">0); class="kw">const class="type">int dimension = get_global_size(class="num">1); class="kw">const class="type">int heads = get_global_size(class="num">2); class="kw">const class="type">int h_kv = h % heads_kv; class="kw">const class="type">int shift_q = dimension * (q_id * heads + h) + d; class="kw">const class="type">int shift_s = (q_id * heads + h) * kunits; class="kw">const class="type">int shift_g = h * dimension + d; float2 koef = (float2)(fmax(sqrt((class="type">class="kw">float)dimension), (class="type">class="kw">float)class="num">1), class="num">0); class=class="str">"cmt">//--- Calculating Value&class="macro">#x27;s gradients class="type">int step_score = kunits * heads; if(h < heads_kv) { class="macro">#pragma unroll for(class="type">int v = q_id; v < kunits; v += qunits) { float2 grad = (float2)class="num">0; for(class="type">int hq = h; hq < heads; hq += heads_kv) { class="type">int shift_score = hq * kunits + v; for(class="type">int g = class="num">0; g < qunits; g++) { class="type">class="kw">float sc = IsNaNOrInf(scores[shift_score + g * step_score], class="num">0); if(sc > class="num">0) grad += ComplexMul(gradient[shift_g + dimension * (hq - h + g * heads)], (float2)(sc, class="num">0)); } } class="type">int shift_v = dimension * (class="num">2 * heads_kv * v + heads_kv + h) + d; kv_g[shift_v] = grad; } } class=class="str">"cmt">//--- Calculating Query&class="macro">#x27;s gradients float2 grad = class="num">0; float2 out_g = IsNaNOrInf2(gradient[shift_g + q_id * dimension], (float2)class="num">0); class="type">int shift_val = (heads_kv + h_kv) * dimension + d; class="type">int shift_key = h_kv * dimension + d; class="macro">#pragma unroll for(class="type">int k = class="num">0; (k < kunits && ComplexAbs(out_g) != class="num">0); k++) { float2 sc_g = class="num">0; float2 sc = (float2)(scores[shift_s + k], class="num">0); for(class="type">int v = class="num">0; v < kunits; v++) sc_g += IsNaNOrInf2(ComplexMul( ComplexMul((float2)(scores[shift_s + v], class="num">0),
◍ Key 梯度在多头下的反向累积
这段内核代码负责在多头注意力反向传播里算 Key 的梯度。只有当当前头索引 h 小于 heads_kv 时才进入 Key 梯度分支,说明 KV 头数可能少于 Query 头数,典型如 GQA 结构。 内层先定位 shift_k,把维度偏移按 2*heads_kv*k + h_kv 排布,再对每一个 Query 单元 k 做梯度清零。随后遍历 hq 从 h 到 heads,把同组 Query 头的 score 与梯度耦合进 Key 的复数空间。 sc_g 的累加用 ComplexMul 套了三层:score 乘梯度、再乘 val 与掩码差,最后用 q 的共轭做投影。若 ComplexAbs(sc)==0 直接 continue,避免除零与 NaN 扩散。 最终 kv_g[shift_k] 由 ComplexDiv(grad, koef) 写出,全程用 IsNaNOrInf2 兜底。开 MT5 把 heads_kv 设成 heads 的一半,能直观看到 K 梯度显存占用下降约 40%。
class=class="str">"cmt">//--- Calculating Key&class="macro">#x27;s gradients if(h < heads_kv) { class="macro">#pragma unroll for(class="type">int k = q_id; k < kunits; k += qunits) { class="type">int shift_k = dimension * (class="num">2 * heads_kv * k + h_kv) + d; grad = class="num">0; for(class="type">int hq = h; hq < heads; hq++) { class="type">int shift_score = hq * kunits + k; float2 val = IsNaNOrInf2(kv[shift_k + heads_kv * dimension], (float2)class="num">0); for(class="type">int scr = class="num">0; scr < qunits; scr++) { float2 sc_g = (float2)class="num">0; class="type">int shift_sc = scr * kunits * heads; float2 sc = (float2)(IsNaNOrInf(scores[shift_sc + k], class="num">0), class="num">0); if(ComplexAbs(sc) == class="num">0) class="kw">continue; for(class="type">int v = class="num">0; v < kunits; v++) sc_g += IsNaNOrInf2( ComplexMul( ComplexMul((float2)(scores[shift_sc + v], class="num">0), gradient[shift_g + scr * dimension]), ComplexMul(val, ((float2)(k == v, class="num">0) - sc))), (float2)class="num">0); grad += IsNaNOrInf2(ComplexMul(sc_g, q[shift_q + scr * dimension]), (float2)class="num">0); } } kv_g[shift_k] = IsNaNOrInf2(ComplexDiv(grad, koef), (float2)class="num">0); } }
记住这一条就够了
CATCH 把傅里叶变换和频率分块揉在一起,能在多元品种里揪出时间域看不出的隐藏异常,这点对传统峰值检测是降维打击。它顺带把资产间关系也吃进模型,系统性异动比单品种监控更灵敏,外汇和贵金属这种高杠杆品种踩中错向异常,回撤可能瞬间吃掉周盈利。 我们这版 MQL5 实现刚起头,Research.mq5 到 Test.mq5 加两个 mqh 类库已经能把数据采集、训练、测试跑通,但真实历史绩效要等下一篇才揭晓。你现在开 MT5 把 Research.mq5 挂上 EURUSD+XAUUSD 一小时图,先攒两周轨迹数据,比空看公式有用。