将互信息作为渐进特征选择的准则(基础篇)
📘

将互信息作为渐进特征选择的准则(基础篇)

第 1/3 篇

「用互信息给特征做渐进筛选」

在 MT5 的策略研究里,特征冗余会拖慢模型收敛并放大过拟合。把互信息(Mutual Information)当作渐进特征选择的判别量,能按变量对目标的信息贡献逐轮纳入或剔除。 具体做法是:每轮计算候选特征与收益标签的互信息值,设定阈值(例如 0.05 nats),高于阈值的进入下一轮,低于的暂弃;随着样本窗口滑动,特征集可能从 12 个收敛到 4~6 个。 实盘前请在 MT5 用历史数据回测该选择流程,观察特征数下降后样本外误差是否同步走低,外汇与贵金属品种波动剧烈,信号失效风险高,结论仅具概率倾向。

互信息为何能挖出隐藏的依赖

互信息在处理非线性耦合时比相关系数更诚实:它不假设线性关系,而是直接度量一个变量携带另一个变量多少信息量,因此常能抓出皮尔逊系数看不见的预测线索。 Hanchuan Peng、Fuhui Long 和 Chris Ding 在《基于互信息的特征选择》里给出的框架,把特征选择拆成三条准则——最大依赖性、最大相关性、最小冗余性,后续我们要落地的就是这套 mRMR 思路。 本篇先解决一个前置麻烦:连续变量的互信息没法直接数格子,得靠核密度或 k 近邻去估计;估完才能谈挑选。后面会先用合成数据验证算法,再塞进真实行情特征里看冗余度压下来之后模型是否更干净。

◍ 连续互信息估计:从分箱到自适应分区

连续变量的互信息 I(X;Y) 不能像离散变量那样直接求和,因为取值域无限,必须先处理概率密度未知的问题。最朴素的做法是固定宽度分箱后套离散公式,但实测中对 bin 数量极度敏感——bin 选错,估计值能波动数倍,基本没法用在选变量上。 帕尔森窗(Parzen window)绕开了分箱,用滑动窗直接在样本上估密度:窗中心附近的点权重高、远处衰减,全定义域积分应为 1。常用高斯窗,带一个尺度参数 σ。麻烦的是 σ 稍微选偏,密度估计就抖得厉害,所以它也不适合对精度敏感的预测因子评估。 自适应分区是更靠谱的路线。它从粗划分起步,对每个分区算互信息,超阈值就递归细分子区,把算力压在信息密集处,避开固定分箱过度平滑或过度拟合的坑。递归停太早会低估(向下偏差),停太晚会被噪声推高估计值。 细分与否靠卡方检验把关:把分区切成 2×2 列联表,算观测与独立假设下期望的偏差,自由度 1 的临界值做界。2×2 不显著但分区仍大,再上 4×4 检验兜底复杂关系;都不显著就冻结该区,显著则拆四块,过小的作终节点。这样能在 MT5 里自己写个候选变量筛选器,先跑一遍自适应分区再进模型。

「互信息估计里的递归切分怎么管」

mutualinfo.mqh 里的 Capm 类用自适应分区来估连续变量的互信息,递归切分过程靠一个自定义结构体 IntStack 记账。它六个成员分三组:Xstart/Xstop 框住某变量在分区内的排名索引范围,Ystart/Ystop 框住另一变量,DataStart/DataStop 则记下该包围盒里数据点在总表中的起止索引,供子分区直接切片。 构造函数 Capm(no_split, dep_vals, chi_critical_value=6.0) 先落两个关键开关:no_split 决定近似相等的值是否被强制归同一区,chi_critical_value 是卡方检验阈值,一般取 4.0~8.0,6.0 是常用折中。随后分配索引、临时值、排名数组,把 dep_vals 拷进临时向量排序,原序存 m_indices,再回写排名并标并列值。 fit() 方法接收一个同维向量,算它与构造时变量的互信息;多次传不同向量就能批量扫特征。算法把整集压栈作根分区,循环弹栈做卡方检验,显著就拆四块重压栈,不显著或点过少则算该终端区的联合/边缘概率并累加。遇错返回 EMPTY_VALUE。外汇与贵金属数据噪声大,卡方阈值设太低易过拟合,实盘前建议在 MT5 用历史 tick 跑一遍看分区深度。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| class="type">int type stack structure                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct IntStack
  {
   class="type">int Xstart ;      class=class="str">"cmt">// X value(rank) at which this rectangle starts
   class="type">int Xstop ;       class=class="str">"cmt">// And stops
   class="type">int Ystart ;      class=class="str">"cmt">// Ditto for Y
   class="type">int Ystop ;
   class="type">int DataStart ;   class=class="str">"cmt">// Starting index into indices for the cases in this
   class="type">int DataStop ;    class=class="str">"cmt">// rectangle, and the(inclusive) ending index
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  constructor                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
Capm::Capm(class="type">bool no_split,vector &dep_vals, class="type">class="kw">double chi_critical_value = class="num">6.0)
  {
   m_no_split = no_split;
   m_size = class="type">int(dep_vals.Size()) ;
   m_chi_crit = chi_critical_value;
   if(ArrayResize(m_indices,m_size)<class="num">0 || !m_tempvals.Resize(m_size) || ArrayResize(m_ranks,m_size)<class="num">0
      || (m_no_split && ArrayResize(m_tied_ranks,m_size)<class="num">0))
     {
      Print(__FUNCTION__, " Arrayresize error ", GetLastError());
      class="kw">return;
     }
   for(class="type">int i=class="num">0 ; i<m_size ; i++)
     {
      m_tempvals[i] = dep_vals[i] ;
      m_indices[i] = i ;
     }
   qsortdsi(class="num">0, m_size-class="num">1, m_tempvals, m_indices) ;
   for(class="type">int i=class="num">0 ; i<m_size ; i++)
     {
      m_ranks[m_indices[i]] = i ;
      if(! m_no_split)
        class="kw">continue ;
      if(i < m_size-class="num">1  &&
         m_tempvals[i+class="num">1] - m_tempvals[i] < class="num">1.e-12 * (class="num">1.0+fabs(m_tempvals[i])+fabs(m_tempvals[i+class="num">1])))
        m_tied_ranks[i] = class="num">1 ;
      else
        m_tied_ranks[i] = class="num">0 ;
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

自适应划分法算互信息的类骨架

用自适应划分(Adaptive Partitioning)估计连续变量的互信息,核心思路是把样本空间按卡方检验递归切分,而不是事先固定网格。下面这段 MQL5 类声明给出了最小可用接口:构造时传入因变量秩、是否尊重结值(ties),以及卡方临界值,默认 6.0。 默认 chi_critical_value = 6.0 对应自由度近似 1 时的显著性门槛,调小会更激进地切分、调大则倾向少拆。m_no_split 为 true 时会保留相等观测的结,避免把本应同秩的样本强行分开。 fit() 接收一列原始向量 raw,内部先拷贝到 m_tempvals 并把 m_indices 初始化为 0..m_size-1,相当于给每个样本发一张『身份证』,后续所有切分只动索引、不搬数据。这种写法在样本量上千时也能把拷贝开销压到最低。 开 MT5 自建 EA 时,直接把下面类头和方法前段贴进 include 文件,先跑通 m_size 与秩数组的初始化,再补递归栈逻辑,就能拿到互信息估计值用于特征筛选。

MQL5 / C++
class=class="str">"cmt">//| Mutual information class="kw">using the Adaptive partitioning method                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class Capm
  {
class="kw">public:
                        Capm(class="type">bool no_split,vector &dep_vals, class="type">class="kw">double chi_critical_value = class="num">6.0) ;
                       ~Capm(class="type">void) ;
  class="type">class="kw">double                fit(vector &raw) ;
class="kw">private:
  class="type">bool                  m_no_split;    class=class="str">"cmt">// respect ties
  class="type">int m_size ;                          class=class="str">"cmt">// Number of cases
  class="type">int m_ranks[] ;                       class=class="str">"cmt">// &class="macro">#x27;Dependent&class="macro">#x27; variable ranks
  class="type">int m_tied_ranks[] ;                  class=class="str">"cmt">// tied[i] != class="num">0 if case with rank i == case with rank i+class="num">1
  class="type">class="kw">double m_chi_crit ;                   class=class="str">"cmt">// Chi-square test criterion
  class="type">int                   m_indices[];    class=class="str">"cmt">// indices
  vector                m_tempvals;     class=class="str">"cmt">// temp values
  } ;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  mutual information for continuous variables                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double Capm::fit(vector &raw)
  {
  class="type">int i, k, ix, iy, nstack, splittable ;
  class="type">int current_indices[], x[], fullXstart, fullXstop, fullYstart, fullYstop, ipos ;
  class="type">int trialXstart[class="num">4], trialXstop[class="num">4], trialYstart[class="num">4], trialYstop[class="num">4] ;
  class="type">int ipx, ipy, xcut[class="num">4], ycut[class="num">4], iSubRec, x_tied[], ioff ;
  class="type">int X_AllTied, Y_AllTied ;
  class="type">int centerX, centerY, currentDataStart, currentDataStop ;
  class="type">int actual[class="num">4], actual44[class="num">16] ;
  vector expected(class="num">16), xfrac(class="num">4), yfrac(class="num">4) ;
  class="type">class="kw">double diff, testval;
  class="type">class="kw">double px, py, pxy, MI ;
  IntStack stack[];
  if(ArrayResize(stack,class="num">1,class="num">256)<class="num">0)
    {
     Print(__FUNCTION__," arrayresize error ", GetLastError());
     class="kw">return EMPTY_VALUE;
    }
  if(ArrayResize(current_indices,m_size)<class="num">0 || ArrayResize(x,m_size)<class="num">0
     || (m_no_split && ArrayResize(x_tied,m_size)<class="num">0))
    {
     Print(__FUNCTION__, " Arrayresize error ", GetLastError());
     class="kw">return EMPTY_VALUE;
    }
  for(i=class="num">0 ; i<m_size ; i++)
    {
     m_tempvals[i] = raw[i] ;
     m_indices[i] = i ;
    }

◍ Kendall 秩相关的并列值处理与分治栈

下面这段是 Kendall Tau 计算里收尾的分治逻辑:先对临时数组做快速排序得到秩索引,再把秩映射回原位置,并标记那些数值差小于 1e-12*(1+a+b) 的并列点。

若 m_no_split 为假,则对相邻元素做容差比较,差足够小就记 x_tied[i]=1,否则为 0;这个 1e-12 的相对容差能避免浮点噪声把本应并列的报价拆成不同秩。外汇与贵金属 tick 数据常有重复报价,忽略这一步会让相关性估计偏高,属高风险数据陷阱。 随后用 stack 结构体把 X/Y 的起止区间和原始数据区间压栈,nstack 从 1 开始循环弹出;centerX、centerY 取区间中点,若中点落在 x_tied / m_tied_ranks 标记位则向两侧偏移寻找非并列中心,保证分割点不卡在并列块里。 直接把下面代码贴进 MT5 的自定义指标或脚本里,配合你自己的 m_tempvals 数组,就能验证并列容差对秩相关结果的影响。

MQL5 / C++
  if(!qsortdsi(class="num">0, m_size-class="num">1, m_tempvals, m_indices))
    {
      Print(__FUNCTION__, " Arrayresize error ", GetLastError());
      class="kw">return EMPTY_VALUE;
    }
  for(i=class="num">0 ; i<m_size ; i++)
    {
      x[m_indices[i]] = i ;
      if(! m_no_split)
        class="kw">continue ;
      if(i < m_size-class="num">1  &&
        m_tempvals[i+class="num">1] - m_tempvals[i] < class="num">1.e-12 * (class="num">1.0+fabs(m_tempvals[i])+fabs(m_tempvals[i+class="num">1])))
        x_tied[i] = class="num">1 ;
      else
        x_tied[i] = class="num">0 ;
    }
  for(i=class="num">0 ; i<m_size ; i++)
    {
      m_indices[i] = i ;
    }
  stack[class="num">0].Xstart = class="num">0 ;
  stack[class="num">0].Xstop = m_size-class="num">1 ;
  stack[class="num">0].Ystart = class="num">0 ;
  stack[class="num">0].Ystop = m_size-class="num">1 ;
  stack[class="num">0].DataStart = class="num">0 ;
  stack[class="num">0].DataStop = m_size-class="num">1 ;
  nstack = class="num">1 ;
  MI = class="num">0.0 ;
  class="kw">while(nstack > class="num">0)
    {
      --nstack ;
      fullXstart = stack[nstack].Xstart ;
      fullXstop = stack[nstack].Xstop ;
      fullYstart = stack[nstack].Ystart ;
      fullYstop = stack[nstack].Ystop ;
      currentDataStart = stack[nstack].DataStart ;
      currentDataStop = stack[nstack].DataStop ;
      centerX = (fullXstart + fullXstop) / class="num">2 ;
      X_AllTied = (x_tied.Size())  && (x_tied[centerX] != class="num">0) ;
      if(X_AllTied)
        {
         for(ioff=class="num">1 ; centerX-ioff >= fullXstart ; ioff++)
           {
            if(! x_tied[centerX-ioff])
              {
               X_AllTied = class="num">0 ;
               centerX -= ioff ;
               break ;
              }
            if(centerX + ioff == fullXstop)
              break ;
            if(! x_tied[centerX+ioff])
              {
               X_AllTied = class="num">0 ;
               centerX += ioff ;
               break ;
              }
           }
        }
      centerY = (fullYstart + fullYstop) / class="num">2 ;
      Y_AllTied = (m_tied_ranks.Size())  && (m_tied_ranks[centerY] != class="num">0) ;
      if(Y_AllTied)
        {
         for(ioff=class="num">1 ; centerY-ioff >= fullYstart ; ioff++)
           {

常见问题

相关系数只测线性或单调关系,互信息测任意统计依赖;非线性隐藏耦合它也能给出非零值,适合渐进式剔掉无关特征。
简单分箱会丢细节,自适应分区按数据密度切分更稳;样本少时箱宽调大防空箱,样本多可细化以提高分辨。
小布可对接你的行情与因子数据,直接跑互信息排序并标出低信息特征,你只需看结果决定删哪个。
设最小样本数或互信息增益阈值,低于就停;盲目切深会过拟合噪声,反而给出虚高互信息。
并列值按平均秩赋值,分治栈里单独计tie项;不处理会偏保守,低估秩相关进而干扰筛选顺序。