深度神经网络 (第 III 部)。样品选择和降维·进阶篇
(2/3)·标签噪音正悄悄拖垮你的模型精度,PCA、ICA与自编码器该怎么选
PCA 怎么把高维行情压成低维信号
主分量分析(PCA)是降维里最直白的路子:在原始数据空间里找一块设定好维度的超平面,把样本投影上去,挑投影误差最小、也就是方差之和最大的那块平面。这套思路底子很老,核心不复杂,但落到 MT5 做特征压缩时很实用。 约化维度 d 一般由你自己定。做可视化(d=2 或 3)或者单纯想省内存,d 很好选;但拿去当模型输入时,d 选多少没先验答案。 有个启发式办法:PCA 生成的各阶约化空间是互相嵌套的,协方差矩阵的特征向量和特征值算一次,就能切任意 d。把特征值按从大到小排成图,右手边趋近于零的砍掉,或者定个阈值让曲线右侧面积占总量 5% 或 1%,d 就出来了。 实战里,当候选预测因子明显多于 50 个,先用 PCA 把噪声因子压一波,后续训练会更干净。外汇和贵金属波动受宏观与流动性扰动大,高维因子里混着大量噪音,这类预处理只降低维度、不保证方向,信号失效风险仍在。
◍ ICA 怎么从价格因子里拆出独立信号
独立分量分析(ICA)比 PCA 晚出场,但在信号分离上更狠:它假设观测到的 P 个变量是 p 个相互独立变量的线性混合,目标就是把原始独立源恢复出来。ICA 不要求分量正交,只要求统计独立,这比 PCA 里“只是不相关”要严格得多,对汇率、贵金属这类多因子搅在一起的高风险行情数据更有拆解价值。 caret 里的 preProcess() 同时封装了 PCA 和 ICA。ICA 分两阶段:先在训练集上算参数(含白化矩阵 K 与混合矩阵 W),之后用 predict() 把训练集和新数据统一转换。实验里对 x.cap(含异常值插补的 7906×11 数据集)指定 pcaComp=5、n.comp=3,不指定 ICA 正则化方式,跑出来的 prePCA 与 preICA 各自带 mean、std 以及 rotation / K、W 矩阵。 算法链路很直白:先居中得 X,投影到主分量方向得 PCA = X*K,再乘混合矩阵得 ICA = X*K*W。用 predict::caret 与手算参数两种方式得到的 PCA、ICA 结果一致。 别把 caret 当唯一选项。pcaPP 算 PCA 对主分量识别更稳;fastICA 包参数更灵活,建议直接用它而不是 caret 里的 ica 封装。外汇与贵金属杠杆高、波动剧烈,任何降维信号都只是概率倾向,实盘前请在 MT5 导出的 R 数据上复跑确认。
> ls(env) [class="num">1] "cap1" "cap2" "Close" "Data" "dataSet" [class="num">6] "dataSetCap" "dataSetClean" "dataSetOut" "High" "i" [class="num">11] "k" "k.cap" "k.out" "lof.x" "lof.x.cap" [class="num">16] "Low" "lower" "med" "Open" "out.ftlm" [class="num">21] "out.ftlm1" "pr" "pre.outl" "preProClean" "Rlof.x" [class="num">26] "Rlof.x.cap" "sk" "sk.cap" "sk.out" "test" [class="num">31] "test.out" "train" "train.out" "upper" "Volume" [class="num">36] "x" "x.cap" "x.out" require(caret) evalq({ prePCA <- preProcess(x.cap, pcaComp = class="num">5, method = Hmisc::Cs(center, scale, pca)) preICA <- preProcess(x.cap, n.comp = class="num">3, method = "ica") }, env) > str(env$prePCA) List of class="num">20 $ dim : class="type">int [class="num">1:class="num">2] class="num">7906 class="num">11 $ bc : NULL $ yj : NULL $ et : NULL $ invHyperbolicSine: NULL $ mean : Named num [class="num">1:class="num">11] -class="num">0.001042 -class="num">0.003567 -class="num">0.000155 -class="num">0.000104 -class="num">0.000267 ... ..- attr(*, "names")= chr [class="num">1:class="num">11] "ftlm" "stlm" "rbci" "pcci" ... $ std : Named num [class="num">1:class="num">11] class="num">0.091 class="num">0.237 class="num">0.1023 class="num">0.0377 class="num">0.0356 ...
「预处理对象里的 PCA 与 ICA 结构差异」
在 R 环境里用 str() 扫一眼 env$preProcess 和 env$preICA,能直接看出两套预处理管道的配置分叉。前者走 PCA 路线:rotation 矩阵是 11×5 的数值块,pcaComp 和 numComp 都锁在 5,thresh 设 0.95,说明用前 5 个主成分去覆盖 95% 方差倾向。 后一个 env$preICA 则是纯 ICA 结构:rotation 为 NULL,pcaComp / numComp 皆空,但在 ica 子列表里塞了 11×3 的混解矩阵 K,row.norm 为 FALSE。维度上 preICA$dim 显示 7906×11,即 7906 根样本棒、11 个原始特征(ftlm/stlm/rbci/pcci 等)。 两个对象的 mean 向量也值得核对:preICA 里 ftlm 均值约 -0.001042、std 约 0.091,stlm 均值 -0.003567、std 0.237。开 MT5 把对应指标导出到 R,跑一遍 caret::preProcess,对照这两段 str 输出,能确认自己特征工程没接错管道。外汇与贵金属数据高频噪声大,这类降维预处理仅降低过拟合概率,实盘仍属高风险。
用 R 复算确认 PCA 与 ICA 投影一致
在 MT5 之外做特征降维验证时,R 的 caret::preProcess 对象会吐出一套内部结构:W 是一个 3×3 数值矩阵,首行打印值约为 -0.587、0.77、-0.25、-0.734、-0.636,wildcards 为长度 2 的列表且含 PCA 与 ICA 两个空字符字段,k 设为 5,knnSummary 是函数,bagImp/median/data 均为 NULL,对象 class 标记为 "preProcess"。 下面这段 R 代码先把 x.cap 分别用 prePCA 和 preICA 做 predict,得到 pca 与 ica;随后手动用 scale 减均值除标准差,再乘 rotation(PCA)或 K %*% W(ICA),生成 pca1、ica1。all_equal 回测显示两者完全相同,说明 predict 内部就是标准白化后乘投影矩阵。 外汇与贵金属行情的高噪声会让 ICA 分量不稳定,建议在 MT5 里用相同均值/方差参数复算一遍,确认你自己的特征管线没有悄悄漂移。
require(magrittr) evalq({ pca <- predict(prePCA, x.cap) ica <- predict(preICA, x.cap) pca1 <- ((x.cap %>% scale(prePCA$mean, prePCA$std)) %*% prePCA$rotation) ica1 <- ((x.cap %>% scale(preICA$mean, preICA$std)) %*% preICA$ica$K) %*% preICA$ica$W colnames(ica1) <- colnames(ica1, do.NULL = FALSE, prefix = &class="macro">#x27;ICA&class="macro">#x27;) },env) evalq(all_equal(pca, pca1), env) # [class="num">1] TRUE evalq(all_equal(ica, ica1), env) # [class="num">1] TRUE
◍ PPCA 把降维变成可计算的概率题
经典 PCA 只给超平面方向,概率主分量分析(PPCA)用概率模型重写了同一件事:它恢复的是整个数据的变异模型,而不只是协方差矩阵特征向量定义的那个面。换句话说,解里既含基向量方向,也含这些基向量的长度,数据在所有方向上的散布都被描述了。 这套概率框架带来几个实打实的好处:能用 EM 算法在 d≪D 时更高效地求解;缺失值直接并进隐藏变量,EM 照跑;还能接混合物分布、贝叶斯选维、生成新样本、按类建分布做分类。likelihood 值本身就成了通用准则,不同概率模型能直接比,离群点也容易被拎出来。 真要落地,R 里的 pcaMethods 包把 PPCA 和一堆非线性变体打包好了。它的 ppca 方法能容忍 10%–15% 的 NA,bpca 靠贝叶斯+EM 容忍 >10% 缺失且不强制负载正交,nlpca 用自编码结构吃缺失值,nipals 只接 ≤5% 缺失。包里所有方法返回统一的 pcaRes 对象,用 pca() 按命名参数切类型。 下面三行是在 MT5 之外的 R 环境装包并载入的代码,跑通后你就能用上述算法评估自己导出的 tick 缺失数据。外汇与贵金属数据高频断裂常见,用前先确认缺失比例落在区间内,这类方法对样本结构敏感,结果仅代表概率倾向而非确定结论。
source("https:class=class="str">"cmt">//bioconductor.org/biocLite.R") biocLite("pcaMethods") library(pcaMethods)
「用自编码器压到三维再切分样本」
自编码器(AE)是输入层与输出层神经元数相等的多层网络,核心任务是把输入数据尽可能无失真地重建。训练完之后,输入层到首个隐藏层的权重矩阵 W1 就是载荷,把原始矩阵 Xin 乘上 W1 得到的约化矩阵,本质上等同于主分量;用 predict(hidden.output=TRUE) 也能直接取出隐藏层输出。 想明显降维时可以串两个 AE:比如 12 个输入先跑 12-7-12,取隐藏层 7 维再喂给 7-3-7,最终压到 3 维(12→3)。隐藏神经元越少、n.hidden/n.in 比例越低,复原误差越大——这是调参时首先要盯的数。 下面这段 R 代码把前一篇存好的 Part_1.RData 结果拆成训练/验证/测试三份(各 1000 行:1–2000、2001–3000、3001–4000),先做去常量和缺失剔除,再用四分位上下限加 5%/95% 封顶做异常值插补,最后得到 DTcap 列表供后续建模。 [CODE] require(FCNN4R) require(deepnet) require(darch) require(tidyverse) require(magrittr) #----清理--------------------- require(caret) require(pipeR) evalq( { train = 1:2000 val = 2001:3000 test = 3001:4000 DT <- list() dataSet %>% preProcess(., method = c("zv", "nzv", "conditionalX")) %>% predict(., dataSet) %>% na.omit -> dataSetClean list(train = dataSetClean[train, ], val = dataSetClean[val, ], test = dataSetClean[test, ]) -> DT rm(dataSetClean, train, val, test) }, env) #------异常值------------- require(foreach) evalq({ DTcap <- list() foreach(i = 1:3) %do% { DT[[i]] %>% select(-c(Data, Class)) %>% as.data.frame() -> x if (i == 1) { foreach(i = 1:ncol(x), .combine = "cbind") %do% { prep.outlier(x[ ,i]) %>% unlist() } -> pre.outl colnames(pre.outl) <- colnames(x) } foreach(i = 1:ncol(x), .combine = "cbind") %do% { stopifnot(exists("pre.outl", envir = env)) lower = pre.outl['lower.25%', i] upper = pre.outl['upper.75%', i] med = pre.outl['med', i] cap1 = pre.outl['cap1.5%', i] cap2 = pre.outl['cap2.95%', i] treatOutlier(x = x[ ,i], impute = T, fill = T, lower = lower, upper = upper, med = med, cap1 = cap1, cap2 = cap2) } %>% as.data.frame() -> x.cap colnames(x.cap) <- colnames(x) return(x.cap) } -> DTcap foreach(i = 1:3) %do% { cbind(DTcap[[i]], Class = DT[[i]]$Class) } -> DTcap DTcap$train <- DTcap[[1]] DTcap$val <- DTcap[[2]] DTcap$test <- DTcap[[3]] rm(lower, upper, med, cap1, cap2, x.cap, x) }, env) #------常规化----------- evalq( 约化后画变体-协方差图,能直接看到 V1/V2/V3 被目标变量等级切开,但训练、验证、测试三集的分布是偏斜的。把噪音样本剔掉再画一次,图像大概率会更干净,这一步留给你在 MT5 之外的 R 环境里自己跑。 顺带提一句 NLPCA 里的分层变体(h-NLPCA):它不光给最优非线性子空间,还强制分量按色散等级排、彼此不相关,相当于做了非线性白化。对于后续想接 ICA 或线性分类器的交易者,这种预处理能消掉数据里的复杂非线性关系,让后续方法可以用线性假设。外汇与贵金属市场高频噪声大,这类降维白化只是概率上提升信号分离度,不保证实盘胜率,请自担高风险。
require(FCNN4R) require(deepnet) require(darch) require(tidyverse) require(magrittr) #----清理--------------------- require(caret) require(pipeR) evalq( { train = class="num">1:class="num">2000 val = class="num">2001:class="num">3000 test = class="num">3001:class="num">4000 DT <- list() dataSet %>% preProcess(., method = c("zv", "nzv", "conditionalX")) %>% predict(., dataSet) %>% na.omit -> dataSetClean list(train = dataSetClean[train, ], val = dataSetClean[val, ], test = dataSetClean[test, ]) -> DT rm(dataSetClean, train, val, test) }, env) #------异常值------------- require(foreach) evalq({ DTcap <- list() foreach(i = class="num">1:class="num">3) %do% { DT[[i]] %>% select(-c(Data, Class)) %>% as.data.frame() -> x if (i == class="num">1) { foreach(i = class="num">1:ncol(x), .combine = "cbind") %do% { prep.outlier(x[ ,i]) %>% unlist() } -> pre.outl colnames(pre.outl) <- colnames(x) } foreach(i = class="num">1:ncol(x), .combine = "cbind") %do% { stopifnot(exists("pre.outl", envir = env)) lower = pre.outl[&class="macro">#x27;lower.class="num">25%&class="macro">#x27;, i] upper = pre.outl[&class="macro">#x27;upper.class="num">75%&class="macro">#x27;, i] med = pre.outl[&class="macro">#x27;med&class="macro">#x27;, i] cap1 = pre.outl[&class="macro">#x27;cap1.class="num">5%&class="macro">#x27;, i] cap2 = pre.outl[&class="macro">#x27;cap2.class="num">95%&class="macro">#x27;, i] treatOutlier(x = x[ ,i], impute = T, fill = T, lower = lower, upper = upper, med = med, cap1 = cap1, cap2 = cap2) } %>% as.data.frame() -> x.cap colnames(x.cap) <- colnames(x) class="kw">return(x.cap) } -> DTcap foreach(i = class="num">1:class="num">3) %do% { cbind(DTcap[[i]], Class = DT[[i]]$Class) } -> DTcap DTcap$train <- DTcap[[class="num">1]] DTcap$val <- DTcap[[class="num">2]] DTcap$test <- DTcap[[class="num">3]] rm(lower, upper, med, cap1, cap2, x.cap, x) }, env) #------常规化----------- evalq(