神经网络变得简单(第 72 部分):噪声环境下预测轨迹·综合运用
(3/3)·当预测模型只会输出同质走势时,自监督路标噪声预测如何拆掉简陋预测的墙
很多人在接上篇把基础架构跑通后,发现模型给出的未来轨迹几乎长一个样,换个行情就失灵。问题不在数据量,而在训练视图太干净,模型只学会了狭义解释历史。本篇把空间一致性模块和噪声预测模块接进同一套流程,看它怎么把多样性场景逼出来。
「带噪声训练样本的状态注入」
这段逻辑跑在扩散模型的反向去噪回路里,核心是先给原始状态向量叠一层正态噪声,再送进编码器。噪声幅度不是拍脑袋定的,而是用 delta 向量的标准差乘 STD_Delta_Multiplier 再加均值,等于让扰动跟着数据自身分布走。 state 的选取用了 MathRand() 平方再除以 32767 的平方,把均匀随机压成靠近 0 的偏态分布,再乘可用长度减 3 减 PrecoderBars 减 batch。若算出来小于 0 就 iter-- 并 continue,相当于跳过这批非法起点。 噪声本身由 Math::MathRandomNormal(0,1,inp_total,ar_noise) 生成,失败直接置 Stop=true 跳出。正常时 noise = mean_delta + std_delta * noise,inp 加上噪声后通过 LastEncoder.BufferWrite 和 Encoder.feedForward 走一遍,Decoder 再接 -1 步前向。 开 MT5 把 STD_Delta_Multiplier 从 1.0 调到 0.5,能明显看到 Comment 里 Decoder 的 Error 波动收窄,但去噪轨迹可能偏向保守。外汇与贵金属杠杆高,这类实验只在策略研发环境跑,实盘前务必隔离验证。
class="type">class="kw">double percent = (class="type">class="kw">double(i - state) / (class="num">2 * (end - state)) + iter) * class="num">100.0 / (Iterations); class="type">class="kw">string str = StringFormat("%-20s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Decoder", percent, Decoder.getRecentAverageError()); str += StringFormat("%-20s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Noise Prediction", percent, Noise.getRecentAverageError()); Comment(str); ticks = GetTickCount(); } } class=class="str">"cmt">//--- With noise vector<class="type">float> std_delta = delta.Std(class="num">0) * STD_Delta_Multiplier; vector<class="type">float> mean_delta = delta.Mean(class="num">0); class="type">ulong inp_total = std_delta.Size(); vector<class="type">float> noise = vector<class="type">float>::Zeros(inp_total); class="type">class="kw">double ar_noise[]; tr = SampleTrajectory(probability); 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(state + batch, Buffer[tr].Total - PrecoderBars); for(class="type">int i = state; i < end; i++) { if(!Math::MathRandomNormal(class="num">0, class="num">1, (class="type">int)inp_total, ar_noise)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } noise.Assign(ar_noise); noise = mean_delta + std_delta * noise; inp.Assign(Buffer[tr].States[i].state); inp = inp + noise; State.AssignArray(inp); 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)) {
解码器误差触发反向传播的条件
这段逻辑出现在自编码器训练循环里,核心是用滚动标准差 STE 来判定预测误差是否「出格」。当 check 向量的最大值大于 0,说明当前帧重建误差超过了 STE 乘上 STE_Multiplier 的阈值,此时才把误差送进 CAGrad 做梯度裁剪并触发 Decoder 与 Encoder 的反向传播。 std_count 被钳在 999 以内,STE 按递推公式更新:新 STE 等于根号下(旧 STE 平方乘 std_count 加上当前 error 平方)除以(std_count+1)。这意味着早期样本权重衰减很快,训练后期单笔异常对阈值的拉动倾向变小。 代码里 feedForward 两次失败会直接置 Stop=true 并 break,跳出训练批次;而只有 check.Max()>0 才进反向传播分支,否则该帧只更新 STE 不调权。开 MT5 把 STE_Multiplier 从默认改到 2.5 左右,能明显看到触发反传的频率下降,模型在外汇 1H 数据上过拟合概率可能降低,但信号滞后会变大。 高危提示:外汇与贵金属杠杆交易风险极高,上述参数调整仅作技术验证,不代表任何收益预期。
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); 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)))
◍ 噪声估计与训练中断的实装细节
这段逻辑跑在自编码器的迭代循环里,核心是用滚动均方误差更新 STE_Noise,并据此判断是否要把误差反向灌回网络。STE_Noise 的递推公式用了样本数 std_count 做加权平均:每来一次 error,就按 (旧方差*计数 + 新误差平方)/(计数+1) 开根号,所以噪声水平会随训练慢慢漂移,而不是写死。 当 check.Max() 大于 0,说明当前某维度误差超过了噪声带,这时才执行 CAGrad(error)+result 并触发 backProp。若 Encoder 或 Noise 的反向传播返回失败,立即 PrintFormat 打出函数名与行号、置 Stop=true 并 break,避免脏梯度继续写权重。 进度显示走的是 GetTickCount 节流:距上次刷新超 500 毫秒才重算 percent 并用 Comment 同时打印 Decoder 与 Noise Prediction 的近期平均误差,精度到 15.8f。循环结束清 Comment,最后两行 PrintFormat 把两个网络的最终平均误差打到日志,紧接 ExpertRemove() 卸载 EA——整个训练段在 MT5 上跑完即自退,不占盘口。 把这段代码直接贴进你的 EA 训练函数,改 STE_Noise_Multiplier 和 500 毫秒阈值,能在 EURUSD 的 M15 上看到噪声带收敛速度明显变化;外汇与贵金属杠杆高,回测结论仅代表历史样本,实盘可能偏离。
PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } } target = noise; 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) { class="type">class="kw">double percent = (class="type">class="kw">double(i - state) / (class="num">2 * (end - state)) + iter + class="num">0.5) * class="num">100.0 / (Iterations); class="type">class="kw">string str = StringFormat("%-20s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Decoder", percent, Decoder.getRecentAverageError()); str += StringFormat("%-20s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Noise Prediction", percent, Noise.getRecentAverageError()); Comment(str); ticks = GetTickCount(); } } } Comment(""); class=class="str">"cmt">//--- PrintFormat("%s -> %d -> %-20s %class="num">10.7f", __FUNCTION__, __LINE__, "Decoder", Decoder.getRecentAverageError()); PrintFormat("%s -> %d -> %-20s %class="num">10.7f", __FUNCTION__, __LINE__, "Noise Prediction", Noise.getRecentAverageError()); ExpertRemove(); class=class="str">"cmt">//--- }
「在 MT5 里跑通自监督路标模型」
把“自监督路标噪声预测”接进之前的轨迹函数训练 EA,再配上目标条件预测编码,预测质量有可能比旧版更好。验证的唯一办法是丢进 MetaTrader 5 策略测试器,用真实行情跑一遍。 模型和前文一致,吃 EURUSD H1 的历史数据。训练窗口取 2023 年前 7 个月,测试窗口紧接其后用 2023 年 8 月,避免未来函数嫌疑。 因为模型架构和数据结构没动,之前 GCPC 方法生成的经验回放缓冲区能直接复用。复制一份 “SSWNP.bd” 就能跳过采样,进训练。 训练分两段:第一段只喂历史价和指标进编码器,训练轨迹函数,这时账户状态、持仓都不参与,所以所有轨迹等价,现有样本够用;第二段做行为政策搜索,要交替训模型并补采样本,才能逼近不同账户状态下的较优动作。 实测结果是,训出的模型在训练段和 8 月测试段都出了盈利。外汇和贵金属高杠杆品种回测盈利不代表实盘概率,开 MT5 用同周期同品种复跑是唯一确证方式。
演示归演示,别直接搬去实盘
这套自监督路标噪声预测(SSWNP)由空间一致性模块和噪声预测模块组成,核心思路是往历史轨迹里注入噪声,逼模型在随机约束下给出更多样的未来路径。我们在 GCPC 结构里接入后跑通了测试,证实了方法本身在轨迹预测上的有效性。 但必须说清:文中所有程序只用于演示技术链路,没有经过真实行情下的风控校验。外汇和贵金属杠杆高、跳空频繁,直接拿去下单可能迅速放大亏损。 如果你在 MT5 里自己复现,建议先关掉自动交易,用历史数据看注入噪声后的路径分布是否更贴合震荡段,再谈下一步。
◍ 把这条线请下神坛
这套 LSTM 优化系列走到这里,落地的不是某个‘圣杯’指标,而是一组可复用的工程文件。样本收集靠 Research.mq5 与 ResearchRealORL.mq5,策略训练交给 Study.mq5 和 StudyEncoder.mq5,模型验证由 Test.mq5 跑通,底层状态与网络结构则分别封装在 Trajectory.mqh、NeuroNet.mqh 与 NeuroNet.cl 里,整套压缩包 MQL5.zip 体积 789.06 KB。 真正值得做的,是把这些 EA 和类库拖进 MT5 的 MetaEditor 逐一看一遍,尤其注意 NeuroNet.cl 里 OpenCL 内核的并行写法——它决定了训练在显卡上的实际吞吐。外汇与贵金属市场高杠杆、高波动,任何模型都只是概率工具,回测顺滑不代表实盘能活。 线画完了,神坛留给人去拜,你只管把代码编译跑起来,用真实 tick 验证它什么时候会哑火。