机器学习模型的变量评估和选择·进阶篇
(2/3)· 很多模型跑崩不是算法不行,而是喂进去的变量早就该清场和重排了
◍ 因子不是越多越好:过滤与封装两条路
实测把输入因子堆上去并不总能改善模型,反而可能拖累表现。多数情况下,真正起作用的预测因子只有 3–5 个,这一点在 rminer、caret、SuperLearner、mlr 这些聚合包的内置变量重要性函数里都能复现。 按 John、Kohavi 和 Pfleger 1994 年的分法,缩减因子基本就两类。过滤法在模型之外先算因子相关性,只留满足阈值的进模型,比如分类任务里逐个检查因子和标签的模糊关系,明显相关的才用;封装法则把因子当搜索输入、把模型效率当输出,靠递归增减、遗传算法或模拟退火去凑最优组合。 过滤更快,但挑选标准不直接挂钩模型效率;封装能盯着效率找组合,代价是反复训模型、调参,计算时间陡增。本文不碰封装,只拆一种我看能避开上述短板的过滤思路。外汇与贵金属预测因子建模波动剧烈,高杠杆下误判风险极高,相关结论仅作概率性参考。
因子筛选的三种务实切法
预测因子跑完外部评估后,会得到一个信息容量排序——也就是每个变量对模型质量的贡献权重。这一步不是装饰,是直接决定后续训练噪声量的关口。 实际落地通常只有三条路:固定抓重要度最高的 N 个;按全集合的百分比截 top 段;或者设一道重要度阈值,高于它的才进池子。 三种方式里的数量、百分比、阈值本身都能丢进优化器里扫参。外汇与贵金属行情高杠杆、跳空频繁,因子冗余会放大数据过拟合概率,筛完建议先在小布盯盘里回测一轮再上实盘。 下面把输入和输出数据集按这三种切法分别备好,才能跑后续的对照试验。
「把11个振荡器压成17维输入矩阵」
建模用的样本取自 EURUSD 的 M30 周期,最近 4000 根 K 线,不区分指标优先级,统一塞进输入集。原始计划是 11 个常见振荡器,再从中抽变量,最终拼出 17 维特征矩阵供后续训练。
以 MACD 为例,用 magrittr 的管道 %>% 可以把「算指标→转数据帧→加衍生列→抽列→回矩阵」写成一眼能读完的链式调用:先取 macd 与 signal 两列,给数据帧加 vsig = signal 一阶差分后头部补 NA 再乘 10,最后只留 signal 和 vsig。
代码里 In() 函数把 ADX、aroon、ATR、CCI、chaikinVolatility、CMO、MACD、RSI、stoch、SMI、volatility 全部按 n=16 算完 cbind 起来。直接在 MT5 导出的 M30 EURUSD csv 上跑 x <- In(p = 16) 能看到 summary:DX 中位数 16.64、ADX 均值 20.72、oscDX 均值 4.23,且各列有 16~31 个 NA 头尾填充。
外汇与贵金属属高杠杆品种,这套输入构造只解决特征来源,不预示任何方向;NA 行数会随参数 p 变化,调大 p 头部空值只会更多。
In <- function(p = class="num">16){ require(TTR) require(dplyr) require(magrittr) adx <- ADX(price, n = p) %>% as.data.frame %>% mutate(.,oscDX = DIp -DIn) %>% transmute(.,DX, ADX, oscDX) %>% as.matrix() ar <- aroon(price[ ,c(&class="macro">#x27;High&class="macro">#x27;, &class="macro">#x27;Low&class="macro">#x27;)], n = p)%>% extract(,class="num">3) atr <- ATR(price, n = p, maType = "EMA") %>% extract(,class="num">1:class="num">2) cci <- CCI(price[ ,class="num">2:class="num">4], n = p) chv <- chaikinVolatility(price[ ,class="num">2:class="num">4], n = p) cmo <- CMO(price[ ,&class="macro">#x27;Med&class="macro">#x27;], n = p) macd <- MACD(price[ ,&class="macro">#x27;Med&class="macro">#x27;], class="num">12, class="num">26, class="num">9) %>% as.data.frame() %>% mutate(., vsig = signal %>% diff %>% c(NA,.) %>% multiply_by(class="num">10)) %>% transmute(., sign = signal, vsig) %>% as.matrix() rsi <- RSI(price[ ,&class="macro">#x27;Med&class="macro">#x27;], n = p) stoh <- stoch(price[ ,class="num">2:class="num">4], nFastK = p, nFastD =class="num">3, nSlowD = class="num">3, maType = "EMA")% as.data.frame() %>% mutate(., oscK = fastK - fastD)% transmute(.,slowD, oscK)%>% as.matrix() smi <- SMI(price[ ,class="num">2:class="num">4],n = p, nFast = class="num">2, nSlow = class="num">25, nSig = class="num">9) vol <- volatility(price[ ,class="num">1:class="num">4], n = p, calc = "yang.zhang", N = class="num">144) In <- cbind(adx, ar, atr, cci, chv, cmo, macd, rsi, stoh, smi, vol) class="kw">return(In) } x <- In(p = class="num">16) > summary(x) DX ADX oscDX Min. : class="num">0.02685 Min. : class="num">5.291 Min. :-class="num">93.889 1st Qu.: class="num">8.11788 1st Qu.:class="num">14.268 1st Qu.: -class="num">9.486 Median :class="num">16.63550 Median :class="num">18.586 Median : class="num">5.889 Mean :class="num">20.70162 Mean :class="num">20.716 Mean : class="num">4.227 3rd Qu.:class="num">29.90428 3rd Qu.:class="num">24.885 3rd Qu.: class="num">19.693 Max. :class="num">79.80812 Max. :class="num">59.488 Max. : class="num">64.764 NA&class="macro">#x27;s :class="num">16 NA&class="macro">#x27;s :class="num">31 NA&class="macro">#x27;s :class="num">16 ar tr atr Min. :-class="num">100.0000 Min. :class="num">0.0000000 Min. :class="num">0.000224 1st Qu.: -class="num">50.0000 1st Qu.:class="num">0.0002500 1st Qu.:class="num">0.000553 Median : -class="num">6.2500 Median :class="num">0.0005600 Median :class="num">0.000724 Mean : -class="num">0.8064 Mean :class="num">0.0008031 Mean :class="num">0.000800 3rd Qu.: class="num">50.0000 3rd Qu.:class="num">0.0010400 3rd Qu.:class="num">0.000970 Max. : class="num">100.0000 Max. :class="num">0.0150300 Max. :class="num">0.003104 NA&class="macro">#x27;s :class="num">16 NA&class="macro">#x27;s :class="num">1 NA&class="macro">#x27;s :class="num">16 cci chv cmo
◍ 多指标样本分布的统计切面
把一段行情切片后跑描述统计,能看到各指标的真实散布而不是肉眼印象。下面这组汇总覆盖了 sign、vsig、rsi、slowD、oscK、SMI、signal、vol 八个序列,每个都带 NA 计数,说明取样窗口里有断点。 rsi 的 Median 落在 49.40、Mean 49.56,基本围绕 50 中轴,但 Min 12.59 / Max 89.42 提示极端超卖超买都出现过;slowD 的 Median 0.4720、Mean 0.4859,3rd Qu. 到 0.7124,整体偏多头排列的概率略高。 oscK 的 Mean 是 -0.000017,几乎为零,但 Min -0.415723 / Max 0.448486,摆动对称;SMI 的 Median -5.238、Mean -4.089,轻微偏负,Max 75.079 对比 Min -74.122 接近对称。vol 的 Mean 0.012337、Max 0.048948,是 Min 0.003516 的约 14 倍,波动聚类特征明显。 NA 数量值得注意:vsig 缺 34 个、signal 缺 33 个、SMI 缺 25 个,回测前要先决定是向前填充还是直接剔除,否则均值会被样本数不同的序列拉偏。外汇与贵金属杠杆高,这类统计仅描述历史切片,对后续走势只具参考倾向。
Min. :-class="num">515.375 Min. :-class="num">0.67428 Min. :-class="num">88.5697 1st Qu.: -class="num">84.417 1st Qu.:-class="num">0.33704 1st Qu.:-class="num">29.9447 Median : -class="num">5.674 Median : class="num">0.03057 Median : -class="num">2.4055 Mean : -class="num">1.831 Mean : class="num">0.11572 Mean : -class="num">0.6737 3rd Qu.: class="num">83.517 3rd Qu.: class="num">0.44393 3rd Qu.: class="num">28.0323 Max. : class="num">387.814 Max. : class="num">3.25326 Max. : class="num">94.0649 NA&class="macro">#x27;s :class="num">15 NA&class="macro">#x27;s :class="num">31 NA&class="macro">#x27;s :class="num">16 sign vsig rsi Min. :-class="num">0.38844 Min. :-class="num">0.43815 Min. :class="num">12.59 1st Qu.:-class="num">0.07124 1st Qu.:-class="num">0.05054 1st Qu.:class="num">39.89 Median :-class="num">0.00770 Median : class="num">0.00009 Median :class="num">49.40 Mean :-class="num">0.00383 Mean :-class="num">0.00013 Mean :class="num">49.56 3rd Qu.: class="num">0.05075 3rd Qu.: class="num">0.05203 3rd Qu.:class="num">58.87 Max. : class="num">0.38630 Max. : class="num">0.34871 Max. :class="num">89.42 NA&class="macro">#x27;s :class="num">33 NA&class="macro">#x27;s :class="num">34 NA&class="macro">#x27;s :class="num">16 slowD oscK SMI Min. :class="num">0.0499 Min. :-class="num">0.415723 Min. :-class="num">74.122 1st Qu.:class="num">0.2523 1st Qu.:-class="num">0.043000 1st Qu.:-class="num">33.002 Median :class="num">0.4720 Median : class="num">0.000294 Median : -class="num">5.238 Mean :class="num">0.4859 Mean :-class="num">0.000017 Mean : -class="num">4.089 3rd Qu.:class="num">0.7124 3rd Qu.: class="num">0.045448 3rd Qu.: class="num">22.156 Max. :class="num">0.9448 Max. : class="num">0.448486 Max. : class="num">75.079 NA&class="macro">#x27;s :class="num">19 NA&class="macro">#x27;s :class="num">17 NA&class="macro">#x27;s :class="num">25 signal vol Min. :-class="num">71.539 Min. :class="num">0.003516 1st Qu.:-class="num">31.749 1st Qu.:class="num">0.008204 Median : -class="num">5.319 Median :class="num">0.011274 Mean : -class="num">4.071 Mean :class="num">0.012337 3rd Qu.: class="num">19.128 3rd Qu.:class="num">0.015312 Max. : class="num">71.695 Max. :class="num">0.048948 NA&class="macro">#x27;s :class="num">33 NA&class="macro">#x27;s :class="num">16
用随机均匀森林拆解预测因子权重
把 ZigZag 信号当目标变量,用最小折线长度 25/75 点算两条 ZZ,取短 leg 那条做输入拼表,删掉 condition="0" 的未定义行和目标的 "0" 类,类型分布基本均衡,可直接进模型。 两个输入变量相关性超 90% 的是 rsi 和 SMI,剔掉后留剩余因子做评估。用 Random Uniform Forests 包算变量重要度,它把 Importance 拆成 contribution(降预测误差的整体贡献)+ interaction(对其他变量的影响)。 按 2:3 切训练/测试集,spatialSign 规范化到 [-1,1],doParallel 自动并行。训练内部误差 1%、方差 21.26%;Breiman 边界给预期误差 18.42%、上限 27.76%,测试外部误差 19.97%,低于上限且不大于内部误差,大概率不用重训。 全局重要度看出现频次,但前十名和「含相互影响」的排名对不上:tr 全局排末尾本该丢,却因强相互影响升到第 6。用前十变量重跑,测试误差降到 17.55%,比上限 28.18% 低,模型质量明显改善。 部分依赖图能直接看分割效果:cci 分割还行、signal 两类几乎全覆盖、tr 分割合理、chv 很差。分类视角下 tr 对类型 1 远重于 -1,oscK 反之。互相影响图里一阶最重要是 cci、二阶是 atr,热力图确认两者联合作用最强。 外汇/贵金属行情高噪,这套评估只降维度提概率,不保证信号对。开 MT5 把 ZZ leg 设 25/75 点导出,喂进 R 的 Random Uniform Forests,先删 rsi+SMI 再看 tr 的相互影响排名,比盲用全局重要度更稳。
「从离散因子里淘出能用的规则」
预测因子先被离散化:slowD 和 sign 完全没被分离,直接从数据集剔除;signal、vsig、cci、oscDX 只切了两段,其余变量分了 3 到 6 类。用 induction rules 包跑出来的规则,会把预测因子和目标绑定,比如一条规则里 idx 指向 6(atr)、4(ar)、11(vsig),当 atr 在 (85.1, Inf]、ar 在 (0.00137, Inf]、vsig 在 (0.0374, Inf] 时,consequent 判 class=1,laplace 字段给出该规则的置信区间。 测试集上做验证时,约 300 个样本被标为无足轻重可丢弃。拿精简后的集解析规则,预测质量反而比保留全集时更高一些。要注意 RandomUniformForests 这种算法每次重新加载都会给出略微不同的结果,重复试验拿不到可复现结论,外汇和贵金属市场本身高风险,这类概率结论只能当倾向性参考。 规则计算相当耗时,下面这段 R 侧函数负责把价格序列转成 ZigZag 及其转向信号,供上游离散化使用。ch 参数若大于 1 会按点位精度缩放,mode 控制取中价、高低价或收盘价;循环里把 NA 往前填充,再用 diff 算方向符号。
ZZ <- function(pr = price, ch = ch , mode="m") { require(TTR) if(ch > class="num">1) ch <- ch/(class="num">10 ^ (Dig - class="num">1)) if(mode == "m"){pr <- pr[ ,&class="macro">#x27;Med&class="macro">#x27;]} if(mode == "hl") {pr <- pr[ ,c("High", "Low")]} if(mode == "cl") {pr <- pr[ ,c("Close")]} zz <- ZigZag(pr, change = ch, percent = F, retrace = F, lastExtreme = T) n <- class="num">1:length(zz) for(i in n) { if(is.na(zz[i])) zz[i] = zz[i-class="num">1]} dz <- zz %>% diff %>% c(class="num">0,.) sig <- sign(dz) class="kw">return(cbind(zz, sig)) } out1 <- ZZ(ch = class="num">25) out2 <- ZZ(ch = class="num">50) > matplot(tail(cbind(out1[ ,class="num">1], out2[ ,class="num">1]), class="num">500), t="l") > data <- cbind(as.data.frame(x) , Class = factor(out1[ ,class="num">2])) %>% + na.omit > data <- data[data$Class != class="num">0, ] > data$Class <- rminer::delevels(data$Class, c("class="num">0", "class="num">1"), "class="num">1") > table(data$Class) -class="num">1 class="num">1 class="num">1980 class="num">1985 > descCor <- cor(data[ ,-ncol(data)]) > summary(descCor[upper.tri(descCor)]) Min. 1st Qu. Median Mean 3rd Qu. Max. -class="num">0.20170 class="num">0.03803 class="num">0.26310 class="num">0.31750 class="num">0.57240 class="num">0.95730 > highCor <- caret::findCorrelation(descCor, cutoff = .class="num">90) > highCor [class="num">1] class="num">12 class="num">15 > data.f <- data[ ,-highCor] > descCor <- cor(data.f[ ,-ncol(data.f)]) > summary(descCor[upper.tri(descCor)]) Min. 1st Qu. Median Mean 3rd Qu. Max. -class="num">0.20170 class="num">0.03219 class="num">0.21610 class="num">0.27060 class="num">0.47820 class="num">0.89880 > idx <- rminer::holdout(y = data.f$Class) > prep <- caret::preProcess(x = data.f[idx$tr, -ncol(data.f)], + method = c("spatialSign")) > x.train <- predict(prep, data.f[idx$tr, -ncol(data.f)]) > x.test <- predict(prep, data.f[idx$ts, -ncol(data.f)]) > y.train <- data.f[idx$tr, ncol(data.f)] > y.test <- data.f[idx$ts, ncol(data.f)] > ruf <- randomUniformForest( X = x.train, + Y = y.train, + xtest = x.test, + ytest = y.test, + mtry = class="num">1, ntree = class="num">300, + threads = class="num">2, + nodesize = class="num">2 + ) 为了易于计算,将标记 -class="num">1 class="num">1 已经转换成 class="num">1 class="num">2 ,并用于内部替换。 > print(ruf) Call: