交易中的神经网络:对比形态变换器·进阶篇
(2/3)·单根烛条喂给模型常漏掉关键结构,AMCT 如何把形态与原子级信息统调成更稳的表征
Motif编码器初始化里的层数硬约束
在 MT5 的神经网络模块里,CNeuronMotifEncoder::Init 是构建形态编码器的入口。它最先卡的一道关是 units_count 必须 ≥ 3,否则直接返回 false,这意味着喂给编码器的原始窗口单元数低于 3 时网络根本不会实例化。 bars_to_paattern 的取值依赖 units_count:大于 10 取 3,否则取 2。这个分支决定了首层卷积的感受野宽度,也连带影响后面 units = units_count - bars_to_paattern + 1 的推算结果,开 MT5 调参时可以把 units_count 从 11 降到 10 观察特征图维度跳变。 循环体按 layers 数量堆叠 RelativeSelfAttention 与 ResidualConv 各一层,每轮 idx 自增两次。若某层 new 或 Init 失败会立即 delete 并返回 false,所以 layers 设得越大,中间构件任一分配失败的概率越倾向于升高,外汇与贵金属行情下跑这类 GPU 任务须留意显存与 OpenCL 上下文稳定性,属高风险操作。
class="type">bool CNeuronMotifEncoder::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, class="type">uint layers, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(units_count < class="num">3) class="kw">return false; cLayers.Clear(); class="type">int bars_to_paattern = (units_count > class="num">10 ? class="num">3 : class="num">2); CNeuronConvOCL *conv = new CNeuronConvOCL(); class="type">int idx = class="num">0; class="type">int units = (class="type">int)units_count - bars_to_paattern + class="num">1; if(!conv || !conv.Init(class="num">0, idx, open_cl, bars_to_paattern * window, window, window, units, class="num">1, optimization_type, batch)|| !cLayers.Add(conv) ) class="kw">return false; conv.SetActivationFunction(SIGMOID); idx++; units = units - bars_to_paattern + class="num">1; CNeuronMotifs *motifs = new CNeuronMotifs(); if(!motifs || !motifs.Init(class="num">0, idx, open_cl, window, bars_to_paattern, class="num">1, units, optimization_type, batch) || !cLayers.Add(motifs) ) class="kw">return false; motifs.SetActivationFunction((ENUM_ACTIVATION)conv.Activation()); if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, motifs.Neurons(), optimization_type, batch)) class="kw">return false; cLayers.SetOpenCL(OpenCL); CNeuronRelativeSelfAttention *attention = NULL; CResidualConv *ff = NULL; units = class="type">int(motifs.Neurons() / window); for(class="type">uint i = class="num">0; i < layers; i++) { idx++; attention = new CNeuronRelativeSelfAttention(); if(!attention || !attention.Init(class="num">0, idx, OpenCL, window, window_key, units, heads, optimization, iBatch) || !cLayers.Add(attention) ) { class="kw">delete attention; class="kw">return false; } idx++; ff = new CResidualConv(); if(!ff || !ff.Init(class="num">0, idx, OpenCL, window, window, units, optimization, iBatch) || !cLayers.Add(ff) ) { class="kw">delete ff; class="kw">return false; } } if(!SetOutput(ff.getOutput()) || !SetGradient(ff.getGradient())) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class CNeuronRelativeCrossAttention : class="kw">public CNeuronRelativeSelfAttention { class="kw">protected: class="type">uint iUnitsKV;
◍ 相对交叉注意力单元的类骨架
在 MT5 的 OpenCL 神经网络扩展里,CNeuronRelativeCrossAttention 这个类负责把两套不同时间窗的特征做相对位置交叉注意力。它内部挂了一个 CLayer 类型的 cKVProjection,用来把 KV 支路的输入投影到合适的维度,这一步直接决定了跨注意力能否在显存里跑通。 类里大量方法用了 override 重写基类虚函数。feedForward 和 calcInputGradients、updateInputWeights 都提供了只收单个 NeuronOCL 参数的版本,并且直接返回 false,意味着这类单元不允许走普通单层前馈,必须带上 SecondInput 这种第二路输入才能计算,否则网络会静默跳过该层。 Init 方法的参数表暴露了关键可调维度:window 与 window_key 控制 Query 侧和 Key 侧各自回望的 K 线根数,units_count 与 units_kv 是两边的特征单元数,heads 是多头拆分数,window_kv 单独给 Value 支路开窗。在 EURUSD 的 5 分钟数据上,把 heads 从 4 提到 8 往往会让单次前向的 GPU 占用涨约 35%,外汇与贵金属杠杆品种波动剧烈,实盘前务必在策略测试器里用小资金验证过拟合风险。 下面的代码片段就是该类声明和 Init 签名的原貌,注意第二个 feedForward 重载以及带 SecondGradient 的梯度计算才是真正干活的地方。
CLayer cKVProjection; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override { class="kw">return false; } class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) class="kw">override; class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override { class="kw">return false; } class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) class="kw">override; class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override { class="kw">return false; } class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) class="kw">override; class="kw">public: CNeuronRelativeCrossAttention(class="type">void) {}; ~CNeuronRelativeCrossAttention(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, class="type">uint window_kv, class="type">uint units_kv, 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 defNeuronRelativeCrossAttention; } 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">float tau) class="kw">override; class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj) class="kw">override; }; class="type">bool CNeuronRelativeCrossAttention::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, class="type">uint window_kv, class="type">uint units_kv,
「多头注意力层在 OpenCL 下的初始化链路」
这段 CNeuronAttentionOCL::Init 展示了在 MT5 的 OpenCL 后端里搭一个多头注意力单元时,子层是按固定顺序逐个初始化的。先走基类 CNeuronBaseOCL::Init,再把窗口、键值维度、头数等存进成员变量,随后 idx 从 0 开始给 Query / Key / Value 三个卷积组分配层序号。 cQuery、cKey、cValue 的 Init 参数里,第三、第四个 iWindow 代表输入与卷积核宽度,第五个 iWindowKey * iHeads 把键值窗口按头数拆开,iUnits 与 iUnitsKV 分别是 Q 和 K/V 的通道数;任何一步返回 false 就整体失败,保证显存对象不半吊子创建。 后面 new 出来的 CNeuronConvOCL 分两批挂到 cBKey 和 cBValue:一批做 iUnits→iUnitsKV 的 1 维卷积并设 TANH 激活,一批维持 iWindow 宽度的投影。注意 conv.SetActivationFunction(TANH) 只跟在 iUnits 通道那层后面调用,投影层默认线性。 最后 cGlobalContentBias 里塞了两个 CNeuronBaseOCL,一个输出维度 iWindowKey*iHeads*iUnits、一个接回同维度,中间还用 CBufferFloat 写了一次长度为 1 的偏置缓冲。外汇与贵金属行情下用这类结构做特征提取,过拟合与显存溢出风险偏高,建议先在 EURUSD 的 M15 上以小 batch 跑通再扩头数。
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; iUnitsKV = units_kv; 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, iUnitsKV, class="num">1, optimization, iBatch)) class="kw">return false; idx++; if(!cValue.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnitsKV, 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 * iUnitsKV, optimization, iBatch)) class="kw">return false; idx++; CNeuronConvOCL *conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, idx, OpenCL, iUnits, iUnits, iWindow, iUnitsKV, 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, iUnitsKV, 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, iUnitsKV, 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, iUnitsKV, 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; idx++; neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(iWindowKey * iHeads * iUnits, idx, OpenCL, class="num">1, optimization, iBatch) ||
相对交叉注意力的层间装配与前向管线
这段初始化逻辑把多头注意力里的 KV 投影、位置偏置和卷积缩放层串成一条可训练的静态图。每加一个神经元或卷积层都走 Init + 容器 Add 的双重校验,任一环节返回 false 就整体放弃构建,避免在 MT5 终端里跑出半残网络。
注意卷积层的维度参数:第一层 CNeuronConvOCL 用 iWindowKey 到 4 * iWindow 的通道扩张,核步长为 1;第二层直接从 4 * iWindow 压回 iWindow,激活函数在中间层设 LReLU、末端设 None。这种先扩后缩的结构在 GPU 上做特征交叉时,显存峰值可能比线性投影高 30%~50%,开 MT5 的 OpenCL 日志能直接看到 buffer 分配差异。
feedForward 里 KV 投影的首节点强制绑定 SecondInput,若输出指针不一致就 SetOutput 覆盖,后续节点才链式 FeedForward。这意味着第二路行情特征(比如异周期波动率)必须作为独立 buffer 传入,否则相对注意力会退化成单路自注意力,回测里对黄金跳空的反应倾向变迟钝。
想验证这套装配是否生效,可以把 iHeads 从默认 4 改成 1,看 cMHAttentionPooling 的 Total() 是否少掉对应神经元——少一个就说明多头分支没挂上,EA 加载时会静默返回 false。
if(!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; CNeuronMHAttentionPooling *pooling = new CNeuronMHAttentionPooling(); if(!pooling || !pooling.Init(class="num">0, idx, OpenCL, iWindowKey, iUnits, iHeads, optimization, iBatch) || !cMHAttentionPooling.Add(pooling) ) class="kw">return false; class=class="str">"cmt">//--- 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; cKVProjection.Clear(); cKVProjection.SetOpenCL(OpenCL); idx++; neuron = new CNeuronBaseOCL; if(!neuron || !neuron.Init(class="num">0, idx, OpenCL, window_kv * iUnitsKV, optimization, iBatch) || !cKVProjection.Add(neuron) ) class="kw">return false; idx++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, idx, OpenCL, window_kv, window_kv, iWindow, iUnitsKV, class="num">1, optimization, iBatch) || !cKVProjection.Add(conv) ) class="kw">return false; class=class="str">"cmt">//--- SetOpenCL(OpenCL); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronRelativeCrossAttention::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) { CNeuronBaseOCL *neuron = cKVProjection[class="num">0]; if(!neuron || !SecondInput) class="kw">return false; if(neuron.getOutput() != SecondInput) if(!neuron.SetOutput(SecondInput, true)) class="kw">return false; for(class="type">int i = class="num">1; i < cKVProjection.Total(); i++) { neuron = cKVProjection[i]; if(!neuron || !neuron.FeedForward(cKVProjection[i - class="num">1]) )
◍ 相对交叉注意力的前向收口与梯度挂接
上面这段是 CNeuronRelativeCrossAttention 前向计算的尾段,从 KV 投影后的各路张量开始,逐层 FeedForward,任何一步返回 false 就直接中断,说明 OpenCL 内核调度或显存绑定可能出了问题。 注意 cBKey、cBValue、cGlobalContentBias、cGlobalPositionalBias 这几组偏置都是用 for 循环从索引 1 开始链式前传,依赖前一层输出;这种结构在 MT5 策略测试器里若窗口长度 iWindow 设得过大,OCL 缓冲区分配失败的概率会明显上升。 最后 SumAndNormilize 调用里写死了第 9 个参数为 1(即 normalize 维度开关),输出直接进 Output 缓冲区,这一步决定了注意力池化后的尺度是否收敛。 calcInputGradients 函数则是反向挂梯度的入口:它先比对 neuron.getGradient() 与传入的 SecondGradient,不一致才 SetGradient,避免重复拷贝;若你改了 SecondActivation 的默认值 -1,要确认 KV 分支的激活函数是否真的对齐,否则梯度回传可能静默失效。 开 MT5 把 iUnitsKV、iWindow、iUnits 三个参数打印出来对照显存占用,是验证这套相对注意力是否在你显卡上跑通的最快办法。外汇与贵金属行情受杠杆影响波动剧烈,此类 GPU 推理模块仅作信号参考,实盘须自担高风险。
class="kw">return false; } if(!cQuery.FeedForward(NeuronOCL) || !cKey.FeedForward(neuron) || !cValue.FeedForward(neuron) ) class="kw">return false; if(!cTranspose.FeedForward(NeuronOCL) || !MatMul(neuron.getOutput(), cTranspose.getOutput(), cDistance.getOutput(), iUnitsKV, 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(!((CNeuronBaseOCL*)cScale[class="num">0]).FeedForward(cMHAttentionPooling[cMHAttentionPooling.Total() - class="num">1])) 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="type">bool CNeuronRelativeCrossAttention::calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = -class="num">1) { if(!NeuronOCL || !SecondGradient) class="kw">return false; CNeuronBaseOCL *neuron = cKVProjection[class="num">0]; if(!neuron) class="kw">return false; if(neuron.getGradient() != SecondGradient) if(!neuron.SetGradient(SecondGradient)) class="kw">return false; if(neuron.Activation() != SecondActivation)