您应当知道的 MQL5 向导技术(第 34 部分):采用非常规 RBM 进行价格嵌入·进阶篇
(2/3)· 当趋势逆转时常用设置集体失灵,用非传统 RBM 降维提取价格特征或许能多一层缓冲
「RBM 的正负压相位与权重复用机制」
受限玻尔兹曼机跑起来就两个周期:正相位把输入往隐藏层送,负相位用同一套权重把隐藏层重建回输入。它本质是无监督的——训练数据没标签,输入自己就是自己的目标值,重建误差代替了常规监督里的外部标注。 在 MT5 里我们写 C_u_rbm 类继承 Cmlp,但只留两层结构(输入层节点数必须多于隐藏层,否则 validated 置 false)。虽然只有 1 个权重矩阵 weights[0],却要配 2 个乖离向量、2 个增量向量和 2 个梯度矩阵,因为正、负相位各做一次反向传播,权重每次更新两次。 正相位第一次碰权重矩阵,乘积进第一个乖离向量并抓梯度;负相位复用同权重,但乖离向量是新的,乘积出来的是对输入数据的重建。重建值和原始输入的差就是增量,喂回反向传播。外汇与贵金属行情用这类模型做特征降维时,样本非平稳,过拟合概率高,需严控学习率。 下面两段是相位函数的核心:GetPositive 算隐藏层输出,GetNegative 用 output 乘回权重得到重建 label。注意负相位偏置用的是 biases[1] 而非 biases[0],这是两相位解耦的关键点。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void C_u_rbm::GetPositive(class="type">void) { vector _positive = weights[class="num">0].MatMul(inputs), _output; _positive += biases[class="num">0]; _positive.Activation(_output, THIS.activation); output = _output; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void C_u_rbm::GetNegative(class="type">void) { vector _negative = output.MatMul(weights[class="num">0]), _output; _negative += biases[class="num">1]; _negative.Activation(_output, THIS.activation); label = _output; } class="macro">#include <My\Cmlp-.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Unconventional RBM that uses: | class=class="str">"cmt">//| reconstruction-error instead of free-energy | class=class="str">"cmt">//| and back-propagation instead of contrastive divergence | class=class="str">"cmt">//+------------------------------------------------------------------+ class C_u_rbm : class="kw">public Cmlp { class="kw">protected: class="kw">public: class="type">void GetPositive(); class="type">void GetNegative(); class="type">void BackPropagate(class="type">class="kw">double LearningRate = class="num">0.1); class="type">class="kw">double Get(ENUM_REGRESSION_METRIC R) { class="kw">return(label.RegressionMetric(inputs, R)); } class="type">void C_u_rbm(Smlp &MLP) : Cmlp(MLP) { validated = false; class="type">int _layers = ArraySize(MLP.arch); if(_layers == class="num">2 && MLP.arch[class="num">0] > MLP.arch[class="num">1]) { ArrayResize(biases, _layers); class=class="str">"cmt">//
◍ 多层感知机权重容器的初始化细节
在 MT5 里搭多层感知机,第一步不是算前向传播,而是把每一层的梯度与 delta 容器按架构尺寸撑开。代码里先用 ArrayResize 把 gradients、gradients_1st_moment、gradients_2nd_moment、sum_gradients、sum_gradients_update 以及 deltas 系列各数组统一扩到 _layers 长度,这是 Adam 类优化器要用的动量占位。 随后循环 i 从 0 到 _layers-1,取 _rows = MLP.arch[_layers-1-i]、_columns = MLP.arch[i],即对称地用输出侧层数做行、输入侧层数做列。biases[i] 按 _rows 初始化并填 MLP.initial_bias,权重梯度矩阵则 Init(_rows,_columns) 后全部 Fill(0.0),一阶二阶矩与累加器同理清零下。 有个易踩的坑:架构校验不通过时走 else 分支,用 printf 抛出 __FUNCSIG__ 加具体尺寸信息,包括 ArrayMaximum(MLP.arch) 与 ArrayMinimum(MLP.arch) 算出的层大小极值和激活函数枚举名。你拷这段去自己 EA 里跑,若终端日志出现 'invalid network arch',先核对 MLP.arch 数组是否含 0 或超限值,而不是怀疑优化器写错。 外汇与贵金属行情用神经网络拟合本身属高风险尝试,历史结构失配对权重的冲击可能很大,实盘前务必在策略测试器用多段样本外数据验容器初始化是否每次重置干净。
ArrayResize(gradients, _layers); ArrayResize(gradients_1st_moment, _layers); ArrayResize(gradients_2nd_moment, _layers); ArrayResize(sum_gradients, _layers); ArrayResize(sum_gradients_update, _layers); class=class="str">"cmt">// ArrayResize(deltas, _layers); ArrayResize(deltas_1st_moment, _layers); ArrayResize(deltas_2nd_moment, _layers); ArrayResize(sum_deltas, _layers); ArrayResize(sum_deltas_update, _layers); class=class="str">"cmt">// hidden_layers = class="num">0; class="type">bool _norm_validated = true; for(class="type">int i = class="num">0; i < _layers; i++) { class="type">int _rows = MLP.arch[_layers - class="num">1 - i], _columns = MLP.arch[i]; class=class="str">"cmt">// biases[i].Init(_rows); biases[i].Fill(MLP.initial_bias); class=class="str">"cmt">// gradients[i].Init(_rows, _columns); gradients[i].Fill(class="num">0.0); class=class="str">"cmt">// gradients_1st_moment[i].Init(_rows, _columns); gradients_1st_moment[i].Fill(class="num">0.0); gradients_2nd_moment[i].Init(_rows, _columns); gradients_2nd_moment[i].Fill(class="num">0.0); class=class="str">"cmt">// sum_gradients[i].Init(_rows, _columns); sum_gradients[i].Fill(class="num">0.0); sum_gradients_update[i].Init(_rows, _columns); sum_gradients_update[i].Fill(class="num">0.0); class=class="str">"cmt">// deltas[i].Init(_rows); deltas[i].Fill(class="num">0.0); deltas_1st_moment[i].Init(_rows); deltas_1st_moment[i].Fill(class="num">0.0); deltas_2nd_moment[i].Init(_rows); deltas_2nd_moment[i].Fill(class="num">0.0); sum_deltas[i].Init(_rows); sum_deltas[i].Fill(class="num">0.0); sum_deltas_update[i].Init(_rows); sum_deltas_update[i].Fill(class="num">0.0); } validated = true; } else { printf(__FUNCSIG__ + " invalid network arch! Settings size is: %i, Max layer size is: %i, Min layer size is: %i, and activation is %s ", _layers, MLP.arch[ArrayMaximum(MLP.arch)], MLP.arch[ArrayMinimum(MLP.arch)], EnumToString(MLP.activation) );
RBM 反向传播里的梯度与权重更新
这段 C_u_rbm::BackPropagate 的实现把受限玻尔兹曼机的训练闭环写得很直接:先算各层 delta,再算梯度,最后按学习率回写权重和偏置。默认 LearningRate=0.1,在 MT5 里跑小样本行情特征重构时,这个初值通常能让损失在百步内进入平缓区,但具体收敛速度取决于输入归一化质量。 代码先用 label.LossGradient 拿到输出层误差梯度,再通过权重矩阵乘当前激活反向推隐藏层 delta;注意 _negative 与 _positive 都套了 Derivative 做激活函数求导,Hadamard 逐元素相乘才是真正的链式法则落点。 梯度矩阵由 deltas 与输入/输出的转置相乘得到,循环里 h 从 1 到 0 仅两轮:h==1 时梯度需转置才能对齐权重维度,随后 weights[0] 和 biases[h] 同步减去学习率乘梯度。外汇与贵金属品种用这类自编码做特征压缩时波动剧烈,过拟合概率偏高,建议先拿历史 Tick 做离线验证再上实盘。
class="type">void C_u_rbm::BackPropagate(class="type">class="kw">double LearningRate = class="num">0.1) { class=class="str">"cmt">//COMPUTE DELTAS vector _loss = label.LossGradient(inputs, THIS.loss); class=class="str">"cmt">// vector _negative = output.MatMul(weights[class="num">0]), _negative_derivative; _negative.Derivative(_negative_derivative, THIS.activation); deltas[class="num">1] = Hadamard(_loss, _negative_derivative); class=class="str">"cmt">// vector _positive = weights[class="num">0].MatMul(inputs), _positive_derivative; _positive.Derivative(_positive_derivative, THIS.activation); matrix _weights; _weights.Copy(weights[class="num">0]); _weights.Transpose(); vector _product = _weights.MatMul(deltas[class="num">1]); deltas[class="num">0] = Hadamard(_product, _positive_derivative); class=class="str">"cmt">//COMPUTE GRADIENTS gradients[class="num">0] = TransposeCol(deltas[class="num">0]).MatMul(TransposeRow(inputs)); gradients[class="num">1] = TransposeCol(deltas[class="num">1]).MatMul(TransposeRow(output)); class=class="str">"cmt">// UPDATE WEIGHTS AND BIASES for(class="type">int h = class="num">1; h >= class="num">0; h--) { matrix _gradients; _gradients.Copy(gradients[h]); if(h == class="num">1) { _gradients = _gradients.Transpose(); } weights[class="num">0] -= LearningRate * _gradients; biases[h] -= LearningRate * deltas[h]; } }
「信号类里怎么接住 RBM 的嵌入输出」
把自定义 RBM 类塞进信号类后,EA 只是半成品——按设计,RBM 隐藏层数值要再喂给一个 MLP 当输入。RBM 在这里干的是嵌入的活:把价格变化压进更小维度。我们的实测网络是 8-4 结构,即可见层 8 节点、隐藏层 4 节点。 我倾向把 RBM 当分类器而非回归器用,所以损失函数锁了分类交叉熵、激活用 soft-sign。这些在代码里是常量,不参与参数优化,避免过拟合噪声。 负责嵌入的函数叫 Embedder。注意 MT5 向量复制价格时索引是倒序的:最高索引装的是最新值,链式训练 RBM→MLP 时要同时拿到 RBM 输入和 MLP 标签,所以 if 判断写得长。 我们采用 RBM 与 MLP 几乎同步训练:在每个训练点上先训 RBM 再跟训 MLP,这比先跑完 RBM 再训 MLP 更省事。外汇与贵金属波动剧烈,这种嵌套结构实盘前务必在策略测试器跑通。 下面这段代码就是 Embedder 本体,核心在 _in_new 与 _in_old 做差得到价格变化,再送进 RBM 正向反向传播:
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSignalEmbedding::Embedder(vector &Extraction) { m_learning.rate = m_learning_rate; for(class="type">int i = class="num">1; i <= m_epochs; i++) { U_RBM.LearningType(m_learning, i); for(class="type">int ii = m_train_set; ii >= class="num">0; ii--) { vector _in, _in_new, _in_old, _out, _out_new, _out_old; if ( _in_new.Init(__RBM_VISIBLE) && _in_new.CopyRates(m_symbol.Name(), m_period, class="num">8, ii + __MLP_OUTPUTS, __RBM_VISIBLE) && _in_new.Size() == __RBM_VISIBLE && _in_old.Init(__RBM_VISIBLE) && _in_old.CopyRates(m_symbol.Name(), m_period, class="num">8, ii + __MLP_OUTPUTS + class="num">1, __RBM_VISIBLE) && _in_old.Size() == __RBM_VISIBLE && _out_new.Init(__MLP_OUTPUTS) && _out_new.CopyRates(m_symbol.Name(), m_period, class="num">8, ii, __MLP_OUTPUTS) && _out_new.Size() == __MLP_OUTPUTS && _out_old.Init(__MLP_OUTPUTS) && _out_old.CopyRates(m_symbol.Name(), m_period, class="num">8, ii + __MLP_OUTPUTS, __MLP_OUTPUTS) && _out_old.Size() == __MLP_OUTPUTS ) { _in = _in_new - _in_old; _out = _out_new - _out_old; U_RBM.Set(_in); U_RBM.GetPositive(); U_RBM.GetNegative(); U_RBM.BackPropagate(m_learning.rate); Extraction = Extractor(U_RBM.output, _out, ii > class="num">0); } } } }