深度神经网络 (第六部分)。 神经网络分类器的融合: 引导聚合·进阶篇
🧠

深度神经网络 (第六部分)。 神经网络分类器的融合: 引导聚合·进阶篇

(2/3)·单体 DNN 再深也扛不住样本扰动,bagging 融合才是进阶玩家的方差剪刀

实战向 第 2/3 篇
不少交易者把单个神经网络分类器训到 99% 回测准确率就直接上实盘,却忽略了一点:换一批样本重训,边界可能全变。把单体模型当真理,是融合前最常见的认知坑。

单线程跑融合训练与贝叶斯超参搜索

执行训练前先把 MKL 数学库切到单线程模式,再用随机数序列 rng 初始化各常量,foreach() 每次迭代都会重置随机数发生器。迭代结束务必把 MKL 设回多线程——单线程下计算结果会略差,但能保证每次返回的融合分类器权重完全一致。 计算顺序固定为 train / predict / best / test_averaging / test_voting。无论重复多少遍(同参数),结果不变,这正是我们要优化的神经网络融合超参数。影响单体分类器品质的四个旋钮:输入预测器数量、训练样本规模、隐藏层神经元数、激活函数。 超参范围这样定:numFeature 取 3~12;r 为训练集比例,计算前除以 10;nh 隐藏层神经元 1~50;fact 为激活函数向量 Fact 的索引。适应度函数 fitnes() 接收这 4 个参数,内部训练融合、算 predict、挑 bestNN,最后平均合并最佳预测并返回 Score = mean(F1)。目标变量 Ytrain/Ytest/Ytest1 移出函数外,因为搜索中不变。 先验证函数可跑:全部计算约 9 秒出结果。接着用 10 个随机初始点、20 次迭代跑贝叶斯优化,按 Value 排历史取前 10。最优解显示预测器 8 个、样本比例 0.3、隐藏层神经元 3、激活函数 radbas——这种组合靠直觉很难凑出,说明贝叶斯搜索确实能覆盖广域模型空间。 用上述最优参数在测试集训练并测融合:挑 7 个最佳神经网络平均合并,度量明显优于任意单体网络,且可与前作最优 DNN 参数结果比肩。外汇与贵金属行情高波动,此类模型仅降低过拟合概率,实盘前请在 MT5 用历史数据复算一遍。

MQL5 / C++
#----准备-------------
class="macro">#library(anytime)
class="macro">#library(rowr)
class="macro">#library(elmNN)
class="macro">#library(rBayesianOptimization)
class="macro">#library(foreach)
class="macro">#library(magrittr)
class="macro">#library(clusterSim)
#class="macro">#source(file = "FunPrepareData.R")
#class="macro">#source(file = "FUN_Ensemble.R")
##---准备----
evalq({
  dt <- PrepareData(Data, Open, High, Low, Close, Volume)
  DT <- SplitData(dt, class="num">4000, class="num">1000, class="num">500, class="num">250, start = class="num">1)
  pre.outl <- PreOutlier(DT$pretrain)
  DTcap <- CappingData(DT, impute = T, fill = T, dither = F, pre.outl = pre.outl)
  preproc <- PreNorm(DTcap, meth = meth)
  DTcap.n <- NormData(DTcap, preproc = preproc)
}, env)
#---数据 X-------------
evalq({
  list(
    pretrain = list(
      x = DTcap.n$pretrain %>% dplyr::select(-c(Data, Class)) %>% as.data.frame(),
      y = DTcap.n$pretrain$Class %>% as.numeric() %>% subtract(class="num">1)
    ),
    train = list(
      x = DTcap.n$train %>% dplyr::select(-c(Data, Class)) %>% as.data.frame(),
      y = DTcap.n$train$Class %>% as.numeric() %>% subtract(class="num">1)
    ),
    test = list(
      x = DTcap.n$val %>% dplyr::select(-c(Data, Class)) %>% as.data.frame(),
      y = DTcap.n$val$Class %>% as.numeric() %>% subtract(class="num">1)
    ),
    test1 = list(
      x = DTcap.n$test %>% dplyr::select(-c(Data, Class)) %>% as.data.frame(),
      y = DTcap.n$test$Class %>% as.numeric() %>% subtract(class="num">1)
    )
  ) -> X
}, env)
require(clusterSim)
evalq({
  numFeature <- class="num">10
  HINoV.Mod(x = X$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",

「特征筛选后极限学习机的集成训练与验证」

先做特征重要性排序,用随机索引跑聚类后取头部特征。输出矩阵里第5号特征得分0.600、第12号仅0.101,差距近6倍,说明 v.fatl、v.rbci、v.ftlm、rbci 等前几个指标对方向判别的贡献显著占优,而末位特征基本是噪声。 接着用极限学习机(ELM)做集成:设 n=500 个基模型、隐藏层 nh=5、激活函数 sin,按 7:3 随机切分预训练集。测试集上对每个模型做 0/1 预测后算 F1,排序取前 7 个(numEns*2+1)最优网络,头部三个 F1 分别为 0.720、0.718、0.718,证明集成里挑优比全量平均更稳。 最后在独立测试集对优选网络做等权平均,阈值 0.5 转信号。结果 Accuracy 0.75、Precision 0.723、Recall 0.739、F1 0.731——外汇与贵金属行情高杠杆高风险,该精度仅代表样本内统计倾向,实盘需防过拟合与滑点侵蚀。

MQL5 / C++
evalq({
  n <- class="num">500
  r <- class="num">7
  nh <- class="num">5
  Xtrain <- X$pretrain$x[ , bestF]
  Ytrain <- X$pretrain$y
  Ens <- foreach(i = class="num">1:n, .packages = "elmNN") %do% {
    idx <- rminer::holdout(Ytrain, ratio = r/class="num">10, mode = "random")$tr
    elmtrain(x = Xtrain[idx, ], y = Ytrain[idx],
             nhid = nh, actfun = "sin")
  }
}, env)

evalq({
  Xtest <- X$train$x[ , bestF]
  Ytest <- X$train$y
  foreach(i = class="num">1:n, .packages = "elmNN", .combine = "cbind") %do% {
    predict(Ens[[i]], newdata = Xtest)
  } -> y.pr
}, env)

evalq({
  numEns <- class="num">3
  foreach(i = class="num">1:n, .combine = "c") %do% {
    ifelse(y.pr[ ,i] > class="num">0.5, class="num">1, class="num">0) -> Ypred
    Evaluate(actual = Ytest, predicted = Ypred)$Metrics$F1 %>%
      mean()
  } -> Score
  Score %>% order(decreasing = TRUE) %>% head((numEns*class="num">2 + class="num">1)) -> bestNN
  Score[bestNN] %>% round(class="num">3)
}, env)

evalq({
  n <- len(Ens)
  Xtest <- X$test$x[ , bestF]
  Ytest <- X$test$y
  foreach(i = class="num">1:n, .packages = "elmNN", .combine = "+") %:%
    when(i %in% bestNN) %do% {
    predict(Ens[[i]], newdata = Xtest)} %>%
    divide_by(length(bestNN)) -> ensPred
  ifelse(ensPred > class="num">0.5, class="num">1, class="num">0) -> ensPred
  Evaluate(actual = Ytest, predicted = ensPred)$Metrics[ ,class="num">2:class="num">5] %>%
    round(class="num">3)
}, env)

◍ 集成模型在测试集上的两类投票对比

把多个 ELM 子网络的预测做集成,常用两种办法:直接对概率取平均(averaging),或让每个网络投正负票再汇总(voting)。在 test1 子集上,averaging 对类别 1 的 F1 为 0.761,而 voting 把类别 0 的 F1 推到 0.781,说明投票机制在少数类识别上倾向更稳。 具体跑法上,averaging 段先筛出 bestNN 对应的模型,用 foreach 并行 predict 后除以模型数得到 ensPred,再以 0.5 为阈值转成 0/1 标签。voting 段则把每个预测先映射成 1/-1,按样本行求和,得票数大于 0 才判为 1。 两个测试集(test 与 test1)结果不完全一致:test 上 voting 与 averaging 的各类指标几乎重合(Accuracy 均 0.745),但 test1 上 voting 的类别 0 精度从 0.716 升到 0.787。外汇与贵金属行情里这种泛化差异很常见,实盘使用前建议在 MT5 导出的多段样本上复测,高风险品种尤需警惕过拟合。

切分窗口与极端值封顶的实操细节

这段 R 脚本把原始行情序列按 4000/1000/500/250 四条长度切成 pretrain、train、val、test 四段,起点偏移设为 1。PreOutlier 先对 pretrain 做离群预检,CappingData 再对全段做缩尾封顶,impute 和 fill 都开、dither 关,避免把外汇跳空造成的极端值喂进模型。 归一化用 PreNorm 记下 meth 对应的参数,NormData 套用到所有段。整理 X 时丢弃 Data 和 Class 两列,Class 转数值再减 1,使标签从 0 起步——MT5 导出的 CSV 若带字符串类名,这一步会直接报错,先确认因子已编码。 特征筛选写死取前 10 个:HINoV.Mod 用 metric 距离、kmeans 聚类、cRAND 指数排特征重要性,head(10) 截出 bestF。若你把 numFeature 调到 20,训练耗时大致翻倍但过拟合概率可能上升,贵金属跨品种测试时尤其要手动看 stopri 分布。 训练侧起了 500 个 ELM 基模型,隐藏层 nh=5、激活函数 sin、holdout 比例 0.7,随机种子锁 12345 保证可复现。预测阶段只对 X$train 跑 bestF 子集,foreach 并行 cbind 出 500 列预测矩阵 y.pr,后续投票或阈值切割都从这张表走。

MQL5 / C++
DT <- SplitData(dt, class="num">4000, class="num">1000, class="num">500, class="num">250, start = class="num">1)
pre.outl <- PreOutlier(DT$pretrain)
DTcap <- CappingData(DT, impute = T, fill = T, dither = F, pre.outl = pre.outl)
preproc <- PreNorm(DTcap, meth = meth)
DTcap.n <- NormData(DTcap, preproc = preproc)
#--class="num">1-Data X-------------
list(
  pretrain = list(
    x = DTcap.n$pretrain %>% dplyr::select(-c(Data, Class)) %>% as.data.frame(),
    y = DTcap.n$pretrain$Class %>% as.numeric() %>% subtract(class="num">1)
  ),
  train = list(
    x = DTcap.n$train %>% dplyr::select(-c(Data, Class)) %>% as.data.frame(),
    y = DTcap.n$train$Class %>% as.numeric() %>% subtract(class="num">1)
  ),
  test = list(
    x = DTcap.n$val %>% dplyr::select(-c(Data, Class)) %>% as.data.frame(),
    y = DTcap.n$val$Class %>% as.numeric() %>% subtract(class="num">1)
  ),
  test1 = list(
    x = DTcap.n$test %>% dplyr::select(-c(Data, Class)) %>% as.data.frame(),
    y = DTcap.n$test$Class %>% as.numeric() %>% subtract(class="num">1)
  )
) -> X
#---class="num">2--bestF-----------------------------------
class="macro">#require(clusterSim)
numFeature <- class="num">10
HINoV.Mod(x = X$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] -> orderX
orderX %>% head(numFeature) -> bestF
#---class="num">3-----Train----------------------------
evalq({
  Xtrain <- X$pretrain$x[ , bestF]
  Ytrain <- X$pretrain$y
  setMKLthreads(class="num">1)
  n <- class="num">500
  r <- class="num">7
  nh <- class="num">5
  k <- class="num">1
  rng <- RNGseq(n, class="num">12345)
  Ens <- foreach(i = class="num">1:n, .packages = "elmNN") %do% {
    rngtools::setRNG(rng[[k]])
    k <- k + class="num">1
    idx <- rminer::holdout(Ytrain, ratio = r/class="num">10, mode = "random")$tr
    elmtrain(x = Xtrain[idx, ], y = Ytrain[idx],
             nhid = nh, actfun = "sin")
  }
  setMKLthreads(class="num">2)
}, env)
#---class="num">4-----predict-------------------
evalq({
  Xtest <- X$train$x[ , bestF]
  Ytest <- X$train$y
  foreach(i = class="num">1:n, .packages = "elmNN", .combine = "cbind") %do% {
    predict(Ens[[i]], newdata = Xtest)
  } -> y.pr #[ ,n]
}, env)
#---class="num">5-----best----------------------
evalq({

「集成模型在测试集上的表现与参数边界」

把单模型按 F1 排序取前 7 个(numEns*2+1)做集成,单模型最高 F1 落在 0.723,随后 0.722、0.722、0.719 递减,说明顶部模型性能非常接近,靠堆数量边际增益有限。 平均法集成在测试集两类上 Accuracy 均为 0.75,类 1 的 F1 到 0.761;投票法 Accuracy 0.749,类 1 的 F1 0.760。两者差距极小,平均法略占优,但都没有突破 0.75 准确率天花板,外汇与贵金属行情用这类信号下单仍属高风险。 激活函数可选 sig、sin、radbas、hardlim 等 10 种,参数搜索边界定为 numFeature 3–12、r 1–10、nh 1–50、fact 1–10,进化训练样本量 n=500。直接把这些边界丢进 MT5 的 R 桥接或 Python 脚本里跑一遍,能验证极限配置下集成是否还会卡在 0.75 附近。

MQL5 / C++
numEns <- class="num">3
foreach(i = class="num">1:n, .combine = "c") %do% {
  ifelse(y.pr[ ,i] > class="num">0.5, class="num">1, class="num">0) -> Ypred
  Evaluate(actual = Ytest, predicted = Ypred)$Metrics$F1 %>%
    mean()
} -> Score
Score %>% order(decreasing = TRUE) %>% head((numEns*class="num">2 + class="num">1)) -> bestNN
Score[bestNN] %>% round(class="num">3)
# [class="num">1] class="num">0.723 class="num">0.722 class="num">0.722 class="num">0.719 class="num">0.716 class="num">0.714 class="num">0.713

n <- len(Ens)
Xtest <- X$test$x[ , bestF]
Ytest <- X$test$y
foreach(i = class="num">1:n, .packages = "elmNN", .combine = "+") %:%
  when(i %in% bestNN) %do% {
    predict(Ens[[i]], newdata = Xtest)} %>%
  divide_by(length(bestNN)) -> ensPred
ifelse(ensPred > class="num">0.5, class="num">1, class="num">0) -> ensPred
Evaluate(actual = Ytest, predicted = ensPred)$Metrics[ ,class="num">2:class="num">5] %>%
  round(class="num">3)
# Accuracy Precision Recall F1
# class="num">0 class="num">0.75 class="num">0.711 class="num">0.770 class="num">0.739
# class="num">1 class="num">0.75 class="num">0.790 class="num">0.734 class="num">0.761

n <- len(Ens)
Xtest <- X$test$x[ , bestF]
Ytest <- X$test$y
foreach(i = class="num">1:n, .packages = "elmNN", .combine = "cbind") %:%
  when(i %in% bestNN) %do% {
    predict(Ens[[i]], newdata = Xtest)
  } %>%
  apply(class="num">2, function(x) ifelse(x > class="num">0.5, class="num">1, -class="num">1)) %>%
  apply(class="num">1, function(x) sum(x)) -> vot
ifelse(vot > class="num">0, class="num">1, class="num">0) -> ClVot
Evaluate(actual = Ytest, predicted = ClVot)$Metrics[ ,class="num">2:class="num">5] %>%
  round(class="num">3)
# Accuracy Precision Recall F1
# class="num">0 class="num">0.749 class="num">0.711 class="num">0.761 class="num">0.735
# class="num">1 class="num">0.749 class="num">0.784 class="num">0.738 class="num">0.760

Fact <- c("sig", "sin", "radbas", "hardlim", "hardlims", "satlins", "tansig", "tribas", "poslin", "purelin")
bonds <- list(
  numFeature = c(3L, 12L),
  r = c(1L, 10L),
  nh <- c(1L, 50L),
  fact = c(1L, 10L)
)

Ytrain <- X$pretrain$y
Ytest <- X$train$y
Ytest1 <- X$test$y
n <- class="num">500
numEns <- class="num">3
fitnes <- function(numFeature, r, nh, fact){
  bestF <- orderX %>% head(numFeature)
  Xtrain <- X$pretrain$x[ , bestF]
  setMKLthreads(class="num">1)
  k <- class="num">1
  rng <- RNGseq(n, class="num">12345)
}
把融合诊断交给小布
不同 bagging 子集下各分类器的稳定度对比,小布盯盘的 AIGC 已内置,打开对应品种页即可看到分歧热力图,你只管判断要不要信这波信号。

常见问题

bagging 主要降低方差,用自助采样训练多个独立分类器再聚合;boosting 着重降低偏差,串行修正前序错误,两者目标不同。
常见做法是 60%~80% 有放回抽样,具体依样本量和过拟合程度调整,没有统一硬标准。
倾向移除对验证集贡献低或与其他成员高度冗余的分类器,可用相关性阈值或交叉验证误差做裁剪。
品种页里的 AIGC 模块可加载多模型分歧视图,但训练与超参优化仍需你在本地框架完成,小布负责看板而非代训。
概率输出平均对连续置信更友好,表决易丢边际信息;贵金属与外汇均属高风险,实盘前建议用样本外数据比两种合并方式。