将互信息作为渐进特征选择的准则·进阶篇
(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 差值即可验证。
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 联合分布的非独立结构;外汇与贵金属行情里这类样本区间往往伴随流动性断层,实盘验证前务必认清高杠杆下的回测偏差风险。
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 从默认调小一档,可能让更多区域通过分裂,但过拟合概率会上升;外汇与贵金属属高风险品种,参数改动前先用历史数据回测。
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 的有效长度就收缩一轮。外汇与贵金属杠杆高,回测结论仅代表历史概率,实盘需自担风险。
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,曲线平滑度通常有肉眼可见变化,属于可立刻验证的调参动作。
{
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,跑一遍置换就能知道你筛出的特征是不是纯噪声。