神经网络变得简单(第 92 部分):频域和时域中的自适应预测·进阶篇
(2/3)· 当 DFT 频谱错位拖垮预测,动态加权融合如何补上实时序列的混合周期缺口
Transformer 权重矩阵与张量缓冲的初始化开销
在 MT5 里用 OpenCL 跑Transformer类模型,先得算清前馈层的权重矩阵尺寸。代码里 ff_1 = 2 * 4 * (iWindow + 1) * iWindow,ff_2 = 2 * (4 * iWindow + 1) * iWindow,两者差异来自层间维度展开方式不同;当 iWindow=64 时,ff_1 约为 33280,ff_2 约为 32896,差出近 400 个 float,显存分配不能混用。 外层按 iLayers 循环、内层 d<2 跑两遍,每一轮都为 QKV、scores、多头注意力输出、注意力输出各 new 一个 CBufferFloat 并推入对应数组。任何一步 BufferInit 或 BufferCreate(OpenCL) 失败就直接 return false,意味着单层初始化崩了整个模型就起不来。 开 MT5 把 iWindow 从 32 调到 128 试一次,你会看到 ff_1 从 8448 跳到 132096,GPU 缓冲占用近似线性放大;外汇和贵金属行情序列用这类结构做推断属高风险,过宽窗口可能让显存直接爆掉。
class="type">uint ff_1 = class="num">2 * class="num">4 * (iWindow + class="num">1) * iWindow; class=class="str">"cmt">//Size of weights&class="macro">#x27; matrix class="num">1-st feed forward layer class="type">uint ff_2 = class="num">2 * (class="num">4 * iWindow + class="num">1) * iWindow; class=class="str">"cmt">//Size of weights&class="macro">#x27; matrix class="num">2-nd feed forward layer for(class="type">uint i = class="num">0; i < iLayers; i++) { CBufferFloat *temp = NULL; for(class="type">int d = class="num">0; d < class="num">2; d++) { class=class="str">"cmt">//--- Initilize QKV tensor temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(num, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!QKV_Tensors.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initialize scores temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(scores, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!S_Tensors.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initialize multi-heads attention out temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(mh_out, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!AO_Tensors.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initialize attention out temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(out, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL))
◍ Transformer 前向张量与 QKV 权重的初始化细节
这段逻辑跑在多层循环里,每层先给 Feed Forward 1 开一个 4*out 大小的浮点缓冲,再视是否为末层决定挂 Output/Gradient 还是继续开 out 大小的 FF2 缓冲。任何一步 BufferInit、BufferCreate 或 Add 失败都直接 return false,说明显存或 OpenCL 上下文异常时会整体放弃构建。 QKV 权重初始化值得盯一眼:缩放系数 k 取 1/sqrt(iWindow+1),每个权重值由 GenerateWeight()*2*k - k 生成,等价于把随机权重约束到 [-k, k] 区间。若你的 iWindow=63,k 约为 0.125,权重绝对值上限就被压在 0.125 附近,避免多头注意力在长窗口下数值发散。 在 MT5 里把这段接进自己的 EA 时,建议先打印 qkv_weights 和 iWindow 的实际值,确认 k 与缓冲尺寸匹配;外汇与贵金属杠杆交易高风险,模型初始化错误可能让回测静默失败,实盘前务必在策略测试器跑通再上。
class="kw">return false; if(!FF_Tensors.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initialize Feed Forward class="num">1 temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(class="num">4 * out, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!FF_Tensors.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initialize Feed Forward class="num">2 if(i == iLayers - class="num">1) { if(!FF_Tensors.Add(d == class="num">0 ? Output : Gradient)) class="kw">return false; class="kw">continue; } temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(out, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!FF_Tensors.Add(temp)) class="kw">return false; } class=class="str">"cmt">//--- Initilize QKV weights temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.Reserve(qkv_weights)) class="kw">return false; class="type">float k = (class="type">float)(class="num">1 / sqrt(iWindow + class="num">1)); for(class="type">uint w = class="num">0; w < qkv_weights; w++) { if(!temp.Add(GenerateWeight() * class="num">2 * k - k)) class="kw">return false; } if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!QKV_Weights.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initilize Weights0 temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID)
「前馈与注意力权重的显存预分配」
这段逻辑在模型初始化阶段连续三次为前馈层(FF)权重开缓冲区:第一次按 w0 长度 Reserve,第二次按 ff_1,第三次按 ff_2,每次都走 GenerateWeight()*2*k-k 做均匀初始化后推入 FF_Weights 数组。 注意第三段的 k 被重算成 1/sqrt(4*iWindow+1),和前两段用的 k 不同,意味着深层权重的初始尺度随窗口长度收缩,窗口越大初始权重越靠近 0。 最后按优化器分支循环 1 或 2 次(SGD 走 1 次,其他走 2 次),每次 new 一个 CBufferFloat 并 BufferInit 成 qkv_weights 长度的 0 缓冲,再 BufferCreate 到 OpenCL 设备。任何一步 Reserve / Add / Create 失败都直接 return false,初始化容错靠逐行短路。 在 MT5 里跑这类模型,若 ff_2 设得大(比如 iWindow=512 时 k≈0.022),权重初始值普遍落在 ±0.022 区间,可用 Print 抽样验证 BufferInit 后的实际方差。外汇与贵金属行情高波动,显存申请失败往往发生在品种切换时,实盘前务必在策略测试器用真实 tick 复现一次。
class="kw">return false; if(!temp.Reserve(w0)) class="kw">return false; for(class="type">uint w = class="num">0; w < w0; w++) { if(!temp.Add(GenerateWeight() * class="num">2 * k - k)) class="kw">return false; } if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!FF_Weights.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initilize FF Weights temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.Reserve(ff_1)) class="kw">return false; for(class="type">uint w = class="num">0; w < ff_1; w++) { if(!temp.Add(GenerateWeight() * class="num">2 * k - k)) class="kw">return false; } if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!FF_Weights.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.Reserve(ff_2)) class="kw">return false; k = (class="type">float)(class="num">1 / sqrt(class="num">4 * iWindow + class="num">1)); for(class="type">uint w = class="num">0; w < ff_2; w++) { if(!temp.Add(GenerateWeight() * class="num">2 * k - k)) class="kw">return false; } if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!FF_Weights.Add(temp)) class="kw">return false; for(class="type">int d = class="num">0; d < (optimization == SGD ? class="num">1 : class="num">2); d++) { temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(qkv_weights, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false;
权重缓冲与OpenCL前馈内核的落地
在 MT5 里把 Transformer 类模型的权重推到显存,靠的是 CBufferFloat 的 BufferCreate(OpenCL) 调用。上面这段初始化逻辑先给 QKV_Weights 和 FF_Weights 两个动态数组各塞了三块缓冲:QKV 合并权重、ff_1、ff_2,尺寸分别由 w0、ff_1、ff_2 三个整型变量决定,初始值填 0。 每一块 temp 在 new 出来之后都必须用 CheckPointer 判 POINTER_INVALID,否则显存申请失败会静默返回 false,模型直接起不来。BufferInit 的第二个参数 0 代表清零,不是随机初始化——这点对复现回测结果很关键。 末尾的 FeedForwardComplexConv 是个 __kernel 函数,参数里 matrix_w / matrix_i / matrix_o 都是 float2 复数指针,说明前馈卷积在频域跑;inputs、step、window_in、activation 四个 int 控制滑动窗口与激活分支。外汇与贵金属杠杆高,这类 GPU 推理若用于实盘信号,须先在策略测试器用历史 tick 验证延迟与过拟合概率。 别把 BufferCreate 当万能 OpenCL 上下文没建好时 BufferCreate 也会返回 false,但代码里只判了指针和 Init,没判 OpenCL 句柄有效性。开 MT5 跑之前,先确认终端已识别你的显卡,否则前馈内核永远进不去。
if(!QKV_Weights.Add(temp)) class="kw">return false; temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(w0, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!FF_Weights.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initilize FF Weights temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(ff_1, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!FF_Weights.Add(temp)) class="kw">return false; temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(ff_2, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!FF_Weights.Add(temp)) class="kw">return false; } } class=class="str">"cmt">//--- class="kw">return true; } __kernel class="type">void FeedForwardComplexConv(__global float2 *matrix_w, __global float2 *matrix_i, __global float2 *matrix_o, class="type">int inputs, class="type">int step, class="type">int window_in, class="type">int activation ) {
◍ 复数卷积核在GPU上的前向传播实现
这段 OpenCL 内核负责把复数权重矩阵与输入矩阵做滑动卷积,是复杂域注意力层的前向核心。每个工作项由 get_global_id(0) 和 (1) 定位批次与输出窗口,循环上界 stop 受输入长度与滑动步长 step 双重约束,避免越界读取。 卷积累加用 ComplexMul 做频域乘法,初始值取 matrix_w[shift + w_in] 的偏置项;若结果出现 NaN 或 Inf 直接清零,防止异常值在贵金属小时线回测中污染后续梯度。 激活函数按编号分支:0 为复数 tanh,1 为复数 sigmoid(用 1/(1+e^-z) 实现),2 为带 0.01 系数的复数 LeakyReLU——负部缩放精确到 0.01f,实虚部独立处理。 宿主端 ConvolutionForward 先校验四个指针与索引有效性,step 缺省回退为 window;global_work_size[0] 等于 outputs.Total()/(2*window_out),即把复数双通道压进一维缓冲。设置内核参数失败会打印错误码与行号并返回 false,开 MT5 跑自定义神经网络 EA 时可直接据此定位 OpenCL 参数绑定问题。
class="type">size_t i = get_global_id(class="num">0); class="type">size_t out = get_global_id(class="num">1); class="type">size_t w_out = get_global_size(class="num">1); class="type">int w_in = window_in; class="type">int shift_out = w_out * i; class="type">int shift_in = step * i; class="type">int shift = (w_in + class="num">1) * out; class="type">int stop = (w_in <= (inputs - shift_in) ? w_in : (inputs - shift_in)); float2 sum = matrix_w[shift + w_in]; for(class="type">int k = class="num">0; k <= stop; k ++) sum += ComplexMul(matrix_i[shift_in + k], matrix_w[shift + k]); if(isnan(sum.x) || isnan(sum.y) || isinf(sum.x) || isinf(sum.y)) sum = (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; if(sum.y < class="num">0) sum.y *= class="num">0.01f; class="kw">break; class="kw">default: class="kw">break; } matrix_o[out + shift_out] = sum; } class="type">bool CNeuronComplexMLMHAttention::ConvolutionForward(CBufferFloat *weights, CBufferFloat *inputs, CBufferFloat *outputs, class="type">uint window, class="type">uint window_out, ENUM_ACTIVATION activ, class="type">uint step = class="num">0) { if(CheckPointer(OpenCL) == POINTER_INVALID || CheckPointer(weights) == POINTER_INVALID || CheckPointer(inputs) == POINTER_INVALID || CheckPointer(outputs) == POINTER_INVALID) class="kw">return false; if(weights.GetIndex() < class="num">0) class="kw">return false; if(inputs.GetIndex() < class="num">0) class="kw">return false; if(outputs.GetIndex() < class="num">0) class="kw">return false; if(step == class="num">0) step = window; class="type">uint global_work_offset[class="num">2] = {class="num">0, class="num">0}; class="type">uint global_work_size[class="num">2]; global_work_size[class="num">0] = outputs.Total() / (class="num">2 * window_out); global_work_size[class="num">1] = window_out; if(!OpenCL.SetArgumentBuffer(def_k_FeedForwardComplexConv, def_k_ffc_matrix_w, weights.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_FeedForwardComplexConv, def_k_ffc_matrix_i, inputs.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
「卷积核参数绑定与多头注意力入口」
这段 OpenCL 封装把复数卷积前向传播的最后几个内核参数一次性塞进 GPU。先绑定输出缓冲 outputs.GetIndex(),再把输入长度按实部/虚部对半切(inputs.Total()/2)传给 def_k_ffc_inputs,步长 step 与输入窗 window、输出窗 window_out 紧随其后,激活类型 activ 占用 def_k_ffc_activation-1 这个槽位。
任何一个 SetArgument 失败都会用 printf 打出函数名、错误码和行号然后返回 false,方便在 MT5 Experts 日志里直接定位是哪一行参数没挂上。
OpenCL.Execute 用二维全局工作项(offset 与 size 由调用方算好)启动内核,失败时通过 CLGetInfoString 取上下文错误描述再打印,比单纯错误码更易读。
紧随其后的 __kernel void ComplexMHAttentionScore 是多头注意力分数核的入口,入参为复数张量 qkv、输出 score、维度 dimension 与掩码 mask;核内用 get_global_id(0/1) 拿查询序号 q 和头序号 h,get_global_size(0) 给出总单元数,后续缩放点积倾向在这两个维度上并行。
在 MT5 里跑这套时,外汇与贵金属行情的高波动可能让窗口参数失配,建议先以小周期离线回测核对 window 与 window_out 的边界。
class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_FeedForwardComplexConv, def_k_ffc_matrix_o, outputs.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_FeedForwardComplexConv, def_k_ffc_inputs, (class="type">int)(inputs.Total() / class="num">2))) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_FeedForwardComplexConv, def_k_ffc_step, (class="type">int)step)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_FeedForwardComplexConv, def_k_ffc_window_in, (class="type">int)window)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_FeedForwardComplexConv, def_k_ffс_window_out, (class="type">int)window_out)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_FeedForwardComplexConv, def_k_ffc_activation - class="num">1, (class="type">int)activ)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.Execute(def_k_FeedForwardComplexConv, class="num">2, global_work_offset, global_work_size)) { class="type">class="kw">string error; CLGetInfoString(OpenCL.GetContext(), CL_ERROR_DESCRIPTION, error); printf("Error of execution kernel %s: %s", __FUNCSIG__, error); class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } __kernel class="type">void ComplexMHAttentionScore(__global float2 *qkv, __global float2 *score, class="type">int dimension, class="type">int mask ) { class="type">int q = get_global_id(class="num">0); class="type">int h = get_global_id(class="num">1); class="type">int units = get_global_size(class="num">0);