深度神经网络(第八部分)。 提高袋封融合的分类品质·综合运用
🧠

深度神经网络(第八部分)。 提高袋封融合的分类品质·综合运用

(3/3)· 当你已训好一组 ELM 分类器,却卡在噪声样本和阈值选择拖垮胜率,这篇用实验收口

进阶 第 3/3 篇
很多人训完神经网络融合就直接上阈值切类标签,从没查过初始数据里的异常值正在偷偷拉低分类品质。把降噪和超参优化省掉一步,回测好看实盘就崩。外汇贵金属杠杆高,信号失真可能直接放大亏损。

「用箱线图比对四种数据修复后的输入分布」

做特征工程时,同一批测试样本经过 origin、repaired、removed、relab 四种处理,输入空间可能已经漂移。直接看均值容易被骗,箱线图能把中位数、四分位和离群点一次摊开。 上面这段 R 脚本把 InputTest 的前 10 列横向画成箱线图,每组处理占一格,红色竖线固定在 0、0.5、1.0 三个位置,相当于给归一化边界打了参照尺。你能一眼看出哪组处理的变量被压在了 [0,1] 区间内,哪组还挂着超出边界的离群须。 InputTest1 的子集也走了同样流程,2×2 布局下 mai 参数把边距收到 0.3/0.3/0.4/0.2,图更挤但并列对比更直观。外汇与贵金属数据高频跳空,removed 组若把异常样本直接删掉,箱线图的中位线往往会比 repaired 组更靠中间,这是修复策略改变了分布形态的实锤。 开 R 把 env 环境 load 进来,照这段把 mfrow 和 abline 的 v 向量抄过去跑一遍,重点盯 repaired 与 relab 两组的须长差异,大概率能反推出你训练集里标签修正的激进程度。

MQL5 / C++
abline(v = c(class="num">0, class="num">0.5, class="num">1.0), col = class="num">2)
boxplot(env$res.repaired$InputTest[ ,class="num">1:class="num">10], horizontal = T, main = "res.repaired$InputTest[ ,class="num">1:class="num">10]")
abline(v = c(class="num">0, class="num">0.5, class="num">1.0), col = class="num">2)
boxplot(env$res.removed$InputTest[ ,class="num">1:class="num">10], horizontal = T, main = "res.removed$InputTest[ ,class="num">1:class="num">10]")
abline(v = c(class="num">0, class="num">0.5, class="num">1.0), col = class="num">2)
boxplot(env$res.relab$InputTest[ ,class="num">1:class="num">10], horizontal = T, main = "res.relab$InputTest[ ,class="num">1:class="num">10]")
abline(v = c(class="num">0, class="num">0.5, class="num">1.0), col = class="num">2)
par(mfrow = c(class="num">1, class="num">1))
#------Ris InputTest1------
par(mfrow = c(class="num">2, class="num">2), mai = c(class="num">0.3, class="num">0.3, class="num">0.4, class="num">0.2))
boxplot(env$res.origin$InputTest1[ ,class="num">1:class="num">10], horizontal = T, main = "res.origin$InputTest1[ ,class="num">1:class="num">10]")
abline(v = c(class="num">0, class="num">0.5, class="num">1.0), col = class="num">2)
boxplot(env$res.repaired$InputTest1[ ,class="num">1:class="num">10], horizontal = T, main = "res.repaired$InputTest1[ ,class="num">1:class="num">10]")
abline(v = c(class="num">0, class="num">0.5, class="num">1.0), col = class="num">2)
boxplot(env$res.removed$InputTest1[ ,class="num">1:class="num">10], horizontal = T, main = "res.removed$InputTest1[ ,class="num">1:class="num">10]")
abline(v = c(class="num">0, class="num">0.5, class="num">1.0), col = class="num">2)
boxplot(env$res.relab$InputTest1[ ,class="num">1:class="num">10], horizontal = T, main = "res.relab$InputTest1[ ,class="num">1:class="num">10]")
abline(v = c(class="num">0, class="num">0.5, class="num">1.0), col = class="num">2)
par(mfrow = c(class="num">1, class="num">1))

连续预测转类标签的阈值怎么定

把神经网络输出的连续预测变成类标签,核心就是选阈值切一刀。融合的 500 个网络在 origin / repaired / removed / relabeled 四组数据上各自输出连续值,形状接近但范围不同,removed 组偏离最明显,所以阈值不能一套用到底。 实际落地有四种基础阈值:half 固定 0.5;med 取验证集中位数,比 half 更稳但只能拿 InputTrain 先算、后续子集复用;mce 最小化分类误差;both 最大化 youdensIndex = 灵敏度 + 特异度 - 1。最佳阈值永远在 InputTrain 上定,再套到 InputTest 和 InputTest1。 代码里 GetThreshold() 只实装了前四种。外层循环跑 4 种阈值类型、4 核并行,内层循环对 500 个网络逐个算 InputTrain 阈值,返回 500×4 矩阵。实测 origin 组 med 相比 half 在 F1 上没肉眼可见的提升。 跨组分布有个规律值得记:InputTest 子集度量明显好于 InputTrain,InputTest1 又比 InputTest 差一截;half 阈值除了 relabeled 组,其余都不比别的类型差。外汇与贵金属模型融合高风险,阈值结论仅基于回测样本,换周期可能失效。 别把 0.5 当默认真理 half 在多数组不输 med/mce,但 removed 组形态异类,直接套 0.5 容易把连续预测切歪,建议先跑一遍 Threshold 矩阵再决定。

MQL5 / C++
library("doFuture")
#---predX1------------------
evalq({
 group <- qc(origin, repaired, removed, relabeled)
 predX1 <- vector("list", class="num">4)
 foreach(i = class="num">1:class="num">4, .packages = "elmNN") %do% {
 x <- denoiseX1pretrain[[i]]$x %>% as.matrix()
 y <- denoiseX1pretrain[[i]]$y
 SEED = class="num">12345
 createEns(r = 7L, nh = 5L, fact = 7L, X = x, Y = y) -> ens
 GetInputData(Ens = ens, X = x, Y = y) -> pred
 class="kw">return(list(ensemble = ens, pred = pred))
 } -> predX1
 names(predX1) <- group
}, env)
#--function-------------------------
evalq({
  import_fun("InformationValue", optimalCutoff, CutOff)
  import_fun("InformationValue", youdensIndex, th_youdens)
  GetThreshold <- function(X, Y, type){
    class="kw">switch(type,
           half = class="num">0.5,
           med = median(X),
           mce = CutOff(Y, X, "misclasserror"),
           both = CutOff(Y, X,"Both"),
           ones = CutOff(Y, X, "Ones"),
           zeros = CutOff(Y, X, "Zeros")
    )
  }
}, env)
#--threshold--train--origin--------
evalq({
  Ytest = X1$train$y
  Ytest1 = X1$test$y
  Ytest2 = X1$test1$y
  testX1 <- vector("list", class="num">4)
  names(testX1) <- group
  type <- qc(half, med, mce, both)
  registerDoFuture()
  cl <- makeCluster(class="num">4)
  plan(cluster, workers = cl)
  foreach(i = class="num">1:class="num">4, .combine = "cbind") %dopar% {# type
    foreach(j = class="num">1:class="num">500, .combine = "c") %do% {
      GetThreshold(predX1$origin$pred$InputTrain[ ,j], Ytest, type[i])
    }
  }  -> testX1$origin$Threshold
  stopCluster(cl)
  dimnames(testX1$origin$Threshold) <- list(NULL,type)
  }, env)
> env$testX1$origin$Threshold %>% str()
num [class="num">1:class="num">500, class="num">1:class="num">4] class="num">0.5 class="num">0.5 class="num">0.5 class="num">0.5 class="num">0.5 class="num">0.5 class="num">0.5 class="num">0.5 class="num">0.5 class="num">0.5 ...
- attr(*, "dimnames")=List of class="num">2
  ..$ : NULL
  ..$ : chr [class="num">1:class="num">4] "half" "med" "mce" "both"
> env$testX1$origin$Threshold %>% head()
    half       med       mce       both

◍ 阈值切分下的四类样本评分分布

上方数据块里 6 行记录,首列是样本序号,第二列固定为 0.5 基准线,后两列是某次训练集与测试集在阈值判定后的输出值,例如第 3 行测试侧读到 0.5394191,相对 0.5 基准已明显偏移,说明该折样本被模型判为偏正类的概率倾向抬升。 代码段先用 evalq 在 env 环境里跑了三段并行评估:train、test、test1 分别对应训练集、测试集、第二组测试集,核心逻辑都是对 500 个弱预测列按 4 类阈值(origin / repaired / removed / relabeling)做二值化,再算 F1 均值。 末尾的 boxplot 只画了 k=1(即 origin 类)的 Threshold 与两类 Score 箱线图,横线上标了 0、0.5、0.7 三条参考线——0.7 那条红色线可作为过拟合警戒位,若 InputTestScore 中位数掉到 0.5 以下,模型外推能力可能走弱。外汇与贵金属行情下用这套阈值框架做信号过滤,仍属高风险,实盘前建议在 MT5 用历史数据复跑确认分布。

MQL5 / C++
#--train--------------------
evalq({
  foreach(i = class="num">1:class="num">4, .combine = "cbind") %do% {# type
    foreach(j = class="num">1:class="num">500, .combine = "c") %do% {
      ifelse(predX1$origin$pred$InputTrain[ ,j] > testX1$origin$Threshold[j, i], class="num">1, class="num">0) ->.;
      Evaluate(actual = Ytest, predicted = .)$Metrics$F1 %>% mean()
    }
  } -> testX1$origin$InputTrainScore
  dimnames(testX1$origin$InputTrainScore)[[class="num">2]] <- type
}, env)
#--test-----------------------------
evalq({
  foreach(i = class="num">1:class="num">4, .combine = "cbind") %do% {# type
    foreach(j = class="num">1:class="num">500, .combine = "c") %do% {
      ifelse(predX1$origin$pred$InputTest[ ,j] > testX1$origin$Threshold[j, i], class="num">1, class="num">0) ->.;
      Evaluate(actual = Ytest1, predicted = .)$Metrics$F1 %>% mean()
    }
  } -> testX1$origin$InputTestScore
  dimnames(testX1$origin$InputTestScore)[[class="num">2]] <- type
}, env)
#--test1-----------------------------
evalq({
  foreach(i = class="num">1:class="num">4, .combine = "cbind") %do% {
    foreach(j = class="num">1:class="num">500, .combine = "c") %do% {
      ifelse(predX1$origin$pred$InputTest1[ ,j] > testX1$origin$Threshold[j, i], class="num">1, class="num">0) ->.;
      Evaluate(actual = Ytest2, predicted = .)$Metrics$F1 %>% mean()
    }
  } -> testX1$origin$InputTest1Score
  dimnames(testX1$origin$InputTest1Score)[[class="num">2]] <- type
}, env)
k <- 1L class="macro">#origin
# k <- 2L class="macro">#repaired
# k <- 3L class="macro">#removed
# k <- 4L class="macro">#relabeling
par(mfrow = c(class="num">1,class="num">4), mai = c(class="num">0.3, class="num">0.3, class="num">0.4, class="num">0.2))
boxplot(env$testX1[[k]]$Threshold, horizontal = F,
        main = paste0(env$group[k],"$$Threshold"),
        col = c(class="num">2,class="num">4,class="num">5,class="num">6))
abline(h = c(class="num">0, class="num">0.5, class="num">0.7), col = class="num">2)
boxplot(env$testX1[[k]]$InputTrainScore, horizontal = F,
        main = paste0(env$group[k],"$$InputTrainScore"),
        col = c(class="num">2,class="num">4,class="num">5,class="num">6))
abline(h = c(class="num">0, class="num">0.5, class="num">0.7), col = class="num">2)
boxplot(env$testX1[[k]]$InputTestScore, horizontal = F,
        main = paste0(env$group[k],"$$InputTestScore"),
        col = c(class="num">2,class="num">4,class="num">5,class="num">6))

「用阈值扫描给模型分组打分」

这段 R 脚本做的是一件很实在的事:对 4 种数据处置分组(原始 / 修复 / 剔除 / 重标)分别跑阈值寻优,再算 F1 分数。阈值线画在 0、0.5、0.7 三处,红色标记,方便肉眼看箱线图分布偏移。 abline(h = c(0, 0.5, 0.7), col = 2) boxplot(env$testX1[[k]]$InputTest1Score, horizontal = F, main = paste0(env$group[k],"$$InputTest1Score"), col = c(2,4,5,6)) abline(h = c(0, 0.5, 0.7), col = 2) par(mfrow = c(1, 1)) 上面四行先把测试集分数画成箱线图,4 种分组配色区分;横轴不翻转,纵向对比中位数和离群点更直观。 library("doFuture") #--threshold--train--------- evalq({ k <- 1L #origin #k <- 2L #repaired #k <- 3L #removed #k <- 4L #relabeling type <- qc(half, med, mce, both) Ytest = X1$train$y Ytest1 = X1$test$y Ytest2 = X1$test1$y registerDoFuture() cl <- makeCluster(4) plan(cluster, workers = cl) while (k <= 4) { # group foreach(i = 1:4, .combine = "cbind") %dopar% {# type foreach(j = 1:500, .combine = "c") %do% { GetThreshold(predX1[[k]]$pred$InputTrain[ ,j], Ytest, type[i]) } } -> testX1[[k]]$Threshold dimnames(testX1[[k]]$Threshold) <- list(NULL,type) k <- k + 1 } stopCluster(cl) }, env) 阈值寻优部分用 doFuture 起了 4 个 worker 并行,对每组 500 列预测值调 GetThreshold,得到 4 类切分方式(half / med / mce / both)的阈值矩阵。 #--train---- evalq({ k <- 1L #origin #k <- 2L #repaired #k <- 3L #removed #k <- 4L #relabeling while (k <= 4) { foreach(i = 1:4, .combine = "cbind") %do% { foreach(j = 1:500, .combine = "c") %do% { ifelse(predX1[[k]]$pred$InputTrain[ ,j] > testX1[[k]]$Threshold[j, i], 1, 0) ->.; Evaluate(actual = Ytest, predicted = .)$Metrics$F1 %>% mean() } } -> testX1[[k]]$InputTrainScore dimnames(testX1[[k]]$InputTrainScore)[[2]] <- type k <- k + 1 } }, env) #--test----- evalq({ k <- 1L #origin #k <- 2L #repaired #k <- 3L #removed #k <- 4L #relabeling while (k <= 4) { foreach(i = 1:4, .combine = "cbind") %do% { foreach(j = 1:500, .combine = "c") %do% { ifelse(predX1[[k]]$pred$InputTest[ ,j] > testX1[[k]]$Threshold[j, i], 1, 0) ->.; Evaluate(actual = Ytest1, predicted = .)$Metrics$F1 %>% mean() } } -> testX1[[k]]$InputTestScore dimnames(testX1[[k]]$InputTestScore)[[2]] <- type k <- k + 1 } }, env) 训练集与测试集都按同一套阈值二值化,再算平均 F1;500 次重复让分数更稳定。外汇与贵金属信号模型用这类流程时,样本外 F1 若掉到 0.5 以下,过拟合概率偏高,需回看特征工程。 别把 0.7 当万能刀 脚本里硬画了 0.7 参考线,但阈值是数据驱动算出来的,分组间最优值可能落在 0.3–0.8 区间,照抄参考线会掩盖真实分歧。

MQL5 / C++
abline(h = c(class="num">0, class="num">0.5, class="num">0.7), col = class="num">2)
boxplot(env$testX1[[k]]$InputTest1Score, horizontal = F,
      main = paste0(env$group[k],"$$InputTest1Score"),
      col = c(class="num">2,class="num">4,class="num">5,class="num">6))
abline(h = c(class="num">0, class="num">0.5, class="num">0.7), col = class="num">2)
par(mfrow = c(class="num">1, class="num">1))
library("doFuture")
#--threshold--train---------
evalq({
  k <- 1L class="macro">#origin
  class="macro">#k <- 2L class="macro">#repaired
  class="macro">#k <- 3L class="macro">#removed
  class="macro">#k <- 4L class="macro">#relabeling
  type <- qc(half, med, mce, both)
  Ytest = X1$train$y
  Ytest1 = X1$test$y
  Ytest2 = X1$test1$y
  registerDoFuture()
  cl <- makeCluster(class="num">4)
  plan(cluster, workers = cl)
  class="kw">while (k <= class="num">4) { # group
    foreach(i = class="num">1:class="num">4, .combine = "cbind") %dopar% {# type
      foreach(j = class="num">1:class="num">500, .combine = "c") %do% {
        GetThreshold(predX1[[k]]$pred$InputTrain[ ,j], Ytest, type[i])
      }
    } -> testX1[[k]]$Threshold
    dimnames(testX1[[k]]$Threshold) <- list(NULL,type)
    k <- k + class="num">1
  }
  stopCluster(cl)
}, env)
#--train----
evalq({
  k <- 1L class="macro">#origin
  class="macro">#k <- 2L class="macro">#repaired
  class="macro">#k <- 3L class="macro">#removed
  class="macro">#k <- 4L class="macro">#relabeling
  class="kw">while (k <= class="num">4) {
    foreach(i = class="num">1:class="num">4, .combine = "cbind") %do% {
      foreach(j = class="num">1:class="num">500, .combine = "c") %do% {
        ifelse(predX1[[k]]$pred$InputTrain[ ,j] > testX1[[k]]$Threshold[j, i], class="num">1, class="num">0) ->.;
        Evaluate(actual = Ytest, predicted = .)$Metrics$F1 %>% mean()
      }
    } -> testX1[[k]]$InputTrainScore
    dimnames(testX1[[k]]$InputTrainScore)[[class="num">2]] <- type
    k <- k + class="num">1
  }
}, env)
#--test-----
evalq({
  k <- 1L class="macro">#origin
  class="macro">#k <- 2L class="macro">#repaired
  class="macro">#k <- 3L class="macro">#removed
  class="macro">#k <- 4L class="macro">#relabeling
  class="kw">while (k <= class="num">4) {
    foreach(i = class="num">1:class="num">4, .combine = "cbind") %do% {
      foreach(j = class="num">1:class="num">500, .combine = "c") %do% {
        ifelse(predX1[[k]]$pred$InputTest[ ,j] > testX1[[k]]$Threshold[j, i], class="num">1, class="num">0) ->.;
        Evaluate(actual = Ytest1, predicted = .)$Metrics$F1 %>% mean()
      }
    } -> testX1[[k]]$InputTestScore
    dimnames(testX1[[k]]$InputTestScore)[[class="num">2]] <- type
    k <- k + class="num">1
  }
}, env)

阈值与分数分布的箱线图核对

上面那段脚本在 k 从 2 到 4 循环时,把四种数据变形(repaired / removed / relabeling 等)下的预测阈值和训练、测试得分都算了出来,并塞进 env$testX1[[k]] 里。 真正落地看效果的是最后那组 par(mfrow=c(1,4)) 下的四个 boxplot:Threshold、InputTrainScore、InputTestScore、InputTest1Score 各占一格,颜色按 2/4/5/6 区分四类模型。横着的红线画在 0、0.5、0.7 三处,相当于给你一个肉眼可判的及格线——中位数如果压在 0.5 以下,这组特征工程大概率没带来边际增益。 外汇与贵金属行情受杠杆和跳空影响,这类分数分布只反映样本内统计关系,实盘复制可能失真,务必用小样本先跑。 想自己验证,直接把 k 改回 1L(origin 那行取消注释)重跑,箱线图会多一组基线对照,能看出修复和重标注到底动了多少分位。

MQL5 / C++
class="macro">#k <- 2L class="macro">#repaired
class="macro">#k <- 3L class="macro">#removed
class="macro">#k <- 4L class="macro">#relabeling
class="kw">while (k <= class="num">4) {
  foreach(i = class="num">1:class="num">4, .combine = "cbind") %do% {
    foreach(j = class="num">1:class="num">500, .combine = "c") %do% {
      ifelse(predX1[[k]]$pred$InputTest1[ ,j] > testX1[[k]]$Threshold[j, i], class="num">1, class="num">0) ->.;
      Evaluate(actual = Ytest2, predicted = .)$Metrics$F1 %>% mean()
    }
  } -> testX1[[k]]$InputTest1Score
  dimnames(testX1[[k]]$InputTest1Score)[[class="num">2]] <- type
  k <- k + class="num">1
}
}, env)
# k <- 1L class="macro">#origin
k <- 2L class="macro">#repaired
# k <- 3L class="macro">#removed
# k <- 4L class="macro">#relabeling
par(mfrow = c(class="num">1,class="num">4), mai = c(class="num">0.3, class="num">0.3, class="num">0.4, class="num">0.2))
boxplot(env$testX1[[k]]$Threshold, horizontal = F,
        main = paste0(env$group[k],"$$Threshold"),
        col = c(class="num">2,class="num">4,class="num">5,class="num">6))
abline(h = c(class="num">0, class="num">0.5, class="num">0.7), col = class="num">2)
boxplot(env$testX1[[k]]$InputTrainScore, horizontal = F,
        main = paste0(env$group[k],"$$InputTrainScore"),
        col = c(class="num">2,class="num">4,class="num">5,class="num">6))
abline(h = c(class="num">0, class="num">0.5, class="num">0.7), col = class="num">2)
boxplot(env$testX1[[k]]$InputTestScore, horizontal = F,
        main = paste0(env$group[k],"$$InputTestScore"),
        col = c(class="num">2,class="num">4,class="num">5,class="num">6))
abline(h = c(class="num">0, class="num">0.5, class="num">0.7), col = class="num">2)
boxplot(env$testX1[[k]]$InputTest1Score, horizontal = F,
        main = paste0(env$group[k],"$$InputTest1Score"),
        col = c(class="num">2,class="num">4,class="num">5,class="num">6))
abline(h = c(class="num">0, class="num">0.5, class="num">0.7), col = class="num">2)
par(mfrow = c(class="num">1, class="num">1))

◍ 融合模型里挑七个网络再取平均预测

在 InputTrain 子集上,先按 F1 度量给每组数据(origin / repaired / removed / relabeled)挑出 7 个最优神经网络做修剪。具体做法是对 InputTrainScore 降序排,取前 2*nb+1 个索引,nb 设为 3,于是正好 7 个。脚本跑完会输出四组的最佳索引矩阵,且能直接看到 mce 与 both 两种阈值类型的索引经常完全重合。 以 origin 组为例,half 阈值下入选索引为 415/191/469/220/265/393/75,而 mce 与 both 的前三行都是 415/220/191,说明这两类阈值倾向选中同一批网络。repaired 组里 half 与 med 的第一名分别是 393 和 393,但 mce/both 第一名是 154,分组差异比 origin 更明显。 挑出 7 个网络后,在 InputTrain、InputTest、InputTest1 及每种阈值类型内对连续预测做平均。repaired 组的统计显示最后两种阈值(mce、both)的平均预测统计量一致,InputTest 子集也复现了这一现象,意味着这两类阈值的融合输出在样本内和样本外都趋同。 平均完要用 InputTrain 单独算最佳阈值(th_aver),再把连续值转类标签算全量度量。origin 组在 TestScore 上 half/half 组合拿到 0.750,但 Test1Score 掉到 0.735;med/mce/both 变体在两个子集都稳定在 ~0.745。removed 组 med/half 在 TestScore 冲到 0.769,Test1Score 却降到 0.732。relabeled 组修剪时 med/mce/both 更优,平均时 both 或 med 更佳。数值会因随机种子不同而有浮动,跑自己的数据可能和上面有出入。 外汇与贵金属信号建模高风险,以上为历史子集度量,实盘泛化能力未验证,须以 MT5 接真实报价回测为准。

MQL5 / C++
#--bestNN----------------------------------------
evalq({
  nb <- 3L
  k <- 1L
  class="kw">while (k <= class="num">4) {
    foreach(j = class="num">1:class="num">4, .combine = "cbind") %do% {
      testX1[[k]]$InputTrainScore[ ,j] %>% order(decreasing = TRUE) %>% head(class="num">2*nb + class="num">1)
    } -> testX1[[k]]$bestNN
    dimnames(testX1[[k]]$bestNN) <- list(NULL, type)
    k <- k + class="num">1
  }
}, env)
> env$testX1$origin$bestNN
     half med mce both
[class="num">1,]  class="num">415  class="num">75 class="num">415  class="num">415
[class="num">2,]  class="num">191 class="num">190 class="num">220  class="num">220
[class="num">3,]  class="num">469 class="num">220 class="num">191  class="num">191
[class="num">4,]  class="num">220 class="num">469 class="num">469  class="num">469
[class="num">5,]  class="num">265 class="num">287  class="num">57  class="num">444
[class="num">6,]  class="num">393 class="num">227 class="num">393   class="num">57
[class="num">7,]   class="num">75 class="num">322 class="num">444  class="num">393
> env$testX1$repaired$bestNN
     half med mce both
[class="num">1,]  class="num">393 class="num">393 class="num">154  class="num">154
[class="num">2,]  class="num">415  class="num">92 class="num">205  class="num">205
[class="num">3,]  class="num">205 class="num">154 class="num">220  class="num">220
[class="num">4,]  class="num">462 class="num">190 class="num">393  class="num">393
[class="num">5,]  class="num">435 class="num">392 class="num">287  class="num">287
[class="num">6,]  class="num">392 class="num">220  class="num">90   class="num">90
[class="num">7,]  class="num">265 class="num">287 class="num">415  class="num">415
> env$testX1$removed$bestNN
     half med mce both
[class="num">1,]  class="num">283 class="num">130 class="num">283  class="num">283
[class="num">2,]  class="num">207 class="num">110 class="num">300  class="num">300
[class="num">3,]  class="num">308 class="num">308 class="num">110  class="num">110
[class="num">4,]  class="num">159 class="num">134 class="num">192  class="num">130

「近邻集成后的训练集预测均值」

把四个分组、四种类型下挑出的近邻模型做简单平均,能压住单模型过拟合的抖动。上面那段 R 风格脚本在 env 里循环 k=1..4,对每组 testX1[[k]]$bestNN 选出的列,把 predX1 的训练输入按行求和再除以 (2*nb+1),得到 TrainYpred 四列:Y.aver_half / med / mce / both。 跑完 str() 能看到矩阵维度是 1001 行 × 4 列,说明训练样本量固定在 1001;summary() 里 four 列均值分别在 0.5172 / 0.5010 / 0.5012 / 0.5012,中位数靠拢 0.53~0.55,但最小值出现 -0.22 到 -0.41,最大值到 1.18,说明平均后仍有越界,不能直接当概率。 别把正态当圣经 这些均值分布偏厚尾,外汇与贵金属行情里用同类近邻平均做信号,遇到跳空可能给出远超 [0,1] 的伪置信,实盘前务必在 MT5 用历史 tick 重算分母与边界。

MQL5 / C++
#--Averaging--train------------------------
evalq({
  k <- 1L
  class="kw">while (k <= class="num">4) {# group
    foreach(j = class="num">1:class="num">4, .combine = "cbind") %do% {# type
      bestNN <- testX1[[k]]$bestNN[ ,j]
      predX1[[k]]$pred$InputTrain[ ,bestNN] %>%
        apply(class="num">1, function(x) sum(x)) %>%
        divide_by((class="num">2*nb + class="num">1))
    } -> testX1[[k]]$TrainYpred
    dimnames(testX1[[k]]$TrainYpred) <- list(NULL, paste0("Y.aver_", type))
    k <- k + class="num">1
  }
}, env)
> env$testX1$repaired$TrainYpred %>% str()
num [class="num">1:class="num">1001, class="num">1:class="num">4] class="num">0.849 class="num">0.978 class="num">0.918 class="num">0.785 class="num">0.814 ...
- attr(*, "dimnames")=List of class="num">2
  ..$ : NULL
  ..$ : chr [class="num">1:class="num">4] "Y.aver_half" "Y.aver_med" "Y.aver_mce" "Y.aver_both"
> env$testX1$repaired$TrainYpred %>% summary()
  Y.aver_half       Y.aver_med       Y.aver_mce      Y.aver_both   
Min.   :-class="num">0.2202   Min.   :-class="num">0.4021   Min.   :-class="num">0.4106   Min.   :-class="num">0.4106  
1st Qu.: class="num">0.3348   1st Qu.: class="num">0.3530   1st Qu.: class="num">0.3512   1st Qu.: class="num">0.3512  
Median : class="num">0.5323   Median : class="num">0.5462   Median : class="num">0.5462   Median : class="num">0.5462  
Mean   : class="num">0.5172   Mean   : class="num">0.5010   Mean   : class="num">0.5012   Mean   : class="num">0.5012  
3rd Qu.: class="num">0.7227   3rd Qu.: class="num">0.7153   3rd Qu.: class="num">0.7111   3rd Qu.: class="num">0.7111  
Max.   : class="num">1.1874   Max.   : class="num">1.0813   Max.   : class="num">1.1039   Max.   : class="num">1.1039  
#--Averaging--test------------------------
evalq({
  k <- 1L

四种集成平均的预测分布与阈值提取

这段脚本在 env 环境里对 4 组数据(origin / repaired / removed / relabeling)分别做集成平均预测。外层 while 循环 k 从 1 跑到 4,内层 foreach 按 j=1:4 遍历 half、med、mce、both 四种类型,取出 bestNN 指定的神经网列,对 InputTest 或 InputTest1 每行求和后除以 (2*nb+1) 得到平均输出,写回 TestYpred / Test1Ypred。 从 repaired 组的 summary 看,Y.aver_half 均值 0.5114、中位数 0.5177,而 Y.aver_med 均值掉到 0.4976、最小值 -0.5055,说明不同平均方式在样本外预测的集中程度差异明显。外汇与贵金属信号若直接套用这类平均输出,须警惕分布偏移带来的高风险。 随后 th_aver 块保持 k 循环结构,对 TrainYpred 每列调用 GetThreshold,按 4 种阈值类型逐一计算并拼成矩阵。dimnames 把行命名为 type、列对齐 TrainYpred 列名,方便后续按组调取临界值做分类。 在 MT5 里验证时,可先把 nb 与 bestNN 的选取逻辑对齐,再比对 TestYpred 的分位区间;若 Med 类最小值低于 -0.5 而 Half 类最大值冲到 1.19 以上,大概率说明某组集成权重需要重算。

MQL5 / C++
class="kw">while (k <= class="num">4) {# group
    foreach(j = class="num">1:class="num">4, .combine = "cbind") %do% {# type
        bestNN <- testX1[[k]]$bestNN[ ,j]
        predX1[[k]]$pred$InputTest[ ,bestNN] %>%
            apply(class="num">1, function(x) sum(x)) %>%
            divide_by((class="num">2*nb + class="num">1))
    } -> testX1[[k]]$TestYpred
    dimnames(testX1[[k]]$TestYpred) <- list(NULL, paste0("Y.aver_", type))
    k <- k + class="num">1
}
evalq({
  k <- 1L
  class="kw">while (k <= class="num">4) {# group
    foreach(j = class="num">1:class="num">4, .combine = "cbind") %do% {# type
        bestNN <- testX1[[k]]$bestNN[ ,j]
        predX1[[k]]$pred$InputTest1[ ,bestNN] %>%
            apply(class="num">1, function(x) sum(x)) %>%
            divide_by((class="num">2*nb + class="num">1))
    } -> testX1[[k]]$Test1Ypred
    dimnames(testX1[[k]]$Test1Ypred) <- list(NULL, paste0("Y.aver_", type))
    k <- k + class="num">1
  }
}, env)
evalq({
  k <- 1L class="macro">#origin
  type <- qc(half, med, mce, both)
  Ytest = X1$train$y
  Ytest1 = X1$test$y
  Ytest2 = X1$test1$y
  class="kw">while (k <= class="num">4) { # group
    foreach(j = class="num">1:class="num">4, .combine = "cbind") %do% {# type subset
        foreach(i = class="num">1:class="num">4, .combine = "c") %do% {# type threshold
            GetThreshold(testX1[[k]]$TrainYpred[ ,j], Ytest, type[i])
        }
    } -> testX1[[k]]$th_aver
    dimnames(testX1[[k]]$th_aver) <- list(type, colnames(testX1[[k]]$TrainYpred))
    k <- k + class="num">1
  }
}, env)

◍ 四组样本在三类阈值下的 F1 打分循环

这段脚本把 origin / repaired / removed / relabeling 四组数据处理结果,分别跑一遍训练集、测试集、测试集1的 F1 评估。每组内部用 half、med、mce、both 四种子集方式(j=1:4),再对 4 个阈值档位(i=1:4)做二分类截断:预测概率大于对应 th_aver 就判 1,否则判 0。 截断后调用 Evaluate 算 F1,取均值并 round(3) 保留三位小数,最终 cbind 成 4×4 的 TrainScore / TestScore / Test1Score 矩阵。dimnames 把行名设为 type、列名沿用预测对象的列名,方便后续横向比对哪组样本、哪种阈值组合更稳。 从 env$testX1$origin$TrainScore 的打印骨架看,输出是 4 行(half/med/mce/both)对着 Y.aver_half、Y.aver_med、Y.aver_mce、Y.aver_both 四列。你在 MT5 外接 R 桥或本地回测框架里复刻时,重点核对 th_aver 是否随 k 分组独立计算——若四组共用同一阈值矩阵,F1 差异会被严重低估。外汇与贵金属品种波动结构不同,直接套用易放大过拟合风险。

MQL5 / C++
k <- 1L class="macro">#origin
class="macro">#k <- 2L class="macro">#repaired
class="macro">#k <- 3L class="macro">#removed
class="macro">#k <- 4L class="macro">#relabeling
type <- qc(half, med, mce, both)
class="kw">while (k <= class="num">4) { # group
  foreach(j = class="num">1:class="num">4, .combine = "cbind") %do% {# type subset
    foreach(i = class="num">1:class="num">4, .combine = "c") %do% {# type threshold
      ifelse(testX1[[k]]$TrainYpred[ ,j] > testX1[[k]]$th_aver[i,j], class="num">1, class="num">0) -> clAver
      Evaluate(actual = Ytest, predicted = clAver)$Metrics$F1 %>
        mean() %>% round(class="num">3)
    }
  }  -> testX1[[k]]$TrainScore
  dimnames(testX1[[k]]$TrainScore) <- list(type, colnames(testX1[[k]]$TrainYpred))
  k <- k + class="num">1
}
#---Metrics--test-------------------------------------
evalq({
  k <- 1L class="macro">#origin
  class="macro">#k <- 2L class="macro">#repaired
  class="macro">#k <- 3L class="macro">#removed
  class="macro">#k <- 4L class="macro">#relabeling
  type <- qc(half, med, mce, both)
  class="kw">while (k <= class="num">4) { # group
    foreach(j = class="num">1:class="num">4, .combine = "cbind") %do% {# type subset
      foreach(i = class="num">1:class="num">4, .combine = "c") %do% {# type threshold
        ifelse(testX1[[k]]$TestYpred[ ,j] > testX1[[k]]$th_aver[i,j], class="num">1, class="num">0) -> clAver
        Evaluate(actual = Ytest1, predicted = clAver)$Metrics$F1 %>
          mean() %>% round(class="num">3)
      }
    }  -> testX1[[k]]$TestScore
    dimnames(testX1[[k]]$TestScore) <- list(type, colnames(testX1[[k]]$TestYpred))
    k <- k + class="num">1
  }
}, env)
#---Metrics--test1-------------------------------------
evalq({
  k <- 1L class="macro">#origin
  class="macro">#k <- 2L class="macro">#repaired
  class="macro">#k <- 3L class="macro">#removed
  class="macro">#k <- 4L class="macro">#relabeling
  type <- qc(half, med, mce, both)
  class="kw">while (k <= class="num">4) { # group
    foreach(j = class="num">1:class="num">4, .combine = "cbind") %do% {# type subset
      foreach(i = class="num">1:class="num">4, .combine = "c") %do% {# type threshold
        ifelse(testX1[[k]]$Test1Ypred[ ,j] > testX1[[k]]$th_aver[i,j], class="num">1, class="num">0) -> clAver
        Evaluate(actual = Ytest2, predicted = clAver)$Metrics$F1 %>
          mean() %>% round(class="num">3)
      }
    }  -> testX1[[k]]$Test1Score
    dimnames(testX1[[k]]$Test1Score) <- list(type, colnames(testX1[[k]]$Test1Ypred))
    k <- k + class="num">1
  }
}, env)

「修复后样本集的分数抬升现象」

把原始训练集和修复后训练集的评分拉出来对比,能看出数据修补对模型泛化有直接影响。原始训练里 half 组四项得分在 0.707~0.712 之间徘徊,med 组最低掉到 0.707,整体偏弱。 切换到 repaired 训练集后,TestScore 出现明显跳升:half 组 Y.aver_half 从 0.711 升到 0.759,Y.aver_med 从 0.708 升到 0.761;med 组 Y.aver_half 也到了 0.754。这种跨组一致性抬升说明修补逻辑不是过拟合某一项。 不过外汇与贵金属行情具有高波动、高杠杆风险,样本修复带来的评分改善只代表回测环境内的概率倾向,实盘仍可能失效。建议把这套 repaired 流程接进 MT5 的回测脚本,用你自己的品种数据重跑一次确认。

异常样本清洗前后的评分对照

把测试集里被标错的样本做三种处理——修复(repaired)、删除(removed)、重标(relabeled),再跑同一套模型,能直接看出数据质量对分数的拉动。下面这组交叉表里,Y.aver_half / med / mce / both 代表四种特征聚合方式,行是样本处理分支。 修复分支下,med 行测试一阶段(Test1Score)拿到 0.738 / 0.748 / 0.744 / 0.744,比 mce 行的 0.697 / 0.720 / 0.677 / 0.677 整体高出约 4~7 个点,说明中位数聚合对错误标签更不敏感。 删除分支更有意思:TrainScore 各列都在 0.713~0.725 之间挤得很紧,但 TestScore 的 half 行冲到 0.761 / 0.769 / 0.761 / 0.751,是全部组合里的峰值;直接扔掉脏样本,泛化分数反而最好。 删除分支的 Test1Score 里 both 列在 med 行有 0.748,而 mce 行 only 0.685,差距拉到 6 个点以上。外汇与贵金属样本本身噪声大、跳空多,这种清洗实验在 MT5 导出的 tick 数据上建议自己复跑一遍,别直接信聚合后的平均数。

◍ 重标注后中值策略的得分跃升

上面这组对照来自对测试集 testX1 做 relabeled 重标注后的模型评分输出。原始 TestScore 里,half 分组在 Y.aver_med 列只有 0.715,而重标注后同一列冲到 0.748,med 分组自身也从 0.715 提到 0.748。 更关键的是 Test1Score:med 分组在 Y.aver_med 重标注后拿到 0.748,both 分组在 Y.aver_med 也到 0.737,均明显高于 half 分组的 0.498。说明用中值(med)做标签修正,比用 half 平均更贴合样本分布。 外汇与贵金属行情里这类重标注逻辑直接关联样本偏移风险,实盘使用前建议在 MT5 策略测试器用历史 tick 复跑一遍,确认 med 标签在你的品种周期上不会过拟合。

MQL5 / C++
> env$testX1$relabeled$TestScore
      Y.aver_half Y.aver_med Y.aver_mce Y.aver_both
half      class="num">0.672      class="num">0.559      class="num">0.529      class="num">0.529
med       class="num">0.715      class="num">0.715      class="num">0.711      class="num">0.711
mce       class="num">0.712      class="num">0.715      class="num">0.717      class="num">0.717
both      class="num">0.710      class="num">0.718      class="num">0.720      class="num">0.720
> env$testX1$relabeled$TestScore
      Y.aver_half Y.aver_med Y.aver_mce Y.aver_both
half      class="num">0.719      class="num">0.572      class="num">0.555      class="num">0.555
med       class="num">0.736      class="num">0.748      class="num">0.746      class="num">0.746
mce       class="num">0.739      class="num">0.747      class="num">0.745      class="num">0.745
both      class="num">0.710      class="num">0.756      class="num">0.754      class="num">0.754
> env$testX1$relabeled$Test1Score
      Y.aver_half Y.aver_med Y.aver_mce Y.aver_both
half      class="num">0.664      class="num">0.498      class="num">0.466      class="num">0.466
med       class="num">0.721      class="num">0.748      class="num">0.740      class="num">0.740
mce       class="num">0.739      class="num">0.732      class="num">0.716      class="num">0.716
both      class="num">0.734      class="num">0.737      class="num">0.735      class="num">0.735

「给四类去噪数据各自搜出神经网络最优超参」

前面的融合实验都基于拍脑袋定的超参数,没针对具体数据调过。神经网络和别的模型一样,超参数得按数据集重新搜,才有概率拿到更好的分类表现。这里把去噪后的数据拆成 origin、repaired、removed、relabeled 四组,每组都要单独做贝叶斯优化找精确最优解。 要优化的四个维度是:预测因子数量 numFeature 限 3~13;训练样本占比 r 映射 10%~100%(代码里写 1~10);隐藏层神经元 nh 限 1~51;激活函数类型 fact 取 1~10 对应 Fact 列表里的 10 种。适应度函数返回平均 F1 得分和融合类标签,阈值固定 0.5,用于修剪选优和连续预测平均,实测至少作为首次逼近挺稳。 SEED 这个随机种子不能忽视。我用相同数据和参数跑了两遍,SEED=12345 和 SEED=1235809 结果不同,后者得分明显更好,所以后续脚本默认用它。优化脚本起始随机初始化 20 个点、再迭代 20 次,跑完大概要等半小时(看机器性能),出来的得分降序排,取前三塞进 best.res 或 best.res1。 别把一次随机种子当真理 每次优化因 RNG 起点不同,超参和分数都会漂。多试几个 SEED,前十个结果拉开差距会比只看前三更明显,四组数据的优胜超参收齐后才能搭最终融合。

MQL5 / C++
##===OPTIM===============================
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
            "tribas", #: triangular basis
            "poslin", #: positive linear
            "purelin") #: linear
  bonds <- list(
    numFeature = c(3L, 13L),
    r = c(1L, 10L),
    nh = c(1L, 51L),
    fact = c(1L, 10L)
  )
}, env)
#---Fitnes -FUN-----------
evalq({
  n <- class="num">500
  numEns <- class="num">3
  # SEED <- c(class="num">12345, class="num">1235809)
  fitnes <- function(numFeature, r, nh, fact){
    bestF <- orderX %>% head(numFeature)
    k <- class="num">1
    rng <- RNGseq(n, SEED)
    #---train---
    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, bestF], y = Ytrain[idx],
               nhid = nh, actfun = Fact[fact])
    }
    #---predict---
    foreach(i = class="num">1:n, .packages = "elmNN", .combine = "cbind") %do% {
      predict(Ens[[i]], newdata = Xtest[ , bestF])
    } -> y.pr #[ ,n]
    #---best---
    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
    #---test-aver--------

贝叶斯优化挑出的集成参数组合

这段脚本把前面的 ELM 神经网络集成塞进了贝叶斯优化框架,目标函数 fitnes 返回 F1 分数,用 rBayesianOptimization 在 bonds 定义的参数空间里搜最优。四组数据(origin / repaired / removed / relabeled)各自跑一轮,init_points=20、n_iter=20、acq='ucb'、kappa=2.576,相当于每组做 40 次评估。 单轮未优化时,numFeature=10、r=7、nh=5、fact=2 在测试集上跑出 Score=0.741,系统耗时 5.99 秒(user 5.89 + system 0.00)。外汇与贵金属行情噪声大,这类集成模型仅代表样本内概率倾向,实盘仍属高风险。 优化后 origin 组历史排前三的回合里,最好一组成绩 Value=0.769,对应 numFeature=10、r=7、nh=20、fact=2;另两组落在 0.766(6/4/38/2 与 4/3/15/2)。nh 从 5 拉到 20 明显抬了分,说明隐藏节点数偏少是该集成的瓶颈。 想复现就先把 SEED 锁成 1235809,直接 evalq 里改 bonds 上下界和 n_iter,看自己品种样本上 F1 是否也卡在 0.74 附近。

MQL5 / C++
foreach(i = class="num">1:n, .packages = "elmNN", .combine = "+") %:%
  when(i %in% bestNN) %do% {
    predict(Ens[[i]], newdata = Xtest1[ , bestF])} %>%
  divide_by(length(bestNN)) -> ensPred
  ifelse(ensPred > class="num">0.5, class="num">1, class="num">0) -> ensPred
  Evaluate(actual = Ytest1, predicted = ensPred)$Metrics$F1 %>%
    mean() %>% round(class="num">3) -> Score
  class="kw">return(list(Score = Score, Pred = ensPred))
}, env)
evalq({
  Ytrain <- X1$pretrain$y
  Ytest <- X1$train$y
  Ytest1 <- X1$test$y
  Xtrain <- X1$pretrain$x
  Xtest <- X1$train$x
  Xtest1 <- X1$test$x
  orderX <- orderX1
  SEED <- class="num">1235809
  system.time(
    res <- fitnes(numFeature = class="num">10, r = class="num">7, nh = class="num">5, fact = class="num">2)
  )
}, env)
user  system elapsed
  class="num">5.89    class="num">0.00    class="num">5.99
env$res$Score
[class="num">1] class="num">0.741
#---Optim Ensemble-----
library(rBayesianOptimization)
evalq({
  Ytest <- X1$train$y
  Ytest1 <- X1$test$y
  Xtest <- X1$train$x
  Xtest1 <- X1$test$x
  orderX <- orderX1
  SEED <- class="num">1235809
  OPT_Res <- vector("list", class="num">4)
  foreach(i = class="num">1:class="num">4) %do% {
    Xtrain <- denoiseX1pretrain[[i]]$x
    Ytrain <- denoiseX1pretrain[[i]]$y
    BayesianOptimization(fitnes, bounds = bonds,
                          init_grid_dt = NULL, init_points = class="num">20,
                          n_iter = class="num">20, acq = "ucb", kappa = class="num">2.576,
                          eps = class="num">0.0, verbose = TRUE,
                          maxit = class="num">100, control = c(class="num">100, class="num">50, class="num">8))
  } -> OPT_Res1
  group <- qc(origin, repaired, removed, relabeled)
  names(OPT_Res1) <- group
}, env)
#---OptPar------
evalq({
  foreach(i = class="num">1:class="num">4) %do% {
    OPT_Res[[i]] %$% History %>% dp$arrange(desc(Value)) %>% head(class="num">3)
  } -> best.res
  names(best.res) <- group
}, env)
evalq({
  foreach(i = class="num">1:class="num">4) %do% {
     OPT_Res1[[i]] %$% History %>% dp$arrange(desc(Value)) %>% head(class="num">3)
  } -> best.res1
  names(best.res1) <- group
}, env)
env$best.res
# $origin
#    Round numFeature r nh fact Value
# class="num">1     class="num">39         class="num">10 class="num">7 class="num">20    class="num">2 class="num">0.769
# class="num">2     class="num">12          class="num">6 class="num">4 class="num">38    class="num">2 class="num">0.766
# class="num">3     class="num">38          class="num">4 class="num">3 class="num">15    class="num">2 class="num">0.766

◍ 特征筛选三轮迭代的回测读数

在 MT5 外接的 R 环境里跑特征约简,三轮交叉验证给出的折损曲线很平。第一轮保留 5 个特征时最优 Value 0.767,第二轮换 7 个特征掉到 0.766,第三轮 28 个特征仍卡在 0.766,说明堆特征对拟合提升倾向边际递减。 执行 $removed 剔除低贡献变量后,前三轮 Value 落在 0.764 / 0.764 / 0.763,比原始轮次仅低 0.003 以内,但特征数从最高 28 压到 1~19。外汇与贵金属样本下这种压缩大概率不掉显著预测力,但杠杆品种高风险仍在,别当保本依据。 $relabeled 重打标签的轮次读数最差,Value 从 0.746 滑到 0.738,提示标签噪声会直接吃掉约 0.03 的回测区分度。 直接看 env$best.res1:origin 组 19 与 32 号特征组合拿到 0.777,repaired 组 26 号配 9 特征拿 0.772。开 MT5 把这两个特征集接进 EA 输入数组,跑一遍你自己的品种,比对实时样本外 Value 再决定留哪组。

「重标注后特征维度与命中率的此消彼长」

上面这组输出是某轮特征工程在四种数据处理策略下的参数与评估对照:origin、repaired、removed、relabeled。每一行末尾的 0.760~0.777 是对应折交叉验证的 AUC 类指标,数值越靠近 1 代表区分能力越强,但外汇与贵金属样本存在过拟合倾向,实盘前需在 MT5 用 out-of-sample 数据复核。 重标注(relabeled)之后,numFeature 从 origin 的 8 跳到 12,r 从 3 升到 5,nh 反而从 41 压缩到 8,fact 由 2 变 9,验证值 0.777 为四组最高。说明标签重构后模型更依赖多特征组合,但邻近窗口 nh 收窄明显,对局部价格结构的敏感度可能提高。 对比 removed 组:numFeature=5、r=4、nh=17、fact=2,验证值 0.766,参数更精简却未显著掉点。做特征筛选时,可优先在 MT5 用 removed 配置跑回测,再用 relabeled 做对照,观察 EURUSD 或 XAUUSD 在 2023—2024 样本外的概率偏移。

MQL5 / C++
[CODE]
class="num">1    class="num">30          class="num">5 class="num">4 class="num">17   class="num">2 class="num">0.770
class="num">2     class="num">8          class="num">8 class="num">2 class="num">13   class="num">6 class="num">0.769
class="num">3    class="num">32          class="num">5 class="num">3 class="num">22   class="num">7 class="num">0.766
$relabeled
  Round numFeature r nh fact Value
class="num">1    class="num">34         class="num">12 class="num">5  class="num">8   class="num">9 class="num">0.777
class="num">2    class="num">33          class="num">9 class="num">5  class="num">4   class="num">9 class="num">0.763
class="num">3    class="num">36         class="num">12 class="num">7  class="num">4   class="num">9 class="num">0.760
#---best.param-------------------
evalq({
  foreach(i = class="num">1:class="num">4, .combine = "rbind") %do% {
    OPT_Res1[[i]]$Best_Par %>% unname()
  } -> best.par1
  dimnames(best.par1) <- list(group, qc(numFeature, r, nh, fact))
}, env)
> env$best.par1
             numFeature r nh fact
origin             class="num">8 class="num">3 class="num">41  class="num">2
repaired           class="num">9 class="num">4 class="num">17  class="num">3
removed            class="num">5 class="num">4 class="num">17  class="num">2
relabeled         class="num">12 class="num">5  class="num">8  class="num">9
[/CODE]

把阈值组合也扔进优化器里跑一遍

超参数优化对分类品质通常只是小幅拉升,真正影响更大的其实是修剪与平均阶段用的阈值类型组合。前面那轮优化一直锁死在「一半/一半」的恒定阈值搭配上,这种组合大概率不是最优解。 我们把两个阈值类型扩成待优化变量:th1 取值 1~2,控制挑选最佳神经网络时的修剪融合阈值;th2 取值 1~4,控制把融合平均预测转成类标签时的阈值。fitness 函数随之改了两处形参,best 模块按 th1 算阈值,test-average 模块按 th2 调 GetThreshold() 定阈。 单轮迭代耗时和之前差不多,跑完优化后每组数据挑出 10 组最佳超参数。结果上品质略有改善,但最佳参数组合和先前不考阈值组合时差异明显;靠重标号(repaired)和删噪声(removed)的数据组拿下了最高品质得分。外汇与贵金属信号建模属高风险,这类微改善不保证实盘胜率。 脚本落在 Optim_mVIII.R,核心参数边界如下,可直接抄去改自己的优化空间。

MQL5 / C++
##===OPTIM===============================
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
            "tribas", #: triangular basis
            "poslin", #: positive linear
            "purelin") #: linear
  bonds_m <- list(
    numFeature = c(3L, 13L),
    r = c(1L, 10L),
    nh = c(1L, 51L),
    fact = c(1L, 10L),
    th1 = c(1L, 2L),
    th2 = c(1L, 4L)
  )
}, env)
#---Fitnes -FUN-----------
evalq({
  n <- 500L
  numEns <- 3L
  # SEED <- c(class="num">12345, class="num">1235809)
  fitnes_m <- function(numFeature, r, nh, fact, th1, th2){
    bestF <- orderX %>% head(numFeature)
    k <- 1L
    rng <- RNGseq(n, SEED)
    #---train---
    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, bestF], y = Ytrain[idx],
               nhid = nh, actfun = Fact[fact])
    }
    #---predict---
    foreach(i = class="num">1:n, .packages = "elmNN", .combine = "cbind") %do% {
      predict(Ens[[i]], newdata = Xtest[ , bestF])
    } -> y.pr #[ ,n]
    #---best---
    ifelse(th1 == 1L, class="num">0.5, median(y.pr)) -> th
    foreach(i = class="num">1:n, .combine = "c") %do% {
      ifelse(y.pr[ ,i] > th, 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
    #---test-aver--------
    foreach(i = class="num">1:n, .packages = "elmNN", .combine = "+") %:%
      when(i %in% bestNN) %do% {
        predict(Ens[[i]], newdata = Xtest1[ , bestF])} %>%
      divide_by(length(bestNN)) -> ensPred

◍ 贝叶斯优化跑出来的集成参数实况

上面这段把集成模型的阈值函数和适应度评估封进 env 环境,先用固定参数 numFeature=10、r=7、nh=5、fact=2、th1=1、th2=4 跑了一遍 fitnes_m,system.time 显示用户态 6.13 秒、系统态 0.04 秒、墙上时间 6.32 秒,返回 Score 为 0.748。 接着用 rBayesianOptimization 做贝叶斯搜索,对四种数据清洗分支(origin / repaired / removed / relabeled)各跑 20 个初始点加 20 轮迭代,acq='ucb'、kappa=2.576。origin 分支历史 Top 行里,Round 19 与 Round 25 都摸到 Value=0.778,对应参数分别为 numFeature=8,r=3,nh=41,fact=2,th1=2,th2=4 和 numFeature=6,r=8,nh=51,fact=8,th1=2,th2=4。 把这组 0.778 和前面固定参数的 0.748 放一起看,阈值 th1 从 1 调到 2 后 F1 抬了 3 个点,说明原始阈值偏激进。外汇与贵金属行情噪声大、杠杆风险高,这类集成信号仅作概率参考,实盘前务必用 MT5 历史数据复算一遍。 可以直接把 Round 19 的参数抄进 fitnes_m 重跑,对比你本地 X1 分割下的 Score 是否落在 0.77 附近,若偏差大说明数据泄漏或随机种子不一致。

MQL5 / C++
th <- GetThreshold(ensPred, Yts$Ytest1, type[th2])
ifelse(ensPred > th, class="num">1, class="num">0) -> ensPred
Evaluate(actual = Ytest1, predicted = ensPred)$Metrics$F1 %>%
  mean() %>% round(class="num">3) -> Score
class="kw">return(list(Score = Score, Pred = ensPred))
}, env)
#---res fitnes-------
evalq({
  Ytrain <- X1$pretrain$y
  Ytest <- X1$train$y
  Ytest1 <- X1$test$y
  Xtrain <- X1$pretrain$x
  Xtest <- X1$train$x
  Xtest1 <- X1$test$x
  orderX <- orderX1
  SEED <- class="num">1235809
  th1 <- class="num">1
  th2 <- class="num">4
  system.time(
    res_m <- fitnes_m(numFeature = class="num">10, r = class="num">7, nh = class="num">5, fact = class="num">2, th1, th2)
  )
}, env)
  user  system elapsed
  class="num">6.13    class="num">0.04    class="num">6.32
> env$res_m$Score
[class="num">1] class="num">0.748
#---Optim Ensemble-----
library(rBayesianOptimization)
evalq({
  Ytest <- X1$train$y
  Ytest1 <- X1$test$y
  Xtest <- X1$train$x
  Xtest1 <- X1$test$x
  orderX <- orderX1
  SEED <- class="num">1235809
  OPT_Res1 <- vector("list", class="num">4)
  foreach(i = class="num">1:class="num">4) %do% {
    Xtrain <- denoiseX1pretrain[[i]]$x
    Ytrain <- denoiseX1pretrain[[i]]$y
    BayesianOptimization(fitnes_m, bounds = bonds_m,
                          init_grid_dt = NULL, init_points = class="num">20,
                          n_iter = class="num">20, acq = "ucb", kappa = class="num">2.576,
                          eps = class="num">0.0, verbose = TRUE,
                          maxit = class="num">100)
  } -> OPT_Res_m
  group <- qc(origin, repaired, removed, relabeled)
  names(OPT_Res_m) <- group
}, env)
#---OptPar------
evalq({
  foreach(i = class="num">1:class="num">4) %do% {
    OPT_Res_m[[i]] %$% History %>% dp$arrange(desc(Value)) %>% head(class="num">10)
  } -> best.res_m
  names(best.res_m) <- group
}, env)
$origin
   Round numFeature  r nh fact th1 th2 Value
class="num">1     class="num">19           class="num">8  class="num">3 class="num">41    class="num">2  class="num">2  class="num">4 class="num">0.778
class="num">2     class="num">25           class="num">6  class="num">8 class="num">51    class="num">8  class="num">2  class="num">4 class="num">0.778
class="num">3     class="num">39           class="num">9  class="num">1 class="num">22    class="num">1  class="num">2  class="num">4 class="num">0.777
class="num">4     class="num">32           class="num">8  class="num">1 class="num">21    class="num">2  class="num">2  class="num">4 class="num">0.772

「特征筛选后的模型稳定性变化」

上面两组表格是同一套价格行为特征在修复缺失值前后的排序输出。左侧未修复版本里,Round 5 的 numFeature=10、Value=0.769,Round 9 冲到 numFeature=40、Value=0.768,特征数翻倍但评分不增反降,说明噪声特征在拖累模型。 执行 $repaired 之后,Round 1 的 numFeature 压到 39、Value 升到 0.782,Round 2 只用 numFeature=2 就拿到 0.775。特征维度大幅收缩,评分却整体抬升,过拟合倾向可能由此减弱。 外汇与贵金属市场的高风险在于:这种数值改善只在历史样本上可验证,换到实时 tick 流里概率优势未必延续。开 MT5 把自家品种跑一遍同样的修复流程,重点看 Round 1 的 Value 是否也能从 0.769 区抬进 0.78 区。

特征重排后第7号因子的稳定性

上面两张表是同一组随机森林因子在两次轮询下的输出。第一次未命名小节里,Round 1 的 numFeature=40、r=7、nh=2、fact=39、th1=8、th2=1、Value=0.786,被高亮标出,说明该组合在首轮里重要性居前。 第二次重标号(relabeled)后,原本排第 1 的因子被换到了 numFeature=7 的位置,对应 r=8、nh=1、fact=13、th1=1、th2=2、Value=0.778,仍被高亮。Value 从 0.786 降到 0.778,降幅约 0.008,但高亮状态没变,说明这个因子对标签的解释力在重采样下相对稳。 对比两次完整前十行:首轮 Value 从 0.786 递减到 0.764,重排后只给了 Round 1 一行,无法看全序列。做 MT5 上的特征筛选时,建议把 relabel 前后的高亮行都打印出来,若某因子在 numFeature 乱序后 Value 波动小于 0.01,才倾向纳入最终模型。外汇与贵金属波动大,这类统计结论仅代表历史样本倾向,实盘高风险。

◍ 参数寻优后的分组表现与最佳组合

上面这张表是某轮网格寻优后,按拟合优度(末列 0.758~0.768)排出来的前十组结果。第一列是组号,后面依次对应特征数、r、nh、fact、th1、th2 等参数,最后一位是评估指标。可以看到第 2 组指标 0.768 最高,第 9、10 组已掉到 0.758,参数敏感度不低。 代码块里用 R 的 foreach 把四个子模型(origin / repaired / removed / relabeled)的最优参数行拼成矩阵 best.par_m。从注释里的打印结果看,origin 组用 8 个特征、r=3、nh=41、fact=2、th1=2、th2=4;而 relabeled 组 nh 骤降到 13,说明重标注后隐藏层需求大幅压缩。 在 MT5 里验证这类结论时,建议先把 origin 组的 8/3/41/2/2/4 直接塞进你的 EA 输入参数跑一遍样本外,再切到 relabeled 的 8/1/13/1/2/4 对比净值曲线。外汇与贵金属波动具有高风险,参数优不代表实盘概率占优,样本外衰减可能很快。

MQL5 / C++
#---best.param-------------------
evalq({
  foreach(i = class="num">1:class="num">4, .combine = "rbind") %do% {
    OPT_Res_m[[i]]$Best_Par %>% unname()
  } -> best.par_m
  dimnames(best.par_m) <- list(group, qc(numFeature, r, nh, fact, th1, th2))
}, env)
# > env$best.par_m------------------------
#             numFeature r nh fact th1 th2
# origin             class="num">8 class="num">3 class="num">41    class="num">2   class="num">2   class="num">4
# repaired           class="num">7 class="num">8 class="num">39    class="num">8   class="num">1   class="num">4
# removed            class="num">7 class="num">2 class="num">39    class="num">8   class="num">1   class="num">3
# relabeled          class="num">8 class="num">1 class="num">13    class="num">1   class="num">2   class="num">4

「用多数表决把多个融合叠成超级信号」

把优化跑出来的若干最佳融合直接级联,比单看一个融合更抗过拟合。思路很直白:每组数据取前 5 个最佳预测,标签 0 改成 -1,按行加总,大于 3 判为 1(买入倾向),小于 -3 判为 -1(卖出倾向),中间留 0 当作“不确定”。 回测里这套简单多数表决把分类品质明显抬了上去。removed 组 Balanced Accuracy 到 0.8227,repaired 组 0.8209;再把各组最佳轮次预测叠起来只留最大表决值,整体 Balanced Accuracy 冲到 0.8362。外汇和贵金属波动大、滑点跳空频繁,这类信号只代表概率倾向,实盘前必须用 MT5 历史数据重跑一遍。 第三类标签 0 怎么处理全看使用者。EA 可以避开市场等新信号,也可以原地挂观望单,但测试时必须把这种行为模型写进回测逻辑,否则度量会虚高。 换 InputTest1 不重新调参,直接套级联也大概率有正面效果。把 5.2 节的平均输出接进来,连续预测先转成 [-1,1] 再按 4 种平均阈值重标成 [-1,0,1],relabeled 方法在测试集上比 5.4 节原结果好出一截,级联融合让 Accuracy 提升约 5%–7%。 别把正态当圣经 投票阈值 3 和 -3 是经验切分,不是统计必然。样本分布偏移时,把切分点做成参数扫一遍再定。

MQL5 / C++
#--Index-best-------------------
evalq({
  prVot <- vector("list", class="num">4)
  foreach(i = class="num">1:class="num">4) %do% { class="macro">#group
    best.res_m[[i]]$Round %>% head(class="num">5) -> ind
    OPT_Res_m[[i]]$Pred  %>% dp$select(ind)  ->.;
    apply(., class="num">2, function(.) ifelse(. == class="num">0, -class="num">1, class="num">1)) ->.;
    apply(., class="num">1, function(x) sum(x)) ->.;
    ifelse(. > class="num">3, class="num">1, ifelse(. < -class="num">3, -class="num">1, class="num">0))
  } -> prVot
  names(prVot) <- group
}, env)
evalq({
  foreach(i = class="num">1:class="num">4) %do% { class="macro">#group
   Ytest1  ->.;
    ifelse(. == class="num">0, -class="num">1, class="num">1) ->.;
    cbind(actual = ., pred = prVot[[i]]) %>% as.data.frame() ->.;
    dp$filter(., pred != class="num">0) -> tabl
    Eval(tabl$actual, tabl$pred)
  } -> Score
  names(Score) <- group
}, env)
env$Score

修复项与剔除项的分类效能对照

对样本做两类处理后再跑二分类器:一类是做了缺失值修复(repaired),另一类是直接剔除了含缺失的样本(removed)。两类都按 -1 为正类来统计,方便看方向性偏差。 修复后的集合里,整体准确率 0.8201,95% 置信区间落在 (0.7798, 0.8558),Kappa 0.6358,说明模型一致性中等偏上。混淆矩阵显示实际 -1 共 191 例、预测命中 147 例,实际 1 共 226 例、命中 195 例;正类敏感度 0.8258、特异度 0.8159。 剔除样本的集合准确率略降到 0.8186,CI 为 (0.7789, 0.8539),No Information Rate 从 0.5731 升到 0.6。其混淆矩阵里实际 -1 的 196 例中预测对 145 例,实际 1 的 234 例中对 207 例,正类召回掉到 0.740,但负类召回升到 0.885。 两组 P-Value [Acc > NIR] 都远小于 2e-16,模型显著优于盲猜多数类。外汇与贵金属行情受杠杆与跳空影响,此类统计结论仅代表样本内倾向,实盘需以小资金验证。

◍ 重贴标签后的混淆矩阵对照

把原始标签翻成 -1 / 1 两类后,模型在 386 个样本上的整体准确率落在 0.8151,95% 置信区间(0.7741, 0.8515),明显高于 No Information Rate 的 0.528,P 值小于 2e-16,说明不是靠瞎猜基线类别混过去的。 重贴标签前的版本里,'Positive' 类被定为 -1,Sensitivity 0.8430、Specificity 0.8023,Balanced Accuracy 0.8227;重贴后两类更均衡,Prevalence 从 0.4000 变到 0.4720,负类(1)的 Recall 升到 0.828、F1 到 0.824,但 Balanced Accuracy 微降到 0.8148。 两次 McNemar 检验反差很实:重贴前 P=0.009208,两模型/两次判定差异显著;重贴后 P=0.9087,基本可视为同一判断水平。外汇与贵金属行情受杠杆与跳空影响,这类统计结论只代表历史样本区间,实盘概率可能偏移,开 MT5 用自己品种重跑一份混淆矩阵再信。

「多数投票阈值的盈亏边界」

把四个模型组的预测按列做符号化(0 映射为 -1,非 0 映射为 1),再对每行求和得到 prVotSum。统计分布里,投票和落在 -20 与 20 两端的样本数分别是 166 和 209,中间 0 值附近只有 4 个,说明模型意见高度极化时才有明确方向。 用 ifelse 把 prVotSum 截断:大于 18 判多、小于 -18 判空、其余判 0(观望)。过滤掉观望样本后,多空二分类的 Accuracy 为 0.835,Kappa 0.6674,No Information Rate 仅 0.5573,P-Value 小于 2e-16,说明该阈值规则显著优于瞎猜基线。 混淆矩阵里实际 -1 被预测 -1 的有 141 个、误判 37 个;实际 1 被预测 1 的有 172 个、误判 25 个。外汇与贵金属杠杆高,这套投票阈值在样本外可能衰减,开 MT5 接自己的 tick 数据回测前,先把 ±18 这个硬 cutoff 当成可调参数而非圣旨。

多数投票怎么在四组阈值上落地

这套分类器融合里,正类被标成 -1,检测覆盖率为 0.4747,平衡准确率读到 0.8362。也就是说样本里接近一半被模型捕捉到,而正负样本被误判的代价大致对称,不是偏科严重的单向识别。 代码用三层循环做投票:最外层 k 从 1 走到 4 代表四个数据分组;中间 j 遍历 half、med、mce、both 四种平均方式;最里层 i 同样走四种阈值。每个基模型预测值超过对应阈值记 +1,否则记 -1,四个 i 累加后若大于 2 判为 1,小于 -2 判为 -1,夹在中间判 0,相当于要过半数才下定论。 训练、测试、测试1三块逻辑完全一致,只把 TrainYpred / TestYpred / Test1Ypred 换了数据源,结果分别塞进 Train.clVoting、Test.clVoting、Test1.clVoting。外汇与贵金属行情受流动性突变影响大,这种投票框架在实盘只是概率倾斜,开 MT5 接历史 tick 回测前先确认你的阈值矩阵 th_aver 维度是 4×4,否则 cbind 会直接报错。

MQL5 / C++
#---train-------------------------------------
evalq({
  k <- 1L class="macro">#origin
  type <- qc(half, med, mce, both)
  VotAver <- vector("list", class="num">4)
  names(VotAver) <- group
  class="kw">while (k <= class="num">4) { # group
    foreach(j = class="num">1:class="num">4, .combine = "cbind") %do% {# type aver
      foreach(i = class="num">1:class="num">4, .combine = "+") %do% {# type threshold
        ifelse(testX1[[k]]$TrainYpred[ ,j] > testX1[[k]]$th_aver[i,j], class="num">1, -class="num">1)
      } ->.;
      ifelse(. > class="num">2, class="num">1, ifelse(. < -class="num">2, -class="num">1, class="num">0))  
    }  -> VotAver[[k]]$Train.clVoting
    dimnames(VotAver[[k]]$Train.clVoting) <- list(NULL, type)
    k <- k + class="num">1
  }
}, env)
#---test------------------------------
evalq({
  k <- 1L class="macro">#origin
  type <- qc(half, med, mce, both)
  class="kw">while (k <= class="num">4) { # group
    foreach(j = class="num">1:class="num">4, .combine = "cbind") %do% {# type aver
      foreach(i = class="num">1:class="num">4, .combine = "+") %do% {# type threshold
        ifelse(testX1[[k]]$TestYpred[ ,j] > testX1[[k]]$th_aver[i,j], class="num">1, -class="num">1)
      } ->.;
      ifelse(. > class="num">2, class="num">1, ifelse(. < -class="num">2, -class="num">1, class="num">0))  
    }  -> VotAver[[k]]$Test.clVoting
    dimnames(VotAver[[k]]$Test.clVoting) <- list(NULL, type)
    k <- k + class="num">1
  }
}, env)
#---test1-------------------------------
evalq({
  k <- 1L class="macro">#origin
  type <- qc(half, med, mce, both)
  class="kw">while (k <= class="num">4) { # group
    foreach(j = class="num">1:class="num">4, .combine = "cbind") %do% {# type aver
      foreach(i = class="num">1:class="num">4, .combine = "+") %do% {# type threshold
        ifelse(testX1[[k]]$Test1Ypred[ ,j] > testX1[[k]]$th_aver[i,j], class="num">1, -class="num">1)
      } ->.;
      ifelse(. > class="num">2, class="num">1, ifelse(. < -class="num">2, -class="num">1, class="num">0))  
    }  -> VotAver[[k]]$Test1.clVoting
    dimnames(VotAver[[k]]$Test1.clVoting) <- list(NULL, type)
    k <- k + class="num">1
  }
}, env)
#---Metrics--train-------------------------------------
evalq({
  k <- 1L class="macro">#origin
  type <- qc(half, med, mce, both)
  class="kw">while (k <= class="num">4) { # group
    foreach(i = class="num">1:class="num">4) %do% {# type threshold
      Ytest ->.;

◍ 投票集成在训练与测试集上的 F1 对照

这段脚本把四类样本处理组(origin / repaired / removed / relabeled)分别跑训练集、测试集与测试集二阶段的投票集成打分,核心指标是过滤掉预测为 0 的样本后,按 actual 与 pred 算出的 F1 均值并四舍五入到三位小数。 训练集结果里,removed 组在 med、mce、both 三种阈值下都拿到 0.755,是四个组里的最高值;relabeled 组的 half 阈值只有 0.717,明显偏低。测试集部分用 Ytest1 做 actual 映射,逻辑与训练集一致,只是数据源换成 VotAver[[k]]$Test.clVoting。 测试集二阶段(Ytest2 / Test1.clVoting)再重复一遍同样流程,最终把 4 个组的 TrainScoreVot 用 foreach 的 rbind 合并成矩阵,行名按 group、列名按 type(half/med/mce/both)排好。 开 R 环境把这段 evalq 块跑一遍,重点看 removed 组在 both 阈值下训练 0.755、测试集是否同步占优,能直接判断哪类标签清洗方式更扛过拟合。外汇与贵金属行情序列做这类标记清洗时波动更剧烈,过拟合风险偏高,结论仅作概率性参考。

MQL5 / C++
ifelse(. == class="num">0, -class="num">1, class="num">1) ->.;
cbind(actual = ., pred = VotAver[[k]]$Train.clVoting[ ,i]) %>%
  as.data.frame() ->.;
  dp$filter(., pred != class="num">0) -> tbl
Evaluate(actual = tbl$actual, predicted = tbl$pred)$Metrics$F1 %>%
  mean() %>% round(class="num">3)
class="macro">#Eval(tbl$actual,tbl$pred)
} -> VotAver[[k]]$TrainScoreVot
names(VotAver[[k]]$TrainScoreVot) <- type
k <- k + class="num">1
}
}, env)
#---Metrics--test-------------------------------------
evalq({
  k <- 1L class="macro">#origin
  type <- qc(half, med, mce, both)
  class="kw">while (k <= class="num">4) { # group
    foreach(i = class="num">1:class="num">4) %do% {# type threshold
      Ytest1 ->.;
      ifelse(. == class="num">0, -class="num">1, class="num">1) ->.;
      cbind(actual = ., pred = VotAver[[k]]$Test.clVoting[ ,i]) %>%
        as.data.frame() ->.;
      dp$filter(., pred != class="num">0) -> tbl
      Evaluate(actual = tbl$actual, predicted = tbl$pred)$Metrics$F1 %>%
        mean() %>% round(class="num">3)
      class="macro">#Eval(tbl$actual,tbl$pred)
    } -> VotAver[[k]]$TestScoreVot
    names(VotAver[[k]]$TestScoreVot) <- type
    k <- k + class="num">1
  }
}, env)
#---Metrics--test1-------------------------------------
evalq({
  k <- 1L class="macro">#origin
  type <- qc(half, med, mce, both)
  class="kw">while (k <= class="num">4) { # group
    foreach(i = class="num">1:class="num">4) %do% {# type threshold
      Ytest2 ->.;
      ifelse(. == class="num">0, -class="num">1, class="num">1) ->.;
      cbind(actual = ., pred = VotAver[[k]]$Test1.clVoting[ ,i]) %>%
        as.data.frame() ->.;
      dp$filter(., pred != class="num">0) -> tbl
      Evaluate(actual = tbl$actual, predicted = tbl$pred)$Metrics$F1 %>%
        mean() %>% round(class="num">3)
      class="macro">#Eval(tbl$actual,tbl$pred)
    } -> VotAver[[k]]$Test1ScoreVot
    names(VotAver[[k]]$Test1ScoreVot) <- type
    k <- k + class="num">1
  }
}, env)
#----TrainScoreVot-------------------
evalq({
  foreach(k = class="num">1:class="num">4, .combine = "rbind") %do% {   # group
    VotAver[[k]]$TrainScoreVot %>% unlist() %>% unname()
  } -> TrainScoreVot
  dimnames(TrainScoreVot) <- list(group, type)
}, env)

「投票聚合后的交叉验证分数对比」

把四个模型组的投票结果按 half / med / mce / both 四种集成方式汇总,得到 TestScoreVot 矩阵:removed 组在 half 列拿到 0.801、both 列 0.809,relabeled 组 both 列冲到 0.816,均高于 origin 组的 0.774~0.804 区间。 换到 Test1ScoreVot(另一测试切片)时,removed 组 half 列 0.759 仍居前,但 relabeled 组 both 列掉到 0.713,说明标签重标策略在分布偏移时可能失效,外汇与贵金属行情切换期尤其要警惕这种回测漂移。 Variant-2 改用求和阈值法:把投票矩阵按行加总,大于 3 判多、小于 -3 判空、其余判 0,再过滤掉 pred=0 的样本算 F1。Train 与 Test 分别走 while(k<=4) 循环写回 VotAver[[k]]$*ScoreVotSum,这种硬阈值比比例投票更挑食,样本稀疏时容易把信号滤光。

MQL5 / C++
evalq({
  foreach(k = class="num">1:class="num">4, .combine = "rbind") %do% {  # group
    VotAver[[k]]$Test1ScoreVot %>% unlist() %>% unname()
  } -> Test1ScoreVot
  dimnames(Test1ScoreVot) <- list(group, type)
}, env)

#==Variant-class="num">2==
#--TrainScoreVotSum---
evalq({
  k <- 1L
  class="kw">while(k <= class="num">4){ # group
    VotAver[[k]]$Train.clVoting ->.;
    apply(., class="num">1, function(x) sum(x)) ->.;
    ifelse(. > class="num">3, class="num">1, ifelse(. < -class="num">3, -class="num">1, class="num">0)) -> VotAver[[k]]$Train.clVotingSum
    ifelse(Ytest == class="num">0, -class="num">1, class="num">1) ->.;
    cbind(actual = ., pred = VotAver[[k]]$Train.clVotingSum) ->.;
    as.data.frame(.) ->.;
    dp$filter(., pred != class="num">0) ->.;
    Evaluate(actual = .$actual, predicted = .$pred)$Metrics$F1 ->.;
    mean(.) %>% round(class="num">3) -> VotAver[[k]]$TrainScoreVotSum
    k <- k + class="num">1
  }
}, env)

投票集成下四类样本处理的F1对比

上面这段 R 风格脚本把四个分组(origin / repaired / removed / relabeled)在投票集成后的训练、测试与测试1集 F1 汇总到一张表。核心逻辑是先按阈值 3 与 -3 把连续投票和转成 -1/0/1 类别信号,再过滤掉预测为 0 的观望样本,只用非零投票计算 F1。 从 env$ScoreVotSum 的输出看,relabeled 组在 TestScoreVotSum 上拿到 0.825,明显高于 origin 的 0.807、repaired 的 0.802 和 removed 的 0.810;但在 Test1ScoreVotSum 上反而跌到 0.711,低于其余三组 0.748~0.765 的区间。说明重标注策略可能提升了主测试集区分度,却让另一验证集泛化倾向走弱。 外汇与贵金属行情里做这类集成信号,样本重标注带来的过拟合概率不可忽视,实盘前应在 MT5 用历史 tick 复算该投票阈值,而非直接信任离线 0.825 这类数字。

MQL5 / C++
ifelse(. > class="num">3, class="num">1, ifelse(. < -class="num">3, -class="num">1, class="num">0)) -> VotAver[[k]]$Test1.clVotingSum
ifelse(Ytest2 == class="num">0, -class="num">1, class="num">1) ->.;
cbind(actual = ., pred = VotAver[[k]]$Test1.clVotingSum) ->.;
as.data.frame(.) ->.;
dp$filter(., pred != class="num">0) ->.;
Evaluate(actual = .$actual, predicted = .$pred)$Metrics$F1 ->.;
mean(.) %>% round(class="num">3) -> VotAver[[k]]$Test1ScoreVotSum
class="macro">#Eval(tbl$actual,tbl$pred)
k <- k + class="num">1
}
}, env)
evalq({
 foreach(k = class="num">1:class="num">4, .combine = "c") %do% {  # group
  VotAver[[k]]$TrainScoreVotSum %>% unlist() %>% unname()
 } -> TrainScoreVotSum

 foreach(k = class="num">1:class="num">4, .combine = "c") %do% {  # group
  VotAver[[k]]$TestScoreVotSum %>% unlist() %>% unname()
 } -> TestScoreVotSum

 foreach(k = class="num">1:class="num">4, .combine = "c") %do% {  # group
  VotAver[[k]]$Test1ScoreVotSum %>% unlist() %>% unname()
 } -> Test1ScoreVotSum

 ScoreVotSum <- cbind(TrainScoreVotSum, TestScoreVotSum, Test1ScoreVotSum)
 dimnames(ScoreVotSum ) <- list(group, qc(TrainScoreVotSum, TestScoreVotSum,
Test1ScoreVotSum))
}, env)
> env$ScoreVotSum
              TrainScoreVotSum TestScoreVotSum Test1ScoreVotSum
origin               class="num">0.763           class="num">0.807           class="num">0.762
repaired             class="num">0.752           class="num">0.802           class="num">0.748
removed              class="num">0.761           class="num">0.810           class="num">0.765
relabeled            class="num">0.766           class="num">0.825 (!!)       class="num">0.711

◍ 去噪策略下融合模型的度量对比

针对 pretrain 子集里的噪声样本,实验用了三种处理思路:把标错的样本重新分配但不改类(repaired)、直接从子集删掉噪声样本(removed)、把噪声单独隔离成一类(relabeled)。加上原始组,共得到 origin / repaired / removed / relabeled 四组数据,各自训练由 500 个 ELM 分类器组成的融合,再对 Х1 的 train/test/test1 三个子集做连续预测。 在 InputTrain 上给每个融合里的 500 个网络算 4 类阈值,把连续预测转成 0/1 类标签后统计 mean(F1)。可视化分布显示:第一,测试子集的度量普遍高于 InputTrainS;第二,repaired 和 removed 组的度量直观上优于另两组。 挑出每组融合里 mean(F1) 最高的 7 个网络做平均连续预测,基于 TrainYpred 算阈值 th_aver。不同组的修剪与平均阈值组合,度量落在 0.75–0.77,其中 removed 组(删噪声)最好。 优化超参数后,各组度量稳定到 0.77+;再加后期处理的修剪平均阈值,已处理噪声组稳定约 0.78+。用最佳超参做超级融合、按数据组简单多数表决,repaired 和 removed 组冲到 0.82+;级联组合超级融合预测,最终度量 0.836,相当于品质提升 6–7%。外汇与贵金属相关的信号建模属高风险,回测度量不代表实盘胜率。 在早前融合平均预测上重验:removed 组 Test 子集得 0.8+,级联组合后所有组 Test 子集均 0.8+。简单投票级联组合预测,确实可能抬高分类品质,开 MT5 接 ELM 融合时可先删噪声组试水。

「把多组融合级联起来才最稳」

前面几节跑完的实验里,单看 repaired 和 removed 两种噪声样本处理,分类品质就有明显抬升;修剪与平均阈值类型及其组合,也会显著改变输出分布。这些都属于单点优化,幅度可观但还没到天花板。 真正把分类品质顶到最高的,是把若干组融合再拼成一个超级融合,用简单多数表决把各自的预测级联起来。这个做法在实测里给出的提升幅度最大,明显优于只调神经网络超参数或后期处理那类微调。 神经网络超参与后期处理的优化,对得分只是轻微贡献。更务实的做法是:新数据进来先跑一轮初次优化,之后发现品质往下掉再定期重做,周期长短靠实验自己标定,别预设固定频率。外汇与贵金属信号若套这套级联思路,需先认清样本失配对实盘的高风险。

把这条线请下神坛

上面列出的 PartVIII 压缩包只有 23.24 KB,里面 15 个 R 脚本各管一摊:从 Importar.R 导入、FunStacking_VIII.R 做堆叠融合,到 Threshold.R 定阈值、Voting.R 用多数表决把超级融合级联起来。想复现的人直接下 ZIP 进 RStudio 按脚本名顺序跑,别指望复制粘贴就能出信号。 代码里 OPT_Res[[i]]$History 和 Yts$Ytest1 正是评论区 rashmikeyur 报错找不到的对象——它们由 Prepare_VIII.R 和 FunPrepareData_VII.R 在前序阶段生成,漏跑任一脚本就会报「未找到对象」。外汇与贵金属行情高波动,这类神经网络融合只是概率工具,实盘前务必在 MT5 用历史数据核对每一步数据结构。 论坛里有人拿七个最佳集合的平均连续预测去接 Keras,也有人想绑进 EA 测不同数据集;这些玩法可行,但融合输出从 VotAver[[k]]Test.clVoting[501,j] 这种 501 行结构里取,和 MT5 的报价流不是同一时态。把它当研究素材而非圣杯,自己调一次超参数再来谈胜率。

MQL5 / C++
testX1[[k]]$TrainYpred[ ,j]
VotAver[[k]]Train.clVoting[class="num">1001,j]
VotAver[[k]]Test.clVoting[class="num">501,j]
VotAver[[k]]Test1.clVoting[class="num">251,j]
#---OptPar------
evalq({
  foreach(i = class="num">1:class="num">4) %do% {
    OPT_Res[[i]] %$% History %>% dp$arrange(desc(Value)) %>% head(class="num">3)
  } -> best.res
  names(best.res) <- group
}, env)
evalq({
  foreach(i = class="num">1:class="num">4) %do% {
    OPT_Res1[[i]] %$% History %>% dp$arrange(desc(Value)) %>% head(class="num">3)
  } -> best.res1
  names(best.res1) <- group
}, env)
#---test-aver--------
    foreach(i = class="num">1:n, .packages = "elmNN", .combine = "+") %:%
      when(i %in% bestNN) %do% {
        predict(Ens[[i]], newdata = Xtest1[ , bestF])} %>%
      divide_by(length(bestNN)) -> ensPred
      th <- GetThreshold(ensPred, Yts$Ytest1, type[th2])
      ifelse(ensPred > th, class="num">1, class="num">0) -> ensPred
      Evaluate(actual = Ytest1, predicted = ensPred)$Metrics$F1 %>%
        mean() %>% round(class="num">3) -> Score
把融合诊断交给小布盯盘
这些关于噪声样本占比、阈值切分效果的复盘维度,小布盯盘的 AIGC 已内置,打开对应品种页即可看到连续预测的分布,你只管判断超级融合是否过拟合。

常见问题

文中用 R 环境对 outliers 做参数定义后估算,常用稳健统计或邻近插补;实盘建议先跑小样本看分类度量变化再定。
可以,把连续预测列粘贴进品种页的 AIGC 诊断框,小布会按你给的候选阈值画出类标签转换后的混淆矩阵倾向。
实验显示概率上更稳,但取决于各子融合相关性;若超参高度雷同,表决增益可能趋近于零。
建议网格搜素配合测试子集度量,优先保住 ROC 下面积,再微调修剪比例,避免贵金属跳空时段漏信号。
R 对融合度量和统计检验支持更完整,MT5 只负责报价与特征提取,分工跑更省终端资源。