第三代神经网络:深度网络·进阶篇
🧠

第三代神经网络:深度网络·进阶篇

(2/3)· 从 RBM 到 SAE,把二代神经网的短板补成可落盘的预测骨架

实战向进阶 第 2/3 篇
不少交易者把 MLP 直接堆几层就当深度网络用,结果过拟合比单层还严重。二代网络的局部极小和梯度消失问题不解决,层数越多越容易产出漂亮但无用的曲线。先理清自动编码器与 RBM 的差异,才谈得上真正训练深度结构。

解码阈值与并行组合对信号稳定性的影响

在均衡数据集上训练的同结构网络(隐含层30-30-30),回测最近500根柱形时资金曲线较非均衡样本有明显改善,但更关键的是解码方式。用大于均值和小于均值的朴素方案,不如把预测值卡在0.4–0.6区间之外:仅当输出 >0.6 或 <0.4 才触发信号,中间不稳定带直接丢弃,资金曲线表现更好。 为对比解码影响,Testing() 函数引入额外参数 dec,可切 'mean' 与 '60/40' 两种模式。固定伪随机数发生器位置后复跑,60/40 解码的资金曲线优于均值法,且仍有调优空间。 进一步测多网络组合:同参数训练3个和4个网络,预测取简单平均,60/40解码。2核机器上用 foreach + doParallel 拉起集群,线程数为核数倍数时耗时最低;周期拉长到300时,1个网络、3个组合、4个组合的预测时间差异显著,但资金曲线无明显优劣分化(黑线1网、蓝线3网、红线4网重叠度高)。 隐含层全设三层但神经元数不同的三组对照里,最近1000根柱形上最大利润从472点(终值351点)到543点不等,仅调神经元规模就大幅改观。外汇与贵金属杠杆高、滑点随机,这类神经网络信号仅作概率倾向参考,实盘前务必在MT5用历史数据重跑验证。 下面这段 R 函数把 OHLC 向量翻转为从旧到新的矩阵并补了 Med、CO 两列,是后续算 ADX、Aroon 等指标的底料: pr.OHLC <- function (o, h, l, c) { #Unite quote vectors into a matrix having previously expanded them #Indexing of time series of vectors in R starts with 1. #Direction of indexing is from old to new ones. price <- cbind(Open = rev(o), High = rev(h), Low = rev(l), Close = rev(c)) Med <- (price[, 2] + price[, 3])/2 CO <- price[, 4] - price[, 1] #add Med and CO to the matrix price <- cbind(price, Med, CO) } 逐行拆解:

  • 函数入参 o,h,l,c 分别是开高低收向量,rev() 把顺序倒成旧→新,cbind 拼成 price 矩阵;
  • Med 取每行 High 与 Low 的平均,即中间价;
  • CO 是 Close 减 Open,表征单根柱形实体方向;
  • 最后 cbind 把 Med、CO 作为新列并入,供 TTR 包算 ADX(n=16 时 ADX 中位约18.1,DX 中位16.95)等使用。
MQL5 / C++
pr.OHLC <- function(o, h, l, c)
{
class="macro">#Unite quote vectors into a matrix having previously expanded them
class="macro">#Indexing of time series of vectors in R starts with class="num">1.
class="macro">#Direction of indexing is from old to new ones.
price <- cbind(Open = rev(o), High = rev(h), Low = rev(l), Close = rev(c))
Med <- (price[, class="num">2] + price[, class="num">3])/class="num">2
CO <- price[, class="num">4] - price[, class="num">1]
class="macro">#add Med and CO to the matrix
price <- cbind(price, Med, CO)
}

「把指标丢进同一张特征矩阵再看」

上面那段是把 MACD、OSMA、RSI、随机指标、SMI、波动率逐个跑 summary 的结果。MACD 中位数 -0.0057、均值 -0.0114,OSMA 中位数仅 0.00034,说明样本里多数时间柱动能偏弱、围绕零轴纠缠,不是单边市。 RSI(16) 中位数 47.15、均值 46.53,快 K 中位数 0.445、慢 D 均值 0.462,都贴着 50 中轴,和 MACD 的零轴黏合互相印证。波动率(Yang.Zhang, n=16)中位数 0.002638、最大值 0.01284,极端值约为中位数的 4.9 倍,贵金属或外汇品种在消息窗口拉出长影时这类跳变容易出现。 真正落地的是 In() 函数:把 ADX、Aroon 震荡、CCI、Chaikin 波动率、CMO、MACD、OSMA、RSI、Stoch、SMI、波动率用 cbind 拼成 4000 行特征矩阵 X。最后六行里 ADX 从 18.17 爬到 21.82、aroon oscillator 最后一行冲到 62.5,趋势强度在尾段有抬头倾向,但 DIp 54.98 对 DIn 26.85 未拉开绝对优势,外汇高杠杆下仍可能假突破。 开 MT5 把这段逻辑改写成 MQL5 的 CiMACD / CiRSI 轮询,或直接在 R 里 source 同构代码,把 X 输出成 csv 丢进小布做相关性扫描,比肉眼翻指标更快。

MQL5 / C++
In<-function(p = class="num">16){
  adx<-ADX(price, n = p);
  ar<-aroon(price[ ,c(&class="macro">#x27;High&class="macro">#x27;, &class="macro">#x27;Low&class="macro">#x27;)], n=p)[ ,&class="macro">#x27;oscillator&class="macro">#x27;];
  cci<-CCI(price[ ,class="num">2:class="num">4], n = p);
  chv<-chaikinVolatility(price[ ,class="num">2:class="num">4], n = p);
  cmo<-CMO(price[ ,&class="macro">#x27;Med&class="macro">#x27;], n = p);
  macd<-MACD(price[ ,&class="macro">#x27;Med&class="macro">#x27;], class="num">12, class="num">26, class="num">9)[ ,&class="macro">#x27;macd&class="macro">#x27;];
  osma<-macd - MACD(price[ ,&class="macro">#x27;Med&class="macro">#x27;],class="num">12, class="num">26, class="num">9)[ ,&class="macro">#x27;signal&class="macro">#x27;];
  rsi<-RSI(price[ ,&class="macro">#x27;Med&class="macro">#x27;], n = p);
  stoh<-stoch(price[ ,class="num">2:class="num">4],class="num">14, class="num">3, class="num">3);
  smi<-SMI(price[ ,class="num">2:class="num">4],n = p, nFast = class="num">2, nSlow = class="num">25, nSig = class="num">9);
  vol<-volatility(price[ ,class="num">1:class="num">4],n = p,calc="yang.zhang", N=class="num">96);
  In<-cbind(adx, ar, cci, chv, cmo, macd, osma, rsi, stoh, smi, vol);
  class="kw">return(In)
}
X<-In()
tail(X)

◍ 用堆叠自编码器给 ZigZag 信号做分类验证

这段 R 脚本把 ZigZag 拐点速度转成 0/1 信号:dz 为正判买(0),为负判卖(1)。原始 Out() 输出里 0 类 1567 个、1 类 2423 个,样本明显偏斜,直接喂模型会向卖方倾斜。 Clearing() 先按列剔除含 NA 的行,3957 行干净数据进池子;Balancing() 发现最大类比最小类超 15% 就做 upSample 重采样,把 y 从因子 1/2 重编码为 0/1,避免类别失衡吃掉泛化能力。 训练用 rminer 的 holdout 按 8:2 切分,spatialSign 预处理把特征向量投影到单位球面上,削弱量纲差异。三层 100 节点的 SAE(tanh 激活、学习率 0.6、动量 0.5、10 轮)在 13.09 秒跑完,测试集预测值中位数 0.5881、均值 0.5116。 以均值 0.5116 为阈值转硬分类后,混淆矩阵给出准确率 0.7259,95% 置信区间(0.6965, 0.754),无信息率仅 0.5293——说明模型相对盲猜有约 20 个点溢价,但外汇与贵金属属高风险品种,该精度不代表实盘胜率,仅作特征工程可行性参考。

MQL5 / C++
Out<-function(ch=class="num">0.0037){
  # ZigZag has values on each bar and not only in the points
  zz<-ZigZag(price[ ,&class="macro">#x27;Med&class="macro">#x27;], change = ch, percent = F, retrace = F, lastExtreme = T);
  n<-class="num">1:length(zz);
  # On the last bars substitute the undefined values for the last known ones
  for(i in n) { if(is.na(zz[i])) zz[i] = zz[i-class="num">1];}
  class="macro">#Define the speed of ZigZag changes and move one bar forward
  dz<-c(diff(zz), NA);
  class="macro">#If the speed >class="num">0 - signal = class="num">0(Buy), if <class="num">0, signal = class="num">1 (Sell) otherwise NA
  sig<-ifelse(dz>class="num">0, class="num">0, ifelse(dz<class="num">0, class="num">1, NA));
  class="kw">return(sig);
}
> Y<-Out()
> table(Y)
Y
    class="num">0    class="num">1
 class="num">1567 class="num">2423
Clearing<-function(x, y){
dt<-cbind(x,y);
n<-ncol(dt)
dt<-na.omit(dt)
class="kw">return(dt);
}
> dt<-Clearing(X,Y); nrow(dt)
[class="num">1] class="num">3957
Balancing<-function(DT){
class="macro">#Calculate a table with a number of classes
cl<-table(DT[ ,ncol(DT)]);
class="macro">#If the divergence is less than class="num">15%, class="kw">return the initial matrix
if(max(cl)/min(cl)<= class="num">1.15) class="kw">return(DT)
class="macro">#Otherwise level by the greater side
DT<-if(max(cl)/min(cl)> class="num">1.15){
upSample(x = DT[ ,-ncol(DT)],y = as.factor(DT[ , ncol(DT)]), yname = "Y")
}
class="macro">#Convert y(factor) into a number
DT$Y<-as.numeric(DT$Y)
class="macro">#Recode y from class="num">1,class="num">2 into class="num">0,class="num">1
DT$Y<-ifelse(DT$Y == class="num">1, class="num">0, class="num">1)
class="macro">#Convert dataframe to matrix
DT<-as.matrix(DT)
class="kw">return(DT);
}
dt.b<-Balancing(dt)
x<-dt.b[ ,-ncol(dt.b)]
y<-dt.b[ , ncol(dt.b)]
> library(&class="macro">#x27;rminer&class="macro">#x27;)
> t<-holdout(y, ratio = class="num">8/class="num">10, mode = "random")
> spSign<-preProcess(x[t$tr, ], method = "spatialSign")
> x.tr<-predict(spSign, x[t$tr, ])
> x.ts<-predict(spSign, x[t$ts, ])
sae.dnn.train(x, y, hidden = c(class="num">10), activationfun = "sigm", learningrate = class="num">0.8, momentum = class="num">0.5, learningrate_scale = class="num">1, output = "sigm", sae_output = "linear",
  numepochs = class="num">3, batchsize = class="num">100, hidden_dropout = class="num">0, visible_dropout = class="num">0)
> system.time(SAE<-sae.dnn.train(x= x.tr, y= y[t$tr], hidden=c(class="num">100,class="num">100,class="num">100), activationfun = "tanh", learningrate = class="num">0.6, momentum = class="num">0.5, learningrate_scale = class="num">1.0, output = "sigm", sae_output = "linear", numepochs = class="num">10, batchsize = class="num">100, hidden_dropout = class="num">0, visible_dropout = class="num">0))
开始训练 sae ......
训练 layer class="num">1 autoencoder ...
训练 layer class="num">2 autoencoder ...
训练 layer class="num">3 autoencoder ...
sae 训练完毕。
开始训练深度神经网络 ......
深度神经网络训练完毕。
   user  system elapsed
  class="num">12.92    class="num">0.00    class="num">13.09
> pr.sae<-nn.predict(SAE, x.ts);
> summary(pr.sae)
      V1
 Min.   :class="num">0.2649
 1st Qu.:class="num">0.2649
 Median :class="num">0.5881
 Mean   :class="num">0.5116
 3rd Qu.:class="num">0.7410
 Max.   :class="num">0.7410
> pr<-ifelse(pr.sae>mean(pr.sae), class="num">1, class="num">0)
> confusionMatrix(y[t$ts], pr)
模糊矩阵统计
            Reference
Prediction   class="num">0   class="num">1
         class="num">0 class="num">316 class="num">128
         class="num">1 class="num">134 class="num">378

          Accuracy : class="num">0.7259
          class="num">95% CI : (class="num">0.6965, class="num">0.754)
 No Information Rate : class="num">0.5293

自编码信号在样本外 500 根的实测表现

把训练好的稀疏自编码模型(SAE)直接丢到最近 500 根 K 线上做预测,用 pr.sae1 > mean(pr.sae1) 做二分类切分,得到 235 根看空、-1 类,265 根看多、1 类;同期真实标签里 -1 类 201 根、1 类 299 根,样本分布并不对称。 套用混淆矩阵后,整体准确率 0.768,95% 置信区间 (0.7285, 0.8043),No Information Rate 仅 0.53,P-Value [Acc > NIR] 小于 2.2e-16,说明模型显著优于瞎猜基线。Kappa 0.5305,Mcnemar 检验 P-Value 0.002184,两类错误不对称被统计确认。 细分指标上,Sensitivity 0.6809、Specificity 0.8453,正类(-1)的 Pos Pred Value 0.7960、Neg Pred Value 0.7492;Balanced Accuracy 0.7631。模型在贵金属(CO 品种)样本外更擅长抓空单精度,多单漏报偏多。 资金曲线用 cumsum(价格变动 * 信号) 累加:模型信号曲线(红)长期站在零轴上方,基准真实标签曲线(黑)波动更剧烈且回撤更深。外汇与贵金属杠杆高、滑点跳空频繁,这条样本外曲线只证明信号结构有边际优势,实盘仍可能因点差和延迟吃掉大部分超额。

MQL5 / C++
> new.x<-predict(spSign,tail(dt[ ,-ncol(dt)], class="num">500))
> pr.sae1<-nn.predict(SAE, new.x)
> pr.sig<-ifelse(pr.sae1>mean(pr.sae1), -class="num">1, class="num">1)
> table(pr.sig)
pr.sig
-class="num">1   class="num">1
class="num">235 class="num">265
> new.y<-ifelse(tail(dt[  , ncol(dt)], class="num">500) == class="num">0, class="num">1, -class="num">1)
> table(new.y)
new.y
-class="num">1   class="num">1
class="num">201 class="num">299
> cm1<-confusionMatrix(new.y, pr.sig)
> cm1
> bal<-cumsum(tail(price[ , &class="macro">#x27;CO&class="macro">#x27;], class="num">500) * pr.sig)
> plot(bal, t = "l")
> abline(h = class="num">0)
> bal.zz<-cumsum(tail(price[ , &class="macro">#x27;CO&class="macro">#x27;], class="num">500) * new.y)
> plot(bal.zz,  t = "l")
> lines(bal,  col = class="num">2)

「用栈式自编码器给行情打二分信号」

这段 R 风格训练函数把特征矩阵 X 与标签 Y 送进栈式自编码器(SAE),先按 8:2 随机切分,再用 spatialSign 做空间符号归一化,削弱量纲对隐藏层的干扰。隐藏层设为 c(30,30,30)、学习率 0.7 时,测试集分类错误率 Err 返回 0.1376,也就是约 86% 的样本被分对,外汇与贵金属品种上这属于可复现但波动较大的基线水平,实盘仍属高风险。 预测阶段把网络输出按自身均值二值化:高于均值给 1,否则给 0;Testing 函数里则反转成交易站位 -1/1,并用 tail(price[,'CO'], bar) 做累计盈亏曲线(bar 默认 500 根)。注意信号只是方向倾向,不是胜率保证,MT5 里拿历史 tick 重算前先确认你的 dt 最后一列确实是 0/1 标签。 想自己跑通,直接把 Estimation 的 hidden 改成单隐层 c(10) 对比错误率,再逐步加到三层,观察 Err 是否从 0.13 附近继续下探;若过拟合,把 Ep 从 10 降到 5、Bs 从 50 提到 100 通常能让曲线更平。

MQL5 / C++
Estimation<-function(X, Y, r = class="num">8/class="num">10, m = "random", norm = "spatialSign",
                      h = c(class="num">10), act = "tanh", LR = class="num">0.8, Mom = class="num">0.5,
                      out = "sigm", sae = "linear", Ep = class="num">10, Bs = class="num">50,
                      CM=F){
  class="macro">#Indices of the training and test data set
  t<-holdout(Y, ratio = r, mode = m)
  class="macro">#Parameters of preprocessing
  prepr<-preProcess(X[t$tr,  ], method = norm)
  class="macro">#Divide into train and test data sets with preprocessing
  x.tr<-predict(prepr, X[t$tr,  ])
  x.ts<-predict(prepr, X[t$ts,  ])
  y.tr<- Y[t$tr]; y.ts<- Y[t$ts]
  class="macro">#Train the model
  SAE<-sae.dnn.train(x = x.tr , y = y.tr , hidden = h,
                     activationfun = act,
                     learningrate = LR, momentum = Mom,
                     output = out, sae_output = sae,
                     numepochs = Ep, batchsize = Bs)
  class="macro">#Obtain a forecast on the test data set
  pr.sae<-nn.predict(SAE, x.ts)
  class="macro">#Recode it into signals class="num">1,class="num">0
  pr<-ifelse(pr.sae>mean(pr.sae), class="num">1, class="num">0)
  class="macro">#Calculate the Accuracy coefficient or classification error
  if(CM) err<-unname(confusionMatrix(y.ts, pr)$overall[class="num">1])
  if(!CM) err<-nn.test(SAE, x.ts, y.ts, mean(pr.sae))
  class="kw">return(err)
}
> Err<-Estimation(X = dt[ ,-ncol(dt)], Y = dt[ ,ncol(dt)], h=c(class="num">30, class="num">30, class="num">30), LR= class="num">0.7)
开始训练 sae ......
training layer class="num">1 autoencoder ...
training layer class="num">2 autoencoder ...
training layer class="num">3 autoencoder ...
sae 训练完毕。
开始训练深度神经网络 ......
深度神经网络训练完毕。
> Err
[class="num">1] class="num">0.1376263
Testing<-function(dt1, dt2, r=class="num">8/class="num">10, m = "random", norm = "spatialSign",
                  h = c(class="num">10), act = "tanh", LR = class="num">0.8, Mom = class="num">0.5,
                  out = "sigm", sae = "linear", Ep = class="num">10, Bs=class="num">50,
                  pr = T, bar = class="num">500){
  X<-dt1[  ,-ncol(dt1)]
  Y<-dt1[  ,ncol(dt1)]
  t<-holdout(Y,  ratio = r,  mode = m)
  prepr<-preProcess(X[t$tr,  ], method = norm)
  x.tr<-predict(prepr, X[t$tr,  ])
  y.tr<- Y[t$tr];
  SAE<-sae.dnn.train(x = x.tr , y = y.tr , hidden = h,
                     activationfun = act,
                     learningrate = LR, momentum = Mom,
                     output = out, sae_output = sae,
                     numepochs = Ep, batchsize = Bs)
  X<-dt2[ ,-ncol(dt2)]
  Y<-dt2[ ,ncol(dt2)]
  x.ts<-predict(prepr, tail(X, bar))
  y.ts<-tail(Y, bar)
  pr.sae<-nn.predict(SAE, x.ts)
  sig<-ifelse(pr.sae>mean(pr.sae), -class="num">1, class="num">1)
  sig.zz<-ifelse(y.ts == class="num">0, class="num">1,-class="num">1 )
  bal<-cumsum(tail(price[  ,&class="macro">#x27;CO&class="macro">#x27;], bar) * sig)

◍ 两种信号阈值在回测里的分叉

把深度神经网络跑完之后,真正决定净值是 dec 参数而不是网络结构。dec=1 用预测均值做分界:pr.sae 大于均值就空、否则多;dec=2 换成 0.6/0.4 双阈值,中间段直接零仓。 同一组种子 set.seed<-1245、同样隐藏层 c(30,30,30) 与学习率 0.7,Bal1 与 Bal2 在 500 根 bar 的尾段走出明显不同的权益曲线,红线 Bal1 与黑线 Bal2 在 plot 里交叉数次,说明阈值切法对样本内结果影响不亚于特征工程。 并行化那段只是把 sae.dnn.train 丢进 foreach 的 %dopar% 里,用 detectCores 吃满物理核,clusterSetRNGStream 保证每次复现;但外汇与贵金属杠杆极高,这种回测分叉只代表历史样本内的概率倾向,实盘可能迅速失效。 真要验,就把 Testing.1 的 dec 在 1 和 2 之间切几次,看 abline(v=c(150,350)) 标出的两段行情里哪套阈值更扛回撤。

MQL5 / C++
Testing.class="num">1<-function(dt1, dt2, r = class="num">8/class="num">10, m = "random", norm = "spatialSign",
                     h = c(class="num">10), act = "tanh", LR = class="num">0.8, Mom = class="num">0.5,
                     out = "sigm", sae = "linear", Ep = class="num">10, Bs = class="num">50,
                     pr = T, bar = class="num">500, dec=class="num">1){
  X<-dt1[ ,-ncol(dt1)]
  Y<-dt1[ ,ncol(dt1)]
  t<-holdout(Y, ratio = r, mode = m)
  prepr<-preProcess(X[t$tr, ], method = norm)
  x.tr<-predict(prepr, X[t$tr, ])
  y.tr<- Y[t$tr];
  SAE<-sae.dnn.train(x = x.tr , y = y.tr , hidden = h,
                     activationfun = act,
                     learningrate = LR, momentum = Mom,
                     output = out, sae_output = sae,
                     numepochs = Ep, batchsize = Bs)
  X<-dt2[ ,-ncol(dt2)]
  Y<-dt2[ ,ncol(dt2)]
  x.ts<-predict(prepr, tail(X, bar))
  y.ts<-tail(Y, bar)
  pr.sae<-nn.predict(SAE, x.ts)
  class="macro">#Вариант +/- mean
  if(dec == class="num">1) sig<-ifelse(pr.sae>mean(pr.sae), -class="num">1, class="num">1)
  class="macro">#Вариант class="num">60/class="num">40
  if(dec == class="num">2) sig<-ifelse(pr.sae>class="num">0.6, -class="num">1, ifelse(pr.sae<class="num">0.4, class="num">1, class="num">0))
  sig.zz<-ifelse(y.ts == class="num">0, class="num">1,-class="num">1 )
  bal<-cumsum(tail(price[ ,&class="macro">#x27;CO&class="macro">#x27;], bar) * sig)
  bal.zz<-cumsum(tail(price[ ,&class="macro">#x27;CO&class="macro">#x27;], bar) * sig.zz)
  if(pr) class="kw">return(bal)
  if(!pr) class="kw">return(bal.zz)
}
让小布替你跑这套
深度网络的数据预处理和样本平衡很耗神,小布盯盘的 AIGC 已内置特征清洗与分类平衡建议,打开对应品种页即可看到,你只需确认输入因子权重。

常见问题

SAE 采用逐层贪婪预训练,先让每层自编码器学局部特征再微调,比直接反向传播的全连接堆叠更不易陷局部极小。
没有固定值,通常从输入维度的一半起做网格搜索,配合验证集重构误差倾向在 0.05 内收敛较稳。
概率上比简单历史波动更能捕捉隔夜风险,但贵金属受数据面冲击时仍可能失真,需结合分类平衡处理。
目前知识库页提供特征诊断与样本切分辅助,模型部署仍需在 MT5 端通过本文第 4 节命令完成,小布负责前置省力。
每层预训练加微调的迭代量大,单线程在 96 周期窗口下可能跑数小时,并行能把耗时压缩到可接受范围。