第三代神经网络:深度网络·进阶篇
(2/3)· 从 RBM 到 SAE,把二代神经网的短板补成可落盘的预测骨架
解码阈值与并行组合对信号稳定性的影响
在均衡数据集上训练的同结构网络(隐含层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)等使用。
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 丢进小布做相关性扫描,比肉眼翻指标更快。
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 个点溢价,但外汇与贵金属属高风险品种,该精度不代表实盘胜率,仅作特征工程可行性参考。
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(价格变动 * 信号) 累加:模型信号曲线(红)长期站在零轴上方,基准真实标签曲线(黑)波动更剧烈且回撤更深。外汇与贵金属杠杆高、滑点跳空频繁,这条样本外曲线只证明信号结构有边际优势,实盘仍可能因点差和延迟吃掉大部分超额。
> 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 通常能让曲线更平。
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)) 标出的两段行情里哪套阈值更扛回撤。
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) }