交易中的神经网络:具有预测编码的混合交易框架(StockFormer)·进阶篇
(2/3)·多数RL交易模型漏掉资产间隐性关联,这篇看StockFormer怎么用预测编码补上
把历史价量丢进强化学习就以为模型懂市场,是常见误判。噪声环境里资产间的隐性依赖往往没被编码进状态,策略容易在最需要时失准。StockFormer 的思路是先抽隐藏态再谈决策,而不是让RL直接啃原始序列。
卷积核里的 float4 打包与梯度回传
这段代码是 OpenCL 内核里对多头卷积做向量化累加的核心片段。case 2 时输入 inp 用 float4 装了相邻 2 个特征再加常量 1 和 0,权重 weight 同样取 2 个滑动权重加一个窗口高度位置权重和 0,本质是把偏置项硬编码进向量避免额外加法。 case 3 与 default 把打包长度扩到 3 或 4,default 里四个分量全部来自 matrix_i 与 matrix_w 的连续偏移,说明当卷积窗口余数不足时仍按满 4 通道塞满 float4,利用 SIMD 宽度吃满 GPU 吞吐。 每次循环 sum += IsNaNOrInf(dot(inp, weight), 0) 都做了 NaN/Inf 防护,点积结果异常直接归零,防止贵金属跳空行情下梯度爆炸把显存写崩。外层再对 sum 做一次 IsNaNOrInf 后才送进 Activation,输出写回 matrix_o 对应偏移。 CalcHiddenGradientMHConv 内核签名里 matrix_g、matrix_ig 与 outputs、step 参数齐备,意味着隐藏层梯度会沿输入梯度矩阵回传,step 控制多头间 strides,调大 step 可能让显存带宽压力陡增,建议在 MT5 策略测试器用 EURUSD 1H 先跑 5000 根 K 线观察内核耗时。
inp = (float4)(matrix_i[shift_var_in + shift_in + k],
matrix_i[shift_var_in + shift_in + k + class="num">1], class="num">1, class="num">0);
weight = (float4)(matrix_w[shift_var_w + shift + k], matrix_w[shift_var_w + shift + k + class="num">1],
matrix_w[shift_var_w + shift + window_in_h], class="num">0);
break;
case class="num">3:
inp = (float4)(matrix_i[shift_var_in + shift_in + k], matrix_i[shift_var_in + shift_in + k + class="num">1],
matrix_i[shift_var_in + shift_in + k + class="num">2], class="num">1);
weight = (float4)(matrix_w[shift_var_w + shift + k], matrix_w[shift_var_w + shift + k + class="num">1],
matrix_w[shift_var_w + shift + k + class="num">2], matrix_w[shift_var_w + shift + shift_w_h]);
break;
class="kw">default:
inp = (float4)(matrix_i[shift_var_in + shift_in + k], matrix_i[shift_var_in + shift_in + k + class="num">1],
matrix_i[shift_var_in + shift_in + k + class="num">2], matrix_i[shift_var_in + shift_in + k + class="num">3]);
weight = (float4)(matrix_w[shift_var_w + shift + k], matrix_w[shift_var_w + shift + k + class="num">1],
matrix_w[shift_var_w + shift + k + class="num">2], matrix_w[shift_var_w + shift + k + class="num">3]);
break;
}
sum += IsNaNOrInf(dot(inp, weight), class="num">0);
}
sum = IsNaNOrInf(sum, class="num">0);
class=class="str">"cmt">//---
matrix_o[shift_var_out + out + shift_out] = Activation(sum, activation);;
}
}
__kernel class="type">void CalcHiddenGradientMHConv(__global class="type">float *matrix_w,
__global class="type">float *matrix_g,
__global class="type">float *matrix_o,
__global class="type">float *matrix_ig,
const class="type">int outputs,
const class="type">int step,◍ 多头卷积核的并行梯度回传写法
在 MT5 的 OpenCL 神经层里,多头卷积(MHConv)的前向与权重更新靠全局 ID 切分数据。上面这段内核先用 get_global_id(0) 取输入索引 i、get_global_size(0) 取总输入数 inputs,再用 get_global_id(1) 取样本编号 v,三者共同算出各样本在矩阵里的偏移 shift_var_in、shift_var_out 和 shift_var_w。 窗口按 heads 均分:window_in_h = (window_in + heads - 1) / heads,window_out_h 同理。这种向上取整写法保证 head 数不能整除窗口时也不丢边界。回测中 heads=4、window_in=64 时,window_in_h 实际为 16,与直观均分一致。 梯度累加循环里,k 从 start 走到 stop,start 由 (i - window_in + step) / step 与 0 取大得出,stop 受 outputs / window_out 截断。内层按 head 和 window_out_h 展开,shift_g 越界或 shift_w 超权重组上限就 break,避免越界读显存。 最后一行 matrix_ig[shift_var_in + i] = Deactivation(sum, out, activation) 把累加梯度做反激活写回输入梯度矩阵。CNeuronMHConvOCL 类仅多一个 iHeads 成员,并 override 了 feedForward 与 updateInputWeights,说明多头逻辑收敛在这两个虚函数内,改卷积结构时优先动它们。外汇与贵金属行情的高波动可能让这类网络权重剧烈漂移,实盘前务必在 MT5 策略测试器跑多样本验证。
const class="type">int window_in, const class="type">int window_out, const class="type">int activation, const class="type">int shift_out, const class="type">int heads ) { const class="type">size_t i = get_global_id(class="num">0); const class="type">size_t inputs = get_global_size(class="num">0); const class="type">size_t v = get_global_id(class="num">1); const class="type">int shift_var_in = v * inputs; const class="type">int shift_var_out = v * outputs; const class="type">int shift_var_w = v * window_out * (window_in + class="num">1); const class="type">int window_in_h = (window_in + heads - class="num">1) / heads; const class="type">int window_out_h = (window_out + heads - class="num">1) / heads; class="type">float sum = class="num">0; class="type">float out = matrix_o[shift_var_in + i]; const class="type">int w_start = i % step; const class="type">int start = max((class="type">int)((i - window_in + step) / step), class="num">0); class="type">int stop = (w_start + step - class="num">1) / step; stop = min((class="type">int)((i + step - class="num">1) / step + class="num">1), stop) + start; if(stop > (outputs / window_out)) stop = outputs / window_out; for(class="type">int k = start; k < stop; k++) { class="type">int head = (k % window_out) / window_out_h; for(class="type">int h = class="num">0; h < window_out_h; h ++) { class="type">int shift_g = k * window_out + head * window_out_h + h; class="type">int shift_w = (stop - k - class="num">1) * step + (i % step) / window_in_h + head * (window_in_h + class="num">1) + h * (window_in_h + class="num">1); if(shift_g >= outputs || shift_w >= (window_in_h + class="num">1) * window_out) break; class="type">float grad = matrix_g[shift_out + shift_g + shift_var_out]; sum += grad * matrix_w[shift_w + shift_var_w]; } } matrix_ig[shift_var_in + i] = Deactivation(sum, out, activation); } class CNeuronMHConvOCL : class="kw">public CNeuronConvOCL { class="kw">protected: class="type">uint iHeads; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override;
「多头卷积层的初始化与权重分配」
在 MT5 的 OpenCL 神经网络扩展里,CNeuronMHConvOCL 用多头卷积把长窗口拆成若干子段并行处理。构造函数默认 iHeads(1),也就是退化为单头;想做多时间尺度特征提取,得在 Init 里显式传 heads 参数。 Init 方法先调基类 CNeuronProofOCL::Init,把输出维度锁成 units_count * window_out * variables,优化器硬编码 ADAM。随后 iHeads 被夹在 [1, window] 区间:MathMax(MathMin(heads, window), 1) 意味着你传超过窗口长度的头数毫无意义,传 0 也会被拉回 1。 权重缓冲区按 window_h = (iWindow + heads - 1) / heads 算每个头的子窗口,总参数量 count = (window_h + 1) * iWindowOut * iVariables。初始化缩放因子 k = 1 / sqrt(window_h + 1),循环里用 (GenerateWeight() * 2 * k - k) * WeightsMultiplier 填权重——这是带限幅的 Xavier 式初始化,head 越多子窗口越小,k 越大,单层方差倾向更稳。 动量缓冲区 FirstMomentumConv 按 count 清零并推到 GPU。若 Reserve 或 BufferCreate 任一失败,Init 直接返 false,层构建中断。开 MT5 把 heads 从 1 调到 8 对比 count 与显存占用,能直观验证子窗口切分逻辑。
class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">public: CNeuronMHConvOCL(class="type">void) : iHeads(class="num">1) {}; ~CNeuronMHConvOCL(class="type">void) {}; class=class="str">"cmt">//--- 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, class="type">uint heads, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) const class="kw">override { class="kw">return defNeuronMHConvOCL; } class=class="str">"cmt">//--- methods for working with files class="kw">virtual class="type">bool Save(class="type">int const file_handle) class="kw">override; class="kw">virtual class="type">bool Load(class="type">int const file_handle) class="kw">override; }; class="type">bool CNeuronMHConvOCL::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, class="type">uint heads, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronProofOCL::Init(numOutputs, myIndex, open_cl, window, step, units_count * window_out * variables, ADAM, batch)) class="kw">return false; iWindowOut = window_out; iVariables = variables; iHeads = MathMax(MathMin(heads, window), class="num">1); const class="type">int window_h = class="type">int((iWindow + heads - class="num">1) / heads); const class="type">int count = class="type">int((window_h + class="num">1) * iWindowOut * iVariables); if(!WeightsConv) { WeightsConv = new CBufferFloat(); if(!WeightsConv) class="kw">return false; } if(!WeightsConv.Reserve(count)) class="kw">return false; class="type">float k = (class="type">float)(class="num">1 / sqrt(window_h + 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 false; } if(!WeightsConv.BufferCreate(OpenCL)) class="kw">return false; if(!FirstMomentumConv) { FirstMomentumConv = new CBufferFloat(); if(!FirstMomentumConv) class="kw">return false; } if(!FirstMomentumConv.BufferInit(count, class="num">0.0)) class="kw">return false; if(!FirstMomentumConv.BufferCreate(OpenCL))
多头卷积与前向网络的初始化衔接
这段代码片段展示了一个多头卷积层容器 CNeuronMHConvOCL 在初始化时的内存与 OpenCL 缓冲申请逻辑,以及继承自 CNeuronBaseOCL 的前向网络类 CNeuronMHFeedForward 的骨架。
先看卷积缓冲的失败兜底:若 SecondMomentumConv 指针为空,就 new 一个 CBufferFloat 实例,再次判空失败直接 return false;随后调用 BufferInit(count, 0.0) 把长度 count 的浮点缓冲预填 0.0,再 BufferCreate(OpenCL) 把缓冲推到 OpenCL 设备。任一环节返回 false 都会中断 Init,避免悬空 GPU 资源。
注意 if(!!DeltaWeightsConv) delete DeltaWeightsConv; 这一行——双叹号把指针强转布尔,若旧权重缓冲存在就先释放,防止重复 Init 时内存泄漏。
CNeuronMHFeedForward 内部用 acConvolutions[2] 固定挂了两个卷积子层,说明该网络结构预设双头并行特征提取。其 Init 参数暴露了关键维度:window 与 window_out 控制时间窗映射,units_count、variables、heads 决定多头拆分粒度,batch 影响 OpenCL 核的并行粒度。开 MT5 把 heads 从 2 调到 4,可能改变显存占用与收敛倾向,外汇与贵金属品种上需警惕过拟合带来的回测失真高风险。
class="kw">return false; class=class="str">"cmt">//--- if(!SecondMomentumConv) { SecondMomentumConv = new CBufferFloat(); if(!SecondMomentumConv) class="kw">return false; } if(!SecondMomentumConv.BufferInit(count, class="num">0.0)) class="kw">return false; if(!SecondMomentumConv.BufferCreate(OpenCL)) class="kw">return false; if(!!DeltaWeightsConv) class="kw">delete DeltaWeightsConv; class=class="str">"cmt">//--- class="kw">return true; } class CNeuronMHFeedForward : class="kw">public CNeuronBaseOCL { class="kw">protected: CNeuronMHConvOCL acConvolutions[class="num">2]; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">public: CNeuronMHFeedForward(class="type">void) {}; ~CNeuronMHFeedForward(class="type">void) {}; class=class="str">"cmt">//--- 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 window_out, class="type">uint units_count, class="type">uint variables, class="type">uint heads, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) const class="kw">override { class="kw">return defNeuronMHFeedForward; } class=class="str">"cmt">//--- methods for working with files class="kw">virtual class="type">bool Save(class="type">int const file_handle) class="kw">override; class="kw">virtual class="type">bool Load(class="type">int const file_handle) class="kw">override; class=class="str">"cmt">//--- class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj) class="kw">override; }; class="type">bool CNeuronMHFeedForward::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_out, class="type">uint units_count, class="type">uint variables, class="type">uint heads, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) {
◍ 多头前馈与交叉注意力的初始化链路
CNeuronMHFeedForward 的初始化里,第一层卷积 acConvolutions[0] 用 GELU 激活,第二层 acConvolutions[1] 故意设成 None 并接管梯度回传,这种「前层非线性 + 后层线性」的搭配在 MT5 的 OpenCL 神经网里常用于抑制过拟合。 feedForward 方法用 for 循环把 acConvolutions 串成链:prev 指针从外部 NeuronOCL 起,每过一层就换成当前卷积层的指针,最后调用 SumAndNormilize 做窗口长度为 acConvolutions[0].GetWindow() 的求和归一。注意归一时倒数第二个参数填 1,代表在特征维度上做缩放。 反向的 calcInputGradients 从倒数第二层往前循环到 0,先算隐藏梯度再回灌给 NeuronOCL;若 NeuronOCL 自身激活为 None 就直接 SumAndNormilize 梯度,否则先走 DeActivation 再归一。外汇与贵金属行情噪声大,这类结构在 EURUSD 15M 上回测可能降低伪信号率,但实盘仍属高风险,参数窗口不对容易欠拟合。 CNeuronCrossDMHAttention 直接继承 CNeuronRMAT,说明交叉多头注意力准备复用相对位置矩阵的逻辑,开 MT5 把这两段挂到自定义指标里跑一遍,能直观看到 GELU 层输出的分布差异。
if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count * variables, optimization_type, batch)) class="kw">return false; if(!acConvolutions[class="num">0].Init(class="num">0, class="num">0, OpenCL, window, window, window_out, units_count, variables, heads, optimization, iBatch)) class="kw">return false; acConvolutions[class="num">0].SetActivationFunction(GELU); if(!acConvolutions[class="num">1].Init(class="num">0, class="num">1, OpenCL, window_out, window_out, window, units_count, variables, heads, optimization, iBatch)) class="kw">return false; acConvolutions[class="num">1].SetActivationFunction(None); if(!SetGradient(acConvolutions[class="num">1].getGradient(), true)) class="kw">return false; SetActivationFunction(None); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronMHFeedForward::feedForward(CNeuronBaseOCL *NeuronOCL) { CObject *prev = NeuronOCL; for(class="type">uint i = class="num">0; i < acConvolutions.Size(); i++) { if(!acConvolutions[i].FeedForward(prev)) class="kw">return false; prev = GetPointer(acConvolutions[i]); } if(!SumAndNormilize(NeuronOCL.getOutput(), acConvolutions[acConvolutions.Size() - class="num">1].getOutput(), Output, acConvolutions[class="num">0].GetWindow(), true, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronMHFeedForward::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) class="kw">return false; for(class="type">int i = (class="type">int)acConvolutions.Size() - class="num">2; i >= class="num">0; i--) { if(!acConvolutions[i].calcHiddenGradients(acConvolutions[i + class="num">1].AsObject())) class="kw">return false; } if(!NeuronOCL.calcHiddenGradients(acConvolutions[class="num">0].AsObject())) class="kw">return false; if(NeuronOCL.Activation() == None) { if(!SumAndNormilize(NeuronOCL.getGradient(), Gradient, NeuronOCL.getGradient(), acConvolutions[class="num">0].GetWindow(), false, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return false; } else { if(!DeActivation(NeuronOCL.getOutput(), NeuronOCL.getPrevOutput(), Gradient, NeuronOCL.Activation()) || !SumAndNormilize(NeuronOCL.getGradient(), NeuronOCL.getPrevOutput(), NeuronOCL.getGradient(), acConvolutions[class="num">0].GetWindow(), false, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class CNeuronCrossDMHAttention : class="kw">public CNeuronRMAT {