神经网络变得简单(第 79 部分):在状态上下文中的特征聚合查询(FAQ)·进阶篇
(2/3)·静态环境描述漏掉了价格变动的动态概率,这篇进阶拆解如何用相邻帧特征约束查询来补上
不少交易者把历史价量直接堆进模型状态就当完整环境,却没意识到模型从未估测状态变化的动态。价格走势里变动的节奏有时比静态截面更能暗示后续走向,沿用图像检测的老思路容易在快速波动里掉链子。
「交叉注意力层的初始化骨架」
在 MT5 的 OpenCL 神经网络扩展里,CNeuronCrossAttention 类把多头交叉注意力拆成了 Q 与 KV 两套独立嵌入。构造函数接收 window、window_key、heads、units_count 等参数,用于描述查询序列长度、键值序列长度与头数,类型标识固定返回 defNeuronCrossAttenOCL。 Init 方法第一步先调用基类 CNeuronBaseOCL::Init,传入 window*units_count 作为展开后的输入宽度;随后用 fmax(x,1) 把六个维度参数全部下限钳到 1,避免零维导致内核崩溃。 Q_Embedding 的第三维设为 iWindowKey*iHeads,KV_Embedding 则是 2*iWindowKey*iHeads——后者多出来的一倍对应 Key 与 Value 拼接。两者激活函数都显式置为 None,说明归一化与缩放交给后续 softmax 处理。 下面这段是 Init 前半部分的可直接拷进自定义EA验证的片段:
class="type">bool CNeuronCrossAttention::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_key, class="type">uint heads, class="type">uint units_count, class="type">uint window_k, class="type">uint units_k, 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 = fmax(window, class="num">1); iWindowKey = fmax(window_key, class="num">1); iUnits = fmax(units_count, class="num">1); iWindow_K = fmax(window_k, class="num">1); iUnits_K = fmax(units_k, class="num">1); iHeads = fmax(heads, class="num">1); activation = None; if(!Q_Embedding.Init(class="num">0, class="num">0, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, optimization_type, batch)) class="kw">return false; Q_Embedding.SetActivationFunction(None); if(!KV_Embedding.Init(class="num">0, class="num">0, OpenCL, iWindow_K, iWindow_K, class="num">2 * iWindowKey * iHeads, iUnits_K, optimization_type, batch)) class="kw">return false; KV_Embedding.SetActivationFunction(None);
◍ 交叉注意力层的显存与内核装配
在 MT5 的 OpenCL 环境里搭交叉注意力,第一步是给 Score 矩阵开显存:大小为 sizeof(float) 乘以 iUnits * iUnits_K * iHeads,标记 CL_MEM_READ_WRITE。若 AddBuffer 返回 INVALID_HANDLE 直接 return false,这一步没过后面全白搭。 多头注意力输出、W0 映射、AttentionOut 以及两层前馈 FF[0]、FF[1] 都用 Init(0,0,OpenCL,...) 初始化,且全部 SetActivationFunction(None)——也就是说该层不做非线性激活,纯粹走线性变换与注意力加权。 梯度句柄要在 FF[1] 初始化后接管:先 BufferFree 再 delete 旧 Gradient,最后 Gradient = FF[1].getGradient()。这样反向传播只追前馈末层的梯度,显存占用倾向更可控。 attentionOut 方法里,global_work_size 设成 {iUnits, iUnits_K, iHeads},而 local_work_size 是 {1, iUnits_K, 1},相当于每个头内把 K 维压到一个本地工作组。四个 SetArgumentBuffer 分别绑 Q_Embedding、KV_Embedding、ScoreIndex、MHAttentionOut 的输出索引,任一步 GetLastError 非 0 就 printf 报错并退出,开 MT5 跑时若控制台刷出 'Error of set parameter kernel' 多半是索引绑错。
ScoreIndex = OpenCL.AddBuffer(class="kw">sizeof(class="type">float) * iUnits * iUnits_K * iHeads, CL_MEM_READ_WRITE); if(ScoreIndex == INVALID_HANDLE) class="kw">return false; if(!MHAttentionOut.Init(class="num">0, class="num">0, OpenCL, iWindowKey * iUnits * iHeads, optimization_type, batch)) class="kw">return false; MHAttentionOut.SetActivationFunction(None); if(!W0.Init(class="num">0, class="num">0, OpenCL, iWindowKey * iHeads, iWindowKey * iHeads, iWindow, iUnits, optimization_type, batch)) class="kw">return false; W0.SetActivationFunction(None); if(!AttentionOut.Init(class="num">0, class="num">0, OpenCL, iWindow * iUnits, optimization_type, batch)) class="kw">return false; AttentionOut.SetActivationFunction(None); if(!FF[class="num">0].Init(class="num">0, class="num">0, OpenCL, iWindow, iWindow, class="num">4 * iWindow, iUnits, optimization_type, batch)) class="kw">return false; if(!FF[class="num">1].Init(class="num">0, class="num">0, OpenCL, class="num">4 * iWindow, class="num">4 * iWindow, iWindow, iUnits, optimization_type, batch)) class="kw">return false; for(class="type">int i = class="num">0; i < class="num">2; i++) FF[i].SetActivationFunction(None); Gradient.BufferFree(); class="kw">delete Gradient; Gradient = FF[class="num">1].getGradient(); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronCrossAttention::attentionOut(class="type">void) { if(!OpenCL) class="kw">return false; class=class="str">"cmt">//--- class="type">uint global_work_offset[class="num">3] = {class="num">0}; class="type">uint global_work_size[class="num">3] = {iUnitsclass=class="str">"cmt">/*Q units*/, iUnits_Kclass=class="str">"cmt">/*K units*/, iHeads}; class="type">uint local_work_size[class="num">3] = {class="num">1, iUnits_K, class="num">1}; ResetLastError(); if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_q, Q_Embedding.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_kv, KV_Embedding.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_score, ScoreIndex)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_out, MHAttentionOut.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; }
交叉注意力前传与内部梯度的 OpenCL 落地
在 MT5 的 OCL 神经网络实现里,CNeuronCrossAttention::feedForward 把一次交叉注意力拆成可验证的串行步骤:Q_Embedding 和 KV_Embedding 各自前传,再走 attentionOut(),随后 W0 映射、SumAndNormilize 做残差归一,最后两层 FF[] 全连接加第二次残差归一。任何一步返回 false 都会中断,实盘加载自定义模型时若某层维度不匹配,这里就是第一现场。 attentionOut() 内核执行前先用 SetArgument 把窗口键长 iWindowKey 以 int 强转写入,再 Execute 三维任务网格(3 维参数、global_work_offset 全 0、global_work_size 与 local_work_size 由调用方给定)。若 SetArgument 或 Execute 失败,printf 会带 __FUNCTION__ 与 __LINE__ 报出具体错误码,方便在 MetaEditor 里直接定位到行。 反向阶段的 AttentionInsideGradients 同样依赖 OpenCL 句柄非空,否则直接返回 false。它把 Q/KV 的输出与梯度缓冲区的索引通过 SetArgumentBuffer 逐个绑给 def_k_MH2AttentionInsideGradients 内核,global_work_size 固定为 {iUnits, iWindowKey, iHeads} 三维。想验证多头划分是否合理,把 iHeads 改小后重编译,若梯度收敛变慢即说明头数本身在起作用。 外汇与贵金属行情高波动,GPU 加速的推理只是降低延迟,不替代风控;在 MT5 策略测试器里用历史 tick 跑一遍该类的 feedForward,核对各缓冲区索引是否越界,比盲目上实盘更稳妥。
if(!OpenCL.SetArgument(def_k_MH2AttentionOut, def_k_mh2ao_dimension, (class="type">int)iWindowKey)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.Execute(def_k_MH2AttentionOut, class="num">3, global_work_offset, global_work_size, local_work_size)) { printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError()); class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronCrossAttention::feedForward(CNeuronBaseOCL *NeuronOCL, CNeuronBaseOCL *Context) { class=class="str">"cmt">//--- if(!Q_Embedding.FeedForward(NeuronOCL)) class="kw">return false; class=class="str">"cmt">//--- if(!KV_Embedding.FeedForward(Context)) class="kw">return false; if(!attentionOut()) class="kw">return false; if(!W0.FeedForward(GetPointer(MHAttentionOut))) class="kw">return false; if(!SumAndNormilize(W0.getOutput(), NeuronOCL.getOutput(), AttentionOut.getOutput(), iWindow)) class="kw">return false; if(!FF[class="num">0].FeedForward(GetPointer(AttentionOut))) class="kw">return false; if(!FF[class="num">1].FeedForward(GetPointer(FF[class="num">0]))) class="kw">return false; if(!SumAndNormilize(FF[class="num">1].getOutput(), AttentionOut.getOutput(), Output, iWindow)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronCrossAttention::AttentionInsideGradients(class="type">void) { if(!OpenCL) class="kw">return false; class=class="str">"cmt">//--- class="type">uint global_work_offset[class="num">3] = {class="num">0}; class="type">uint global_work_size[class="num">3] = {iUnits, iWindowKey, iHeads}; ResetLastError(); if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionInsideGradients, def_k_mh2aig_q, Q_Embedding.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionInsideGradients, def_k_mh2aig_qg, Q_Embedding.getGradientIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionInsideGradients, def_k_mh2aig_kv, KV_Embedding.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionInsideGradients, def_k_mh2aig_kvg, KV_Embedding.getGradientIndex()))
「交叉注意力层的梯度回传与描述符构建」
在 MT5 的 OpenCL 神经网络实现里,交叉注意力层的内层梯度核 def_k_MH2AttentionInsideGradients 需要依次绑定四个参数缓冲:键索引、得分索引、输出梯度索引以及 K 侧单元数 iUnits_K。任何一次 SetArgumentBuffer 或 SetArgument 失败都会通过 printf 打出函数名、错误码与行号,并直接 return false,这意味着 GPU 侧参数未就位时训练不会往下走。
内核实际执行时指定了 3 维工作空间:OpenCL.Execute(def_k_MH2AttentionInsideGradients, 3, global_work_offset, global_work_size)。若执行返回失败,同样打印错误并退出,训练循环可能在这一步静默中断,排查时优先看 Experts 日志里的 Error of execution kernel。
calcInputGradients 方法揭示了反向传播的调用顺序:先算前馈子层 FF[1]、FF[0] 的梯度,再做 SumAndNormilize 归一,随后回溯 W0、注意力内部梯度、KV 与 Q 嵌入。注意 SumAndNormilize 被调用两次,第二次把 prevLayer.getGradient() 同时作为源和目标传入,属于原地累加写法。
CreateDescriptions 负责懒初始化对象数组:当传入的 dot 或 decoder 指针为空时,函数内部 new CArrayObj() 补建,若 new 失败则 return false。在加载自定义 AI 模型前,可单步跟这个函数确认描述符容器是否被正确创建,避免后续网络装配报空指针。
{
printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
class="kw">return false;
}
if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionInsideGradients, def_k_mh2aig_score, ScoreIndex))
{
printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
class="kw">return false;
}
if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionInsideGradients, def_k_mh2aig_outg, MHAttentionOut.getGradientIndex()))
{
printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
class="kw">return false;
}
if(!OpenCL.SetArgument(def_k_MH2AttentionInsideGradients, def_k_mh2aig_kunits, (class="type">int)iUnits_K))
{
printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
class="kw">return false;
}
if(!OpenCL.Execute(def_k_MH2AttentionInsideGradients, class="num">3, global_work_offset, global_work_size))
{
printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
class="kw">return false;
}
class=class="str">"cmt">//---
class="kw">return true;
}
class="type">bool CNeuronCrossAttention::calcInputGradients(CNeuronBaseOCL *prevLayer, CNeuronBaseOCL *Context)
{
if(!FF[class="num">1].calcInputGradients(GetPointer(FF[class="num">0])))
class="kw">return false;
if(!FF[class="num">0].calcInputGradients(GetPointer(AttentionOut)))
class="kw">return false;
if(!SumAndNormilize(FF[class="num">1].getGradient(), AttentionOut.getGradient(), W0.getGradient(), iWindow, false))
class="kw">return false;
if(!W0.calcInputGradients(GetPointer(MHAttentionOut)))
class="kw">return false;
if(!AttentionInsideGradients())
class="kw">return false;
if(!KV_Embedding.calcInputGradients(Context))
class="kw">return false;
if(!Q_Embedding.calcInputGradients(prevLayer))
class="kw">return false;
if(!SumAndNormilize(prevLayer.getGradient(), W0.getGradient(), prevLayer.getGradient(), iWindow, false))
class="kw">return false;
class=class="str">"cmt">//---
class="kw">return true;
}
class="type">bool CreateDescriptions(CArrayObj *dot, CArrayObj *decoder, CArrayObj *actor, CArrayObj *critic)
{
class=class="str">"cmt">//---
CLayerDescription *descr;
class=class="str">"cmt">//---
if(!dot)
{
dot = new CArrayObj();
if(!dot)
class="kw">return false;
}
if(!decoder)
{
decoder = new CArrayObj();◍ 解码器与 actor 网络的层级装配
在 MT5 的强化学习模型里,decoder、actor、critic 三个容器对象必须先做空指针兜底,任一 new 失败就直接 return false,避免后续层描述写入野指针。 解码器第一层从潜变量层 LatentLayer 取描述,把 po.count * po.window 作为基础神经元数,激活函数设 None、优化器 ADAM;这一层输出维度直接由潜层规模决定。 第二层用 defNeuronFAQOCL,units 拷入 {QueryCount, po.count},window_out 固定 16、step=4。第三层接编码器末层做交叉注意力(defNeuronCrossAttenOCL),windows 设 {16, encoder.window},同样 window_out=16、step=4。 actor 网络输入层复用 decoder 末层,prev_count = units[0] * windows[0];其第一层用 defNeuronConcatenate,count=LatentCount、激活 SIGMOID,把账户描述 AccountDescr 作为 step 参入网。外汇与贵金属杠杆高,这套结构只是网络装配,实盘前务必在策略测试器跑回测验证梯度更新是否收敛。
if(!decoder) { decoder = new CArrayObj(); if(!decoder) class="kw">return false; } if(!actor) { actor = new CArrayObj(); if(!actor) class="kw">return false; } if(!critic) { critic = new CArrayObj(); if(!critic) class="kw">return false; } class=class="str">"cmt">//--- Decoder decoder.Clear(); class=class="str">"cmt">//--- Input layer CLayerDescription *po = dot.At(LatentLayer); if(!po || !(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = po.count * po.window; descr.activation = None; descr.optimization = ADAM; if(!decoder.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 = defNeuronFAQOCL; { class="type">int temp[] = {QueryCount, po.count}; ArrayCopy(descr.units, temp); } descr.window = po.window; descr.window_out = class="num">16; descr.optimization = ADAM; descr.step = class="num">4; descr.activation = None; if(!decoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2 CLayerDescription *encoder = dot.At(dot.Total() - class="num">1); if(!encoder || !(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronCrossAttenOCL; { class="type">int temp[] = {QueryCount, encoder.count}; ArrayCopy(descr.units, temp); } { class="type">int temp[] = {class="num">16, encoder.window}; ArrayCopy(descr.windows, temp); } descr.window_out = class="num">16; descr.step = class="num">4; descr.activation = None; descr.optimization = ADAM; if(!decoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- Actor actor.Clear(); class=class="str">"cmt">//--- Input layer encoder = decoder.At(decoder.Total() - class="num">1); if(!encoder || !(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = encoder.units[class="num">0] * encoder.windows[class="num">0]; descr.activation = None; descr.optimization = ADAM; if(!actor.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 = defNeuronConcatenate; descr.count = LatentCount; descr.window = prev_count; descr.step = AccountDescr; descr.optimization = ADAM; descr.activation = SIGMOID; if(!actor.Add(descr)) {
Actor 与 Critic 的逐层堆叠写法
这段构建逻辑把策略网络(Actor)拆成四层,Critic 则复用 Actor 前两层再做三层扩展。每一层都先 new 一个 CLayerDescription,赋值失败立刻 return false 并 delete,避免 MT5 里悬空指针拖垮整个 EA。 Actor 第二层用 LatentCount 个 sigmoid 神经元做隐空间压缩,第三层直接铺 2 * NActions 个无激活节点,第四层切到 defNeuronVAEOCL 类型、输出维度等于 NActions。注意第三层 count 写成 2 * NActions 意味着动作均值与对数方差各占一半,是 VAE 式策略头的典型排布。 Critic 的 Input 和 layer 1 直接 Copy(actor.At(0)) 和 Copy(actor.At(1)),layer 1 额外设 step = NActions 并套 sigmoid,后续两层分别映射到 LatentCount 与 NRewards。Rewards 层用 None 激活,说明价值估计走线性输出。 在 MT5 里把 NActions 设为 4、NRewards 设为 1 时,Actor 第三层会生成 8 个节点,Critic 末层生成 1 个节点。改这几个宏定义就能直接看网络规模变化,外汇与贵金属行情下用这类结构做强化学习策略,回测过拟合概率偏高,属高风险玩法。
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 = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = SIGMOID; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = class="num">2 * NActions; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronVAEOCL; descr.count = NActions; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- Critic critic.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.Copy(actor.At(class="num">0)); if(!critic.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.Copy(actor.At(class="num">1)); descr.step = NActions; descr.optimization = ADAM; descr.activation = SIGMOID; if(!critic.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 = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = SIGMOID; descr.optimization = ADAM; if(!critic.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = NRewards; descr.activation = None; descr.optimization = ADAM; if(!critic.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } CNet DOT; CNet Decoder; CNet Actor;