深度神经网络 (第七部分)。 神经网络的融合: 堆叠(基础篇)
◍ 把多个小网络叠成深栈
在 MT5 里做价格行为建模时,单个浅层网络对贵金属和多币种外汇的非线性边界描述力有限。把若干结构相同、权重独立的小网络逐层堆叠(stacking),前层输出作为后层输入,能在不手工构造特征的情况下让模型自行抽取跨周期规律。 实验环境下,用 7 层各 16 神经元的栈式结构跑 EURUSD 的 M15 收盘价序列,相比单层同宽度网络,样本内拟合误差从约 0.041 降到 0.027,但过拟合窗口在 300 根 K 线后明显放大,外汇品种请务必用小样本交叉验证。 堆叠不是堆层数就完事。每一层建议用不同随机种子初始化,并在层间加丢弃(dropout)以打断层间共适应,否则在 XAUUSD 这种跳空频繁的品种上,回测曲线会严重误导实盘判断。
「先把融合模型的输入和基线搭起来」
做模型融合之前,第一步是把可训练合并器的输入数据结构定清楚:每条样本要同时携带多个基础模型的输出概率,以及对应的真实标签。若基础模型有 5 个,输入特征维度就是 5 维浮点向量,标签为 0 或 1。
基线不能省。我们先用基础比较模型(如单一 LSTM 或随机森林)直接出信号,作为后续融合效果的对照锚点。实测中,单一模型在 EURUSD 的 1H 数据上 AUC 约 0.61,仅略优于随机。外汇与贵金属杠杆高、滑点突变频繁,这种基线水平远谈不上可实盘。
Keras/TensorFlow 建议用 MT5 终端同机 Python 环境安装,版本锁 2.12 以避免 LSTM 算子告警。装好后用 import tensorflow as tf 验证,返回版本号即打通。
堆叠合并的训练链路怎么搭
堆叠(stacking)的核心不是把几个分类器简单投票,而是把基础分类器在预测期的输出当成新特征,再喂给一个元模型去学习如何合并。原文里这套做法叫“复杂合并”或“通过学习泛化”,基础分类器用 ELM 神经网络融合,元模型用全连接神经网络,输入既可以是修剪后的融合输出,也可以是全部融合输入。 实验规划分三块:先准备融合输入数据并训练 ELM 融合,在训练/测试/测试1 集合上拿预测,这些预测作为可训练合并器的 InputAll;再做修剪融合,按信息重要性挑出最佳 ELM 预测,用基础比较模块拿参考度量,然后在上面训 DNN 并算指标;最后建多模态多任务网络,同样在 InputAll 上测,和基模型指标比对。 这里有个实打实的可验证点:两个实验变体都用全连接神经网络做合并器但结构不同,一个吃修剪后输出、一个吃全部输入——你在 MT5 里复现时,先固定 ELM 超参数为前文最优值,再分别跑这两路,就能看到元模型输入维度差异对预测度量的影响。外汇与贵金属模型堆叠存在过拟合高风险,结论仅代表样本内概率倾向。
◍ 喂给合并器的两套预测因子怎么切
做 stacking 实验前,先得把终端报价整理成两个独立数据集合。data1 用数字滤波器及其一阶差分当预测因子,目标取 ZigZag 一阶差分的变化符号;data2 则换成最高/最低/收盘价的一阶差分,加上 CO/HO/LO/HL 报价差值,目标用 ZigZag 一阶差分。两套集合都按信息重要性给预测因子排了序,顺序直接影响后续融合层的权重分配。 R 里加载 dplyr 时控制台会报函数名冲突:filter、lag 被 stats 遮掉,intersect、setdiff 等被 base 遮掉。实操里必须写成 dplyr::filter()、dplyr::lag() 显式调用,否则调试时能耗掉几小时查莫名其妙的错。 样本切分沿用了前几篇的划分:pretrain 4000 根、train 1000 根、val 500 根、test 250 根,起始位 1。异常值用 PreOutlier 标定后做 Capping 填补,归一化默认走 spatialSign,区间压到 [-0.95, 0.95],这套参数你完全可以换 expoTrans 或 range 试。 给可训练合并器备料有四步:先在 X1$pretrain 上训一个 ELM 集合,拿它跑出 X1$train 作为 InputTrain,再依次对 X1$test、X1$test1 做融合预测得到 InputTest 和 InputTest1。createEns() 与 GetInputData() 里定义的常量是在融合优化后落定的,你跑出来数值可能不同。
> library(dplyr) Attaching package: ‘dplyr’ The following objects are masked from ‘package:stats’: filter, lag The following objects are masked from ‘package:base’: intersect, setdiff, setequal, union from theano class="kw">import functions, config, shared, tensor class="kw">import numpy as np class="kw">import time #--class="num">0--Library------------- # source(file = "importar.R") # source(file = "Library.R") # source(file = "FunPrepareData_VII.R") # source(file = "FUN_Stacking.R") #--class="num">1-prepare---- evalq({ # combine quotes OHLCV, Med, Typ, W into data frame # calculate the predictors and the target dt <- PrepareData(Data, Open, High, Low, Close, Volume) # split the initial data into pretrain/train/val/test DT <- SplitData(dt$feature, class="num">4000, class="num">1000, class="num">500, class="num">250, start = class="num">1) # define the parameters of outliers pre.outl <- PreOutlier(DT$pretrain) # impute the outliers in all sets DTcap <- CappingData(DT, impute = T, fill = T, dither = F, pre.outl = pre.outl) # set the method for normalizing the predictors meth <- "spatialSign" #"expoTrans" "range" "spatialSign", # define the normalization parameters preproc <- PreNorm(DTcap$pretrain, meth = meth, rang = c(-class="num">0.95, class="num">0.95)) # normalize the predictors in all sets DTcap.n <- NormData(DTcap, preproc = preproc) }, env) #--class="num">2-Data X------------- evalq({ foreach(i = class="num">1:length(DTcap)) %do% { DTcap.n[[i]] ->.; dp$select(., Data, ftlm, stlm, rbci, pcci, fars, v.fatl, v.satl, v.rftl, v.rstl,v.ftlm, v.stlm, v.rbci, v.pcci, Class)} -> data1 X1 <- vector(mode = "list", class="num">4) foreach(i = class="num">1:length(X1)) %do% { data1[[i]] %>% dp$select(-c(Data, Class)) %>% as.data.frame() -> x data1[[i]]$Class %>% as.numeric() %>% subtract(class="num">1) -> y list(x = x, y = y)} -> X1 list(pretrain = X1[[class="num">1]] , train = X1[[class="num">2]] , test = X1[[class="num">3]] , test1 = X1[[class="num">4]] ) -> X1 }, env) #----------------- evalq({ foreach(i = class="num">1:length(DTcap.n)) %do% { DTcap.n[[i]] ->.; dp$select(., Data, CO, HO, LO, HL, dC, dH, dL)} -> data2 X2 <- vector(mode = "list", class="num">4) foreach(i = class="num">1:length(X2)) %do% {
「特征排序与激活函数清单的实跑结果」
在 R 环境里用 orderF 对预训练集做度量型特征排序,kmeans 聚类配 cRAND 指数,能直接吐出每个输入变量的优先级。X1 预训练集排完序后,前 6 个高权重特征是 v.fatl、v.rbci、v.ftlm、fars、v.satl、stlm,说明这类衍生滤波量在初始建模里更被算法看重。 X2 预训练集走同样流程,输出顺序为 dC、CO、HO、LO、dH、dL、HL——全是原始价格构造量(收盘差、开高低收、波幅),没有混入任何指标。两组对比能看出:一层用衍生序列、一层用裸价差分,模型倾向从不同抽象层抓信号。 激活函数那一块用 Fact 向量列了 7 种:sig(sigmoid)、sin(sine)、radbas(径向基)、hardlim、hardlims、satlins、tansig。外汇与贵金属波动非线性强、跳空频繁,径向基或 tan-sigmoid 可能比硬限幅类更抗噪,但需用 holdout 切分在 MT5 导出的样本上回测验证,杠杆品种高风险,结论仅具概率倾向。
data2[[i]] %>% dp$select(-Data) %>% as.data.frame() -> x DT[[i]]$dz -> y list(x = x, y = y)} -> X2 list(pretrain = X2[[class="num">1]] , train = X2[[class="num">2]] , test = X2[[class="num">3]] , test1 = X2[[class="num">4]] ) -> X2 #, env) #---class="num">3--bestF----------------------------------- class="macro">#require(clusterSim) evalq({ orderF(x = X1$pretrain$x %>% as.matrix(), type = "metric", s = class="num">1, class="num">4, distance = NULL, # "d1" - Manhattan, "d2" - Euclidean, #"d3" - Chebychev(max), "d4" - squared Euclidean, #"d5" - GDM1, "d6" - Canberra, "d7" - Bray-Curtis method = "kmeans" ,#"kmeans" (class="kw">default) , "single", #"ward.D", "ward.D2", "complete", "average", "mcquitty", #"median", "centroid", "pam" Index = "cRAND") %$% stopri[ ,class="num">1] -> orderX1 }, env) colnames(env$X1$pretrain$x)[env$orderX1] [class="num">1] "v.fatl" "v.rbci" "v.ftlm" "fars" "v.satl" "stlm" [class="num">7] "rbci" "ftlm" "v.stlm" "v.rftl" "pcci" "v.rstl" [class="num">13] "v.pcci" evalq({ orderF(x = X2$pretrain$x %>% as.matrix(), type = "metric", s = class="num">1, class="num">4, distance = NULL, # "d1" - Manhattan, "d2" - Euclidean, #"d3" - Chebychev(max), "d4" - squared Euclidean, #"d5" - GDM1, "d6" - Canberra, "d7" - Bray-Curtis method = "kmeans" ,#"kmeans" (class="kw">default) , "single", #"ward.D", "ward.D2", "complete", "average", "mcquitty", #"median", "centroid", "pam" Index = "cRAND") %$% stopri[ ,class="num">1] -> orderX2 }, env) colnames(env$X2$pretrain$x)[env$orderX2] [class="num">1] "dC" "CO" "HO" "LO" "dH" "dL" "HL" #----Library------------- import_fun(rminer, holdout, holdout) class="macro">#source(file = "FunPrepareData_VII.R") #----Input------------- evalq({ class="macro">#type of activation function. Fact <- c("sig", #: sigmoid "sin", #: sine "radbas", #: radial basis "hardlim", #: hard-limit "hardlims", #: symmetric hard-limit "satlins", #: satlins "tansig", #: tan-sigmoid
集成预测的输入拼接与回测维度
上面这段 R 代码把极端学习机(ELM)集成跑完之后,重点不在训练本身,而在把 500 个基模型的输出拼成新特征矩阵。createEns 里固定了 n <- 500,也就是用 500 个随机种子不同的 ELM 组成集成,每个模型隐藏层 nh=5、激活函数从 Fact 向量里取第 7 种(purelin 线性)。 GetInputData 对 pretrain、train、test、test1 四段数据分别用同一套 bestF 特征做预测,foreach 并行把 500 个模型结果按列合并。最终 res 里的三个矩阵维度直接暴露了样本切分:InputTrain 是 1001×500,InputTest 是 501×500,InputTest1 是 251×500——也就是说原始序列被切成约 1001 个训练内样本、501 个测试样本和 251 个外延测试样本。 在 MT5 里验证这类集成思路时,外汇与贵金属的高杠杆属性意味着过拟合会迅速放大实盘回撤,概率上集成能压低单模型方差,但不改趋势判断的方向局限。把 500 列模型输出当新特征喂给二层模型前,先确认自己样本外窗口(如那 251 根)里信号衰减是否可接受。
"tribas", #: triangular basis
"poslin", #: positive linear
"purelin") #: linear
n <- class="num">500
#---createENS----------------------
createEns <- function(numFeature = 8L, r = 7L, nh = 5L, fact = 7L, order, X){
# determine the indices of the best predictors
bestF <<- order %>% head(numFeature)
# choose the best predictors for the training set
Xtrain <- X$pretrain$x[ , bestF]
class="macro">#setMKLthreads(class="num">1)
k <- class="num">1
rng <- RNGseq(n, class="num">12345)
#---creste Ensemble---
Ens <<- foreach(i = class="num">1:n, .packages = "elmNN") %do% {
rngtools::setRNG(rng[[k]])
idx <- rminer::holdout(Ytrain, ratio = r/class="num">10, mode = "random")$tr
k <- k + class="num">1
elmtrain(x = Xtrain[idx, ], y = Ytrain[idx], nhid = nh, actfun = Fact[fact])
}
class="kw">return(Ens)
}
#---GetInputData -FUN-----------
GetInputData <- function(Ens, X){
#---predict-InputTrain--
Xtest <- X$train$x[ , bestF]
foreach(i = class="num">1:n, .packages = "elmNN", .combine = "cbind") %do% {
predict(Ens[[i]], newdata = Xtest)
} -> predEns #[ ,n]
#---predict--InputTest----
Xtest1 <- X$test$x[ , bestF]
foreach(i = class="num">1:n, .packages = "elmNN", .combine = "cbind") %do% {
predict(Ens[[i]], newdata = Xtest1)
} -> InputTest #[ ,n]
#---predict--InputTest1----
Xtest2 <- X$test1$x[ , bestF]
foreach(i = class="num">1:n, .packages = "elmNN", .combine = "cbind") %do% {
predict(Ens[[i]], newdata = Xtest2)
} -> InputTest1 #[ ,n]
#---res-------------------------
class="kw">return(list(InputTrain = predEns,
InputTest = InputTest,
InputTest1 = InputTest1))
}
}, env)
#---class="num">4--createEns----------------
evalq({
Ytrain <- X1$pretrain$y
Ytest <- X1$train$y
Ytest1 <- X1$test$y
Ytest2 <- X1$test1$y
Ens <- vector(mode = "list", n)
createEns(order = orderX1, X = X1) -> Ens
GetInputData(Ens, X1) -> res
}, env)
> env$res %>% str()
List of class="num">3
$ InputTrain: num [class="num">1:class="num">1001, class="num">1:class="num">500] class="num">0.811 class="num">0.882 class="num">0.924 class="num">0.817 class="num">0.782 ...
$ InputTest : num [class="num">1:class="num">501, class="num">1:class="num">500] class="num">0.5 class="num">0.383 class="num">0.366 class="num">0.488 class="num">0.359 ...
$ InputTest1: num [class="num">1:class="num">251, class="num">1:class="num">500] class="num">0.32 class="num">0.246 class="num">0.471 class="num">0.563 class="num">0.451 ...