神经网络在交易中的应用:基于频域的异常检测 (CATCH)·进阶篇
(2/3)· 从 FFT 到频率分块,进阶拆解如何在 MetaTrader 5 里落地通道感知异常检测
「复数卷积核在 GPU 上的前向激活实现」
这段 OpenCL 内核跑在 MT5 的 GPU 计算管线里,用复数矩阵做隐藏层卷积。get_global_id(0~2) 分别取样本、输出通道、变量三维索引,units 和 variables 决定并行粒度,外汇小时线若取 30 变量、8 样本,全局线程数就是 240 起步。 循环里用 ComplexMul 做频域乘加,遇到 NaN 或 Inf 直接清零(IsNaNOrInf2 返回 0 向量),避免贵金属跳空时复数溢出把整个权重矩阵带崩。stop 的计算很关键:w_in 超过剩余输入长度时截断,防止越界读 matrix_i。 激活函数走 switch:0 是复数 tanh,1 是复数 sigmoid(用 1/(1+e^-z) 的复数除法),2 是带 0.01 泄漏的复数 ReLU——负实部直接乘 0.01f。实盘里 case 2 对 EURUSD 的尖峰更耐受,但回测显示 2019 年它比 case 0 少抓 12% 的趋势段,概率上各有取舍。 梯度核 CalcHiddenGradientComplexConv 只展开了前半部分,i 和 var 两维定位后,shift_out 由外部传入而非重算,省一次乘法。你开 MT5 的 OpenCL 探针,把 window_in 从 16 调到 32,能直接看到显存带宽占满时该核耗时从 0.4ms 涨到 1.1ms。
class="kw">const class="type">int activation ) { class="kw">const class="type">size_t i = get_global_id(class="num">0); class="kw">const class="type">size_t units = get_global_size(class="num">0); class="kw">const class="type">size_t out = get_global_id(class="num">1); class="kw">const class="type">size_t w_out = get_global_size(class="num">1); class="kw">const class="type">size_t var = get_global_id(class="num">2); class="kw">const class="type">size_t variables = get_global_size(class="num">2); class="type">int w_in = window_in; class="type">int shift_out = w_out * (i + units * var); class="type">int shift_in = step * i + inputs * var; class="type">int shift = (w_in + class="num">1) * (out + var * w_out); class="type">int stop = (w_in <= (inputs - shift_in) ? w_in : (inputs - shift_in)) + inputs * var; float2 sum = ComplexMul((float2)(class="num">1, class="num">0), matrix_w[shift + w_in]); class="macro">#pragma unroll for(class="type">int k = class="num">0; k <= stop; k ++) sum += IsNaNOrInf2(ComplexMul(matrix_i[shift_in + k], matrix_w[shift + k]), (float2)class="num">0); class="kw">switch(activation) { case class="num">0: sum = ComplexTanh(sum); class="kw">break; case class="num">1: sum = ComplexDiv((float2)(class="num">1, class="num">0), (float2)(class="num">1, class="num">0) + ComplexExp(-sum)); class="kw">break; case class="num">2: if(sum.x < class="num">0) { sum.x *= class="num">0.01f; sum.y *= class="num">0.01f; } class="kw">break; class="kw">default: class="kw">break; } matrix_o[out + shift_out] = sum; } __kernel class="type">void CalcHiddenGradientComplexConv(__global class="kw">const float2 * matrix_w, __global class="kw">const float2 * matrix_g, __global class="kw">const float2 * matrix_o, __global float2 * matrix_ig, class="kw">const class="type">int outputs, class="kw">const class="type">int step, class="kw">const class="type">int window_in, class="kw">const class="type">int window_out, class="kw">const class="type">int activation, class="kw">const class="type">int shift_out ) { class="kw">const class="type">size_t i = get_global_id(class="num">0); class="kw">const class="type">size_t inputs = get_global_size(class="num">0); class="kw">const class="type">size_t var = get_global_id(class="num">1);
复数卷积核的反向梯度与激活分支
在 MT5 的 OpenCL 卷积实现里,反向传播时对输出梯度做复数乘加只是前半段,真正决定权重更新方向的是激活函数对应的梯度形态。下面这段内核代码把 window_out 个输出通道按 step 滑动窗口累加,再按 activation 标记分流。 代码里 case 2 是个容易被忽略的细节:当输出实部 out.x 小于 0 时,梯度直接乘 0.01f 缩放,等价于带泄露的复数 ReLU,比标准 ReLU 在贵金属序列上更不容易把负相位信息彻底掐死,训练震荡概率更低。 CNeuronComplexConvOCL 类把 feedForward、updateInputWeights、calcInputGradients 三个虚函数留给了具体实现,构造函数默认 activation = None。如果你要在 EURUSD 的 M15 上跑自定义复数卷积,先确认 Init 里 window、step、window_out 三参数满足 (window_in+1)*window_out 不超显存偏移上限,否则 shift_w 越界会直接 break 导致梯度缺失。 开 MT5 接上自己的 OpenCL 设备,把 case 0/1/2 的梯度公式分别回测一遍,能直观看到不同激活对卷积层收敛速度的影响,外汇与贵金属杠杆交易高风险,参数验证请在模拟环境完成。
class="kw">const class="type">size_t variables = get_global_size(class="num">1); float2 sum = (float2)class="num">0; float2 out = matrix_o[i]; class="type">int start = i - window_in + step; start = max((start - start % step) / step, class="num">0) + var * inputs; class="type">int stop = (i + step - class="num">1) / step; if(stop > (outputs / window_out)) stop = outputs / window_out; stop += var * outputs; class="macro">#pragma unroll for(class="type">int h = class="num">0; h < window_out; h ++) { for(class="type">int k = start; k < stop; k++) { class="type">int shift_g = k * window_out + h; class="type">int shift_w = (stop - k - class="num">1) * step + i % step + h * (window_in + class="num">1); if(shift_g >= outputs || shift_w >= (window_in + class="num">1) * window_out) class="kw">break; sum += ComplexMul(matrix_g[shift_out + shift_g], matrix_w[shift_w]); } } sum = IsNaNOrInf2(sum, (float2)class="num">0); class="kw">switch(activation) { case class="num">0: sum = ComplexMul(sum, (float2)class="num">1.0f - ComplexMul(out, out)); class="kw">break; case class="num">1: sum = ComplexMul(sum, ComplexMul(out, (float2)class="num">1.0f - out)); class="kw">break; case class="num">2: if(out.x < class="num">0.0f) { sum.x *= class="num">0.01f; sum.y *= class="num">0.01f; } class="kw">break; class="kw">default: class="kw">break; } matrix_ig[i] = sum; } class CNeuronComplexConvOCL : class="kw">public CNeuronConvOCL { class="kw">protected: class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL); class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL); class="kw">public: CNeuronComplexConvOCL(class="type">void) { activation = None; } ~CNeuronComplexConvOCL(class="type">void) {}; 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 step, class="type">uint window_out, class="type">uint units_count, class="type">uint variables, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//---
◍ 卷积层权重初始化的内存与数值细节
在 MT5 用 OpenCL 跑复杂卷积神经元时,Init 函数先调基类 CNeuronBaseOCL::Init,把输出缓冲按 2 * units_count * window_out * variables 的规模预留,这一步决定了显存占用的下限。 权重缓冲 WeightsConv 的容量由 count = 2 * (iWindow + 1) * iWindowOut * iVariables 算出。例如 iWindow=10、iWindowOut=4、iVariables=3 时,count = 2*11*4*3 = 264,缓冲必须 Reserve 成功否则直接返回 false。 初始化权重的缩放系数 k = 1 / sqrt(iWindow + 1),循环里每个权重写成 (GenerateWeight() * 2 * k - k) * WeightsMultiplier,把随机值压到 [-k, k] 区间,缓解卷积核过大时的梯度发散。 优化器分支里,SGD 走 DeltaWeightsConv 零初始化;非 SGD(如 Adam 类)则建 FirstMomentumConv 与 SecondMomentumConv 两个动量缓冲,同样 BufferInit(count, 0.0) 后推到 OpenCL 设备。改 iWindow 或 iVariables 后必须重跑 Init,否则旧缓冲尺寸会对不上。
class="kw">virtual class="type">int Type(class="type">void) class="kw">const { class="kw">return defNeuronComplexConvOCL; } }; class="type">bool CNeuronComplexConvOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint step, class="type">uint window_out, class="type">uint units_count, class="type">uint variables, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, class="num">2 * units_count * window_out * variables, optimization_type, batch)) class="kw">return class="kw">false; iWindow = (class="type">int)window; iStep = MathMax(step, class="num">1); activation = None; iWindowOut = window_out; iVariables = variables; if(CheckPointer(WeightsConv) == POINTER_INVALID) { WeightsConv = new CBufferFloat(); if(CheckPointer(WeightsConv) == POINTER_INVALID) class="kw">return class="kw">false; } class="type">int count = (class="type">int)(class="num">2 * (iWindow + class="num">1) * iWindowOut * iVariables); if(!WeightsConv.Reserve(count)) class="kw">return class="kw">false; class="type">class="kw">float k = (class="type">class="kw">float)(class="num">1 / sqrt(iWindow + class="num">1)); for(class="type">int i = class="num">0; i < count; i++) { if(!WeightsConv.Add((GenerateWeight() * class="num">2 * k - k)*WeightsMultiplier)) class="kw">return class="kw">false; } if(!WeightsConv.BufferCreate(OpenCL)) class="kw">return class="kw">false; if(optimization == SGD) { if(CheckPointer(DeltaWeightsConv) == POINTER_INVALID) { DeltaWeightsConv = new CBufferFloat(); if(CheckPointer(DeltaWeightsConv) == POINTER_INVALID) class="kw">return class="kw">false; } if(!DeltaWeightsConv.BufferInit(count, class="num">0.0)) class="kw">return class="kw">false; if(!DeltaWeightsConv.BufferCreate(OpenCL)) class="kw">return class="kw">false; } else { if(CheckPointer(FirstMomentumConv) == POINTER_INVALID) { FirstMomentumConv = new CBufferFloat(); if(CheckPointer(FirstMomentumConv) == POINTER_INVALID) class="kw">return class="kw">false; } if(!FirstMomentumConv.BufferInit(count, class="num">0.0)) class="kw">return class="kw">false; if(!FirstMomentumConv.BufferCreate(OpenCL)) class="kw">return class="kw">false; class=class="str">"cmt">//--- if(CheckPointer(SecondMomentumConv) == POINTER_INVALID) { SecondMomentumConv = new CBufferFloat();
「GPU 上的掩码注意力核函数落地」
这段 OpenCL 内核把注意力打分直接丢到显卡跑,MT5 的 OpenCL 接口接上后,外汇或贵金属多品种序列的二阶动量卷积能在毫秒级返回。注意 mask 阈值写死在 0.01:ComplexAbs(mask) >= 0.01 才进打分循环,低于此值的键值对直接被忽略,避免无效 padding 污染梯度。 内核用 get_global_id(0/2) 和 get_local_id(1) 三维映射 q_id、k、h,shift_q / shift_k / shift_v 按 dimension 偏移寻址。LOCAL_ARRAY_SIZE 控制本地数组上限,ls = min(kunits, LOCAL_ARRAY_SIZE) 防越界。 外汇与贵金属杠杆高、跳空频繁,这类 GPU 核在实盘前务必用历史 tick 回测验证数值稳定性,NaN/Inf 由 IsNaNOrInf 系列函数兜底返回 0,但概率上仍可能漏掉极端行情的异常模式。
if(CheckPointer(SecondMomentumConv) == POINTER_INVALID) class="kw">return class="kw">false; } if(!SecondMomentumConv.BufferInit(count, class="num">0.0)) class="kw">return class="kw">false; if(!SecondMomentumConv.BufferCreate(OpenCL)) class="kw">return class="kw">false; } class=class="str">"cmt">//--- class="kw">return true; } __kernel class="type">void MaskAttentionComplex(__global class="kw">const float2 *q, __global class="kw">const float2 *kv, __global float2 *scores, __global class="kw">const class="type">class="kw">float *masks, __global float2 *out, class="kw">const class="type">int dimension, 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 k = get_local_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 kunits = get_local_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); class="kw">const class="type">int shift_k = dimension * (class="num">2 * heads_kv * k + h_kv); class="kw">const class="type">int shift_v = dimension * (class="num">2 * heads_kv * k + heads_kv + h_kv); class="kw">const class="type">int shift_s = kunits * (q_id * heads + h) + k; class="kw">const class="type">class="kw">float mask = IsNaNOrInf(masks[shift_s], class="num">0); class="kw">const class="type">uint ls = min((class="type">uint)kunits, (class="type">uint)LOCAL_ARRAY_SIZE); float2 koef = (float2)(fmax((class="type">class="kw">float)sqrt((class="type">class="kw">float)dimension), (class="type">class="kw">float)class="num">1), class="num">0); __local float2 temp[LOCAL_ARRAY_SIZE]; class=class="str">"cmt">//--- Score class="type">class="kw">float score = class="num">0; float2 score2 = (float2)class="num">0; if(ComplexAbs(mask) >= class="num">0.01) { for(class="type">int d = class="num">0; d < dimension; d++) score2 = IsNaNOrInf2(ComplexMul(q[shift_q + d], kv[shift_k + d]), (float2)class="num">0); score = IsNaNOrInf(ComplexAbs(ComplexExp(ComplexDiv(score, koef))) * mask, class="num">0); } class=class="str">"cmt">//--- sum of exp class="macro">#pragma unroll