交易中的神经网络:基于双注意力的趋势预测模型·进阶篇
(2/3)·单变量序列和单层网络为何总在拐点失灵?这篇拆开双特征提取与注意力机制的内部链路
TPM 编码器的前向传播与 dropout 内核装配
在自定义 TPM 编码器里,feedForward 的第一步是特征提取:cFeatureExtraction.FeedForward 若返回 false 则直接退出,保证后续张量维度合法。之后把隐藏态与记忆态做 Concat,再依次过 cConcatenated、cSoftMax 两层前向,拿到 softmax 输出索引 map。 若 bTSinRow 为 false,还会对 softmax 输出做转置,此时 map 切换为 cTranspose 的输出索引。这个分支直接影响 dropout 内核读取哪块显存缓冲,调试时容易忽略。 global_work_size[0] 被设为 (cSoftMax.Neurons() + 3) / 4,也就是按 4 个神经元一组派发 OpenCL work-item。如果你改了 softmax 神经元数,这组除法要同步复核,否则可能漏算尾部神经元。 下面连续四次 SetArgumentBuffer / SetArgument 把 dropout 内核的输入、映射、输出缓冲和维度绑好,任何一步失败都 printf 出函数名、错误码和行号。最后 OpenCL.Execute 用一维偏移与尺寸启动内核,失败同样返回 false,之后才进 LSTM 前向。
if(!cTemp.BufferInit(variables * lenth, class="num">0) || !cTemp.BufferCreate(OpenCL)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronTPMEncoder::feedForward(CNeuronBaseOCL *NeuronOCL) { class=class="str">"cmt">//--- FEATURE EXTRACTION if(!cFeatureExtraction.FeedForward(NeuronOCL)) class="kw">return false; class=class="str">"cmt">//--- Memory and Hidden if(!Concat(m_iHiddenState, m_iMemory, m_iHiddenState, m_iMemory, cMemAndHidden.getOutputIndex(), class="num">1, class="num">1, class="num">0, class="num">0, Neurons())) class="kw">return false; if(!cConcatenated.FeedForward(cFeatureExtraction.AsObject(), cMemAndHidden.getOutput())) class="kw">return false; if(!cSoftMax.FeedForward(cConcatenated.AsObject())) class="kw">return false; class="type">int map = cSoftMax.getOutputIndex(); if(!bTSinRow) { if(!cTranspose.FeedForward(cSoftMax.AsObject())) class="kw">return false; map = cTranspose.getOutputIndex(); } class="type">uint global_work_offset[class="num">1] = {class="num">0}; class="type">uint global_work_size[class="num">1]; global_work_size[class="num">0] = class="type">int(cSoftMax.Neurons() + class="num">3) / class="num">4; ResetLastError(); if(!OpenCL.SetArgumentBuffer(def_k_Dropout, def_k_dout_input, cFeatureExtraction.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_Dropout, def_k_dout_map, map)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_Dropout, def_k_dout_out, cAttentionOut.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_Dropout, def_k_dout_dimension, cSoftMax.Neurons())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.Execute(def_k_Dropout, class="num">1, global_work_offset, global_work_size)) { printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError()); class="kw">return false; } class=class="str">"cmt">//--- LSTM if(!CNeuronLSTMOCL::feedForward(cAttentionOut.AsObject())) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronTPMEncoder::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) class="kw">return false;
◍ 反向传播里卷积梯度的 OpenCL 绑定
LSTM 输出梯度先交给 CNeuronLSTMOCL::calcInputGradients 算完,任何一步返回 false 就直接中断反向传播,说明这一层对上游梯度依赖是硬性的。 下面这段把卷积隐藏层梯度核 def_k_CGConv_HiddenGradient 的参数逐个塞进 OpenCL buffer。global_work_size[0] 直接取 cSoftMax.Neurons() 的数量,意味着 GPU 上每个 softmax 神经元对应一个工作项,规模随输出维度线性扩张。 SetArgumentBuffer 连续绑定了特征提取输出、临时矩阵、转置或 softmax 的输出与梯度、以及注意力输出梯度,任意一次绑定失败都用 printf 打出函数名、错误码和行号后退出。激活函数参数用 NeuronOCL.Activation() 传入,后接一个 int(None) 的占位激活,Execute 以 1 维偏移启动。 执行完后若 bTSinRow 为真,才让 cSoftMax 向 cTranspose 算隐藏梯度;否则跳过。最后 cConcatenated 拿 softmax 对象做梯度回传,NULL 占位表示不传额外浮点缓冲。外汇与贵金属模型训练涉及杠杆与滑点,GPU 加速只解决算力,不消除过拟合风险。 别把绑定顺序当随便写的 MT5 里 OpenCL 核参数位置必须和 kernel 源码声明一一对应,调交换 def_k_cgc_matrix_s 与 def_k_cgc_matrix_sg 的绑定顺序,回测可能不报错但梯度全错,肉眼难查。
if(!CNeuronLSTMOCL::calcInputGradients(cAttentionOut.AsObject())) class="kw">return false; class=class="str">"cmt">//--- class="type">uint global_work_offset[class="num">1] = {class="num">0}; class="type">uint global_work_size[class="num">1]; global_work_size[class="num">0] = cSoftMax.Neurons(); ResetLastError(); if(!OpenCL.SetArgumentBuffer(def_k_CGConv_HiddenGradient, def_k_cgc_matrix_f, cFeatureExtraction.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_CGConv_HiddenGradient, def_k_cgc_matrix_fg, cTemp.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_CGConv_HiddenGradient, def_k_cgc_matrix_s, (bTSinRow ? cSoftMax.getOutputIndex() : cTranspose.getOutputIndex()))) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_CGConv_HiddenGradient, def_k_cgc_matrix_sg, (bTSinRow ? cSoftMax.getGradientIndex() : cTranspose.getGradientIndex()))) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_CGConv_HiddenGradient, def_k_cgc_matrix_g, cAttentionOut.getGradientIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_CGConv_HiddenGradient, def_k_cgc_activationf, NeuronOCL.Activation())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_CGConv_HiddenGradient, def_k_cgc_activations, class="type">int(None))) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.Execute(def_k_CGConv_HiddenGradient, class="num">1, global_work_offset, global_work_size)) { printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError()); class="kw">return false; } if(bTSinRow) { if(!cSoftMax.calcHiddenGradients(cTranspose.AsObject())) class="kw">return false; } if(!cConcatenated.calcHiddenGradients((CObject*)cSoftMax.AsObject(),(CBufferFloat *)NULL,(CBufferFloat *)NULL) ||
「TPM 编码器的梯度回传与权重更新实现」
下面这段 CNeuronTPMEncoder 的反向传播收尾,先对拼接层做反激活并回传梯度,再让特征提取层算隐藏梯度,最后用临时缓冲做反激活与求和归一化,任何一步失败直接返回 false。 反向流程里 DeActivation 与 SumAndNormilize 的配合是关键:前者把输出层激活导数剥离,后者以系数 1 且不对梯度取反(false 参数)完成累加归一,避免 LSTM 门控梯度被重复缩放。 权重更新阶段分三条链路:注意力输出层、特征提取层、以及拼接层(依赖特征提取对象与记忆隐藏输出),三层权重独立更新但顺序不可颠倒,否则前层梯度引用会指向未同步状态。 CNeuronTPM 类在 LSTM 基类上挂了编码器、PLR 特征提取、SoftMax 等 8 个成员,Init 接口要求同时传入 variables、lenth、hidden_size 与 batch,在 MT5 里调参时若 hidden_size 与特征提取层维度不匹配,feedForward 会在运行时报 CL 缓冲越界。
if(!DeActivation(cConcatenated.getOutput(), cConcatenated.getGradient(), cConcatenated.getGradient(), cConcatenated.Activation())) class="kw">return false; if(!cFeatureExtraction.calcHiddenGradients(cConcatenated.AsObject(), cMemAndHidden.getOutput(), cMemAndHidden.getGradient())) class="kw">return false; if(!DeActivation(cFeatureExtraction.getOutput(), GetPointer(cTemp), GetPointer(cTemp), NeuronOCL.Activation()) || !SumAndNormilize(cFeatureExtraction.getGradient(), GetPointer(cTemp), cFeatureExtraction.getGradient(), class="num">1, false)) class="kw">return false; if(!NeuronOCL.calcHiddenGradients(cFeatureExtraction.AsObject())) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronTPMEncoder::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(!CNeuronLSTMOCL::updateInputWeights(cAttentionOut.AsObject())) class="kw">return false; if(!cFeatureExtraction.UpdateInputWeights(NeuronOCL)) class="kw">return false; if(!cConcatenated.UpdateInputWeights(cFeatureExtraction.AsObject(), cMemAndHidden.getOutput())) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class CNeuronTPM : class="kw">public CNeuronLSTMOCL { class="kw">protected: CNeuronTPMEncoder cEncoder; CNeuronPLROCL cFeatureExtraction; CNeuronBaseOCL cMemAndHidden; CNeuronConcatenate cConcatenated; CNeuronSoftMaxOCL cSoftMax; CNeuronBaseOCL cAttentionOut; CNeuronConcatenate cAttAndFeature; CBufferFloat cTemp; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override; class=class="str">"cmt">//--- class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override; class=class="str">"cmt">//--- class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">public: CNeuronTPM(class="type">void){}; ~CNeuronTPM(class="type">void){}; class="kw">virtual class="type">bool Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint variables, class="type">uint lenth, class="type">uint hidden_size, class="type">bool ts_in_row, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//--- 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">int Type(class="type">void) class="kw">override const { class="kw">return defNeuronTPM; } class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); };
TPM 神经元的初始化与前向传播骨架
在 MT5 的 OpenCL 神经网络封装里,CNeuronTPM 的 Init 把编码器、特征提取、记忆拼接和注意力头一次性挂到同一计算图上。注意 cEncoder 与 cFeatureExtraction 的 ts_in_row 参数是互斥传入的:前者用原始值,后者取反,这决定了时序变量是按行还是按列送进 LSTM。 初始化里 cConcatenated 被强制设为 TANH 激活,而 cSoftMax 显式 SetHeads(1),说明该模块默认只跑单头注意力,hidden_size 直接等于 softmax 的输入维度。若你改多头,这里必须同步调 cAttentionOut 与 cAttAndFeature 的通道数,否则 FeedForward 会静默返回 false。 feedForward 里先跑 Encoder 和 FeatureExtraction,再用 Concat 把隐藏态与记忆态拼成 hidden_size*2 的缓冲交给 cMemAndHidden。注意力部分由 cConcatenated 接收编码器输出与记忆拼接体,经 cSoftMax 后由 OpenCL 内核做 dropout 掩码——global_work_size[0] 取 (Neurons()+3)/4,意味着内核按 4 通道向量化打包,显存对齐不对会触发 SetArgumentBuffer 报错并打印行号。 外汇与贵金属行情下用这类 GPU 网络做推理,参数错配可能导致信号延迟或异常,属高风险用法,建议先在策略测试器以 tick 级数据跑通 Init 返回值再上实盘。
class="type">bool CNeuronTPM::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint variables, class="type">uint lenth, class="type">uint hidden_size, class="type">bool ts_in_row, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronLSTMOCL::Init(numOutputs, myIndex, open_cl, hidden_size, optimization_type, batch)) class="kw">return false; if(!SetInputs(hidden_size)) class="kw">return false; if(!cEncoder.Init(class="num">0, class="num">0, OpenCL, variables, lenth, hidden_size, ts_in_row, optimization, iBatch)) class="kw">return false; if(!cFeatureExtraction.Init(class="num">0, class="num">1, OpenCL, variables, lenth, !ts_in_row, optimization, iBatch)) class="kw">return false; if(!cMemAndHidden.Init(class="num">0, class="num">2, OpenCL, hidden_size * class="num">2, optimization, iBatch)) class="kw">return false; if(!cConcatenated.Init(class="num">0, class="num">3, OpenCL, hidden_size, hidden_size, hidden_size * class="num">2, optimization, iBatch)) class="kw">return false; cConcatenated.SetActivationFunction(TANH); if(!cSoftMax.Init(class="num">0, class="num">4, OpenCL, hidden_size, optimization, iBatch)) class="kw">return false; cSoftMax.SetHeads(class="num">1); if(!cAttentionOut.Init(class="num">0, class="num">5, OpenCL, hidden_size, optimization, iBatch)) class="kw">return false; if(!cAttAndFeature.Init(class="num">0, class="num">6, OpenCL, hidden_size, hidden_size, variables * lenth, optimization, iBatch)) class="kw">return false; if(!cTemp.BufferInit(variables * lenth, class="num">0) || !cTemp.BufferCreate(OpenCL)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronTPM::feedForward(CNeuronBaseOCL *NeuronOCL) { class=class="str">"cmt">//--- Encoder if(!cEncoder.FeedForward(NeuronOCL)) class="kw">return false; class=class="str">"cmt">//--- FEATURE EXTRACTION if(!cFeatureExtraction.FeedForward(NeuronOCL)) class="kw">return false; class=class="str">"cmt">//--- Memory and Hidden if(!Concat(m_iHiddenState, m_iMemory, m_iHiddenState, m_iMemory, cMemAndHidden.getOutputIndex(), class="num">1, class="num">1, class="num">0, class="num">0, Neurons())) class="kw">return false; class=class="str">"cmt">//--- Attention if(!cConcatenated.FeedForward(cEncoder.AsObject(), cMemAndHidden.getOutput())) class="kw">return false; if(!cSoftMax.FeedForward(cConcatenated.AsObject())) class="kw">return false; class="type">uint global_work_offset[class="num">1] = {class="num">0}; class="type">uint global_work_size[class="num">1]; global_work_size[class="num">0] = class="type">int(cSoftMax.Neurons() + class="num">3) / class="num">4; ResetLastError(); if(!OpenCL.SetArgumentBuffer(def_k_Dropout, def_k_dout_input, cEncoder.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; }
◍ 编码器前向链路与层描述落地
把 dropout 核的参数绑完之后,真正跑前向时先调 Execute 并给 global_work_size 传 1 维数组,任何一步返回 false 都会在终端打印含 __LINE__ 的错误行号,方便在 MT5 策略测试器里直接定位 OpenCL 内核哪一行没绑上。 注意力输出先喂给 cAttAndFeature.FeedForward,再走 CNeuronLSTMOCL::feedForward;这两行若返回 false 则整个 encode 提前退出。外汇与贵金属行情下用这套 GPU 链路做特征提取,延迟可能随显存占用波动,属高风险实验性做法。 CreateEncoderDescriptions 里输入层节点数等于 HistoryBars * BarDescr,优化器统一用 ADAM;第二层 BatchNorm 的 batch 写死 1e4,意味着训练时按一万样本估算均值方差,小样本回测可能偏估。 第三层用了 defNeuronTPM,window 取 BarDescr、window_out 取 HistoryBars、step 强转 int(false) 即 0,相当于不滑动窗口铺满历史条数。改 LatentCount 或 HistoryBars 后必须重跑该函数重建描述,否则旧编码器维度对不上会直接 false。
if(!OpenCL.SetArgumentBuffer(def_k_Dropout, def_k_dout_map, cSoftMax.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_Dropout, def_k_dout_out, cAttentionOut.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_Dropout, def_k_dout_dimension, cSoftMax.Neurons())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.Execute(def_k_Dropout, class="num">1, global_work_offset, global_work_size)) { printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError()); class="kw">return false; } class=class="str">"cmt">//--- Attention and Features if(!cAttAndFeature.FeedForward(cAttentionOut.AsObject(), cFeatureExtraction.getOutput())) class="kw">return false; class=class="str">"cmt">//--- LSTM if(!CNeuronLSTMOCL::feedForward(cAttAndFeature.AsObject())) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CreateEncoderDescriptions(CArrayObj *encoder) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) class="kw">return false; } class=class="str">"cmt">//--- Encoder encoder.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; class="type">int prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronTPM; descr.count = LatentCount; descr.window = BarDescr; descr.window_out = HistoryBars; descr.step = class="type">int(false); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; }