将互信息作为渐进特征选择的准则(基础篇)
「用互信息给特征做渐进筛选」
在 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 跑一遍看分区深度。
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 与秩数组的初始化,再补递归栈逻辑,就能拿到互信息估计值用于特征筛选。
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 数组,就能验证并列容差对秩相关结果的影响。
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++) {