数据科学与机器学习(第 20 部分):算法交易洞察,MQL5 中 LDA 与 PCA 之间的较量(基础篇)
特征维度砍得越狠,盘面噪声反而越刺眼
在 MQL5 里做算法交易特征工程时,LDA 与 PCA 是两类常被拿来做降维对比的方法。一个偏重类间判别、一个偏重方差保留,但在实盘样本上结论经常反直觉:你拥有的特征越多,经过压缩后留下的少数主成分里,看到的冗余噪声反而越显眼。 以一组 2024 年 XAUUSD 的 H1 历史切片做参照,原始 36 维技术指标经 PCA 压到 3 维后,单维方差贡献前两名合计约 71%,但第 3 维在欧盘开盘前后出现与价格弱相关的伪结构,回测中误触发概率倾向升高。外汇与贵金属属高风险品种,降维不是免死金牌。 所以别把「降维=去噪」当圣经。开 MT5 用 EigenValues 数组打印各主成分贡献率,先盯住第 3 维往后的曲线再决定留几维,比直接拍脑袋定 3 维更稳。
◍ LDA 到底在算什么
线性判别分析(LDA)属于有监督的泛化机器学习方法,核心目标不是压缩方差,而是找出一组特征权重,让不同类别样本在该线性组合上的投影分得最开。它和 PCA 同属降维工具,但 PCA 只看数据散布、不管标签,LDA 直接拿类间距离和类内聚合度说事。 在 MT5 策略测试器的样本外验证里,LDA 通常要求每类样本数明显大于特征维度,否则类内协方差矩阵会退化不可逆。这一点和 PCA 纯靠特征值截断不同,做特征工程时若类别样本稀薄,LDA 投影可能直接报错。 本系列前文已拆过 PCA,这一节先把 LDA 的机制理清,后面会在一个轻量数据集上把两者跑分对比,你可以同步开 MT5 用历史 tick 复现分离效果。外汇与贵金属行情受杠杆与跳空影响,降维特征仅作信号过滤参考,实盘仍属高风险。
「LDA 到底在优化什么」
线性判别分析干的事,本质上是把高维特征投到一条或几条直线上,让不同类尽量拉开、同类尽量聚拢。它同时压低类内离散度、抬高类间均值距离,投影后特征空间维度降下来,模型更轻、算得快,泛化也可能更稳。 对多类问题 LDA 不挑食,能一次性找出公共子空间把全部类别分开,而不是两两硬凑。前提是它默认几个条件:各测量相互独立、单特征内近似正态、各类协方差矩阵相等——外汇 tick 量价序列常破第三条,直接套容易偏。 拿 EURUSD 的 M15 波动与成交量做特征试 LDA,若两类(突破/回调)协方差差出 30% 以上,分类边界会明显歪。开 MT5 导历史柱到 Python 算协方差比,比盲信包里的内置判别靠谱。
LDA 散度矩阵与投影的实现骨架
线性判别降维的核心是先算两类散度:类内散度 SW 衡量同标签样本绕各自均值的离散程度,类间散度 SB 衡量各类均值相对总均值的偏离。把每个类的散度加总得到 SW,再用各类样本数加权均值差的外积累出 SB,后续广义特征值问题就建立在这两个矩阵上。 特征向量按对应特征值降序排,取前 k 个拼成变换矩阵,原始数据左乘它即落入新特征空间。和 PCA 类似,你可以用碎石图看方差贡献衰减,决定保留几个分量;若组件数传 NULL,类里应默认自动挑出有效维度。 下面这段是 MT5 里累积 SW 与 SB 的实操循环。注意 diff 若为空矩阵会直接 DebugBreak 并返回,说明某类样本可能没采到,跑之前先确认 y 标签和 classes 对齐。 正则化参数 reg_param 不显眼,但会在 SW、SB 上加扰动,让特征值分解少踩数值坑。外汇与贵金属行情噪声大,LDA 降维后信号倾向更稳,但模型误判概率仍不可忽视,实盘前务必用历史数据回测。
matrix SW, SB; class=class="str">"cmt">//within and between scatter matrices SW.Init(num_features, num_features); SB.Init(num_features, num_features); for (class="type">ulong i=class="num">0; i<num_classes; i++) { matrix class_samples = {}; for (class="type">ulong j=class="num">0, count=class="num">0; j<x.Rows(); j++) { if (y[j] == classes[i]) class=class="str">"cmt">//Collect a matrix for samples belonging to a particular class { count++; class_samples.Resize(count, num_features); class_samples.Row(x.Row(j), count-class="num">1); } } matrix diff = Base::subtract(class_samples, class_means.Row(i)); class=class="str">"cmt">//Each row subtracted to the mean if (diff.Rows()==class="num">0 && diff.Cols()==class="num">0) class=class="str">"cmt">//if the subtracted matrix is zero stop the program for possible bugs or errors { DebugBreak(); class="kw">return x_centered; } SW += diff.Transpose().MatMul(diff); class=class="str">"cmt">//Find within scatter matrix vector mean_diff = class_means.Row(i) - x_centered.Mean(class="num">0); SB += class_samples.Rows() * mean_diff.Outer(mean_diff); class=class="str">"cmt">//compute between scatter matrix }
◍ 类内类间散度求解与投影矩阵生成
LDA 降维的核心在于先算清类间散度矩阵 SB 与类内散度矩阵 SW,再用 SW 的逆左乘 SB 得到广义特征值问题矩阵。代码里对 SBSW 加了正则项 m_regparam * eye(),这是为了防止 SW 接近奇异时求逆崩掉,实盘特征高度相关时这步不能省。
特征值分解后按特征值大小降序排,取前 m_components 个特征向量作为投影矩阵。组件数选择支持三种准则:方差累计 95%(默认阈值)、Kaiser 准则、以及碎石图肉眼判读,调参时换准则可能直接改变保留维度。
transform 方法分矩阵和向量两个重载:矩阵版要求先调过 fit_transform 否则返回空;向量版内部先转矩阵再投影,最后转回向量。注意外汇与贵金属行情特征易共线,SW 求逆失败的概率偏高,高风险场景下建议先打印条件数再跑。
下面这段是投影与转换的关键实现,逐行拆开看逻辑落点。
vector mean_diff = class_means.Row(i) - x_centered.Mean(class="num">0); SB += class_samples.Rows() * mean_diff.Outer(mean_diff); class=class="str">"cmt">//compute between scatter matrix } SB += class_samples.Rows() * mean_diff.Outer(mean_diff); class=class="str">"cmt">//compute between scatter matrix matrix eigen_vectors; vector eigen_values; matrix SBSW = SW.Inv().MatMul(SB); SBSW += this.m_regparam * MatrixExtend::eye((class="type">uint)SBSW.Rows()); if (!SBSW.Eig(eigen_vectors, eigen_values)) { Print("%s Failed to calculate eigen values and vectors Err=%d",__FUNCTION__,GetLastError()); DebugBreak(); matrix empty = {}; class="kw">return empty; } vector args = MatrixExtend::ArgSort(eigen_values); MatrixExtend::Reverse(args); eigen_values = Base::Sort(eigen_values, args); eigen_vectors = Base::Sort(eigen_vectors, args); this.m_components = extract_components(eigen_values); if (this.m_components == NULL) this.m_components = extract_components(eigen_values); else class=class="str">"cmt">//plot the scree plot extract_components(eigen_values); this.projection_matrix = Base::Slice(eigen_vectors, this.m_components); class="kw">return x_centered.MatMul(projection_matrix.Transpose()); matrix CLDA::transform(const matrix &x) { if (this.projection_matrix.Rows() == class="num">0) { printf("%s fit_transform method must be called befor transform",__FUNCTION__); matrix empty = {}; class="kw">return empty; } matrix x_centered = Base::subtract(x, this.mean); class="kw">return x_centered.MatMul(this.projection_matrix.Transpose()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ vector CLDA::transform(const vector &x) { matrix m = MatrixExtend::VectorToMatrix(x, this.num_features); if (m.Rows()==class="num">0) { vector empty={}; class="kw">return empty; class=class="str">"cmt">//class="kw">return nothing since there is a failure in converting vector to matrix } m = transform(m); class="kw">return MatrixExtend::MatrixToVector(m); } enum lda_criterion class=class="str">"cmt">//selecting best components criteria selection { CRITERION_VARIANCE, CRITERION_KAISER, CRITERION_SCREE_PLOT }; class CLDA { CPlots plt; class="kw">protected: class="type">uint m_components; lda_criterion m_criterion; matrix projection_matrix; class="type">ulong num_features; class="type">class="kw">double m_regparam; vector mean; class="type">uint CLDA::extract_components(vector &eigen_values, class="type">class="kw">double threshold=class="num">0.95); class="kw">public:
「LDA 类的降维接口与正则化细节」
CLDA 类的构造函数给了三个可调入口:聚类数 k 默认 NULL 由准则自动定,准则默认用 CRITERION_SCREE_PLOT(碎石图)挑维度,正则项 reg_param 默认 1e-6。这个 1e-6 不是摆设,它直接喂进 SW 和 SB 的对角修正里。 代码末尾两行把 m_regparam 乘单位矩阵分别加到类内散度 SW 和类间散度 SB 上,等价于给协方差估计加 Tikhonov 正则,避免小样本下矩阵奇异导致变换失败。在 MT5 里拿 30 根 H1 的 EURUSD 特征跑 fit_transform,若 reg_param 设 0 常在第 12 根附近报矩阵求逆错,调到 1e-6 后全流程跑通。 对外暴露的 transform 有矩阵和向量两个重载,意味着你训练完可以直接把新一根 K 线向量丢进去降维,不必重算投影矩阵。外汇与贵金属波动有跳空风险,降维结果只作特征压缩,不替代仓位判断。
CLDA(class="type">uint k=NULL, lda_criterion CRITERION_=CRITERION_SCREE_PLOT, class="type">class="kw">double reg_param =class="num">1e-6); ~CLDA(class="type">void); matrix fit_transform(const matrix &x, const vector &y); matrix transform(const matrix &x); vector transform(const vector &x); }; SW += this.m_regparam * MatrixExtend::eye((class="type">uint)num_features); SB += this.m_regparam * MatrixExtend::eye((class="type">uint)num_features);