交易中的神经网络:具有相对编码的变换器·进阶篇
◍ GPU 上的注意力归一与输出归约
这段 OpenCL 内核干了两件事:先把注意力系数 sc 做局部求和再归一,再把加权后的 value 向量归约写回输出缓冲。外汇与贵金属行情的高频序列喂进这类算子时,浮点异常必须先掐掉——NaN 或 Inf 直接置 0,否则后续累加会污染整块显存。
| 内核开头对 sc 做了防御:`if(isnan(sc) | isinf(sc)) sc = 0;`,随后进入按局部大小 ls 分块的循环,把每个线程算出的 sc 累加到 temp 本地数组,并用 `barrier(CLK_LOCAL_MEM_FENCE)` 保证本地内存可见性。归约采用二分法:`count = (count+1)/2` 直到 count<=1,把 temp 前半段不断加给自身,复杂度为 O(log ls) 而非 O(ls)。 |
|---|
sum 取出 temp[0] 后再次防异常,若 sum<=1e-6f 则兜底为 1,最后 sc /= sum 完成归一,写回 score[shift_s]。这一步决定了注意力权重是否真的和为 1,黄金 1 分钟图上若窗口内有效样本过少,sum 容易掉到 1e-6 量级,归一后被放大的噪声可能误导信号。
输出阶段对 dimension 维每个特征 d,取 v 与 bv 之和乘 sc 得 val,同样做本地归约,仅 k_id==0 的线程把 temp[0] 写进 out[shift_q+d]。想验证可自行把这段塞进 MT5 的 OCL 模板,把 kunits 设成 256、ls 设成 64,看 GPU 占用与数值稳定性。
if(isnan(sc) || isinf(sc)) sc = class="num">0; class=class="str">"cmt">//--- sum of exp for(class="type">int cur_k = class="num">0; cur_k < kunits; cur_k += ls) { if(k_id >= cur_k && k_id < (cur_k + ls)) { class="type">int shift_local = k_id % ls; temp[shift_local] = (cur_k == class="num">0 ? class="num">0 : temp[shift_local]) + sc; } barrier(CLK_LOCAL_MEM_FENCE); } class="type">uint count = min(ls, (class="type">uint)kunits); class=class="str">"cmt">//--- do { count = (count + class="num">1) / class="num">2; if(k_id < ls) temp[k_id] += (k_id < count && (k_id + count) < kunits ? temp[k_id + count] : class="num">0); if(k_id + count < ls) temp[k_id + count] = class="num">0; barrier(CLK_LOCAL_MEM_FENCE); } while(count > class="num">1); class=class="str">"cmt">//--- score class="type">class="kw">float sum = temp[class="num">0]; if(isnan(sum) || isinf(sum) || sum <= 1e-6f) sum = class="num">1; sc /= sum; score[shift_s] = sc; barrier(CLK_LOCAL_MEM_FENCE); class=class="str">"cmt">//--- out for(class="type">int d = class="num">0; d < dimension; d++) { class="type">class="kw">float val_v = v[shift_kv + d]; class="type">class="kw">float val_bv = bv[shift_kv + d]; class="type">class="kw">float val = sc * (val_v + val_bv); if(isnan(val) || isinf(val)) val = class="num">0; class=class="str">"cmt">//--- sum of value for(class="type">int cur_v = class="num">0; cur_v < kunits; cur_v += ls) { if(k_id >= cur_v && k_id < (cur_v + ls)) { class="type">int shift_local = k_id % ls; temp[shift_local] = (cur_v == class="num">0 ? class="num">0 : temp[shift_local]) + val; } barrier(CLK_LOCAL_MEM_FENCE); } class=class="str">"cmt">//--- count = min(ls, (class="type">uint)kunits); do { count = (count + class="num">1) / class="num">2; if(k_id < count && (k_id + count) < kunits) temp[k_id] += temp[k_id + count]; if(k_id + count < ls) temp[k_id + count] = class="num">0; barrier(CLK_LOCAL_MEM_FENCE); } while(count > class="num">1); class=class="str">"cmt">//--- if(k_id == class="num">0) out[shift_q + d] = (isnan(temp[class="num">0]) || isinf(temp[class="num">0]) ? class="num">0 : temp[class="num">0]); } } class CNeuronRelativeSelfAttention : class="kw">public CNeuronBaseOCL { class="kw">protected: class="type">uint iWindow;
相对自注意力层的类成员与接口
在 MT5 的 OpenCL 神经网络扩展里,CNeuronRelativeSelfAttention 用一组 uint 和 int 记录窗口与头数等元参数:iWindowKey 存键窗口长度,iHeads 是多头数量,iUnits 为单元数,iScore 初始化为 -1 表示尚未计算注意力分数。 该类把卷积、转置与偏置层都包成成员对象:cQuery/cKey/cValue 是 CNeuronConvOCL 卷积核,cTranspose 做转置卷积,cBKey/cBValue 及若干 CLayer 偏置负责相对位置编码。cTemp 作为 CBufferFloat 中间缓冲,避免每次前向重复申请显存。 虚函数覆盖体现了训练链路:feedForward 做前向,calcInputGradients 反传梯度,updateInputWeights 更新权重;AttentionOut 与 AttentionGraadient(原文拼写)分别管注意力输出与梯度。Init 接收 window、units_count、heads、batch 等参数,defNeuronRelativeSelfAttention 作为 Type() 返回值区分层类型。
class="type">uint iWindowKey; class="type">uint iHeads; class="type">uint iUnits; class="type">int iScore; class=class="str">"cmt">//--- CNeuronConvOCL cQuery; CNeuronConvOCL cKey; CNeuronConvOCL cValue; CNeuronTransposeOCL cTranspose; CNeuronBaseOCL cDistance; CLayer cBKey; CLayer cBValue; CLayer cGlobalContentBias; CLayer cGlobalPositionalBias; CLayer cMHAttentionPooling; CLayer cScale; CBufferFloat cTemp; class=class="str">"cmt">//--- class="kw">virtual class="type">bool AttentionOut(class="type">void); class="kw">virtual class="type">bool AttentionGraadient(class="type">void); class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">public: CNeuronRelativeSelfAttention(class="type">void) : iScore(-class="num">1) {}; ~CNeuronRelativeSelfAttention(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_key, class="type">uint units_count, 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) class="kw">override const { class="kw">return defNeuronRelativeSelfAttention; } 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">bool WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau) class="kw">override;
class="type">uint iWindowKey; class="type">uint iHeads; class="type">uint iUnits; class="type">int iScore; class=class="str">"cmt">//--- CNeuronConvOCL cQuery; CNeuronConvOCL cKey; CNeuronConvOCL cValue; CNeuronTransposeOCL cTranspose; CNeuronBaseOCL cDistance; CLayer cBKey; CLayer cBValue; CLayer cGlobalContentBias; CLayer cGlobalPositionalBias; CLayer cMHAttentionPooling; CLayer cScale; CBufferFloat cTemp; class=class="str">"cmt">//--- class="kw">virtual class="type">bool AttentionOut(class="type">void); class="kw">virtual class="type">bool AttentionGraadient(class="type">void); class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">public: CNeuronRelativeSelfAttention(class="type">void) : iScore(-class="num">1) {}; ~CNeuronRelativeSelfAttention(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_key, class="type">uint units_count, 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) class="kw">override const { class="kw">return defNeuronRelativeSelfAttention; } 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">bool WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau) class="kw">override;
「相对自注意力层的初始化链路」
在 MT5 的 OpenCL 神经网络扩展里,CNeuronRelativeSelfAttention::Init 负责把多头相对注意力所需的所有子层一次性登记到计算图。它先调用基类 CNeuronBaseOCL::Init,输入维度被强行拉成 window * units_count,这一步若失败直接返回 false,后续全免谈。 紧接着用 idx 从 0 开始自增,依次初始化 cQuery、cKey、cValue 三个卷积类成员,它们的核宽都是 iWindowKey * iHeads,输出通道为 iUnits。任何一层 Init 不通过就退出,这说明该结构对显存与参数连续性相当敏感,外汇高频序列里 units_count 设太大可能直接爆显存。 后面还挂了 cTranspose、cDistance 以及多组 CNeuronConvOCL:其中两组显式设了 TANH 激活,用来逼出有界的关键/价值偏置;最后塞进 cGlobalContentBias 的两个 CNeuronBaseOCL 中,第一个输出缓冲被 BufferInit(1,1) 后立刻 BufferWrite,相当于写死一个全局偏置种子。 开 MT5 把这段抄进自定义 EA 的神经网络头文件,改 iHeads=4、iUnits=16 跑一次 Init,看 idx 能否走到末尾不报 false,就能验证你本地 OpenCL 环境是否真接住了相对注意力层。
class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj) class="kw">override; }; class="type">bool CNeuronRelativeSelfAttention::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_key, class="type">uint units_count, class="type">uint heads, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch)) class="kw">return false; iWindow = window; iWindowKey = window_key; iUnits = units_count; iHeads = heads; class="type">int idx = class="num">0; if(!cQuery.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch)) class="kw">return false; idx++; if(!cKey.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch)) class="kw">return false; idx++; if(!cValue.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch)) class="kw">return false; idx++; if(!cTranspose.Init(class="num">0, idx, OpenCL, iUnits, iWindow, optimization, iBatch)) class="kw">return false; idx++; if(!cDistance.Init(class="num">0, idx, OpenCL, iUnits * iUnits, optimization, iBatch)) class="kw">return false; idx++; CNeuronConvOCL *conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, idx, OpenCL, iUnits, iUnits, iWindow, iUnits, class="num">1, optimization, iBatch) || !cBKey.Add(conv)) class="kw">return false; idx++; conv.SetActivationFunction(TANH); conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch) || !cBKey.Add(conv)) class="kw">return false; idx++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, idx, OpenCL, iUnits, iUnits, iWindow, iUnits, class="num">1, optimization, iBatch) || !cBValue.Add(conv)) class="kw">return false; idx++; conv.SetActivationFunction(TANH); conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch) || !cBValue.Add(conv)) class="kw">return false; idx++; CNeuronBaseOCL *neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(iWindowKey * iHeads * iUnits, idx, OpenCL, class="num">1, optimization, iBatch) || !cGlobalContentBias.Add(neuron)) class="kw">return false; idx++; CBufferFloat *buffer = neuron.getOutput(); buffer.BufferInit(class="num">1, class="num">1); if(!buffer.BufferWrite()) class="kw">return false; neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(class="num">0, idx, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch) || !cGlobalContentBias.Add(neuron)) class="kw">return false;
◍ 相对自注意力模块的初始化链路
这段初始化代码把多头注意力里的偏置、池化与缩放三层子网络逐个挂到对象上,任何一步 Init 或 Add 失败就直接 return false,意味着在 MT5 里跑自定义 OCL 神经网络时,层参数错位会令整个模型构建中断。
先看全局位置偏置:先 new 一个 CNeuronBaseOCL 做偏置载体,Init 的第二个参数是 idx(神经元索引),第三个 1 是输出维度;随后又建一个输入为 0、输出为 iWindowKey*iHeads*iUnits 的基元神经元加入 cGlobalPositionalBias,两个 idx 各加 1。
多头注意力池化 cMHAttentionPooling 里连续堆了 4 个算子:两个基元神经元、两个卷积(CNeuronConvOCL)。第一个卷积核宽 iWindow、输出通道 iUnits,激活设 TANH;第二个卷积激活设 None;SoftMax 层显式调了 SetHeads(iUnits) 切分头数。
缩放分支 cScale 用两个卷积收口,中间激活 LReLU、末尾 None,第二个卷积的窗口参数写死 4 * iWindow 做通道扩张。最后 SetGradient(conv.getGradient(), true) 把梯度回传打开,再 SetOpenCL 绑定上下文。
在 MT5 策略测试器里若报 false,优先查 idx 是否和前层连续、iWindowKey*iHeads*iUnits 乘积是否超出显存申请上限,外汇与贵金属品种上这类 GPU 网络过拟合概率偏高,需以小样本验证。
idx++; neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(iWindowKey * iHeads * iUnits, idx, OpenCL, class="num">1, optimization, iBatch) || !cGlobalPositionalBias.Add(neuron)) class="kw">return false; idx++; buffer = neuron.getOutput(); buffer.BufferInit(class="num">1, class="num">1); if(!buffer.BufferWrite()) class="kw">return false; neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(class="num">0, idx, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch) || !cGlobalPositionalBias.Add(neuron)) class="kw">return false; idx++; neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(class="num">0, idx, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch) || !cMHAttentionPooling.Add(neuron) ) class="kw">return false; idx++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, idx, OpenCL, iWindowKey * iHeads, iWindowKey * iHeads, iWindow, iUnits, class="num">1, optimization, iBatch) || !cMHAttentionPooling.Add(conv) ) class="kw">return false; idx++; conv.SetActivationFunction(TANH); conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iHeads, iUnits, class="num">1, optimization, iBatch) || !cMHAttentionPooling.Add(conv) ) class="kw">return false; idx++; conv.SetActivationFunction(None); CNeuronSoftMaxOCL *softmax = new CNeuronSoftMaxOCL(); if(!softmax || !softmax.Init(class="num">0, idx, OpenCL, iHeads * iUnits, optimization, iBatch) || !cMHAttentionPooling.Add(conv) ) class="kw">return false; softmax.SetHeads(iUnits); idx++; neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(class="num">0, idx, OpenCL, iWindowKey * iUnits, optimization, iBatch) || !cScale.Add(neuron) ) class="kw">return false; idx++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, idx, OpenCL, iWindowKey, iWindowKey, class="num">4 * iWindow, iUnits, class="num">1, optimization, iBatch) || !cScale.Add(conv) ) class="kw">return false; conv.SetActivationFunction(LReLU); idx++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, idx, OpenCL, class="num">4 * iWindow, class="num">4 * iWindow, iWindow, iUnits, class="num">1, optimization, iBatch) || !cScale.Add(conv) ) class="kw">return false; conv.SetActivationFunction(None); class=class="str">"cmt">//--- if(!SetGradient(conv.getGradient(), true)) class="kw">return false; class=class="str">"cmt">//--- SetOpenCL(OpenCL); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronRelativeSelfAttention::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cQuery.FeedForward(NeuronOCL) || !cKey.FeedForward(NeuronOCL) || !cValue.FeedForward(NeuronOCL) )
注意力层前向传播收口与 RMAT 骨架
这段是前向推理的收尾段,任何一步矩阵乘或层前向失败就直接 return false,只有全部跑通才回 true。 先看转置与距离矩阵:cTranspose 喂入 NeuronOCL 后,用 MatMul 算 NeuronOCL 输出与转置输出的乘积,维度参数是 iUnits × iWindow × iUnits × 1,这一步构建的是注意力里的相似度基底。 随后 cBKey[0]、cBValue[0] 以 cDistance 为输入做 FeedForward,而从索引 1 开始,cBKey、cBValue、cGlobalContentBias、cGlobalPositionalBias 各自链式前向,每一层吃上一层输出,循环里但凡一个节点失败就退出。 AttentionOut 之后,cMHAttentionPooling 同样链式前向,再用 MatMul 把末层与首层输出乘到 cScale[0],形状是 1 × iHeads × iWindowKey × iUnits,这是多头池化结果的缩放融合。 最后 cScale 链式前向,SumAndNormilize 以 NeuronOCL 输出和 cScale 末层为输入,在 iWindow 上做带归一化的求和写进 Output,参数里最后的 1 表示沿特定轴。外汇与贵金属行情下用这类结构做信号推断属高风险,过拟合概率不低。 收口之后类定义转向 CNeuronRMAT,它公开继承 CNeuronBaseOCL,内部只挂了一个 CLayer 成员 cLayers,并 override 了 feedForward、calcInputGradients、updateInputWeights 三个虚函数,意味着 RMAT 把多层封装进单一层对象里调度。
class="kw">return false; if(!cTranspose.FeedForward(NeuronOCL) || !MatMul(NeuronOCL.getOutput(), cTranspose.getOutput(), cDistance.getOutput(), iUnits, iWindow, iUnits, class="num">1) ) class="kw">return false; if(!((CNeuronBaseOCL*)cBKey[class="num">0]).FeedForward(cDistance.AsObject()) || !((CNeuronBaseOCL*)cBValue[class="num">0]).FeedForward(cDistance.AsObject()) ) class="kw">return false; for(class="type">int i = class="num">1; i < cBKey.Total(); i++) if(!((CNeuronBaseOCL*)cBKey[i]).FeedForward(cBKey[i - class="num">1])) class="kw">return false; for(class="type">int i = class="num">1; i < cBValue.Total(); i++) if(!((CNeuronBaseOCL*)cBValue[i]).FeedForward(cBValue[i - class="num">1])) class="kw">return false; for(class="type">int i = class="num">1; i < cGlobalContentBias.Total(); i++) if(!((CNeuronBaseOCL*)cGlobalContentBias[i]).FeedForward(cGlobalContentBias[i - class="num">1])) class="kw">return false; for(class="type">int i = class="num">1; i < cGlobalPositionalBias.Total(); i++) if(!((CNeuronBaseOCL*)cGlobalPositionalBias[i]).FeedForward(cGlobalPositionalBias[i - class="num">1])) class="kw">return false; if(!AttentionOut()) class="kw">return false; for(class="type">int i = class="num">1; i < cMHAttentionPooling.Total(); i++) if(!((CNeuronBaseOCL*)cMHAttentionPooling[i]).FeedForward(cMHAttentionPooling[i - class="num">1])) class="kw">return false; if(!MatMul(((CNeuronBaseOCL*)cMHAttentionPooling[cMHAttentionPooling.Total() - class="num">1]).getOutput(), ((CNeuronBaseOCL*)cMHAttentionPooling[class="num">0]).getOutput(), ((CNeuronBaseOCL*)cScale[class="num">0]).getOutput(), class="num">1, iHeads, iWindowKey, iUnits) ) class="kw">return false; for(class="type">int i = class="num">1; i < cScale.Total(); i++) if(!((CNeuronBaseOCL*)cScale[i]).FeedForward(cScale[i - class="num">1])) class="kw">return false; if(!SumAndNormilize(NeuronOCL.getOutput(), ((CNeuronBaseOCL*)cScale[cScale.Total() - class="num">1]).getOutput(), Output, iWindow, 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 CNeuronRMAT : class="kw">public CNeuronBaseOCL { class="kw">protected: CLayer cLayers; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">public: