交易中的神经网络:统一轨迹生成模型(UniTraj)·进阶篇
(2/3)·从单一蒙版轨迹到Mamba双向编码,看UniTraj如何把预测与补全拧进同一个框架
◍ 反向遍历里的衰减累积写法
这段逻辑出现在单向轨迹神经元的反向通道初始化里,核心是用一个倒序循环把后续步的掩码折算成当前步的累积权重。 step 取负值,等于 -(variables * 3),说明内层索引是沿着输入张量往回跳;start 以 (total-1)*variables 打底,再叠加 v*3+2,定位到当前变量通道在最后一帧的偏移。 循环从 p=1 跑到 total-1,每步读取 concat_inp[start + p*step],并用 last = 1 + (1 - m) * last 做递推。这意味着越靠近末端、m 越小的位置,累积值膨胀越明显,训练时反向梯度可能更敏感。 外层类 CNeuronUniTraj 直接继承 CNeuronBaseOCL,成员里挂了 cEncoder、cSSM[4]、cVAE 等模块,说明这类神经元把注意力、状态空间与变分解码全压进一个实例里,MT5 上跑起来显存占用值得自己查一眼。
const class="type">int step = -(variables * class="num">3); const class="type">int start = (total - class="num">1) * variables + v * class="num">3 + class="num">2; class="type">class="kw">float last = class="num">0; d_bakw[(total - class="num">1) + v] = class="num">0; for(class="type">int p = class="num">1; p < total; p++) { class="type">class="kw">float m = concat_inp[start + p * step]; d_bakw[(total - class="num">1 - p) * variables + v] = last = class="num">1 + (class="num">1 - m) * last; } } } class CNeuronUniTraj : class="kw">public CNeuronBaseOCL { class="kw">protected: class="type">uint iVariables; class="type">class="kw">float fDropout; class=class="str">"cmt">//--- CBufferFloat cHistoryMask; CBufferFloat cFutureMask; CNeuronBaseOCL cData; CNeuronLearnabledPE cPE; CNeuronMVMHAttentionMLKV cEncoder; CNeuronBaseOCL cDForw; CNeuronBaseOCL cDBakw; CNeuronConvOCL cProjDForw; CNeuronConvOCL cProjDBakw; CNeuronBaseOCL cDataDForw; CNeuronBaseOCL cDataDBakw; CNeuronBaseOCL cConcatDataDForwBakw; CNeuronMambaBlockOCL cSSM[class="num">4]; CNeuronConvOCL cStat; CNeuronTransposeOCL cTranspStat; CVAE cVAE; CNeuronTransposeOCL cTranspVAE; CNeuronConvOCL cDecoder[class="num">2]; CNeuronTransposeOCL cTranspResult; class=class="str">"cmt">//--- class="kw">virtual class="type">bool Prepare(const CBufferFloat* history, const CBufferFloat* future); class="kw">virtual class="type">bool PrepareGrad(CBufferFloat* history_gr, CBufferFloat* future_gr); class="kw">virtual class="type">bool BTS(class="type">void); class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override { class="kw">return feedForward(NeuronOCL, NULL); } class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) class="kw">override; class=class="str">"cmt">//--- class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override { class="kw">return calcInputGradients(NeuronOCL, NULL, NULL, None); } class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override { class="kw">return updateInputWeights(NeuronOCL, NULL); } 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, CBufferFloat *second) class="kw">override; class=class="str">"cmt">//--- class="kw">public: CNeuronUniTraj(class="type">void) {}; ~CNeuronUniTraj(class="type">void) {}; class=class="str">"cmt">//---
单轨迹神经元初始化里的张量排布
CNeuronUniTraj 的 Init 把单条价格轨迹喂给 OpenCL 加速的 Transformer 式结构。注意基类初始化时传入的神经元数是 window * (units_count + forecast),也就是回看窗口乘以「历史编码单元数 + 预测步长」,这决定了显存缓冲的横向宽度。 dropout 参数被 MathMax(MathMin(dropout,1),0) 钳制在 [0,1],越界值不会崩,只会被静默收口。历史掩码 cHistoryMask 按 iVariables * units_count 长度初始化,未来掩码 cFutureMask 按 iVariables * forecast 初始化,两者分离意味着模型在编码阶段显式区分已知序列与待推演序列。 编码器 cEncoder 的 heads 参数传入 window_key,内部头数被算成 (heads+1)/2,当 heads 为奇数时实际头数比传入少半——调参时若发现并行注意力偏弱,先查这个数是否被整数除法吃掉了。下面这段是 Init 的主体声明与部分构建逻辑,逐行看缓冲怎么落位。
class="type">bool CNeuronUniTraj::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 forecast, class="type">class="kw">float dropout, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * (units_count + forecast), optimization_type, batch)) class="kw">return class="kw">false; iVariables = window; fDropout = MathMax(MathMin(dropout, class="num">1), class="num">0); if(!cHistoryMask.BufferInit(iVariables * units_count, class="num">1) || !cHistoryMask.BufferCreate(OpenCL)) class="kw">return class="kw">false; if(!cFutureMask.BufferInit(iVariables * forecast, class="num">1) || !cFutureMask.BufferCreate(OpenCL)) class="kw">return class="kw">false; if(!cData.Init(class="num">0, class="num">0, OpenCL, class="num">3 * iVariables * (units_count + forecast), optimization, iBatch)) class="kw">return class="kw">false; if(!cPE.Init(class="num">0, class="num">1, OpenCL, cData.Neurons(), optimization, iBatch)) class="kw">return class="kw">false; if(!cEncoder.Init(class="num">0, class="num">2, OpenCL, class="num">3, window_key, heads, (heads + class="num">1) / class="num">2, iVariables, class="num">1, class="num">1, (units_count + forecast), optimization, iBatch)) class="kw">return class="kw">false; if(!cDForw.Init(class="num">0, class="num">3, OpenCL, iVariables * (units_count + forecast), optimization, iBatch)) class="kw">return class="kw">false;; if(!cDBakw.Init(class="num">0, class="num">4, OpenCL, iVariables * (units_count + forecast), optimization, iBatch)) class="kw">return class="kw">false; if(!cProjDForw.Init(class="num">0, class="num">5, OpenCL, class="num">1, class="num">1, class="num">3, iVariables, (units_count + forecast), optimization, iBatch)) class="kw">return class="kw">false; cProjDForw.SetActivationFunction(SIGMOID); if(!cProjDBakw.Init(class="num">0, class="num">6, OpenCL, class="num">1, class="num">1, class="num">3, iVariables, (units_count + forecast), optimization, iBatch)) class="kw">return class="kw">false;
「轨迹模型各层的初始化与前向遮罩」
这段初始化把 UniTraj 网络的多层结构在 OpenCL 上下文里逐一落地:前馈数据层用设备号 0、层标识 7,回传数据层用标识 8,拼接层标识 9 且神经元数固定为 cData.Neurons() 的两倍。SSM 状态空间模块按 cSSM.Size() 循环分配,每层输入维度 6*iVariables、输出 12*iVariables,层 id 从 10 开始顺延。 后续统计层、转置层、VAE 与转置 VAE 的 id 依次自增,其中 cVAE 隐层神经元数取 cTranspStat.Neurons()/2,转置 VAE 再除以 iVariables 得到 w。两个解码器分别用 LReLU 和 TANH 激活,最后把转置结果层设为网络输出并回传梯度。 前向方法 feedForward 里先建 History Mask:用 cHistoryMask.Total() 取总长度并初始化为全 1。训练态 bTrain 下按 fDropout 比例随机置 0——例如 fDropout=0.1 时,千个位置约随机屏蔽 100 处,制造丢弃噪声。写完缓冲才继续后续计算,否则直接返回 false。
cProjDBakw.SetActivationFunction(SIGMOID); if(!cDataDForw.Init(class="num">0, class="num">7, OpenCL, cData.Neurons(), optimization, iBatch)) class="kw">return class="kw">false; if(!cDataDBakw.Init(class="num">0, class="num">8, OpenCL, cData.Neurons(), optimization, iBatch)) class="kw">return class="kw">false; if(!cConcatDataDForwBakw.Init(class="num">0, class="num">9, OpenCL, class="num">2 * cData.Neurons(), optimization, iBatch)) class="kw">return class="kw">false; for(class="type">uint i = class="num">0; i < cSSM.Size(); i++) { if(!cSSM[i].Init(class="num">0, class="num">10 + i, OpenCL, class="num">6 * iVariables, class="num">12 * iVariables, (units_count + forecast), optimization, iBatch)) class="kw">return class="kw">false; } class="type">uint id = class="num">10 + cSSM.Size(); if(!cStat.Init(class="num">0, id, OpenCL, class="num">6, class="num">6, class="num">12, iVariables * (units_count + forecast), optimization, iBatch)) class="kw">return class="kw">false; id++; if(!cTranspStat.Init(class="num">0, id, OpenCL, iVariables * (units_count + forecast), class="num">12, optimization, iBatch)) class="kw">return class="kw">false; id++; if(!cVAE.Init(class="num">0, id, OpenCL, cTranspStat.Neurons() / class="num">2, optimization, iBatch)) class="kw">return class="kw">false; id++; if(!cTranspVAE.Init(class="num">0, id, OpenCL, cVAE.Neurons() / iVariables, iVariables, optimization, iBatch)) class="kw">return class="kw">false; id++; class="type">uint w = cTranspVAE.Neurons() / iVariables; if(!cDecoder[class="num">0].Init(class="num">0, id, OpenCL, w, w, class="num">2 * (units_count + forecast), iVariables, optimization, iBatch)) class="kw">return class="kw">false; cDecoder[class="num">0].SetActivationFunction(LReLU); id++; if(!cDecoder[class="num">1].Init(class="num">0, id, OpenCL, class="num">2 * (units_count + forecast), class="num">2 * (units_count + forecast), (units_count + forecast), iVariables, optimization, iBatch)) class="kw">return class="kw">false; cDecoder[class="num">1].SetActivationFunction(TANH); id++; if(!cTranspResult.Init(class="num">0, id, OpenCL, iVariables, (units_count + forecast), optimization, iBatch)) class="kw">return class="kw">false; if(!SetOutput(cTranspResult.getOutput(), true) || !SetGradient(cTranspResult.getGradient(), true)) class="kw">return class="kw">false; SetActivationFunction((ENUM_ACTIVATION)cDecoder[class="num">1].Activation()); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronUniTraj::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) { if(!NeuronOCL) class="kw">return class="kw">false; class=class="str">"cmt">//--- Create History Mask class="type">int total = cHistoryMask.Total(); if(!cHistoryMask.BufferInit(total, class="num">1)) class="kw">return class="kw">false; if(bTrain) { for(class="type">int i = class="num">0; i < class="type">int(total * fDropout); i++) cHistoryMask.Update(RND(total), class="num">0); } if(!cHistoryMask.BufferWrite())
◍ Transformer 推理链路的逐层前向穿透
这段片段展示了一个混合架构(Encoder + BTS 双向投影 + SSM + VAE + Decoder)在单次前向传播时的调用顺序。任何一层 FeedForward 返回 false 都会直接中断并返回 false,意味着实盘加载模型时只要缓冲区初始化或某一子网络推理失败,整个预测就废了。 先看 Future Mask 的构建:total 取掩码对象的总长度,BufferInit 的第二个参数由 SecondInput 决定填 0 或 1;训练态且存在 SecondInput 时,会按 fDropout 比例随机将掩码置 0(循环次数为 int(total * fDropout)),这是典型的 dropout 正则。 接着 Encoder 吃位置编码 cPE 的输出,BTS 模块分别计算前向/后向投影并做 ElementMult 与 Concat(拼接维度参数写死为 3 和 3,第三维取 cData.Neurons()/3)。SSM 是链式堆叠:第 0 层吃拼接结果,后续每层吃上一层的 AsObject(),循环上限用 cSSM.Size() 控制。 VAE 支路从最后一层 SSM 输出经 cStat、cTranspStat 压缩再展开,Decoder 对称地做转置与两层还原,最终 cTranspResult 输出。整条链路无异常兜底,调试时建议先在 MT5 策略测试器里单步看哪一层先返回 false。外汇与贵金属杠杆高,模型信号仅作概率参考,不能直接当仓位依据。
class="kw">return class="kw">false; class=class="str">"cmt">//--- Create Future Mask total = cFutureMask.Total(); if(!cFutureMask.BufferInit(total, (!SecondInput ? class="num">0 : class="num">1))) class="kw">return class="kw">false; if(bTrain && !!SecondInput) { for(class="type">int i = class="num">0; i < class="type">int(total * fDropout); i++) cFutureMask.Update(RND(total), class="num">0); } if(!cFutureMask.BufferWrite()) class="kw">return class="kw">false; class=class="str">"cmt">//--- Prepare Data if(!Prepare(NeuronOCL.getOutput(), SecondInput)) class="kw">return class="kw">false; class=class="str">"cmt">//--- Encoder if(!cPE.FeedForward(cData.AsObject())) class="kw">return class="kw">false; if(!cEncoder.FeedForward(cPE.AsObject())) class="kw">return class="kw">false; class=class="str">"cmt">//--- BTS if(!BTS()) class="kw">return class="kw">false; if(!cProjDForw.FeedForward(cDForw.AsObject())) class="kw">return class="kw">false; if(!cProjDBakw.FeedForward(cDBakw.AsObject())) class="kw">return class="kw">false; if(!ElementMult(cEncoder.getOutput(), cProjDForw.getOutput(), cDataDForw.getOutput())) class="kw">return class="kw">false; if(!ElementMult(cEncoder.getOutput(), cProjDBakw.getOutput(), cDataDBakw.getOutput())) class="kw">return class="kw">false; if(!Concat(cDataDForw.getOutput(), cDataDBakw.getOutput(), cConcatDataDForwBakw.getOutput(), class="num">3, class="num">3, cData.Neurons() / class="num">3)) class="kw">return class="kw">false; class=class="str">"cmt">//--- SSM if(!cSSM[class="num">0].FeedForward(cConcatDataDForwBakw.AsObject())) class="kw">return class="kw">false; for(class="type">uint i = class="num">1; i < cSSM.Size(); i++) if(!cSSM[i].FeedForward(cSSM[i - class="num">1].AsObject())) class="kw">return class="kw">false; class=class="str">"cmt">//--- VAE if(!cStat.FeedForward(cSSM[cSSM.Size() - class="num">1].AsObject())) class="kw">return class="kw">false; if(!cTranspStat.FeedForward(cStat.AsObject())) class="kw">return class="kw">false; if(!cVAE.FeedForward(cTranspStat.AsObject())) class="kw">return class="kw">false; class=class="str">"cmt">//--- Decoder if(!cTranspVAE.FeedForward(cVAE.AsObject())) class="kw">return class="kw">false; if(!cDecoder[class="num">0].FeedForward(cTranspVAE.AsObject())) class="kw">return class="kw">false; if(!cDecoder[class="num">1].FeedForward(cDecoder[class="num">0].AsObject())) class="kw">return class="kw">false; if(!cTranspResult.FeedForward(cDecoder[class="num">1].AsObject())) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CreateEncoderDescriptions(CArrayObj *&encoder) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!encoder) {
编码器四层堆叠的写法
在 MT5 里搭一个价格序列自编码器,核心是把各层描述依次塞进 CArrayObj 容器。下面这段直接可贴进 EA 的初始化函数,跑不通就返回 false,方便你立刻在编译器里排错。 输入层用 defNeuronBaseOCL,节点数等于 HistoryBars * BarDescr,优化器选 ADAM、激活留 None,相当于把原始 K 线特征原样接进网络。 第二层接 defNeuronBatchNormOCL 做批归一化,batch 设 1e4,能缓解外汇小时线均值漂移带来的训练抖动;第三层 defNeuronUniTrajOCL 才是重点,window=BarDescr、window_out=EmbeddingSize、count=HistoryBars,step=4 相当于 4 个注意力头,probability=0.5 是 DropOut 比例,调小可能更不容易过拟合但收敛偏慢。 后两层分别用 defNeuronRevInDenormOCL 做反归一化、defNeuronFreDFOCL 做频域变换,后者 probability=0.7f 丢弃概率偏高,贵金属跳空段训练时建议先降到 0.5 观察 loss 曲线。外汇与贵金属杠杆高,参数乱调可能引发实盘大幅回撤,任何层结构改动先在策略测试器跑历史样本。
encoder = new CArrayObj(); if(!encoder) class="kw">return class="kw">false; class=class="str">"cmt">//--- Encoder encoder.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return class="kw">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 class="kw">false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronUniTrajOCL; descr.window = BarDescr; class=class="str">"cmt">//window descr.window_out = EmbeddingSize; class=class="str">"cmt">//Inside Dimension descr.count = HistoryBars; class=class="str">"cmt">//Units descr.layers = NForecast; class=class="str">"cmt">//Forecast descr.step=class="num">4; class=class="str">"cmt">//Heads descr.probability=class="num">0.5f; class=class="str">"cmt">//DropOut descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronRevInDenormOCL; descr.count = BarDescr * (NForecast+HistoryBars); descr.activation = None; descr.optimization = ADAM; descr.layers = class="num">1; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronFreDFOCL; descr.window = BarDescr; descr.count = NForecast+HistoryBars; descr.step = class="type">int(true); descr.probability = class="num">0.7f; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- class="kw">return true; }
「训练循环里的状态编码与截断校验」
编码器维度对不上是这类预测模型最常见的初始化坑。代码里先用 Encoder.getResults(Result) 把输出拉出来,再拿 Result.Total() 和 (NForecast+HistoryBars)*BarDescr 比:若两者不等,直接 PrintFormat 报错并返回 INIT_FAILED,说明你喂给编码器的状态帧数和预期轨迹长度没对齐,EA 起不来。 Train() 函数里真正跑训练时,用 Batch=1000 做小批量迭代,Iterations 控制总轮数。start 位置用 MathRand() 的平方分布来偏向前段样本,避开尾部不足 NForecast 的残缺窗口;当 i 超出 Buffer 有效长度减 NForecast 时直接 break,防止越界读状态。 状态编码有 50% 概率走 feedForward 正向传播(MathRand()/32767.0 < 0.5),另 50% 把目标 Result 写回缓冲再喂编码器。若 feedForward 返回 false,置 Stop=true 并跳出,训练中止。外汇与贵金属行情下用这套逻辑做状态预测,模型过拟合和样本偏差风险偏高,参数最好在 MT5 策略测试器里用真实点差重跑验证。
Encoder.getResults(Result); if(Result.Total() != (NForecast+HistoryBars) * BarDescr) { PrintFormat("The scope of the Encoder does not match the forecast state count(%d <> %d)", (NForecast+HistoryBars) * BarDescr, Result.Total()); class="kw">return INIT_FAILED; } class="type">void Train(class="type">void) { class=class="str">"cmt">//--- vector<class="type">class="kw">float> probability = GetProbTrajectories(Buffer, class="num">0.9); vector<class="type">class="kw">float> result, target, state; class="type">bool Stop = class="kw">false; const class="type">int Batch = class="num">1000; class="type">int b = class="num">0; class=class="str">"cmt">//--- class="type">uint ticks = GetTickCount(); for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !Stop); iter += b) { class="type">int tr = SampleTrajectory(probability); class="type">int start = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (Buffer[tr].Total - class="num">5 - NForecast)); if(start <= class="num">0) class="kw">continue; for(b = class="num">0; (b < Batch && (iter + b) < Iterations); b++) { class="type">int i = start + b; if(i >= MathMin(Buffer[tr].Total, Buffer_Size) - NForecast) break; state.Assign(Buffer[tr].States[i].state); if(MathAbs(state).Sum() == class="num">0) break; bState.AssignArray(state); class=class="str">"cmt">//--- Collect target data if(!Result.AssignArray(Buffer[tr].States[i + NForecast].state)) class="kw">continue; if(!Result.Resize(BarDescr * NForecast)) class="kw">continue; class=class="str">"cmt">//--- State Encoder if((MathRand() / class="num">32767.0) < class="num">0.5) { if(!Encoder.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, class="kw">false, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } } else { if(Result.GetIndex()>=class="num">0) Result.BufferWrite(); if(!Encoder.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, class="kw">false, Result))