神经网络变得简单(第 72 部分):噪声环境下预测轨迹·进阶篇
(2/3)· 当模型只学到训练集的窄解读,预测全变成同质曲线,这篇拆开自监督路标噪声预测的内部机制
解码器与噪声预测网络的层结构装配
在 MT5 用 OpenCL 跑扩散类模型时,解码器(decoder)和噪声预测(noise)两套网络是分开搭的。下面这段装配逻辑直接决定了显存占用和推理延迟,建议开 MT5 把 CLayerDescription 的参数逐项对照改。 解码器输入层用 defNeuronBaseOCL,神经元数 = EmbeddingSize,激活函数为 None,优化器 ADAM;第一层把 (HistoryBars + PrecoderBars) * EmbeddingSize 个节点压进 LReLU,第二层换成多头注意力(defNeuronMLMHAttentionOCL),step=4、window_out=16、layers=2,这一步把序列长度折叠回 prev_count/EmbeddingSize。 第三层用 defNeuronMultiModels 挂 3 个模型,window 接上一层 window_out(16),step 接 prev_count。噪声网络前两层复制解码器输入结构,但第一层节点数改为 HistoryBars * EmbeddingSize;仅当 HistoryBars>1 才追加第二层注意力,参数与解码器同构(step=4、window_out=16、layers=2)。 别把层数与显存画等号:window_out=16 且 layers=2 的注意力层,在 EmbeddingSize=64、HistoryBars=128 时,单卡显存可能倾向吃满 2GB 以上,外汇贵金属行情高频重训时务必监控 GPU 占用。
class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- Decoder decoder.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = EmbeddingSize; 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 = defNeuronBaseOCL; prev_count = descr.count = (HistoryBars + PrecoderBars) * EmbeddingSize; descr.activation = LReLU; descr.optimization = ADAM; if(!decoder.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 = defNeuronMLMHAttentionOCL; prev_count = descr.count = prev_count / EmbeddingSize; prev_wout = descr.window = EmbeddingSize; descr.step = class="num">4; descr.window_out = class="num">16; descr.layers = class="num">2; descr.optimization = ADAM; if(!decoder.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 = defNeuronMultiModels; descr.count = class="num">3; descr.window = prev_wout; descr.step = prev_count; descr.activation = None; descr.optimization = ADAM; if(!decoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- Noise Prediction noise.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.Copy(decoder.At(class="num">0)); if(!noise.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 = HistoryBars * EmbeddingSize; descr.activation = LReLU; descr.optimization = ADAM; if(!noise.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- if(HistoryBars > class="num">1) { class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronMLMHAttentionOCL; prev_count = descr.count = prev_count / EmbeddingSize; prev_wout = descr.window = EmbeddingSize; descr.step = class="num">4; descr.window_out = class="num">16; descr.layers = class="num">2; descr.optimization = ADAM;
「噪声分支与编码器的双层构建」
这段逻辑在条件分支里给噪声网络(noise)叠了两层结构。若走 if 分支,第三层用 defNeuronMultiModels 类型,节点数取 BarDescr,窗口为 prev_wout、步长 prev_count,激活函数关掉(None),优化器选 ADAM;若走 else 分支,第二、三层都是 defNeuronBaseOCL,隐层宽 LatentCount,激活用 LReLU,输出层同样 None+ADAM。 每次 new 出 CLayerDescription 后都立即调 noise.Add(descr),返回 false 就 delete 并退出,避免野指针。这种写法在 MT5 里跑时,若 BarDescr 或 LatentCount 传 0,网络会建空层,后续 OnTick 推理可能直接报 4014(数组越界)。 宏里把噪声乘子 STE_Noise_Multiplier 和 STD_Delta_Multiplier 都定为 1.0f/10,意味着噪声预测误差的影响权重压到十分之一,属于偏保守的扩散扰动设定。外汇与贵金属价格序列噪声大,实盘接这套网络前,建议先把 λ、ω 调到 0.05~0.2 区间做样本外回测。 OnInit 中先 LoadTotalBase(),失败就打印错误码并 INIT_FAILED;通过后用 Encoder.Load 读 Enc.nnw,四个 temp 占位接返回值,dtStudied 标记已训练。开 MT5 把这段贴进 EA 初始化,看日志有无 4014 或文件加载错,是最快的验证路径。
if(!noise.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 = defNeuronMultiModels; descr.count = BarDescr; descr.window = prev_wout; descr.step = prev_count; descr.activation = None; descr.optimization = ADAM; if(!noise.Add(descr)) { class="kw">delete descr; class="kw">return false; } } else { class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.optimization = ADAM; descr.activation = LReLU; if(!noise.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; prev_count = descr.count = BarDescr; descr.activation = None; descr.optimization = ADAM; if(!noise.Add(descr)) { class="kw">delete descr; class="kw">return false; } } class=class="str">"cmt">//--- class="kw">return true; } CNet Encoder; CNet Decoder; CNet Noise; class="macro">#define STE_Noise_Multiplier class="num">1.0f/class="num">10 class=class="str">"cmt">// λ determined the impact of noise prediction error class="macro">#define STD_Delta_Multiplier class="num">1.0f/class="num">10 class=class="str">"cmt">// noise factor ω class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- ResetLastError(); if(!LoadTotalBase()) { PrintFormat("Error of load study data: %d", GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- load models class="type">float temp; if(!Encoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true) ||
◍ 模型加载失败后的重建与维度校验
当 Encoder、Decoder 或 Noise 预测网络从本地 .nnw 文件加载失败时,代码会打印 "Init new models" 并走新建分支:先以 CArrayObj 分别持有三套网络的描述对象,再交给 CreateTrajNetDescriptions 填充结构,任意一步失败立即 delete 三个指针并返回 INIT_FAILED。 这种懒加载逻辑意味着你改了网络拓扑却没删旧权重文件时,EA 可能静默用新描述重建——外汇与贵金属行情高波动下,这一行为可能导致实盘推理与回测不一致,建议在 MT5 数据目录手动清掉带 Dec.nnw / NP.nnw 后缀的文件再做验证。 重建成功后,代码对三套网络的输入输出维度做了硬校验:Encoder 的 getResults 总数必须等于 EmbeddingSize;其第 0 层输出必须等于 HistoryBars * BarDescr,否则报 "Input size of Encoder doesn't match state description"。 Decoder 与 Noise 的第 0 层输入都锁死为 EmbeddingSize,而 Noise 的最终输出又要回到 HistoryBars * BarDescr。任何一项对不上,初始化直接 INIT_FAILED,MT5 终端日志会给出具体不匹配数值(如 %d <> %d),调参时优先对照这几个乘积关系。 最后一段为 LastEncoder 与 Gradient 申请 EmbeddingSize 长度的缓冲区,并绑定 OpenCL 上下文;若 BufferCreate 失败会打印 "Error of create buffers: %d" 及 GetLastError 代码,多数情况源于显存不足或 OpenCL 设备未就绪。
!Decoder.Load(FileName + "Dec.nnw", temp, temp, temp, dtStudied, true) || !Noise.Load(FileName + "NP.nnw", temp, temp, temp, dtStudied, true)) { Print("Init new models"); CArrayObj *encoder = new CArrayObj(); CArrayObj *decoder = new CArrayObj(); CArrayObj *noise = new CArrayObj(); if(!CreateTrajNetDescriptions(encoder, decoder, noise)) { class="kw">delete encoder; class="kw">delete decoder; class="kw">delete noise; class="kw">return INIT_FAILED; } if(!Encoder.Create(encoder) || !Decoder.Create(decoder) || !Noise.Create(noise)) { class="kw">delete encoder; class="kw">delete decoder; class="kw">delete noise; class="kw">return INIT_FAILED; } class="kw">delete encoder; class="kw">delete decoder; class="kw">delete noise; class=class="str">"cmt">//--- } class=class="str">"cmt">//--- OpenCL = Encoder.GetOpenCL(); Decoder.SetOpenCL(OpenCL); Noise.SetOpenCL(OpenCL); class=class="str">"cmt">//--- Encoder.getResults(Result); if(Result.Total() != EmbeddingSize) { PrintFormat("The scope of the Encoder does not match the embedding size count(%d <> %d)", EmbeddingSize, Result.Total()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Encoder.GetLayerOutput(class="num">0, Result); if(Result.Total() != (HistoryBars * BarDescr)) { PrintFormat("Input size of Encoder doesn&class="macro">#x27;t match state description(%d <> %d)", Result.Total(), (HistoryBars * BarDescr)); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Decoder.GetLayerOutput(class="num">0, Result); if(Result.Total() != EmbeddingSize) { PrintFormat("Input size of Decoder doesn&class="macro">#x27;t match Encoder output(%d <> %d)", Result.Total(), EmbeddingSize); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Noise.GetLayerOutput(class="num">0, Result); if(Result.Total() != EmbeddingSize) { PrintFormat("Input size of Noise Prediction model doesn&class="macro">#x27;t match Encoder output(%d <> %d)", Result.Total(), EmbeddingSize); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Noise.getResults(Result); if(Result.Total() != (HistoryBars * BarDescr)) { PrintFormat("Output size of Noise Prediction model doesn&class="macro">#x27;t match state description(%d <> %d)", Result.Total(), (HistoryBars * BarDescr)); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- if(!LastEncoder.BufferInit(EmbeddingSize, class="num">0) || !Gradient.BufferInit(EmbeddingSize, class="num">0) || !LastEncoder.BufferCreate(OpenCL) || !Gradient.BufferCreate(OpenCL)) { PrintFormat("Error of create buffers: %d", GetLastError());
初始化失败与模型落盘的细节
EA 在 OnInit 末尾若 EventChartCustom 派发自定义事件失败,会直接 PrintFormat 打出错误码并返回 INIT_FAILED,图表事件 ID 写死为 1、参数 lparam/dparam 均为 0、sparam 传 "Init",这种写法能在日志里立刻区分是初始化事件没发出还是别的环节崩了。 OnDeinit 里有个容易踩的坑:当 reason 等于 REASON_INITFAILED 或 REASON_RECOMPILE 时不存盘,其余情况会把三个网络各自 Save 成 Enc.nnw、Dec.nnw、NP.nnw,文件名拼的是 FileName 前缀加固定后缀,平均误差通过 getRecentAverageError() 写入,TimeCurrent() 记时间戳。 Train 函数开头构造概率向量用 GetProbTrajectories(Buffer, 0.9),置信阈值 0.9 直接写死;batch 大小由 GPTBars + 50 决定,STE 向量长度按 (HistoryBars + PrecoderBars) * 3 清零,STE_Noise 则按 HistoryBars * BarDescr 分配。 采样状态 state 用 MathRand 平方和除以 32767 的平方做偏置抽样,再乘 (Buffer[tr].Total - 3 - PrecoderBars - batch),为负就 iter-- 并重抽,这意味着总迭代次数可能比 Iterations 参数实际跑得多。每次循环先 Clear 三个网络再重置 LastEncoder 的嵌入缓冲,EmbeddingSize 决定缓冲维度。
class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- if(!EventChartCustom(ChartID(), class="num">1, class="num">0, class="num">0, "Init")) { PrintFormat("Error of create study event: %d", GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- if(!(reason == REASON_INITFAILED || reason == REASON_RECOMPILE)) { Encoder.Save(FileName + "Enc.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true); Decoder.Save(FileName + "Dec.nnw", Decoder.getRecentAverageError(), class="num">0, class="num">0, TimeCurrent(), true); Noise.Save(FileName + "NP.nnw", Noise.getRecentAverageError(), class="num">0, class="num">0, TimeCurrent(), true); } class="kw">delete Result; class="kw">delete OpenCL; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Train function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Train(class="type">void) { class=class="str">"cmt">//--- vector<class="type">float> probability = GetProbTrajectories(Buffer, class="num">0.9); class=class="str">"cmt">//--- vector<class="type">float> result, target, inp; matrix<class="type">float> targets; matrix<class="type">float> delta; STE = vector<class="type">float>::Zeros((HistoryBars + PrecoderBars) * class="num">3); STE_Noise = vector<class="type">float>::Zeros(HistoryBars * BarDescr); class="type">int std_count = class="num">0; class="type">int batch = GPTBars + class="num">50; class="type">bool Stop = false; class="type">uint ticks = GetTickCount(); for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !Stop); iter ++) { class="type">int tr = SampleTrajectory(probability); class="type">int state = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (Buffer[tr].Total - class="num">3 - PrecoderBars - batch)); if(state < class="num">0) { iter--; class="kw">continue; } Encoder.Clear(); Decoder.Clear(); Noise.Clear(); LastEncoder.BufferInit(EmbeddingSize, class="num">0);
「分批喂入状态矩阵时的增量计算与异常跳出」
这段逻辑出现在按批次处理历史状态缓冲时,核心是先算出本批处理的终点 end,再用 MathMin 限制不超过总预处理柱数减去 PrecoderBars,避免越界读取。delta 矩阵按 (end-state-1) 行、单状态维度列初始化为零,用来承载相邻状态的差分。 循环里对每一根 i,把当前状态赋给 inp 并写入 State 缓冲;row 为相对偏移。若不是末根,则把 inp 存进 delta 的第 row 行;若 row>0,则用上一行 delta 减当前 inp 回填前一行,形成逐步差分序列。 Encoder、Decoder、Noise 三个网络依次前向传播,任一返回失败就打印函数名与行号、置 Stop 并 break。这种写法能在 MT5 策略测试器里直接暴露是哪一层在前向计算时断掉,方便定位张量尺寸不匹配。 target 取出后按 BarDescr 做 reshape,若 size 大于 BarDescr 就重排成多行,再 Resize 追加 PrecoderBars 行、3 列,给后续预编码目标留白。循环内对 t 的临时 temp 矩阵同样按 BarDescr 重排并截到 3 列,保证多步目标维度一致。外汇与贵金属行情跳空频繁,这类张量 reshape 若没卡准 BarDescr,实盘可能比回测更容易触发 Stop。
class="type">int end = MathMin(state + batch, Buffer[tr].Total - PrecoderBars); delta = matrix<class="type">float>::Zeros(end - state - class="num">1, Buffer[tr].States[state].state.Size()); for(class="type">int i = state; i < end; i++) { inp.Assign(Buffer[tr].States[i].state); State.AssignArray(inp); class="type">int row = i - state; if(i < (end - class="num">1)) delta.Row(inp, row); if(row > class="num">0) delta.Row(delta.Row(row - class="num">1) - inp, row - class="num">1); if(!LastEncoder.BufferWrite() || !Encoder.feedForward((CBufferFloat*)GetPointer(State), class="num">1, false, (CBufferFloat*)GetPointer(LastEncoder))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!Decoder.feedForward(GetPointer(Encoder), -class="num">1, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!Noise.feedForward(GetPointer(Encoder), -class="num">1, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } target.Assign(Buffer[tr].States[i].state); class="type">ulong size = target.Size(); targets = matrix<class="type">float>::Zeros(class="num">1, size); targets.Row(target, class="num">0); if(size > BarDescr) targets.Reshape(size / BarDescr, BarDescr); class="type">ulong shift = targets.Rows(); targets.Resize(shift + PrecoderBars, class="num">3); for(class="type">int t = class="num">0; t < PrecoderBars; t++) { target.Assign(Buffer[tr].States[i + t].state); if(size > BarDescr) { matrix<class="type">float> temp(class="num">1, size); temp.Row(target, class="num">0); temp.Reshape(size / BarDescr, BarDescr); temp.Resize(size / BarDescr, class="num">3);
◍ 误差超限才反向传播
这段逻辑跑在自编码器的训练循环末尾,核心动作是:只有当预测误差偏离滑动标准差足够远时,才触发 Decoder 与 Encoder 的 backProp,否则跳过权重更新。 STE 用递推式更新,std_count 被钳在 999 以内,意味着稳态误差方差估计最多用近千步窗口。check = MathAbs(error) - STE * STE_Multiplier,只要 check 最大值大于 0,就认为当前样本异常,需要 CAGrad(error)+result 回填并反向传播。 噪声分支同理:target 置零后让 Noise 网络重建,error 乘 STE_Noise_Multiplier 再走一遍 STE_Noise 递推,std_count 在此才自增。两个分支任一 backProp 失败就 PrintFormat 打点并 Stop=true 跳出。 循环里还有个 500 毫秒的 ticks 闸:GetTickCount()-ticks>500 时进入分支,通常用于定时让出或上报进度,避免 MT5 单帧算死。外汇与贵金属行情下用这类在线学习,过拟合与滑点风险偏高,参数 multiplier 建议先按 2~3 倍标准差试。
target = temp.Row(temp.Rows() - class="num">1); } targets.Row(target, shift + t); } targets.Reshape(class="num">1, targets.Rows()*targets.Cols()); target = targets.Row(class="num">0); Decoder.getResults(result); vector<class="type">float> error = target - result; std_count = MathMin(std_count, class="num">999); STE = MathSqrt((MathPow(STE, class="num">2) * std_count + MathPow(error, class="num">2)) / (std_count + class="num">1)); vector<class="type">float> check = MathAbs(error) - STE * STE_Multiplier; if(check.Max() > class="num">0) { class=class="str">"cmt">//--- Result.AssignArray(CAGrad(error) + result); if(!Decoder.backProp(Result, (CNet *)NULL) || !Encoder.backPropGradient(GetPointer(LastEncoder), GetPointer(Gradient))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } } target = vector<class="type">float>::Zeros(delta.Cols()); Noise.getResults(result); error = (target - result) * STE_Noise_Multiplier; STE_Noise = MathSqrt((MathPow(STE_Noise, class="num">2) * std_count + MathPow(error, class="num">2)) / (std_count + class="num">1)); std_count++; check = MathAbs(error) - STE_Noise; if(check.Max() > class="num">0) { class=class="str">"cmt">//--- Result.AssignArray(CAGrad(error) + result); if(!Noise.backProp(Result, (CNet *)NULL) || !Encoder.backPropGradient(GetPointer(LastEncoder), GetPointer(Gradient))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } } Encoder.getResults(result); LastEncoder.AssignArray(result); if(GetTickCount() - ticks > class="num">500) {