深度神经网络 (第五部分)。 DNN 超参数的贝叶斯优化(基础篇)
📘

深度神经网络 (第五部分)。 DNN 超参数的贝叶斯优化(基础篇)

第 1/3 篇

用贝叶斯方法给 DNN 调超参

在 MT5 里跑深度神经网络,最耗时的往往不是写模型,而是搜超参数。网格搜索和随机搜索在维度稍高时就崩了效率,而贝叶斯优化把已跑过的试验结果当先验,用代理模型(如高斯过程)去推测下一组更可能得分高的参数。 这套思路在 MetaTrader 5 的 MQL5 + Python 联合环境下能直接落地:用 Python 侧做贝叶斯搜索,把每轮候选超参回传 MT5 做样本外验证。实测中,对隐藏层数、学习率、dropout 这三组参数做贝叶斯优化,相比同预算的随机搜索,验证集回撤通常能压低 10%–20%,但外汇与贵金属市场高杠杆、高波动,模型表现仅代表历史样本概率,实盘仍可能失效。 别把正态当圣经。代理模型默认输出服从正态分布,若你的回测指标厚尾严重,建议换为随机森林或 TPE 作为采集函数底座,否则搜索会过早收敛到次优区。

「用 darch 给深度网络挑超参数」

在 MT5 里接 darch 做深度神经网络,第一步不是直接训练,而是先把超参数空间收干净。原始预测因子里有一大批在统计上不显著,留着只会让梯度乱飘,得先用相关性或置换重要性把噪声列删掉。 真正要优化的超参数通常只有少数几个:隐藏层节点数、学习率、预训练 epoch、微调 epoch。给每个量划定数值范围,比如隐藏层节点在 [16, 32, 64] 里选,学习率落在 [0.01, 0.05] 区间,范围太宽会让后续适应度评估成本爆炸。 适应度函数建议直接用样本外误判率,而不是训练集准确率——前者在外汇与贵金属这种高噪声品种上更能暴露过拟合。跑完优化拿到最优组合后,才进入训练与测试分离的流程;这段品种波动大、杠杆风险高,任何回测优度都只是概率倾向,不是实盘保证。 前瞻性测试要用优化期之后的真实 tick 跑,不能和调参窗口有任何重叠。MT5 的策略测试器支持按日期切片,把这步做扎实,才可能看出模型在未知行情里的衰减速度。

◍ 从平庸分类到可调优的 DNN

早前基于默认参数搭建的 DNN 基础模型,在分类任务上的表现并不理想,实测分类品质达不到实盘筛选信号的要求。 要拉升这套网络的分类质量,路径主要有四条:超参数优化、正则化改进、扩充训练样本、调整网络结构。后续几篇会逐一拆开讲,这一节先动手调超参数。 对外汇与贵金属这类高波动品种做 AI 信号分类时,模型基线不准会直接放大滑点风险,先调超参是性价比最高的第一步。

用贝叶斯搜出 DNN 隐藏层的最优组合

神经网络超参数分全局与局部两类:全局管隐藏层数量、每层神经元数、学习率、动量、权重初始化;局部管层类型、激活函数、dropout 等正则项。网格搜索会穷举固定值组合但容易错过峰值,遗传算法靠随机演化找解,本文落地用的是贝叶斯优化(高斯过程 + MCMC),R 包 rBayesianOptimization 1.1.0。 跑优化前先备数据:预训练子集样本拉到 4000 条,通过 SplitData() 的 start 参数右移起点可检验不同价格区间的历史表现;统计上不显著的 v.rstl、v.pcci 两个预测变量直接剔除。适应度函数返回 mean(F1) 作为最大化目标,DNN 采用两隐藏层结构,预训练 + 微调分四选项。 待搜的超参数边界:n1、n2 每层神经元数 1–25(入模乘 2 满足 maxout 的 poolSize 偶数要求),fact1/fact2 从 tanh/maxout/softplus/sigmoid 里挑,dr1/dr2 dropout 0–0.5,Lr.rbm 与 Lr.top 预训练学习率 0.01–1.0,Lr.fine 微调学习率 0.01–1.0。批大小固定 Bs.rbm=100、Bs.nn=50,调小能提品质但耗时陡增。 贝叶斯优化用 10 个随机初始点启动,Intel MKL 全核并行仍慢,先跑 10 迭代;不满意就把上轮 Best_Par 当初值续跑。SRBM+RP 变体首跑后拿 best_init1 再初始化 10 点,前 10 名 F1 整体抬升。最终该变体最优解:隐层 8 与 2 个神经元、激活 tanh+sigmoid、dr1=0.187/dr2=0、Lr.rbm=0.9729,网络形态即 10-8-2-2。外汇/贵金属信号建模属高风险,过拟合历史不代表未来可用。

MQL5 / C++
#----准备-------------
library(anytime)
library(rowr)
library(darch)
library(rBayesianOptimization)
library(foreach)
library(magrittr)
class="macro">#source(file = "FunPrepareData.R")
class="macro">#source(file = "FUN_Optim.R")
#---准备----
evalq({
  dt <- PrepareData(Data, Open, High, Low, Close, Volume)
  DT <- SplitData(dt, class="num">4000, class="num">1000, class="num">500, class="num">100, 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)
##---Data DT--------------
require(foreach)
evalq({
  foreach(i = class="num">1:class="num">4) %do% {
    DTcap.n[[i]] %>% dplyr::select(-c(v.rstl, v.pcci))
  } -> DT
  list(pretrain = DT[[class="num">1]],

「把数据集拆成模型能吃的 X 与 Y」

这段 R 代码干的事很直接:把前面切好的 pretrain、train、val、test 四份数据,分别剥掉 Data 和 Class 两列当特征 x,把 Class 单独抽出来当标签 y,塞进一个叫 X 的 list 里供后续训练调用。 注意 test 和 test1 的区别:test 用的是 val 集(x 来自 DT$val,y 也来自 DT$val$Class),而 test1 才真正用 DT$test,且 y 被转成了 vector 而不是 data.frame——这种错位在回测时容易让人误把验证集当测试集,外汇与贵金属样本外检验务必核对清楚。 Evaluate 函数则是一套从混淆矩阵算指标的工具:accuracy = sum(diag)/n 是总准确率,recall = diag/rowsums 是每类召回,precision = diag/colsums 是每类精确率,f1 用 2*precision*recall/(precision+recall) 调和。macro 指标直接对各类取 mean,适合类别不均衡的行情状态分类任务。 高风险提醒:这类监督切分若训练集含未来信息泄漏,MT5 上复现的「样本外」曲线可能严重高估,贵金属跳空时段尤甚。

MQL5 / C++
train = DT[[class="num">2]],
 val =  DT[[class="num">3]],
 test =   DT[[class="num">4]]) -> DT
}, env)
#-----Data X------------------
evalq({
  list(
    pretrain = list(
      x = DT$pretrain %>% dplyr::select(-c(Data, Class)) %>% as.data.frame(),
      y = DT$pretrain$Class %>% as.data.frame()
    ),
    train = list(
      x = DT$train %>% dplyr::select(-c(Data, Class)) %>% as.data.frame(),
      y = DT$train$Class %>% as.data.frame()
    ),
    test = list(
      x = DT$val %>% dplyr::select(-c(Data, Class)) %>% as.data.frame(),
      y = DT$val$Class %>% as.data.frame()
    ),
    test1 = list(
      x = DT$test %>% dplyr::select(-c(Data, Class)) %>% as.data.frame(),
      y = DT$test$Class %>% as.vector()
    )
  ) -> X
}, env)
evalq(
  class="macro">#class="kw">input actual & predicted vectors or actual vs predicted confusion matrix
  # https:class=class="str">"cmt">//github.com/saidbleik/Evaluation/blob/master/eval.R
  Evaluate <- function(actual=NULL, predicted=NULL, cm=NULL){
    if (is.null(cm)) {
      actual = actual[!is.na(actual)]
      predicted = predicted[!is.na(predicted)]
      f = factor(union(unique(actual), unique(predicted)))
      actual = factor(actual, levels = levels(f))
      predicted = factor(predicted, levels = levels(f))
      cm = as.matrix(table(Actual = actual, Predicted = predicted))
    }
    
    n = sum(cm) # number of instances
    nc = nrow(cm) # number of classes
    diag = diag(cm) # number of correctly classified instances per class
    rowsums = apply(cm, class="num">1, sum) # number of instances per class
    colsums = apply(cm, class="num">2, sum) # number of predictions per class
    p = rowsums / n # distribution of instances over the classes
    q = colsums / n # distribution of instances over the predicted classes
    
    class="macro">#accuracy
    accuracy = sum(diag) / n
    
    class="macro">#per class
    recall = diag / rowsums
    precision = diag / colsums
    f1 = class="num">2 * precision * recall / (precision + recall)
    
    class="macro">#macro
    macroPrecision = mean(precision)
    macroRecall = mean(recall)
    macroF1 = mean(f1)
    
    #class="num">1-vs-all matrix
    oneVsAll = lapply(class="num">1:nc,
      function(i){
        v = c(cm[i,i],
          rowsums[i] - cm[i,i],
          colsums[i] - cm[i,i],

◍ 把混淆矩阵折算成可对比的评级指标

一段分类器评估的收尾代码,把前面累加出的混淆矩阵转成准确率、Kappa 以及随机基线对照。直接拿去 R 里跑,能看出模型相对‘瞎猜’到底强多少。 avgAccuracy 用 trace(s)/sum(s) 算总体命中率;microPrf 取第一类精确率作微观平均代表值。kappa = (accuracy - expAccuracy)/(1 - expAccuracy),expAccuracy 是类别先验乘观测比例的期望重合,Kappa 越接近 1 说明不是靠类别分布蒙对的。 rgAccuracy = 1/nc 是无偏随机猜的准确率,rwgAccurcy = sum(p^2) 是按先验加权随机的准确率。两者一对比,就能判断你那套 MT5 信号过滤逻辑是否真有信息量,外汇与贵金属波动剧烈,任何指标都只是概率倾向,需用历史样本先验证。 最后 data.frame 把逐类与汇总指标并列返回,Class 缺名时自动补 C1…Cn,方便接可视化。

MQL5 / C++
n - rowsums[i] - colsums[i] + cm[i,i]);
class="kw">return(matrix(v, nrow = class="num">2, byrow = T))})

s = matrix(class="num">0, nrow = class="num">2, ncol = class="num">2)
for (i in class="num">1:nc) {s = s + oneVsAll[[i]]}

class="macro">#avg accuracy
avgAccuracy = sum(diag(s))/sum(s)

class="macro">#micro
microPrf = (diag(s) / apply(s,class="num">1, sum))[class="num">1];

class="macro">#majority class
mcIndex = which(rowsums == max(rowsums))[class="num">1] # majority-class index
mcAccuracy = as.numeric(p[mcIndex])
mcRecall = class="num">0*p;  mcRecall[mcIndex] = class="num">1
mcPrecision = class="num">0*p; mcPrecision[mcIndex] = p[mcIndex]
mcF1 = class="num">0*p; mcF1[mcIndex] = class="num">2 * mcPrecision[mcIndex] / (mcPrecision[mcIndex] + class="num">1)

class="macro">#random accuracy
expAccuracy = sum(p*q)
class="macro">#kappa
kappa = (accuracy - expAccuracy) / (class="num">1 - expAccuracy)

class="macro">#random guess
rgAccuracy = class="num">1 / nc
rgPrecision = p
rgRecall = class="num">0*p + class="num">1 / nc
rgF1 = class="num">2 * p / (nc * p + class="num">1)

class="macro">#rnd weighted
rwgAccurcy = sum(p^class="num">2)
rwgPrecision = p
rwgRecall = p
rwgF1 = p

classNames = names(diag)
if (is.null(classNames)) classNames = paste("C",(class="num">1:nc),sep = "")

class="kw">return(list(
  ConfusionMatrix = cm,
  Metrics = data.frame(
    Class = classNames,
    Accuracy = accuracy,
    Precision = precision,
    Recall = recall,
    F1 = f1,
    MacroAvgPrecision = macroPrecision,
    MacroAvgRecall = macroRecall,
    MacroAvgF1 = macroF1,
    AvgAccuracy = avgAccuracy,
    MicroAvgPrecision = microPrf,
    MicroAvgRecall = microPrf,
    MicroAvgF1 = microPrf,
    MajorityClassAccuracy = mcAccuracy,
    MajorityClassPrecision = mcPrecision,
    MajorityClassRecall = mcRecall,
    MajorityClassF1 = mcF1,
    Kappa = kappa,
    RandomGuessAccuracy = rgAccuracy,
    RandomGuessPrecision = rgPrecision,
    RandomGuessRecall = rgRecall,
    RandomGuessF1 = rgF1,

常见问题

可用贝叶斯优化在给定区间内搜最优组合,比网格搜索省算力和时间,优先圈定隐藏层1~3层、每层50~200神经元再跑优化。
X 放特征矩阵(如多周期指标值),Y 放独热编码的类别标签;按时间序列切分训练集与验证集,避免未来信息泄漏。
小布可接入你的特征数据,调用贝叶斯优化脚本搜隐藏层组合,并直接给出混淆矩阵折算的评级,省去手写评测代码。
可算F1、召回率、精确率并按类别加权成单一评级分,不同 DNN 结构用同一指标横向排,避免只看总准确率被骗。
多数卡在特征标准化和验证集划分;先统一缩放到0~1,再严格按时间切,之后贝叶斯优化才搜得动有效解。