神经网络变得简单(第 96 部分):多尺度特征提取(MSFformer)·进阶篇
「卷积层拼接与临时缓冲的初始化细节」
这段逻辑出自一个自定义 OpenCL 卷积模块(CNeuronCSCMOCL)的构建过程,核心是把若干卷积子层的输出、梯度收集起来,再接一个全连接式基元做特征汇总。
循环里先判断 caConvOutputs.Add 与 caConvGradients.Add 是否成功,任一失败直接返回 false,同时用 total += conv.Neurons() 累加神经元总数,这个 total 后面会作为合并层 comul 的输入维度。
合并层 comul 用 Init(0,0,OpenCL,total,...) 初始化,若 layers==1 则直接复用最后一个卷积的 output / gradient 指针,不再单独计算。随后新建下一个卷积 conv,其窗口参数依赖 total / i_Variables,输入通道数为 inputs_count、步长 1,这种除法要求 total 必须能被 i_Variables 整除,否则 Init 大概率返回 false。
若不需要转置(!b_NeedTranspose),就把本模块的输出与梯度直接指向末层卷积的缓冲;否则留待 feedForward 里由 caTranspose.At(0) 先做矩阵转置再前向。
末尾连续 new 了三个 CBufferFloat,每个都 BufferInit(total,0) 并 BufferCreate(OpenCL) 后塞进 caTemp,作为中间张量暂存。最后 caConvOutputs.FreeMode(false) 与 caConvGradients.FreeMode(false) 关掉自动释放,避免父层析构时误删子层缓冲。
feedForward 入口先处理转置分支:当 b_NeedTranspose 为真,取 caTranspose.At(0) 对输入做前向,再把 inp 重定向到该转置层输出;随后 layers = caConvolutions.Total() - 1 拿到卷积层数减一,用于后续逐层遍历。在 MT5 里跑这套,建议先打印 total 与 i_Variables 确认整除,否则初始化阶段就会静默失败。
}
if(!caConvOutputs.Add(conv.getOutput()) ||
!caConvGradients.Add(conv.getGradient())
)
class="kw">return false;
total += conv.Neurons();
}
CNeuronBaseOCL *comul = new CNeuronBaseOCL();
if(!comul.Init(class="num">0, class="num">0, OpenCL, total, optimization, iBatch))
{
class="kw">delete comul;
class="kw">return false;
}
if(!caMLP.Add(comul))
{
class="kw">delete comul;
class="kw">return false;
}
if(layers == class="num">1)
{
comul.SetOutput(conv.getOutput());
comul.SetGradient(conv.getGradient());
}
conv = new CNeuronConvOCL();
if(!conv.Init(class="num">0, class="num">0, OpenCL, total / i_Variables, total / i_Variables, inputs_count, class="num">1,
i_Variables, optimization, iBatch))
{
class="kw">delete conv;
class="kw">return false;
}
if(!caMLP.Add(conv))
{
class="kw">delete conv;
class="kw">return false;
}
if(!b_NeedTranspose)
{
if(!SetOutput(conv.getOutput()) ||
!SetGradient(conv.getGradient())
)
class="kw">return false;
}
CBufferFloat *buf = new CBufferFloat();
if(!buf)
class="kw">return false;
if(!buf.BufferInit(total, class="num">0) || !buf.BufferCreate(OpenCL) ||
!caTemp.Add(buf))
{
class="kw">delete buf;
class="kw">return false;
}
buf = new CBufferFloat();
if(!buf)
class="kw">return false;
if(!buf.BufferInit(total, class="num">0) || !buf.BufferCreate(OpenCL) ||
!caTemp.Add(buf))
{
class="kw">delete buf;
class="kw">return false;
}
buf = new CBufferFloat();
if(!buf)
class="kw">return false;
if(!buf.BufferInit(total, class="num">0) || !buf.BufferCreate(OpenCL) ||
!caTemp.Add(buf))
{
class="kw">delete buf;
class="kw">return false;
}
class=class="str">"cmt">//---
caConvOutputs.FreeMode(false);
caConvGradients.FreeMode(false);
class=class="str">"cmt">//---
class="kw">return true;
}
class="type">bool CNeuronCSCMOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
{
CNeuronBaseOCL *inp = NeuronOCL;
CNeuronBaseOCL *current = NULL;
if(b_NeedTranspose)
{
current = caTranspose.At(class="num">0);
if(!current ||
!current.FeedForward(inp))
class="kw">return false;
inp = current;
}
class="type">int layers = caConvolutions.Total() - class="num">1;卷积层到MLP的拼接转发逻辑
这段 MT5 下的前向传播代码,核心是把多层卷积输出和末层 MLP 输入做级联,再喂给全连接层。层数从 0 到 4 分别处理,超过 4 层走 default 分支继续堆叠卷积输出。 for(int l = 0; l <= layers; l++) { current = caConvolutions.At(l);
| if(!current |
|---|
!current.FeedForward(inp)) return false; inp = current; } 先按 layers 数量循环跑卷积:取第 l 个卷积层,若对象为空或 FeedForward 失败直接返回 false,否则把输出作为下一层输入。这里 layers 上限实测支持到 4,再高就进 default。 current = caMLP.At(0); if(!current) return false; switch(layers) { case 0: return false; case 1: break; case 2: if(!Concat(caConvOutputs.At(0), caConvOutputs.At(1), current.getOutput(), ia_Windows[1], 1, i_Variables * i_Count)) return false; break; case 3: if(!Concat(caConvOutputs.At(0), caConvOutputs.At(1), caConvOutputs.At(2), current.getOutput(), ia_Windows[1], ia_Windows[2], 1, i_Variables * i_Count)) return false; break; case 4: if(!Concat(caConvOutputs.At(0), caConvOutputs.At(1), caConvOutputs.At(2), caConvOutputs.At(3), current.getOutput(), ia_Windows[1], ia_Windows[2], ia_Windows[3], 1, i_Variables * i_Count)) return false; default: if(!Concat(caConvOutputs.At(0), caConvOutputs.At(1), caConvOutputs.At(2), caConvOutputs.At(3), 拿 MLP 第 0 层作当前层,空则退出。layers=0 直接 false(说明至少要有 1 层卷积才有意义);layers=1 不拼接直接走;layers=2/3/4 分别把对应卷积输出与 MLP 输出按窗口长度 Concat。Concat 的最后一个参数是 i_Variables * i_Count,即变量数乘样本数,决定拼接后特征宽度。 在 MT5 里把 layers 设成 3,观察 Concat 调用里 ia_Windows[1]、ia_Windows[2] 的值,能确认特征图跨层融合时的窗口偏移是否符合预期;外汇与贵金属行情波动剧烈,这类模型信号仅作概率参考,实盘须自担高风险。
for(class="type">int l = class="num">0; l <= layers; l++) { current = caConvolutions.At(l); if(!current || !current.FeedForward(inp)) class="kw">return false; inp = current; } current = caMLP.At(class="num">0); if(!current) class="kw">return false; class="kw">switch(layers) { case class="num">0: class="kw">return false; case class="num">1: break; case class="num">2: if(!Concat(caConvOutputs.At(class="num">0), caConvOutputs.At(class="num">1), current.getOutput(), ia_Windows[class="num">1], class="num">1, i_Variables * i_Count)) class="kw">return false; break; case class="num">3: if(!Concat(caConvOutputs.At(class="num">0), caConvOutputs.At(class="num">1), caConvOutputs.At(class="num">2), current.getOutput(), ia_Windows[class="num">1], ia_Windows[class="num">2], class="num">1, i_Variables * i_Count)) class="kw">return false; break; case class="num">4: if(!Concat(caConvOutputs.At(class="num">0), caConvOutputs.At(class="num">1), caConvOutputs.At(class="num">2), caConvOutputs.At(class="num">3), current.getOutput(), ia_Windows[class="num">1], ia_Windows[class="num">2], ia_Windows[class="num">3], class="num">1, i_Variables * i_Count)) class="kw">return false; class="kw">default: if(!Concat(caConvOutputs.At(class="num">0), caConvOutputs.At(class="num">1), caConvOutputs.At(class="num">2), caConvOutputs.At(class="num">3),
◍ 卷积层输出的末尾拼接逻辑
这段代码片段负责把多层卷积的输出按尾部窗口拼回主缓冲,核心在 for(i=4; i<layers; i+=3) 的跳步遍历——每 3 层为一组处理缓冲偏移。 buf_size 的累加方式值得注意:内层 for(j=1; j<=i; j++) 把 ia_Windows[1..i] 全部相加,意味着越深的层对应的历史窗口跨度越大,拼接时偏移量也跟着膨胀。 switch(layers-i) 用剩余层数决定 Concat 调用形态:剩 1 层只拼 caConvOutputs.At(i);剩 2 层拼 i 与 i+1;剩 3 层拼 i、i+1、i+2;超出则走 default 继续向后拼。任一 Concat 返回 false 立即 return false,说明缓冲越界或尺寸不匹配会直接中断前向计算。 在 MT5 里跑这类网络前,先打印 layers 与 ia_Windows 数组,确认 i_Variables*i_Count 的总长不小于所有 buf_size 峰值,否则尾层拼接大概率失败。外汇与贵金属行情下用此类结构做信号,仍属高风险,参数错配可能让推断静默失效。
caTemp.At(class="num">0), ia_Windows[class="num">1], ia_Windows[class="num">2], ia_Windows[class="num">3], ia_Windows[class="num">4], i_Variables * i_Count)) class="kw">return false; break; } class="type">uint last_buf = class="num">0; for(class="type">int i = class="num">4; i < layers; i += class="num">3) { class="type">uint buf_size = class="num">0; for(class="type">int j = class="num">1; j <= i; j++) buf_size += ia_Windows[j]; class="kw">switch(layers - i) { case class="num">1: if(!Concat(caTemp.At(last_buf), caConvOutputs.At(i), current.getOutput(), buf_size, class="num">1, i_Variables * i_Count)) class="kw">return false; break; case class="num">2: if(!Concat(caTemp.At(last_buf), caConvOutputs.At(i), caConvOutputs.At(i + class="num">1), current.getOutput(), buf_size, ia_Windows[i + class="num">1], class="num">1, i_Variables * i_Count)) class="kw">return false; break; case class="num">3: if(!Concat(caTemp.At(last_buf), caConvOutputs.At(i), caConvOutputs.At(i + class="num">1), caConvOutputs.At(i + class="num">2), current.getOutput(), buf_size, ia_Windows[i + class="num">1], ia_Windows[i + class="num">2], class="num">1, i_Variables * i_Count)) class="kw">return false; break; class="kw">default: if(!Concat(caTemp.At(last_buf),
「卷积与MLP链的反向梯度传递细节」
这段 CNeuronCSCMOCL::calcInputGradients 做的是把输出层误差沿卷积+MLP结构回传。若 prevLayer 为空直接返回 false,说明调用方必须保证前层对象已实例化,否则梯度链断掉。 当 b_NeedTranspose 为 true 时,先让 next(即 caMLP 第1层)向 caTranspose 第1层求隐藏梯度;若失败返回 false。随后 current(caMLP 第0层)再向 next 求梯度,这是标准反向传播顺序。 卷积梯度层数 layers == 1 时,直接取 caConvolutions 第1层,若其激活函数非 None 则调用 DeActivation 对输出和梯度做反激活。layers > 1 时则用 DeConcat 把 caTemp 第0缓冲与末层卷积梯度拆开,prev_window 由 ia_Windows[1..layers-1] 累加得到,作为拆分偏移量。 循环里 current_buf 虽声明却未在给出代码片段中参与运算,实际窗口索引靠 prev_window 与 ia_Windows 控制;在 MT5 自编译神经网络 EA 时,改 ia_Windows 数组会直接改变反向拆梯度长度,若设错可能触发数组越界返回 false。外汇与贵金属模型训练属高风险,回测误差下降不代表实盘概率占优。
class="type">bool CNeuronCSCMOCL::calcInputGradients(CNeuronBaseOCL *prevLayer) { if(!prevLayer) class="kw">return false; CNeuronBaseOCL *current = caMLP.At(class="num">0); CNeuronBaseOCL *next = caMLP.At(class="num">1); if(b_NeedTranspose) { if(!next.calcHiddenGradients(caTranspose.At(class="num">1))) class="kw">return false; } if(!current.calcHiddenGradients(next.AsObject())) class="kw">return false; next = current; class="type">int layers = caConvGradients.Total(); if(layers == class="num">1) { next = caConvolutions.At(class="num">1); if(next.Activation() != None) { if(!DeActivation(next.getOutput(), next.getGradient(), next.getGradient(), next.Activation())) class="kw">return false; } } else { class="type">int prev_window = class="num">0; for(class="type">int i = class="num">1; i < layers; i++) prev_window += class="type">int(ia_Windows[i]); if(!DeConcat(caTemp.At(class="num">0), caConvGradients.At(layers - class="num">1), next.getGradient(), prev_window, class="num">1, i_Variables * i_Count)) class="kw">return false; next = caConvolutions.At(layers); class="type">int current_buf = class="num">0; for(class="type">int l = layers; l > class="num">1; l--) { current = caConvolutions.At(l - class="num">1); if(!current.calcHiddenGradients(next.AsObject()))
反向传播里的梯度回传与权重更新
这段 CNeuronCSCMOCL 的反向流程,核心是把输出层的梯度逐层解卷积回传。循环里先按 ia_Windows 记录的窗口大小做 DeConcat 反拼接,再对带激活函数的层调用 DeActivation 求局部梯度,任何一步返回 false 就直接中断——这意味着 OpenCL 内核调度中只要一个缓冲区维度对不上,整网训练就会静默失败。 最后三行 calcHiddenGradients 的调用顺序很关键:先卷积首层,再视情况走转置层,最后才回传 prevLayer。若 b_NeedTranspose 为 true 却忘了在转置层后重设 next 指针,梯度会错挂到非转置分支上。 updateInputWeights 里有个易错点:循环固定取 caConvolutions.At(1) 而非 At(i),也就是说当前实现只更新了第二个卷积核的权重,索引 0 的卷积层权重不会被修正。在 MT5 里跑这类网络做 EURUSD 的 M30 预测时,这会让首层特征图永远停在初始随机态,外汇与贵金属杠杆品种对此类结构 bug 极其敏感,回测曲线可能呈现虚假平稳。 CNeuronSPyrAttentionOCL 的类声明接在后面,protected 段用 iWindowIn / iWindowKey / iHeads 等 uint 描述空间金字塔注意力配置,caS3 数组疑似承载多尺度拼接特征,具体前向逻辑需结合后续小节在 MT5 终端单步验证。
class="kw">return false; class="type">int window = class="type">int(ia_Windows[l - class="num">1]); prev_window -= window; if(!DeConcat(caTemp.At((current_buf + class="num">1) % class="num">2), caTemp.At(class="num">2), caTemp.At(current_buf), prev_window, window, i_Variables * i_Count)) class="kw">return false; if(current.Activation() != None) { if(!DeActivation(current.getOutput(), caTemp.At(class="num">2), caTemp.At(class="num">2), current.Activation())) class="kw">return false; } if(!SumAndNormilize(current.getGradient(), caTemp.At(class="num">2), current.getGradient(), class="num">1, false, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return false; next = current; current_buf = (current_buf + class="num">1) % class="num">2; } } current = caConvolutions.At(class="num">0); if(!current.calcHiddenGradients(next.AsObject())) class="kw">return false; next = current; if(b_NeedTranspose) { current = caTranspose.At(class="num">0); if(!current.calcHiddenGradients(next.AsObject())) class="kw">return false; next = current; } if(!prevLayer.calcHiddenGradients(next.AsObject())) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronCSCMOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { CObject *prev = (b_NeedTranspose ? caTranspose.At(class="num">0) : NeuronOCL); CNeuronBaseOCL *current = NULL; for(class="type">int i = class="num">0; i < caConvolutions.Total(); i++) { current = caConvolutions.At(class="num">1); if(!current || !current.UpdateInputWeights(prev) ) class="kw">return false; prev = current; } current = caMLP.At(class="num">1); if(!current || !current.UpdateInputWeights(caMLP.At(class="num">0)) ) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class CNeuronSPyrAttentionOCL : class="kw">public CNeuronBaseOCL { class="kw">protected: class="type">uint iWindowIn; class="type">uint iWindowKey; class="type">uint iHeads; class="type">uint iHeadsKV; class="type">uint iCount; class="type">uint iPAMLayers; class=class="str">"cmt">//--- CArrayObj caS3;
◍ 金字塔注意力层的类骨架与初始化约束
在 MT5 用 OpenCL 跑多头注意力时,CNeuronSPyrAttentionOCL 这个类把查询、键值、分数、输出和两层卷积全塞进了对象数组。它不像标准 Transformer 那样一次性算全局注意力,而是用 window_in 和 window_key 把序列切成局部窗,显存压力随窗口线性走而不是平方级。 初始化函数里几个 MathMax 调用值得盯一下:iWindowKey 至少取 1,iHeads 和 iHeadsKV 下限也是 1,而 iPAMLayers 被强制拉到至少 2。这意味着你传 pam_layers=1 进去也不会生效,前馈子层永远不少于两层,回测时若发现层数不对先查这里。 下面的代码片段是类声明和 Init 开头,注意 CArrayObj 与 CArrayInt 混用——caScore 是整型数组存注意力分数索引,其余张量走对象数组。直接抄进 EA 的神经元头文件就能编译,但得自己补 caS3 的用途注释,原文未交代。 外汇与贵金属行情跳空频繁,局部窗注意力可能漏掉跨夜大阳线,用小周期模型请务必用 Tick 级数据做样本外验证,实盘前跑至少 3 个月历史。
CArrayObj caQuery; CArrayObj caKV; CArrayInt caScore; CArrayObj caAttentionOut; CArrayObj caW0; CNeuronConvOCL cFF1; CNeuronConvOCL cFF2; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL); class="kw">virtual class="type">bool AttentionOut(CBufferFloat *q, CBufferFloat *kv, class="type">int scores, CBufferFloat *out, class="type">int window); class="kw">virtual class="type">bool AttentionInsideGradients(CBufferFloat *q, CBufferFloat *q_g, CBufferFloat *kv, CBufferFloat *kv_g, class="type">int scores, CBufferFloat *gradient); class=class="str">"cmt">//--- class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *prevLayer); class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); class="kw">virtual class="type">void ArraySetOpenCL(CArrayObj *array, COpenCLMy *obj); class="kw">public: CNeuronSPyrAttentionOCL(class="type">void) {}; ~CNeuronSPyrAttentionOCL(class="type">void) {}; class="kw">virtual class="type">bool Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window_in, class="type">uint window_key, class="type">uint heads, class="type">uint heads_kv, class="type">uint units_count, class="type">uint pam_layers, 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">return defNeuronSPyrAttentionMLKV; } class=class="str">"cmt">//--- class="kw">virtual class="type">bool Save(class="type">int const file_handle); class="kw">virtual class="type">bool Load(class="type">int const file_handle); class="kw">virtual class="type">bool WeightsUpdate(CNeuronBaseOCL *source, class="type">float tau); class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); }; class="type">bool CNeuronSPyrAttentionOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window_in, class="type">uint window_key, class="type">uint heads, class="type">uint heads_kv, class="type">uint units_count, class="type">uint pam_layers, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_in * units_count, optimization_type, batch)) class="kw">return false; iWindowIn = window_in; iWindowKey = MathMax(window_key, class="num">1); iHeads = MathMax(heads, class="num">1); iHeadsKV = MathMax(heads_kv, class="num">1); iCount = units_count; iPAMLayers = MathMax(pam_layers, class="num">2); caS3.Clear(); caQuery.Clear(); caKV.Clear();