神经网络变得轻松(第十七部分):降低维度·进阶篇
🧠

神经网络变得轻松(第十七部分):降低维度·进阶篇

(2/3)· 当几十个指标叠加多品种多周期,特征维度爆炸,模型训练和可视化都快跑不动了

含代码示例实战向 第 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 把其余方法补上就能跑。

MQL5 / C++
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 降维仅降低维度冗余,不预示方向,任何特征工程都需在历史回测与样本外验证中谨慎评估。

MQL5 / C++
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,省一次无效矩阵拷贝。

MQL5 / C++
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 量级跳变,重建后最好做一次样本反降维,确认误差在可接受区间。

MQL5 / C++
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">//---
把特征工程交给小布盯盘
小布盯盘的 AIGC 已内置多指标降维后的状态切片,打开对应品种页即可看到压缩后的二维散点,你只管判断聚类边界。

常见问题

删指标是人为丢掉整列信息,PCA 是把原特征做线性重组,在尽量保留方差的前提下合成更少的新轴,信息损失可控。
不同指标量纲差太大,不标准化会被数值大的指标主导主成分方向,标准化后各特征权重才公平。
只能近似还原,载荷矩阵能告诉你每个主成分主要吃哪些原指标,但单点精确反向映射通常会丢失细节。
可以,品种页的 AIGC 模块自动拉取多指标序列做降维并出图,不用自己写 MQL5 测试脚本。
有可能,主成分只保大方差结构,极端小概率跳变若方差占比低就会被弱化,实盘需留一个未压缩的预警通道。