神经网络变得轻松(第十七部分):降低维度·进阶篇
(2/3)· 当几十个指标叠加多品种多周期,特征维度爆炸,模型训练和可视化都快跑不动了
◍ 用矩阵运算搭一个 PCA 降维类
在 MT5 里做主成分分析,没必要自己手写协方差和特征值循环。新建一个 CPCA 类继承 CObject,把训练结果存进矩阵 m_Ureduce,再留两个向量 v_Means、v_STDs 记归一化的均值和标准差,b_Studied 标志位管模型是否训完。 构造函数里 b_Studied 置 false,m_Ureduce 初始化成 0 行 0 列;析构留空,类内不嵌对象。Study 方法吃原始数据矩阵,先按 Mean 算各特征均值、STD 算标准差(加个小常数防除零),归一化用矢量运算跑循环按行写进 X,不用嵌套循环。 归一化后协方差矩阵一行矩阵运算搞定,直接覆盖 X。SVD 只返回奇异值向量省资源,累计和除以总和得到上限 1 的递增向量,找第一个 ≥0.99 的位置定降维列数——也就是至少留 99% 信息量。调好 m_Ureduce 大小切标志位退出。 ReduceM 先查标志位和列数匹配,用训练时存的均值/std 向量归一化新数据,再乘降维矩阵返回。为了兼容老 CBufferDouble 动态数组,写了 FromBuffer / FromMatrix 互转接口,注意这些方法不保存指针,调用侧要自己删对象。 模型存取走 Save / Load:Save 先写 b_Studied,训过了再把矩阵和向量经动态缓冲区落盘;Load 按同顺序读回。外汇与贵金属行情高波动,这类降维仅用于特征压缩,不预示方向。 下面这段是类骨架,重点看私有成员和构造初始化,开 MT5 建 pca.mqh 把其余方法补上就能跑。
class CPCA : class="kw">public CObject { class="kw">private: class="type">bool b_Studied; matrix m_Ureduce; vector v_Means; vector v_STDs; CPCA::CPCA() : b_Studied(false) { m_Ureduce.Init(class="num">0, class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CPCA::~CPCA() { } class="type">bool CPCA::Study(matrix &data) { matrix X; class="type">class="kw">ulong total = data.Rows();
PCA训练与降维的落地代码
下面这段实现把多维行情特征压到主成分空间,核心在 Study 方法里先做标准化再求协方差矩阵。标准化时给标准差加了 1e-8 的极小值,避免某列波动近乎为零时除零崩掉,这是实盘矩阵运算里容易忽略的坑。 SVD 分解后拿累计方差占比挑主成分数:while(S[k] < 0.99) 这个循环意味着保留能覆盖 99% 总方差的维度,k 从 0 起算,最终 U 矩阵裁到 k+1 列存进 m_Ureduce。你在 MT5 里跑自己的特征矩阵时,可以把 0.99 调成 0.95 看看降维后信号失真程度。 ReduceM 负责把新数据用已学的均值、标准差标准化后右乘 m_Ureduce 投影,若列数不对或没训练过直接返回空矩阵。FromBuffer 和 FromMatrix 则是双环形缓冲与矩阵互转的桥接,注意 FromBuffer 要求缓冲区总长度能整除 vector_size,否则直接返空。 外汇与贵金属波动具备高杠杆高风险,PCA 降维仅降低维度冗余,不预示方向,任何特征工程都需在历史回测与样本外验证中谨慎评估。
if(!X.Init(total,data.Cols())) class="kw">return false; v_Means = data.Mean(class="num">0); v_STDs = data.STD(class="num">0) + class="num">1e-8; for(class="type">class="kw">ulong i = class="num">0; i < total; i++) { vector temp = data.Row(i) - v_Means; temp /= v_STDs; X = X.Row(temp, i); } X = X.Transpose().MatMul(X / total); matrix U, V; vector S; if(!X.SVD(U, V, S)) class="kw">return false; class="type">class="kw">double sum_total = S.Sum(); if(sum_total<=class="num">0) class="kw">return false; S = S.CumSum() / sum_total; class="type">int k = class="num">0; class="kw">while(S[k] < class="num">0.99) k++; if(!U.Resize(U.Rows(), k + class="num">1)) class="kw">return false; class=class="str">"cmt">//--- m_Ureduce = U; b_Studied = true; class="kw">return true; } matrix CPCA::ReduceM(matrix &data) { matrix result; if(!b_Studied || data.Cols() != m_Ureduce.Rows()) class="kw">return result.Init(class="num">0, class="num">0); class="type">class="kw">ulong total = data.Rows(); if(!X.Init(total,data.Cols())) class="kw">return false; for(class="type">class="kw">ulong r = class="num">0; r < total; r++) { vector temp = data.Row(r) - v_Means; temp /= v_STDs; result = result.Row(temp, r); } class="kw">return result.MatMul(m_Ureduce); } matrix CPCA::FromBuffer(CBufferDouble *data, class="type">class="kw">ulong vector_size) { matrix result; if(CheckPointer(data) == POINTER_INVALID) { result.Init(class="num">0, class="num">0); class="kw">return result; } class=class="str">"cmt">//--- if((data.Total() % vector_size) != class="num">0) { result.Init(class="num">0, class="num">0); class="kw">return result; } class="type">class="kw">ulong rows = data.Total() / vector_size; if(!result.Init(rows, vector_size)) { result.Init(class="num">0, class="num">0); class="kw">return result; } for(class="type">class="kw">ulong r = class="num">0; r < rows; r++) { class="type">class="kw">ulong shift = r * vector_size; for(class="type">class="kw">ulong c = class="num">0; c < vector_size; c++) result[r, c] = data[(class="type">int)(shift + c)]; } class=class="str">"cmt">//--- class="kw">return result; } CBufferDouble *CPCA::FromMatrix(matrix &data) { CBufferDouble *result = new CBufferDouble(); if(CheckPointer(result) == POINTER_INVALID) class="kw">return result; class="type">class="kw">ulong rows = data.Rows(); class="type">class="kw">ulong cols = data.Cols(); if(!result.Reserve((class="type">int)(rows * cols))) { class="kw">delete result; class="kw">return result; }
「PCA 降维类的接口与内存回收细节」
这段 CPCA 类的实现把主成分降维封装成了可复用的 MT5 矩阵工具。注意 ReduceM 入口处的守卫:若未执行过 Study 或样本数不能被 m_Ureduce.Rows() 整除,直接返回 0×0 矩阵,避免错位计算。 看这段矩阵填充的兜底逻辑: for(ulong r=0; r<rows; r++) for(ulong c=0; c<cols; c++) if(!result.Add(data[r,c])) { delete result; return result; } 只要任一单元格 Add 失败就立刻释放已分配矩阵并返回,防止半截数据泄漏。 公开接口同时提供 CBufferDouble* 与 matrix& 两种重载,对外暴露 VectorSize() 返回 m_Ureduce.Cols()、Inputs() 返回 m_Ureduce.Rows(),调用方能用这两个值反推降维前后的维度。外汇与贵金属行情用 PCA 做特征压缩时波动剧烈,属于高风险操作,实盘前建议在 MT5 策略测试器用历史 tick 验证维度匹配。 让小布替你跑这套 把 Sample buffer 的 Total() 先对 Inputs() 取模,不为 0 就别调 Reduce,省一次无效矩阵拷贝。
for(class="type">class="kw">ulong r = class="num">0; r < rows; r++) for(class="type">class="kw">ulong c = class="num">0; c < cols; c++) if(!result.Add(data[r, c])) { class="kw">delete result; class="kw">return result; } class=class="str">"cmt">//--- class="kw">return result; } class="type">bool CPCA::Study(CBufferDouble *data, class="type">int vector_size) { matrix d = FromBuffer(data, vector_size); class="kw">return Study(d); } matrix CPCA::ReduceM(CBufferDouble *data) { matrix result; result.Init(class="num">0, class="num">0); if(!b_Studied || (data.Total() % m_Ureduce.Rows()) != class="num">0) class="kw">return result; result = FromBuffer(data, m_Ureduce.Rows()); class=class="str">"cmt">//--- class="kw">return ReduceM(result); } CBufferDouble *CPCA::Reduce(CBufferDouble *data) { matrix result = ReduceM(data); class=class="str">"cmt">//--- class="kw">return FromMatrix(result); } CBufferDouble *CPCA::Reduce(matrix &data) { matrix result = ReduceM(data); class=class="str">"cmt">//--- class="kw">return FromMatrix(result); } class CPCA : class="kw">public CObject { class="kw">private: class="type">bool b_Studied; matrix m_Ureduce; vector v_Means; vector v_STDs; class=class="str">"cmt">//--- CBufferDouble *FromMatrix(matrix &data); CBufferDouble *FromVector(vector &data); matrix FromBuffer(CBufferDouble *data, class="type">class="kw">ulong vector_size); vector FromBuffer(CBufferDouble *data); class="kw">public: CPCA(); ~CPCA(); class=class="str">"cmt">//--- class="type">bool Study(CBufferDouble *data, class="type">int vector_size); class="type">bool Study(matrix &data); CBufferDouble *Reduce(CBufferDouble *data); CBufferDouble *Reduce(matrix &data); matrix ReduceM(CBufferDouble *data); matrix ReduceM(matrix &data); class=class="str">"cmt">//--- class="type">bool Studied(class="type">void) { class="kw">return b_Studied; } class="type">class="kw">ulong VectorSize(class="type">void) { class="kw">return m_Ureduce.Cols();} class="type">class="kw">ulong Inputs(class="type">void) { class="kw">return m_Ureduce.Rows(); } class=class="str">"cmt">//--- class="kw">virtual class="type">bool Save(const class="type">int file_handle);
◍ 把 PCA 模型落盘与重载的坑位看清
无监督 PCA 类在 MT5 里跑完降维矩阵后,最实际的动作是把 m_Ureduce、v_Means、v_STDs 三组对象写进文件句柄,下次 EA 启动直接 Load 恢复,不必重算样本。Save 先写 b_Studied 标志位,若模型未训练就提前返回 true,避免空矩阵 IO 报错。 Save 里用 FromMatrix 把降维矩阵转成一维双精度缓冲,先 FileWriteLong 记录列数(cols),再调缓冲自身的 Save;均值向量和标准差向量同理,每次写完立刻 delete temp,防止句柄泄漏。任何一步 FileWriteInteger 返回值小于 INT_VALUE 或缓冲保存失败,函数立刻返回 false,调用方需自行处理中断。 Load 的对称逻辑要注意:读回 cols 后必须用 FromBuffer(temp, cols) 重建二维矩阵,只传 temp 会丢维度。三组向量依次 Load 进同一 temp 对象再分别转回 v_Means、v_STDs,顺序和 Save 严格一致,错位一个就会把标准差填进均值。 开 MT5 自己验证时,可在 Save 后打印 m_Ureduce.Cols() 的值,再 Load 回来比对是否相等;外汇与贵金属行情的高波动可能让 STD 量级跳变,重建后最好做一次样本反降维,确认误差在可接受区间。
class="kw">virtual class="type">bool Load(const class="type">int file_handle); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) { class="kw">return defUnsupervisedPCA; } }; class="type">bool CPCA::Save(const class="type">int file_handle) { if(file_handle == INVALID_HANDLE) class="kw">return false; if(FileWriteInteger(file_handle, (class="type">int)b_Studied) < INT_VALUE) class="kw">return false; if(!b_Studied) class="kw">return true; CBufferDouble *temp = FromMatrix(m_Ureduce); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(FileWriteLong(file_handle, (class="type">long)m_Ureduce.Cols()) <= class="num">0) { class="kw">delete temp; class="kw">return false; } if(!temp.Save(file_handle)) { class="kw">delete temp; class="kw">return false; } class="kw">delete temp; temp = FromVector(v_Means); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.Save(file_handle)) { class="kw">delete temp; class="kw">return false; } class="kw">delete temp; temp = FromVector(v_STDs); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.Save(file_handle)) { class="kw">delete temp; class="kw">return false; } class="kw">delete temp; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CPCA::Load(const class="type">int file_handle) { if(file_handle == INVALID_HANDLE) class="kw">return false; b_Studied = (class="type">bool)FileReadInteger(file_handle); if(!b_Studied) class="kw">return true; CBufferDouble *temp = new CBufferDouble(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; class="type">long cols = FileReadLong(file_handle); if(!temp.Load(file_handle)) { class="kw">delete temp; class="kw">return false; } m_Ureduce = FromBuffer(temp, cols); if(!temp.Load(file_handle)) { class="kw">delete temp; class="kw">return false; } v_Means = FromBuffer(temp); if(!temp.Load(file_handle)) { class="kw">delete temp; class="kw">return false; } v_STDs = FromBuffer(temp); class="kw">delete temp; class=class="str">"cmt">//---