深度神经网络 (第五部分)。 DNN 超参数的贝叶斯优化(基础篇)
用贝叶斯方法给 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。外汇/贵金属信号建模属高风险,过拟合历史不代表未来可用。
#----准备------------- 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 上复现的「样本外」曲线可能严重高估,贵金属跳空时段尤甚。
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,方便接可视化。
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,