将互信息作为渐进特征选择的准则·进阶篇
📊

将互信息作为渐进特征选择的准则·进阶篇

(2/3)·连续变量互信息估计误差常让特征选择跑偏,本文给出可落地的密度估计与选择算法

含代码示例实战向 第 2/3 篇

很多交易者在做特征工程时直接套用固定分箱算互信息,结果选出来的指标在样本外几乎失效。连续分布下密度估计对参数极度敏感,选错窗宽就等于给模型喂了噪声。本篇接着第1篇的概念,把估计与选择流程拆成可复现的代码路径。

切分可行性判定与象限期望计数

在 KD 树式空间切分里,先要确认当前区块能不能继续二分。代码用 m_tied_ranks 数组标记某一 rank 行是否全部绑定,若中心行上下碰到全绑定,就把 Y_AllTied 置 0 并偏移 centerY 跳出,X 方向同理;只要 X_AllTied 或 Y_AllTied 任一成立,splittable 直接赋 0,不再往下算。 若还能切,程序把区域按 centerX / centerY 拆成四个 trial 子块,trialXstart[0]=trialXstart[1]=fullXstart、trialXstop[0]=trialXstop[1]=centerX,其余象限类似偏移 1 格。这里 fullXstop-fullXstart+1 是父块宽,子块宽占比乘高占比,就是该象限期望样本数 expected[i],循环里用 1.0 强制浮点防截断。 actual 数组先清 0,再扫 currentDataStart 到 currentDataStop 的索引,按 x[k] 与 centerX、m_ranks[k] 与 centerY 的大小落进四个 actual 计数桶。拿 expected 和 actual 比对,就能判断这次切分是否让样本分布接近均匀——外汇与贵金属行情里这种均匀性假设常失效,高杠杆下误切可能放大信号噪声,开 MT5 把这段塞进自定义指标 OnInit 后打印 expected/actual 差值即可验证。

MQL5 / C++
if(! m_tied_ranks[centerY-ioff])
  {
   Y_AllTied = class="num">0 ;
   centerY -= ioff ;
   break ;
  }
if(centerY + ioff == fullYstop)
   break ;
if(! m_tied_ranks[centerY+ioff])
  {
   Y_AllTied = class="num">0 ;
   centerY += ioff ;
   break ;
  }
   }
  }
if(X_AllTied  ||  Y_AllTied)
   splittable = class="num">0 ;
else
  {
   trialXstart[class="num">0] = trialXstart[class="num">1] = fullXstart ;
   trialXstop[class="num">0] = trialXstop[class="num">1] = centerX ;
   trialXstart[class="num">2] = trialXstart[class="num">3] = centerX+class="num">1 ;
   trialXstop[class="num">2] = trialXstop[class="num">3] = fullXstop ;
   trialYstart[class="num">0] = trialYstart[class="num">2] = fullYstart ;
   trialYstop[class="num">0] = trialYstop[class="num">2] = centerY ;
   trialYstart[class="num">1] = trialYstart[class="num">3] = centerY+class="num">1 ;
   trialYstop[class="num">1] = trialYstop[class="num">3] = fullYstop ;
   for(i=class="num">0 ; i<class="num">4 ; i++)
      expected[i] = (currentDataStop - currentDataStart + class="num">1) *
             (trialXstop[i]-trialXstart[i]+class="num">1.0) / (fullXstop-fullXstart+class="num">1.0) *
             (trialYstop[i]-trialYstart[i]+class="num">1.0) / (fullYstop-fullYstart+class="num">1.0) ;
   actual[class="num">0] = actual[class="num">1] = actual[class="num">2] = actual[class="num">3] = class="num">0 ;
   for(i=currentDataStart ; i<=currentDataStop ; i++)
     {
     k = m_indices[i] ;
     if(x[k] <= centerX)
       {
        if(m_ranks[k] <= centerY)
           ++actual[class="num">0] ;
        else
           ++actual[class="num">1] ;
       }
     else
       {
        if(m_ranks[k] <= centerY)
           ++actual[class="num">2] ;

「卡方不达标时的 4×4 细分兜底」

当 2×2 分割的卡方统计量 testval 未超过临界值 m_chi_crit,且样本窗口在两个维度上都大于 30 点时,算法不会直接放弃独立性判定,而是把区域在 X、Y 方向各切成四等份做 4×4 细分。 代码先以 fullXstart-1、fullYstart-1 为游标,循环算出 xcut[]、ycut[] 四个切割点,并由此得到每段占整体的比例 xfrac[]、yfrac[]。期望频数 expected[ix*4+iy] 就是对应 x、y 比例相乘再乘上 currentDataStop-currentDataStart+1 的总样本数。 随后把 actual44[16] 清零,遍历 currentDataStart 到 currentDataStop 的每一个索引 k,用两层 ix、iy 的 3 次比较定位该点落在哪个 1/4 子格里——注意循环只跑到 <3,因为落进最后一段时 ix、iy 保持 3 即可,无需再比。 这种兜底让样本够大但分布偏态时,仍可能检出 X/Y 联合分布的非独立结构;外汇与贵金属行情里这类样本区间往往伴随流动性断层,实盘验证前务必认清高杠杆下的回测偏差风险。

MQL5 / C++
      else
            ++actual[class="num">3] ;
         }
      }
      testval = class="num">0.0 ;
      for(i=class="num">0 ; i<class="num">4 ; i++)
      {
         diff = fabs(actual[i] - expected[i]) - class="num">0.5 ;
         testval += diff * diff / expected[i] ;
      }
      splittable = (testval > m_chi_crit)  ?  class="num">1 : class="num">0 ;
if(! splittable && fullXstop-fullXstart > class="num">30 && fullYstop-fullYstart > class="num">30)
     {
        ipx = fullXstart - class="num">1 ;
        ipy = fullYstart - class="num">1 ;
        for(i=class="num">0 ; i<class="num">4 ; i++)
         {
          xcut[i] = (fullXstop - fullXstart + class="num">1) * (i+class="num">1) / class="num">4 + fullXstart - class="num">1 ;
          xfrac[i] = (xcut[i] - ipx) / (fullXstop - fullXstart + class="num">1.0) ;
          ipx = xcut[i] ;
          ycut[i] = (fullYstop - fullYstart + class="num">1) * (i+class="num">1) / class="num">4 + fullYstart - class="num">1 ;
          yfrac[i] = (ycut[i] - ipy) / (fullYstop - fullYstart + class="num">1.0) ;
          ipy = ycut[i] ;
         }
        for(ix=class="num">0 ; ix<class="num">4 ; ix++)
         {
          for(iy=class="num">0 ; iy<class="num">4 ; iy++)
            {
             expected[ix*class="num">4+iy] = xfrac[ix] * yfrac[iy] *
                                  (currentDataStop - currentDataStart + class="num">1) ;
             actual44[ix*class="num">4+iy] = class="num">0 ;
            }
         }
        for(i=currentDataStart ; i<=currentDataStop ; i++)
         {
          k = m_indices[i] ;
          for(ix=class="num">0 ; ix<class="num">3 ; ix++)
            {
             if(x[k] <= xcut[ix])
               break ;
            }
          for(iy=class="num">0 ; iy<class="num">3 ; iy++)
            {

◍ 卡方阈值决定区域是否值得再切

这段逻辑在做一件事:用卡方统计量判断当前样本区是否还能继续分裂成更细的子区间。testval 累加的是各网格单元实际频数与期望频数偏差的标准化平方,若 testval 超过 3 倍临界值 m_chi_crit,splittable 置 1,否则放弃细分。 一旦 splittable 成立,代码把当前数据索引拷进 current_indices,并按四个子区间的实际样本量 actual[iSubRec] 压栈。注意判据 actual[iSubRec] >= 3:子样本少于 3 条时不入栈,避免在外汇小时图这类稀疏行情里切出无意义微区,贵金属跳空段也同理。 压栈时若 ArrayResize 失败直接返回 EMPTY_VALUE 并打印错误,说明内存或栈上限出了问题。开 MT5 把 m_chi_crit 从默认调小一档,可能让更多区域通过分裂,但过拟合概率会上升;外汇与贵金属属高风险品种,参数改动前先用历史数据回测。

MQL5 / C++
if(m_ranks[k] <= ycut[iy])
   break ;
   }
   ++actual44[ix*class="num">4+iy] ;
  }
   testval = class="num">0.0 ;
   for(ix=class="num">0 ; ix<class="num">4 ; ix++)
   {
    for(iy=class="num">0 ; iy<class="num">4 ; iy++)
     {
      diff = fabs(actual44[ix*class="num">4+iy] - expected[ix*class="num">4+iy]) - class="num">0.5 ;
      testval += diff * diff / expected[ix*class="num">4+iy] ;
     }
   }
   splittable = (testval > class="num">3 * m_chi_crit)  ?  class="num">1 : class="num">0 ;
    }
   }
   if(splittable)
    {
     for(i=currentDataStart ; i<=currentDataStop ; i++)
       current_indices[i] = m_indices[i] ;
     ipos = currentDataStart ;
     for(iSubRec=class="num">0 ; iSubRec<class="num">4 ; iSubRec++)
      {
       if(actual[iSubRec] >= class="num">3)
        {
         stack[nstack].Xstart = trialXstart[iSubRec] ;
         stack[nstack].Xstop = trialXstop[iSubRec] ;
         stack[nstack].Ystart = trialYstart[iSubRec] ;
         stack[nstack].Ystop = trialYstop[iSubRec] ;
         stack[nstack].DataStart = ipos ;
         stack[nstack].DataStop = ipos + actual[iSubRec] - class="num">1 ;
         ++nstack ;
         if(ArrayResize(stack,nstack+class="num">1,class="num">256)<class="num">0)
          {
           Print(__FUNCTION__," arrayresize error ", GetLastError());
           class="kw">return EMPTY_VALUE;
          }
        if(iSubRec == class="num">0)
         {
          for(i=currentDataStart ; i<=currentDataStop ; i++)
           {
            k = current_indices[i] ;
            if(x[k] <= centerX  &&  m_ranks[k] <= centerY)

象限裁剪时的索引落桶逻辑

上面这段是 KD 散点递归划分里,按子象限把样本索引写回 m_indices 的核心分支。iSubRec 取 0~3 分别对应左上、左下、右上、右下四个区域,每个区域用一层 for 从 currentDataStart 扫到 currentDataStop。 以 iSubRec==1 为例:k = current_indices[i] 取出原始序号,判定 x[k] <= centerX 且 m_ranks[k] > centerY 时,才把该索引塞进 m_indices[ipos++];其余三个分支只是把比较符号翻转,逻辑同构。 在 EURUSD 的 M15 上跑 3000 根 K 线,这种按 centerX/centerY 切分的方式会把约 25% 的样本落进单一象限,递归深度每加一层,m_indices 的有效长度就收缩一轮。外汇与贵金属杠杆高,回测结论仅代表历史概率,实盘需自担风险。

MQL5 / C++
        m_indices[ipos++] = current_indices[i] ;
          }
         }
         else
          if(iSubRec == class="num">1)
           {
            for(i=currentDataStart ; i<=currentDataStop ; i++)
              {
               k = current_indices[i] ;
               if(x[k] <= centerX  &&  m_ranks[k] > centerY)
                 m_indices[ipos++] = current_indices[i] ;
              }
           }
           else
            if(iSubRec == class="num">2)
             {
              for(i=currentDataStart ; i<=currentDataStop ; i++)
               {
                k = current_indices[i] ;
                if(x[k] > centerX  &&  m_ranks[k] <= centerY)
                  m_indices[ipos++] = current_indices[i] ;
               }
             }
             else
              {
               for(i=currentDataStart ; i<=currentDataStop ; i++)
                {
                 k = current_indices[i] ;
                 if(x[k] > centerX  &&  m_ranks[k] > centerY)
                   m_indices[ipos++] = current_indices[i] ;
                }
              }
      }
      else
       {
        if(actual[iSubRec] > class="num">0)

「互信息累加的两种区间写法」

这段逻辑在算互信息(MI)的逐格累加,核心是把 X、Y 两维的区间占比乘上 log 比值后求和。外汇与贵金属市场高频序列里,这种计算对样本窗口 m_size 极度敏感,m_size 偏小会让 px、py 抖动剧烈,MI 可能虚高。 子记录分支里,trialXstop[iSubRec] 减 trialXstart[iSubRec] 加 1.0 再除 m_size,得到单子区间的边际概率;actual[iSubRec] 除 m_size 是联合概率,最后 pxy * log(pxy/(px*py)) 进 MI。全区间分支不走子记录,直接用 fullX/fullY 与 currentData 的起止差,公式结构一致但少一层循环。 开 MT5 把这段塞进自定义指标算 EURUSD 的 M15 互信息,m_size 从 200 调到 500,曲线平滑度通常有肉眼可见变化,属于可立刻验证的调参动作。

MQL5 / C++
      {
         px = (trialXstop[iSubRec] - trialXstart[iSubRec] + class="num">1.0) / m_size ;
         py = (trialYstop[iSubRec] - trialYstart[iSubRec] + class="num">1.0) / m_size ;
         pxy = (class="type">class="kw">double) actual[iSubRec] / m_size ;
         MI += pxy * log(pxy / (px * py)) ;
      }
   }
}
         px = (fullXstop - fullXstart + class="num">1.0) / m_size ;
         py = (fullYstop - fullYstart + class="num">1.0) / m_size ;
         pxy = (currentDataStop - currentDataStart + class="num">1.0) / m_size ;
         MI += pxy * log(pxy / (px * py)) ;
class="kw">return MI;

◍ 用 MRMR 绕开联合依赖的计算墙

做预测变量筛选时,真正难的是联合依赖性:它衡量的是一整组变量和目标变量的整体信息耦合,不是单个特征各自说了算。维度一高,多维分箱立刻稀疏到没法算,再加上组合爆炸,穷举子集在中等规模特征集上就已经不现实。 前向逐步选择是工程上最常用的妥协方案:从空集开始,每步挑一个让当前准则提升最多的变量。它快,但是贪婪的——两个单独都不强、凑一起却显著协同的特征,往往被它漏掉。Peng、Long 和 Ding 的 MRMR 准则给了另一条路:不显式算联合依赖性,也能逼近最优子集。

MRMR 只用了两个量:相关性和冗余性。相关性是候选特征 Xi 与目标 Y 的平均互信息,即 (1/S)·Σ I(Xi,Y);冗余性是 Xi 与已选集 S 中每个特征的平均互信息,即 (1/S)·Σ I(Xi,Xj)。表达式一模一样,区别只在语境——对着 Y 叫相关,对着已选特征叫冗余。

算法步骤很直接:先选 I(Xi,Y) 最高的那个特征进集;之后每步选使 I(Xi,Y) 减去与 S 的平均冗余最大的 Xi。原始论文证明了,这种最大相关最小冗余的贪心法,能在不可行计算联合依赖时有效逼近最优。外汇与贵金属行情受宏观与流动性多重驱动,变量间高度纠缠,用这套筛法前务必清楚:高维筛选仍只是概率性逼近,实盘属高风险。 显著性别拍脑袋,两种蒙特卡洛置换(MCP)检验可直接搬:单特征检验把观察相关性对特征置换后的 0 分布比,显著高就说明真有信息;整体检验把所选集相关性累积和对目标变量置换后的 0 分布比,显著高才说明这组变量整体可能预测 Y。开 MT5 接历史 tick,跑一遍置换就能知道你筛出的特征是不是纯噪声。

把互信息扫描交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到候选变量的互信息排序,你只需判断哪些符合价格行为逻辑。

常见问题

没有通用最优σ,交叉验证结合数据尺度缩放是常见做法;本篇代码用自适应分区绕开了这个敏感参数。
迭代细分会带来额外计算,但集中在高信息区,合成数据上通常比细密固定分箱更省且更准。
可以,小布的品种页已内置AIGC诊断,自动输出候选预测变量的互信息参考,省去自己写MQL5脚本。
适用但需注意高频噪声,贵金属与外汇杠杆品种波动结构不同,冗余阈值可能要重调,属高风险试探。
估计偏高会引入冗余变量,偏低会漏掉非线性依赖,两者都让渐进选择在第3篇回测中表现不稳。