神经网络变得简单(第 83 部分):构象时空连续关注度转换器算法·综合运用
注意力反向传播里的梯度累加细节
这段 OpenCL 内核在做多头注意力机制的反向传播,核心是把 score 梯度往 Key 向量上回传。外层先取当前 query 位置的归一化分数 scr,再对 total 个目标位 g 做加权求和,权重来自 qkv 与输出梯度的点积。 score_grad 的更新用到了 (q==pos) 的指示函数减去 scr,最后统一除以 sqrt(dimension) 做缩放——这一步漏掉会让梯度量级随维度漂移,MT5 里用 CL 内核跑 Transformer 类信号模型时尤其要核对。 Key 梯度那段对 q==0 单独处理,是因为序列首位置没有前驱可累加,只能直接赋值;其余位置走 += 累加。下面把第一段核心循环拆开看。 HiddenGradientTimeDerivative 内核则按 pos / variable / head 三维并行,shift 计算里写死 3*heads*variables*dimension,说明每个时间步存了 Q、K、V 三组向量;dQ/dt 部分用相邻时间步的 dqkv_g 差分估计时间导数,pos 在边界时只取单侧。
class="type">float scr = score[shift_score + q * step_score]; for(class="type">int g = class="num">0; g < total; g++) { class="type">float grad = class="num">0; for(class="type">int d = class="num">0; d < dimension; d++) grad += qkv[shift_value + d] * out_g[shift_out + d + g * step_out]; score_grad += score[shift_score + q * step_score + g] * grad * ((class="type">float)(q == pos) - scr); } score_grad /= sqrt((class="type">float)dimension); class=class="str">"cmt">//--- Key gradient for(class="type">int d = class="num">0; d < dimension; d++) { if(q == class="num">0) { dqkv_g[shift_key + d] = score_grad * qkv[shift_query + q * step + d]; qkv_g[shift_key + d] = score_grad * dqkv[shift_query + q * step + d]; } else { qkv_g[shift_key + d] += score_grad * dqkv[shift_query + q * step + d]; dqkv_g[shift_key + d] += score_grad * qkv[shift_query + q * step + d]; } } }
「注意力层里 Q 与 K 梯度的差分平滑」
在 Conformer 注意力反向传播中,Q 和 K 的梯度并非直接回传,而是先按时间邻域做一阶差分再平均。以 Q 为例:遍历序列位置,若左侧有邻居就累加 dqkv_g[shift_query + i] - 左侧值,右侧有邻居则累加右侧值 - 当前值,count 记录有效邻域数,最后 grad 除以 count 得到局部斜率,写回 qkv_g。 K 的处理逻辑对称,但写回时用了 dqkv_g[shift_key + i] + grad 的叠加形式,意味着 Key 梯度在反向时同时保留原始项与差分修正项,这可能让 Key 的更新在长序列上更平滑。 下面这段是 Q/K 梯度差分核的主体循环,shift 为单头维度跨度,pos 为当前时间步,total 为序列总长: { grad += dqkv_g[shift_query + i + shift] - current; count++; } if(count > 0) grad /= count; qkv_g[shift_query + i] += grad; } //--- dK/dt { int count = 0; float grad = 0; float current = dqkv_g[shift_key + i]; if(pos > 0) { grad += current - dqkv_g[shift_key + i - shift]; count++; } if(pos < (total - 1)) { grad += dqkv_g[shift_key + i + shift] - current; count++; } if(count > 0) grad /= count; qkv_g[shift_key + i] += dqkv_g[shift_key + i] + grad; } 外层 AttentionInsideGradients 在 OpenCL 不可用时会直接返回 false,可用时需把 cQKV / cdQKV 的输出与梯度缓冲绑定到 def_k_HiddenGradientContAtt 内核。任一 SetArgumentBuffer 失败就 printf 出错函数名、错误码和行号并退出,这种细粒度报错在调 MT5 神经网络 EA 时很实用——能直接定位是哪一个缓冲没挂上。 外汇与贵金属市场波动剧烈、杠杆风险高,此类模型仅作信号辅助,实盘前务必在策略测试器用历史数据验证梯度逻辑是否如预期收敛。
{
grad += dqkv_g[shift_query + i + shift] - current;
count++;
}
if(count > class="num">0)
grad /= count;
qkv_g[shift_query + i] += grad;
}
class=class="str">"cmt">//--- dK/dt
{
class="type">int count = class="num">0;
class="type">float grad = class="num">0;
class="type">float current = dqkv_g[shift_key + i];
if(pos > class="num">0)
{
grad += current - dqkv_g[shift_key + i - shift];
count++;
}
if(pos < (total - class="num">1))
{
grad += dqkv_g[shift_key + i + shift] - current;
count++;
}
if(count > class="num">0)
grad /= count;
qkv_g[shift_key + i] += dqkv_g[shift_key + i] + grad;
}◍ 注意力梯度与时间导数的内核参数装配
在 MT5 的 OpenCL 封装里,每个 kernel 执行前都必须用 SetArgumentBuffer / SetArgument 把显存缓冲区和标量塞进去,否则 GPU 端读到的是野指针。下面这段注意力梯度核先挂了分数缓冲 iScore、注意力输出梯度 cAttentionOut.getGradientIndex(),再把维度标量 iDimension 以 int 强转写入,任何一步失败就 printf 打出函数名、GetLastError() 和 __LINE__ 然后 return false。 Execute 的第三个参数传了 3,代表三维 ND-Range;global_work_offset 固定 {0,0,0},global_work_size 则是 {iCount, iVariables, iHeads} 三轴并行规模。若 Execute 返回 false,只报函数名与错误码,不报行号——这和前面 Set 类错误形成区分,排错时先看有没有行号就能定位是装配段还是发射段。 时间导数梯度核 HGTimeDerivative 的装配逻辑同构:先绑 cQKV.getGradientIndex() 与 cdQKV.getGradientIndex() 两个梯度缓冲,再写 def_k_tddimension 标量,最后用同样的 3 维 offset/size 发射。外汇与贵金属行情下跑这类自研核,显存越界会直接让 EA 在实盘掉线,务必先在策略测试器用历史数据小样本验证再上真仓,杠杆品种高风险。
if(!OpenCL.SetArgumentBuffer(def_k_HiddenGradientContAtt, def_k_hgcascore, iScore)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_HiddenGradientContAtt, def_k_hgcaout_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_HiddenGradientContAtt, def_k_hgcadimension, class="type">int(iDimension))) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.Execute(def_k_HiddenGradientContAtt, 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">//--- Time Derivative Gradient { class="type">uint global_work_offset[class="num">3] = {class="num">0, class="num">0, class="num">0}; class="type">uint global_work_size[class="num">3] = {iCount, iVariables, iHeads}; ResetLastError(); if(!OpenCL.SetArgumentBuffer(def_k_HGTimeDerivative, def_k_tdqkv, cQKV.getGradientIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_HGTimeDerivative, def_k_tddqkv, cdQKV.getGradientIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_HGTimeDerivative, def_k_tddimension, class="type">int(iDimension))) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.Execute(def_k_HGTimeDerivative, class="num">3, global_work_offset, global_work_size)) { printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
Conformer 反向传播与权重更新的链路拆解
这段 CNeuronConformer 的成员函数把 Conformer 块的反向传播拆成了四条梯度链:前馈、神经 ODE、多头注意力、以及 QKV 投影。任何一步 calcInputGradients 返回 false 就直接中断,说明梯度回传是严格串行的,某一子层显存或维度不匹配会令整层失效。 calcInputGradients 里神经 ODE 部分用 for(int i=2; i>0; i--) 从 cNODE[2] 往 cNODE[0] 倒序回传,而 updateInputWeights 中对应循环是 for(int i=0; i<3; i++) 正序更新,两者指针 prev 的接力方向正好相反,写自定义层时接错顺序会拿到错位梯度。 注意力侧先由 cW0 接收 cAttentionOut 的梯度,再进 AttentionInsideGradients() 处理内部 softmax/scale,最后 cQKV 把梯度交到 prevLayer。三个 SumAndNormilize 调用都带 iDimension 与 false 参数,表明沿特征维做求和归一但不写回原缓冲,调参时改 iDimension 会直接改变梯度尺度。 updateInputWeights 先更新 cQKV 与 cW0,再循环更新 3 个 NODE 与 2 个 FF 层,共 7 个子模块权重。在 MT5 里若想冻结某子层,应在对应 if(!…UpdateInputWeights) 前加开关,而非注释掉整函数,否则前层梯度仍会被 SumAndNormilize 污染。
class="type">bool CNeuronConformer::calcInputGradients(CNeuronBaseOCL *prevLayer) { class=class="str">"cmt">//--- Feed Forward Gradient if(!cFF[class="num">1].calcInputGradients(GetPointer(cFF[class="num">0]))) class="kw">return false; if(!cFF[class="num">0].calcInputGradients(GetPointer(cNODE[class="num">2]))) class="kw">return false; if(!SumAndNormilize(Gradient, cNODE[class="num">2].getGradient(), cNODE[class="num">2].getGradient(), iDimension, false)) class="kw">return false; class=class="str">"cmt">//--- Neural ODE Gradient CNeuronBaseOCL *prev = GetPointer(cNODE[class="num">1]); for(class="type">int i = class="num">2; i > class="num">0; i--) { if(!cNODE[i].calcInputGradients(prev)) class="kw">return false; prev = GetPointer(cNODE[i - class="num">1]); } if(!cNODE[class="num">0].calcInputGradients(GetPointer(cW0))) class="kw">return false; if(!SumAndNormilize(cW0.getGradient(), cNODE[class="num">2].getGradient(), cW0.getGradient(), iDimension, false)) class="kw">return false; class=class="str">"cmt">//--- MH Attention Gradient if(!cW0.calcInputGradients(GetPointer(cAttentionOut))) class="kw">return false; if(!AttentionInsideGradients()) class="kw">return false; class=class="str">"cmt">//--- Query, Key, Value Graddients if(!cQKV.calcInputGradients(prevLayer)) class="kw">return false; if(!SumAndNormilize(cW0.getGradient(), prevLayer.getGradient(), prevLayer.getGradient(), iDimension, false)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronConformer::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { class=class="str">"cmt">//--- MH Attention if(!cQKV.UpdateInputWeights(NeuronOCL)) class="kw">return false; if(!cW0.UpdateInputWeights(GetPointer(cAttentionOut))) class="kw">return false; class=class="str">"cmt">//--- Neural ODE CNeuronBaseOCL *prev = GetPointer(cW0); for(class="type">int i = class="num">0; i < class="num">3; i++) { if(!cNODE[i].UpdateInputWeights(prev)) class="kw">return false; prev = GetPointer(cNODE[i]); } class=class="str">"cmt">//--- Feed Forward for(class="type">int i = class="num">0; i < class="num">2; i++) { if(!cFF[i].UpdateInputWeights(prev)) class="kw">return false; prev = GetPointer(cFF[i]); } class=class="str">"cmt">//--- class="kw">return true; }
「状态向量与编码器网络搭建」
强化学习模型先把单根 K 线的原始信息压进状态数组:sState.state[shift+1] 存实体上沿相对开盘价的偏移(high-open),shift+2 存下沿偏移(low-open),shift+3 把 tick_volume 除以 1000 做缩放,后续 shift+4~+8 依次塞入 rsi、cci、atr、macd、sign 五个指标值。这样每根 bar 用 9 个 float 描述,HistoryBars 根 bar 拼起来就是编码器输入维度 HistoryBars*BarDescr。 CreateDescriptions 负责把 encoder/actor/critic 三套网络的结构在内存里建出来。函数开头先判空,任一指针为空就 new 一个 CArrayObj,分配失败直接返回 false,避免后面空指针崩在 GPU 端。 编码器第一层是输入层,类型 defNeuronBaseOCL,节点数等于 HistoryBars*BarDescr,激活函数 None,优化器 ADAM;第二层接 BatchNorm(defNeuronBatchNormOCL),batch 取 MathMax(1000, GPTBars),用来稳训练时的数值分布。 第三层用 Embedding(defNeuronEmbeddingOCL),windows 数组设为 {4,1,1,1,2},输出节点数 GPTBars,window_out 设为 EmbeddingSize/2;第四层 Conv(defNeuronConvOCL)把通道扩到 5 倍、窗口与步长对齐上层 window_out,最终 window_out 拉到 EmbeddingSize。外汇与贵金属杠杆高、滑点跳空频繁,这套结构在实盘前务必用 MT5 策略测试器跑过小样本回测再上。
sState.state[shift + class="num">1] = (class="type">float)(Rates[b].high - open); sState.state[shift + class="num">2] = (class="type">float)(Rates[b].low - open); sState.state[shift + class="num">3] = (class="type">float)(Rates[b].tick_volume / class="num">1000.0f); sState.state[shift + class="num">4] = rsi; sState.state[shift + class="num">5] = cci; sState.state[shift + class="num">6] = atr; sState.state[shift + class="num">7] = macd; sState.state[shift + class="num">8] = sign; class="type">bool CreateDescriptions(CArrayObj *encoder, CArrayObj *actor, CArrayObj *critic) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) 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">//--- 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 = MathMax(class="num">1000, GPTBars); 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 = defNeuronEmbeddingOCL; { class="type">int temp[] = {class="num">4, class="num">1, class="num">1, class="num">1, class="num">2}; ArrayCopy(descr.windows, temp); } prev_count = descr.count = GPTBars; class="type">int prev_wout = descr.window_out = EmbeddingSize / class="num">2; if(!encoder.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 = defNeuronConvOCL; descr.count = prev_count * class="num">5; descr.step = descr.window = prev_wout; prev_wout = descr.window_out = EmbeddingSize; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; }
◍ 编码器与策略网络的分层堆法
这段构建逻辑把编码器(encoder)和策略网络(actor)拆成不同层来组装,每一层都用 CLayerDescription 描述神经元类型、数量与窗口参数,任一层 Add 失败就 delete 并返回 false,避免半吊子模型加载进 MT5。 编码器第 4 层先放一个 PEOCL 神经元层,window 直接取上一层输出窗口的 5 倍(prev_wout * 5);随后用 for 循环叠 5 个 ConformerOCL 层,每层 step=4、layers=5、window_out 绑定 EmbeddingSize,这种堆叠倾向增强局部时序特征的抽取能力。 actor 网络从 AccountDescr 个输入节点起步(激活 None、ADAM 优化),第 1 层映射到 EmbeddingSize 并走 SIGMOID;第 2–4 层是 3 个 CrossAttenOCL 交叉注意力层,units 设为 {1, GPTBars*5}、windows 设为 {EmbeddingSize, EmbeddingSize},window_out=16、step=4,把账户状态与行情序列做跨域对齐。 最后 actor 第 5 层用 SIGMOID 压到 LatentCount 个潜变量输出,作为动作分布。外汇与贵金属杠杆高,这类自定义网络若参数错配,回测曲线可能严重过拟合,上 MT5 前建议先单步打印各层 descr 字段确认维度链不断。
class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronPEOCL; descr.count = prev_count; descr.window = prev_wout * class="num">5; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } for(class="type">int i = class="num">0; i < class="num">5; i++) { if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConformerOCL; descr.count = prev_count; descr.window = prev_wout; descr.step = class="num">4; descr.window_out = EmbeddingSize; descr.layers = class="num">5; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } } class=class="str">"cmt">//--- Actor actor.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = AccountDescr; 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 = defNeuronBaseOCL; prev_count = descr.count = EmbeddingSize; 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">2-class="num">4 for(class="type">int i = class="num">0; i < class="num">3; i++) { if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronCrossAttenOCL; { class="type">int temp[] = {class="num">1, GPTBars * class="num">5}; ArrayCopy(descr.units, temp); } { class="type">int temp[] = {EmbeddingSize, EmbeddingSize}; ArrayCopy(descr.windows, temp); } descr.window_out = class="num">16; descr.step = class="num">4; 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">5 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = SIGMOID; descr.optimization = ADAM; if(!actor.Add(descr)) {
Actor与Critic网络层的堆叠细节
这段构建逻辑把强化学习的双网络拆得很直白:Actor 在第 6 层用 defNeuronBaseOCL 铺了 2*NActions 个无激活神经元,第 7 层接 defNeuronVAEOCL 做动作潜变量输出,优化器统一挂 ADAM。 Critic 侧从输入层 NActions 起步,第二层压到 EmbeddingSize 并套 SIGMOID;真正吃算力的是 layer 2-4 的循环——连续 3 层 defNeuronCrossAttenOCL,units 设成 {1, GPTBars*5}、windows 设成 {EmbeddingSize, EmbeddingSize},window_out=16、step=4,相当于在时序横截面上做交叉注意力滑动。 任何一层 new CLayerDescription 失败或 Add 返回 false 都会先 delete 再 return false,避免野指针。开 MT5 把 GPTBars 从默认改到 20,units 第二项会跳到 100,显存占用可能明显上升,建议先在策略测试器跑小样本。外汇与贵金属杠杆高,这类模型过拟合后实盘回撤概率偏大,参数别直接照搬。
class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">6 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">7 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.type = defNeuronBaseOCL; prev_count = descr.count = NActions; descr.activation = None; descr.optimization = ADAM; 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.type = defNeuronBaseOCL; prev_count = descr.count = EmbeddingSize; 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">2-class="num">4 for(class="type">int i = class="num">0; i < class="num">3; i++) { if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronCrossAttenOCL; { class="type">int temp[] = {class="num">1, GPTBars * class="num">5}; ArrayCopy(descr.units, temp); } { class="type">int temp[] = {EmbeddingSize, EmbeddingSize}; ArrayCopy(descr.windows, temp); } descr.window_out = class="num">16; descr.step = class="num">4; descr.activation = None; descr.optimization = ADAM; if(!critic.Add(descr)) { class="kw">delete descr; class="kw">return false; } } class=class="str">"cmt">//--- layer class="num">5 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">6 if(!(descr = new CLayerDescription()))
「Critic 尾部层装配与训练采样循环」
Critic 网络在层 6 之后继续挂了两层:层 6 用 LatentCount 个 SIGMOID 激活的神经元做隐变量映射,优化器选 ADAM;层 7 直接以 NRewards 个无激活(None)神经元输出奖励估计,同样走 ADAM。任一层 Add 失败就 delete 描述符并返回 false,避免悬空对象拖垮后续训练。 训练函数 Train 里先用 GetProbTrajectories 按 0.9 温度系数抽轨迹概率,再用双重 MathRand 平方归一化挑起始 state,偏移量上限由 Buffer[tr].Total - 2 - PrecoderBars - batch 卡死。batch 固定为 GPTBars + 48,state 小于等于 0 时直接 iter-- 并 continue 跳过本轮。 每个 batch 内从 state 跑到 end(end 取 state+batch 与 Buffer[tr].Total - PrecoderBars 的较小值),先清 Encoder 再做状态前向;Encoder.feedForward 若失败打函数名加行号并置 Stop 跳出。Critic 接 bActions 与前层 Encoder 指针做前向,成功后才把下一帧 rewards 赋给 result、下下帧赋给 target,供后续时序差分更新。外汇与贵金属行情下用这套 RL 结构回测,过拟合与滑点风险偏高,参数须自分样本验证。
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">7 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; } class="type">void Train(class="type">void) { class=class="str">"cmt">//--- vector<class="type">float> probability = GetProbTrajectories(Buffer, class="num">0.9); vector<class="type">float> result, target; class="type">bool Stop = false; class=class="str">"cmt">//--- class="type">uint ticks = GetTickCount(); class="type">int tr = SampleTrajectory(probability); class="type">int batch = GPTBars + class="num">48; class="type">int state = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (Buffer[tr].Total - class="num">2 - PrecoderBars - batch)); if(state <= class="num">0) { iter--; class="kw">continue; } Encoder.Clear(); class="type">int end = MathMin(state + batch, Buffer[tr].Total - PrecoderBars); for(class="type">int i = state; i < end; i++) { bState.AssignArray(Buffer[tr].States[i].state); class=class="str">"cmt">//--- State Encoder if(!Encoder.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, false, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; class="kw">break; } class=class="str">"cmt">//--- Critic bActions.AssignArray(Buffer[tr].States[i].action); if(bActions.GetIndex() >= class="num">0) bActions.BufferWrite(); if(!Critic.feedForward((CBufferFloat*)GetPointer(bActions), class="num">1, false, GetPointer(Encoder))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; class="kw">break; } result.Assign(Buffer[tr].States[i + class="num">1].rewards); target.Assign(Buffer[tr].States[i + class="num">2].rewards);
◍ 账户状态与时间周期特征的策略网络喂入
这段逻辑处在强化学习训练回路里,Critic 完成反向传播后,紧接着要构造给 Actor 用的账户特征向量 bAccount。它先取上一条状态(i-1,越界时取 0)的余额与净值,再算当前相对变化率,把 8 个原始账户字段归一化后压进缓冲区。 时间维度被拆成四条周期正弦/余弦:以 2023 全年秒数(约 31536000s)为基准的年周期、月线 PeriodSeconds(PERIOD_MN1)、周线 PERIOD_W1、日线 PERIOD_D1,分别用 MathSin / MathCos(2πx) 编码,x 为 0 时直接给 0 避免除零。这样 Actor 的前馈输入同时带资金曲线斜率和季节节律。 bAccount.GetIndex()>=0 才执行 BufferWrite(),随后 Actor.feedForward 以 bAccount 为输入、Encoder 为上下文做推理;任一步返回 false 就打印函数名加行号。外汇与贵金属杠杆高,这类特征若用于实盘策略,回测过拟合概率偏大,建议先开 MT5 用脚本打印 bAccount 各维度数值核对归一化范围。
result = result - target * DiscFactor;
Result.AssignArray(result);
Critic.TrainMode(true);
if(!Critic.backProp(Result, (CNet *)GetPointer(Encoder)) ||
!Encoder.backPropGradient((CBufferFloat*)NULL))
{
PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
Stop = true;
class="kw">break;
}
class=class="str">"cmt">//--- Policy
class="type">float PrevBalance = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">0];
class="type">float PrevEquity = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">1];
bAccount.Clear();
bAccount.Add((Buffer[tr].States[i].account[class="num">0] - PrevBalance) / PrevBalance);
bAccount.Add(Buffer[tr].States[i].account[class="num">1] / PrevBalance);
bAccount.Add((Buffer[tr].States[i].account[class="num">1] - PrevEquity) / PrevEquity);
bAccount.Add(Buffer[tr].States[i].account[class="num">2]);
bAccount.Add(Buffer[tr].States[i].account[class="num">3]);
bAccount.Add(Buffer[tr].States[i].account[class="num">4] / PrevBalance);
bAccount.Add(Buffer[tr].States[i].account[class="num">5] / PrevBalance);
bAccount.Add(Buffer[tr].States[i].account[class="num">6] / PrevBalance);
class="type">class="kw">double time = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7];
class="type">class="kw">double x = time / (class="type">class="kw">double)(D&class="macro">#x27;class="num">2024.01.class="num">01&class="macro">#x27; - D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;);
bAccount.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1);
bAccount.Add((class="type">float)MathCos(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1);
bAccount.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1);
bAccount.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
if(bAccount.GetIndex() >= class="num">0)
bAccount.BufferWrite();
class=class="str">"cmt">//--- Actor
if(!Actor.feedForward((CBufferFloat*)GetPointer(bAccount), class="num">1, false,
GetPointer(Encoder)))
{
PrintFormat("%s -> %d", __FUNCTION__, __LINE__);训练循环里的断点与进度回显
这段片段处在 Actor-Critic 训练主循环内部,每一轮都先让 Critic 对 Actor 做前向评估,任一环节失败就把 Stop 置 true 并 break,避免错误权重继续回传。 具体看,Critic.feedForward 接收 Actor 与 Encoder 指针,返回 false 时打印函数名与行号后直接退出本轮;Actor.backProp 与 Encoder.backPropGradient 同理,NULL 梯度表示编码器不再向上求梯度。 Critic 切回非训练模式后,用 Result 做反向传播,Actor.backPropGradient 带 -1 步长与 false 标志,Encoder 仍吃空梯度。若耗时超 500 毫秒(GetTickCount 差值),才计算 percent 进度并用 Comment 输出 Actor/Critic 的近期平均误差,精度到 15.8f。 循环结束清掉 Comment,打印最终 Actor 与 Critic 的 getRecentAverageError(10.7f),随后 ExpertRemove 卸载智能交易,整个自学习过程在 MT5 中跑完即停。外汇与贵金属行情高波动,这类离线训练结果仅供策略验证,实盘落地前须自行压力测试。
Stop = true; class="kw">break; } if(!Critic.feedForward((CNet *)GetPointer(Actor), -class="num">1, (CNet*)GetPointer(Encoder))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; class="kw">break; } if(!Actor.backProp(GetPointer(bActions), GetPointer(Encoder)) || !Encoder.backPropGradient((CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; class="kw">break; } Critic.TrainMode(false); if(!Critic.backProp(Result, (CNet *)GetPointer(Encoder)) || !Actor.backPropGradient((CNet *)GetPointer(Encoder), -class="num">1, -class="num">1, false) || !Encoder.backPropGradient((CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; class="kw">break; } if(GetTickCount() - ticks > class="num">500) { class="type">class="kw">double percent = (class="type">class="kw">double(i - state) / ((end - state)) + iter) * class="num">100.0 / (Iterations); class="type">class="kw">string str = StringFormat("%-14s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Actor", percent, Actor.getRecentAverageError()); str += StringFormat("%-14s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Critic", percent, Critic.getRecentAverageError()); Comment(str); ticks = GetTickCount(); } } } Comment(""); class=class="str">"cmt">//--- PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Actor", Actor.getRecentAverageError()); PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Critic", Critic.getRecentAverageError()); ExpertRemove(); class=class="str">"cmt">//--- }
「EURUSD H1 上的真实样本回测」
把前面几篇攒好的样本喂进 MT5 策略测试器,用 2023 年前 7 个月的 EURUSD H1 真实历史做训练,再用同年 8 月的数据做样本外测试,架构和训练算法改过之后单次迭代成本略增,但学习过程比早期版本更稳,迭代次数反而能压下来。 测试模型在 8 月跑了 34 笔交易,18 笔盈利,胜率 52.94%;平均盈利单比平均亏损单高出 52.47%,单笔最大盈利超过同等亏损 2 倍多。 整段测试盈利因子 1.72,净值曲线向上,但最高净值回撤 17.12%、余额回撤 8.96%——外汇品种杠杆高,这种回撤在实盘可能放大,上 MT5 用同样样本复跑前先想清楚仓位。
◍ 把这套变换器搬进 MT5 后的真实边界
前面用 MQL5 把“时空常数关注度变换器”跑通了,训练和测试集都出了盈利曲线,表面看像是把气象领域的连续关注度算法接进了行情序列。但必须说清楚:样本内盈利不等于样本外能活,外汇和贵金属杠杆高、滑点跳空频繁,这类模型在实盘只代表一种概率倾向。 原文作者把神经 ODE 和持续注意力绑在一起,本意是天气预报,我们只是借它的状态演化结构处理价格张量。代码层能复现,不代表参数迁移后还稳。 提醒一句:本文所有程序只是演示方法,不是可跟单信号。真要验证,自己开 MT5 用EURUSD的M15跑一遍训练,看测试集外推折损再决定。
随包附带的七个工程文件
这套 LSTM 多元时间序列预测方案不是只给思路,而是把可跑的源码全部打进 MQL5.zip(1093.34 KB)随文发布。压缩包里按职责拆成七块:Research.mq5 与 ResearchRealORL.mq5 是两个样本收集 EA,前者捞基础样本,后者用 Real-ORL 方法补示例;Study.mq5 管模型训练,Test.mq5 管模型测试;Trajectory.mqh 定义系统状态结构,NeuroNet.mqh 封装建网类,NeuroNet.cl 则是 OpenCL 端的算子库。 想在 MT5 里复现,直接把 zip 解到 MQL5 目录对应子夹即可,EA 与类库路径已分好。外汇与贵金属行情高波动、高杠杆,模型在历史集上表现不代表未来概率,上线前务必用 Test.mq5 在品种实况里跑一轮压力验证。 七个文件各管一段链路,改其中任一类库都可能影响整链收敛,调参时一次只动一处。