您应当知道的 MQL5 向导技术(第 10 部分):非常规 RBM·进阶篇
RBM 为什么从能量差里长出来
把 RBM 看成基于能量的模型,核心就一句话:给数据集的每一种配置算一个标量能量,训练就是改能量函数,让“理想配置”能量低。这里的能量不是物理单位,而是输入数据经过一次变换再还原后,和原始输入之间的差值——差值越小,能量越低,模型越认可这种配置。 普通玻尔兹曼机所有神经元全连接、还没隐藏层,梯度难算到爆炸。RBM 把它受限成“可见层只连隐藏层、同层不连”,才让马尔可夫链蒙特卡罗采样有了落地可能。训练时在负对数似然上做随机梯度下降,梯度分正相和负相:正相压低训练数据的自由能量(提概率),负相降低模型自己生成的样本被当真的可能性。 对外汇和贵金属这种高噪声、高风险的序列,直接拿 RBM 当信号源不现实;但它这种“用隐藏变量补未知层面”的思路,值得在特征降维时验证——MT5 里接 Python 跑个两隐藏层的 RBM,看重构误差能不能稳在 0.05 以下,比盲信指标更有用。
◍ 三层 RBM 的节点配置与无监督训练落点
这套网络按 3 层结构搭:输入层和输出层各 5 个节点,隐藏层 3 个节点。输入取 5 个指标读数——移动平均线、MACD、随机震荡、威廉姆斯百分比范围、相对活力指数;你有权替换来源,但本文固定用这 5 个。常规化输出会同时捕获第一和第二隐藏层的值,所以输出向量长度 = 隐藏层大小 ×2,也就是 6。 权重乘积和激活全交给 ALGLIB 类处理,单个神经元可定制,本文先用默认参数跑通流程。等深入调参时再动不迟,现阶段目标是把数据概率分布抽出来看。反向传播走的是多元链式规则梯度下降,和我们做的对比离散度不是一回事:前者计算更密,得到的权重分布也可能和常规 RBM 差很远。 训练时网络把权重往「输出逼近输入」的方向调。因为输入即目标,这个改过的 RBM 仍算无监督。新数据进来后,隐藏层吐出的数组长度是 6(3 节点 ×2 层)。如果你喂 8 个指标、隐藏层仍留 3 节点,就发生了降维;本文 5 进 6 出,不算降维,只是把 5 个指标值压成一种常规化表达。 这 6 个值可当分类向量直接用。若在每组指标后补上「价格最终变化」做监督,就能拿余弦相似性当权重,去比对未知向量与已知变化向量的加权均值,对下一步变化给出倾向性预测。开 MT5 把隐藏层节点改到 3、输出数组打印出来,验证长度是否为 6 即可。
「把受限玻尔兹曼机塞进 MT5 类里」
这套非常规 RBM 用 MQL5 的 class 封装了感知器最小接口,比前一篇多了个 losses 双精度数组,专门记录输入层与输出层每根柱线、每一数据行的余弦相似性。它的用途很直接:跑测试时,如果当前低于阈值的余弦相似度比上次写文件时更小,就把网络权重导出,方便后续 EA 上生产环境直接加载,不必每次从随机权重重训。
阈值参数叫 loss,默认 0.9,可手动调。权重和输入相乘没必要自己写复杂循环,MQL5 已内嵌 matrix 和 vector 类型,直接乘矩阵省内存也少占计算资源。
网络训练走 ALGLIB 类,开箱即用但代码偏重、场景泛化导致略显晦涩;自己硬编码感知器在测试与部署时效率更高。激活函数和乖离度这类基础定制对性能影响可能很显著,初始探索阶段用 ALGLIB 足够了。
每来一根新 H4 柱或新价格点就重训一次,输入分类权重随验算细化。也可改成季调或半年调,前提是先拿几年数据预训——原文没做,但路子是通的。
用 GBPUSD 的 H4 数据跑了实证:2023.07.01–2023.10.01 优化信号类,2023.10.01–2023.12.25 前向演练,报告看著有前途。外汇高风险,可靠结论应基于更长历史与经纪商真实跳价,纯 ALGLIB 源跑长历史可能是个挑战。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class Crbm { class="kw">protected: class="type">int visible; class=class="str">"cmt">// 可见层节点数:输入特征维度 class="type">int hidden; class=class="str">"cmt">// 隐藏层节点数:提取的隐含特征数 class="type">int sample; class=class="str">"cmt">// 采样次数:每次训练迭代的采样轮数 class="type">class="kw">double loss; class=class="str">"cmt">// 余弦相似度阈值,默认0.class="num">9,低于才考虑写权重 class="type">class="kw">string file; class=class="str">"cmt">// 权重存档文件名 class="kw">public: class="type">bool init; class=class="str">"cmt">// 初始化标志 ... CArrayDouble losses; class=class="str">"cmt">// 记录每根柱线输入输出余弦差异的动态数组 class="type">void Train(matrix &W); class=class="str">"cmt">// 训练函数,W为权重矩阵引用 class="type">void Process(matrix &W, matrix &XY, class="type">bool Compare = false); class=class="str">"cmt">// 处理/预测,XY为数据矩阵 class="type">bool ReadWeights(); class=class="str">"cmt">// 从文件读权重 class="type">bool WriteWeights(); class=class="str">"cmt">// 写权重到文件 class="type">bool Writer(); class=class="str">"cmt">// 通用写盘封装 Crbm(class="type">int Visible, class="type">int Hidden, class="type">int Sample, class="type">class="kw">double Loss, class="type">class="kw">string File); class=class="str">"cmt">// 构造:传节点数、采样、阈值、文件名 ~Crbm(); class=class="str">"cmt">// 析构 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Train Data Matrix | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Crbm::Train(matrix &W) { for(class="type">int s = class="num">0; s < sample; s++)
RBM 新版前向推理与信号取数
上面这段把训练完的受限玻尔兹曼机拿去跑新样本,核心在 Crbm::Process:先按行取权重矩阵 W 的每一行当输入向量,调 base.MLPProcess 得到隐层输出 _y,再把模型神经元下标 6 到 visible+6 这一段拷进 XY 矩阵。注意这里输入维度是 visible,输出只截 7 列,和 GetOutput 里 _xy.Init(m_sample,7) 完全对得上。 对比模式 Compare=true 时会逐维打印输入与输出,并用 LOSS_COSINE 算余弦损失。实盘里建议先开 Compare 跑几百根历史 K 线,看 losses 数组的余弦损失是否稳定在 0.1 以下,再决定要不要接信号。外汇与贵金属波动剧烈,模型偏差放大时亏损概率显著上升,任何输出只作概率参考。 CSignalRBM::GetOutput 负责把实时指标刷进矩阵:m_close、MA、MACD、STOCH、WPR、RVI 全部 Refresh(-1) 取最新值,再判 RBM.init 避免未训练就调用。想验证就直接把这段挂到 MT5 脚本里,打印 _w 和 _xy 的维度,确认和你的样本数 m_sample、可见层 __VISIBLE 一致。
{ for(class="type">int i = class="num">0; i < visible; i++)
{ xy.Set(s, i, W[s][i]);
xy.Set(s, i + visible, W[s][i]);
}
}
train.MLPTrainLM(model, xy, sample, class="num">0.001, class="num">2, info, report);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Process New Vector |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Crbm::Process(matrix &W, matrix &XY, class="type">bool Compare = false)
{ for(class="type">int w = class="num">0; w < class="type">int(W.Rows()); w++)
{ CRowDouble _x = CRowDouble(W.Row(w)), _y;
base.MLPProcess(model, _x, _y);
for(class="type">int i = class="num">6; i < visible + class="num">7; i++)
{ XY[w][i - class="num">6] = model.m_neurons[i];
}
class=class="str">"cmt">//Comparison
vector _input = _x.ToVector();
vector _output = _y.ToVector();
if(Compare)
{ for(class="type">int i = class="num">0; i < class="type">int(_input.Size()); i++)
{ printf(__FUNCSIG__ +
" at: " + IntegerToString(i) +
" we&class="macro">#x27;ve class="kw">input: " + DoubleToString(_input[i]) +
" & y: " + DoubleToString(_y[i])
);
}
class=class="str">"cmt">//Loss
printf(__FUNCSIG__ +
" loss is: " + DoubleToString(_output.Loss(_input, LOSS_COSINE))
);
}
losses.Add(_output.Loss(_input, LOSS_COSINE));
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| RBM Output. |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CSignalRBM::GetOutput(class="type">void)
{
m_close.Refresh(-class="num">1);
MA.Refresh(-class="num">1);
MACD.Refresh(-class="num">1);
STOCH.Refresh(-class="num">1);
WPR.Refresh(-class="num">1);
RVI.Refresh(-class="num">1);
class="type">class="kw">double _output = class="num">0.0;
class="type">int _i=StartIndex();
matrix _w; _w.Init(m_sample,__VISIBLE);
matrix _xy; _xy.Init(m_sample,class="num">7);
if(RBM.init)
{◍ 把多指标快照喂进受限玻尔兹曼机
这段代码干的事很直接:用双重循环把每个样本时刻的 5 个指标值塞进矩阵 _w,再算相邻收盘价差当作监督标签。指标槽位是硬编码的——i==0 取 MA,i==1 取 MACD,i==2 取 WPR,i==3 取 STOCH,i==4 取 RVI,顺序一动后面全错。 样本数由 m_sample 控制,循环里 s 从 0 到 m_sample-1;当 s>0 时才填标签 _xy[s][2*__HIDDEN],等于当前收盘减前一收盘,所以第 0 个样本没有价格变动标签。 喂完数据后 RBM.Train(_w) 先训模型,RBM.Process(_w,_xy) 再做推断。重点在加权融合那段:取第 0 行当基准向量 _x0,对 s>=1 的每行算余弦损失的绝对值当权重,权重乘标签值 _xy[s][6] 累加,最后除以权重和得到 _output。 外汇和贵金属市场波动剧烈、杠杆风险高,这套加权输出只是概率倾向,实盘前务必在 MT5 策略测试器里把 m_sample 从默认改到 200~500 跑一遍,看 _output 分布是否稳定。
for(class="type">int s=class="num">0;s<m_sample;s++) { for(class="type">int i=class="num">0;i<class="num">5;i++) { if(i==class="num">0){ _w[s][i] = MA.GetData(class="num">0,_i+s); } else if(i==class="num">1){ _w[s][i] = MACD.GetData(class="num">0,_i+s); } else if(i==class="num">2){ _w[s][i] = WPR.GetData(class="num">0,_i+s); } else if(i==class="num">3){ _w[s][i] = STOCH.GetData(class="num">0,_i+s); } else if(i==class="num">4){ _w[s][i] = RVI.GetData(class="num">0,_i+s); } } if(s>class="num">0){ _xy[s][class="num">2*__HIDDEN] = m_close.GetData(_i+s)-m_close.GetData(_i+s+class="num">1); } } RBM.Train(_w); RBM.Process(_w,_xy); class="type">class="kw">double _w=class="num">0.0,_w_sum=class="num">0.0; vector _x0=_xy.Row(class="num">0); _x0.Resize(class="num">6); for(class="type">int s=class="num">1;s<m_sample;s++) { vector _x=_xy.Row(s); _x.Resize(class="num">6); class="type">class="kw">double _weight=fabs(class="num">1.0+_x.Loss(_x0,LOSS_COSINE)); _w+=(_weight*_xy[s][class="num">6]); _w_sum+=_weight; } if(_w_sum>class="num">0.0){ _w/=_w_sum; } _output=_w; } class="kw">return(_output); }
「把工具请下神坛」
RBM 变体在 MQL5 里被我们拆成了带输出权重的概率分布,后向与前向测试都指向同一个事实:它更像一枚需要调参的零件,而非开箱即赢的圣杯。随附的 Expert.mq5 与 RBM_r4--.mqh 等文件,和向导组装后即可跑,但外汇与贵金属的高杠杆属性意味着任何信号都只是概率倾斜。 前文已演示过向导装配流程,新手照着做一遍就能在 MT5 策略测试器里看见 6.5 KB 的 EA 跑出回测曲线。真正要花时间的,是对数据输入做筛选——原文提到这可能要更广泛的测试与微调,而非直接实盘。 写这套的人来自乌干达,文章归个人观点,平台不背书准确性。你拿去验证时,记住它只是把监督学习的壳套在受限玻尔兹曼机的简化版上,别神话。