一种采用纯MQL5语言实现的基于能量学习的特征选择算法·进阶篇
(2/3)·从加权近邻到能量函数,手把手用MQL5脚本跑通特征加权训练闭环
◍ FREL类的构造与权重求解入口
FREL 在 MT5 里被封装成 FREL 类,它继承自 PowellsMethod( Powell 优化在 Powells.mqh 里实现)。你真正要调用的只有 WeighVars() 一个方法,其余分箱、自举、优化都在内部跑完。
训练矩阵有固定结构:每行是一个样本,每列是一个候选变量,最后一列必须是目标值。构造函数最少吃一个 matrix 参数,另有两个可选整型参数 numboot(默认1,自举次数)和 bootsize(默认0,单样本量)。若 bootsize 大于样本数,numboot 会自动回退成1、bootsize 改成样本总量,这块用的时候要小心别踩坑。
WeighVars() 的参数里,num_bins_target 把目标值切成2到样本数之间的箱;reg_factor 是正双精度,0 即关正则化;index[] 回写按相关度降序的原始列号,weights[] 回写同序最优权重,总和会被规整到100方便读。方法返回 false 代表过程没跑完。
权重估计先抽目标列做分箱,再进 calc_wt() 做 bootstrap 打乱并调用父类 Optimize(),每次自举的最优权重累加到 w[] 后取平均。被最小化的损失函数在重写的 func() 里,最终平均权重写回用户数组前会排序并转成百分制。
下面这段是构造函数与 WeighVars() 开头的原文,逐行拆一下内存分配与参数守卫逻辑。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ FREL(matrix &in_data,class="type">int numboot=class="num">1, class="type">int bootsize=class="num">0) { m_data = in_data; m_num_boot=(numboot>class="num">0)?numboot:class="num">1; m_bootsize=(bootsize>class="num">2 && bootsize<=class="type">int(m_data.Rows()) && m_num_boot>class="num">1)?bootsize:class="type">int(m_data.Rows()); if(ArrayResize(m_indices, class="type">int(m_data.Rows()))!=class="type">int(m_data.Rows()) || ArrayResize(m_target_bin, class="type">int(m_data.Rows()))!=class="type">int(m_data.Rows()) || ArrayResize(m_trial_weights, class="type">int(m_data.Cols()-class="num">1))!=class="type">int(m_data.Cols()-class="num">1) || ArrayResize(m_work_weights, class="type">int(m_data.Cols()-class="num">1))!=class="type">int(m_data.Cols()-class="num">1) ) { Print(__FUNCTION__, " error ", GetLastError()); m_memory_allocated = false; } else m_memory_allocated = true; } class=class="str">"cmt">//+-----------------------------------------------------------------------+ class=class="str">"cmt">//| Find the most relevant variables from a dataset of candidate variables| class=class="str">"cmt">//+-----------------------------------------------------------------------+ class="type">bool WeighVars(class="type">int num_bins_target, class="type">class="kw">double reg_factor,class="type">int &index[],class="type">class="kw">double &weights[]) { if(!m_memory_allocated) { Print(" INTERNAL ERROR "); class="kw">return false; } if(num_bins_target<=class="num">1 || num_bins_target>class="type">int(m_data.Rows())) {
权重求解里的分箱与归一化细节
这段逻辑出现在某特征加权函数的后半段,先对目标向量做分箱,再反算各候选列的权重并归一。num_bins_target 若小于 2 会直接打印 invalid function parameter 并返回 false,这是调用方最容易踩的坑。 目标列通过 np::vecAsArray 从矩阵最后一列取出,bin_array 将连续收益/价格变化切成 k 段;若实际分出箱数 k 小于请求值,说明存在重复极值导致合并,函数同样返回 false。 权重计算走 calc_wt,随后用每列标准差乘指数权重:weights[var] = m_data.Col(var).Std() * exp(weights[var]),再累加进 sum。最终每层权重乘以 100.0/sum 转成百分比,并用 MathQuickSortDescending 按权重降序排好 index,方便后续截断-topN 特征。 在 MT5 里把 num_bins_target 设成 1 跑同一段,会立即在专家日志看到参数报错而非静默出 0 权重;外汇与贵金属数据波动大,分箱数过低可能让少数极端 K 线吞掉整段区间,实盘前建议先用历史样本测 5~10 箱的稳定性。
Print(__FUNCTION__, " invalid function parameter: num_bins_target. Parameter should be >=class="num">2 "); class="kw">return false; } class="type">int ret=class="num">0; class="type">class="kw">double target[], target_thresholds[] ; class="type">class="kw">double sum ; class="type">int n_cases = class="type">int(m_data.Rows()); m_reg_factor = MathAbs(reg_factor); m_loss = class="num">0.0; if(ArrayResize(index,class="type">int(m_data.Cols()-class="num">1))!=class="type">int(m_data.Cols()-class="num">1) || !np::vecAsArray(m_data.Col(m_data.Cols()-class="num">1),target) ) { Print(__FUNCTION__, " error ", GetLastError()); class="kw">return false; } class="type">int k = num_bins_target ; if(!bin_array(target, k, target_thresholds, m_target_bin)) class="kw">return false; if(k<num_bins_target) { Print("error bins of target vector ", num_bins_target," : ", k); class="kw">return false; } for(class="type">int i=class="num">0 ; i<n_cases ; i++) { if(m_target_bin[i] >= num_bins_target) { Print("error m_target_bin array at index ", i, " is ",m_target_bin[i], " should be less than ", num_bins_target); class="kw">return false; } } ret = calc_wt(num_bins_target,m_loss,weights); if(ret<class="num">0) class="kw">return false; sum = class="num">0.0 ; for(class="type">ulong var=class="num">0 ; var<m_data.Cols()-class="num">1 ; var++) { weights[var] = m_data.Col(var).Std() * exp(weights[var]); sum += weights[var] ; } for(class="type">ulong var=class="num">0 ; var<m_data.Cols()-class="num">1 ; var++) { weights[var] *= class="num">100.0 / sum ; index[var] = class="type">int(var) ; } MathQuickSortDescending(weights,index,class="num">0,class="type">int(weights.Size()-class="num">1)) ; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| calculates the optimal weights of candidate variables | class=class="str">"cmt">//+------------------------------------------------------------------+
「自助采样下的权重迭代怎么写」
这段代码是某加权模型里负责跑自助采样(bootstrap)并累加特征权重的核心函数。它接收目标分箱数、损失值引用和权重数组,先做好容量校验与归零,再进入多层循环。 外层按 m_num_boot 次重采样展开,每次先把 class_count 清零,用 Fisher-Yates 式的洗牌从 m_indices 里抽出不超过 m_bootsize 的子样本,并统计各目标分箱命中数。若某个分箱成员少于 2 个,会打印提示,说明分区数或自助样本量(Frel 相关参数)可能要调。 每次采样后调用 Optimize 拿到 m_trial_weights,把本次损失累加到 loss_value,再把临时权重叠加进 w。最终 w 是多次自助的平均倾向,开 MT5 把 m_num_boot 设成 200 对比 500,能直接看到 w 曲线稳定度变化,外汇与贵金属品种波动大,这类重采样对过拟合敏感,属高风险验证。 别把少于2成员的箱当噪声扔掉 原文提示分箱不足 2 人时只是 Print 警告而非中断,实盘前应先检查你自己的 m_bootsize 是否过小,否则权重会隐性偏向高频分箱。
class="type">int calc_wt(class="type">int num_bins_target,class="type">class="kw">double &loss_value, class="type">class="kw">double &w[]) { class="type">int ret,rand_error, class_count[] ; ret = class="num">0; if(ArrayResize(class_count,num_bins_target)!=num_bins_target || (w.Size()!=class="type">uint(m_data.Cols()-class="num">1) && ArrayResize(w,class="type">int(m_data.Cols()-class="num">1))!=class="type">int(m_data.Cols()-class="num">1))) { Print(__FUNCTION__, " error ", GetLastError()); class="kw">return -class="num">1; } ArrayInitialize(w,class="num">0.0); loss_value = class="num">0.0 ; for(class="type">ulong i=class="num">0 ; i<m_data.Rows() ; i++) m_indices[i] = class="type">int(i) ; for(class="type">int ibootstrap=class="num">0 ; ibootstrap<m_num_boot; ibootstrap++) { Comment(" Bootstrap iteration ", ibootstrap+class="num">1); ArrayInitialize(class_count,class="num">0); class="type">int ii, j, k, m; ii = class="type">int (m_data.Rows()) ; while(ii > class="num">1) { m = class="type">int (m_data.Rows()) - ii ; if(m >= m_bootsize) break ; j = (class="type">int)(MathRandomUniform(class="num">0.0,class="num">1.0,rand_error) * ii) ; if(j >= ii) j = ii - class="num">1 ; k = m_indices[m] ; m_indices[m] = m_indices[m+j] ; m_indices[m+j] = k ; --ii ; ++class_count[m_target_bin[m_indices[m]]] ; } for(class="type">int i=class="num">0 ; i<num_bins_target ; i++) { if(class_count[i] < class="num">2) Print(__FUNCTION__, " class at ", i, " has less than class="num">2 members. 考虑调整参数Frel(number of partitions or bootstrap sample size)"); } ArrayInitialize(m_trial_weights,class="num">0.0); ret += Optimize(m_trial_weights); loss_value += PowellsMethod::GetFret() ; for(class="type">ulong i=class="num">0 ; i<m_data.Cols()-class="num">1 ; i++) w[i] += m_trial_weights[i] ; }
◍ 权重平滑与惩罚项的代码落地
上面这段类方法把 bootstrap 汇总得到的权重数组做归一化,再交给 Powell 优化器去最小化损失。注意 w[i] /= double(m_num_boot) 这一步:它把累计权重除以自助采样次数,得到的其实是平均权重,而非原始累加值。
func() 里对参数 p[i] 做了硬边界处理:当 p[i] > 4.0 时,工作权重走 exp(4.0) + p[i] - 4.0 的线性外推,并对超出部分平方累加进 pen;当 p[i] < -3.0 时对称处理。区间内才用 exp(p[i]) 保证权重恒正。这个 4.0 与 -3.0 的阈值直接限定了优化器可探索的权重对数空间。
loss() 先算 total_loss 并除以样本行数 m_data.Rows() 做平均,再按 m_reg_factor 对权重平方加权(L2 正则)。若你在 MT5 里把 m_reg_factor 设为 0.0,就退化为纯经验损失,过拟合风险会明显上升——外汇与贵金属品种上这种风险尤其高,参数敏感期可能放大回测与实盘的偏离。
直接把这段拷进你的 EA 框架,改 m_num_boot 和 m_reg_factor 两个量,能在 30 秒内看到权重分布和损失值的变化倾向。
for(class="type">ulong i=class="num">0 ; i<m_data.Cols()-class="num">1; i++) w[i] /= class="type">class="kw">double(m_num_boot) ; class="kw">return ret ; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| function minimized by Powells optimization method | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">virtual class="type">class="kw">double func(const class="type">class="kw">double& p[]) { class="type">class="kw">double pen = class="num">0.0 ; for(class="type">ulong i=class="num">0 ; i<m_data.Cols()-class="num">1 ; i++) { if(p[i] > class="num">4.0) { m_work_weights[i] = exp(class="num">4.0) + p[i] - class="num">4.0 ; pen += (p[i] - class="num">4.0) * (p[i] - class="num">4.0) ; } else if(p[i] < -class="num">3.0) { m_work_weights[i] = exp(-class="num">3.0) + p[i] + class="num">3.0 ; pen += (p[i] + class="num">3.0) * (p[i] + class="num">3.0) ; } else m_work_weights[i] = exp(p[i]) ; } class="kw">return (loss(m_work_weights) + pen) ; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| calculates the loss function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double loss(class="type">class="kw">double &w[]) { class="type">class="kw">double totaloss = total_loss(w); totaloss/=class="type">class="kw">double(m_data.Rows()); if(m_reg_factor>class="num">0.0) { for(class="type">ulong i=class="num">0;i<m_data.Cols()-class="num">1;i++) totaloss+=m_reg_factor*pow(w[i],class="num">2.0); } class="kw">return totaloss; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| loss over all data | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double total_loss(class="type">class="kw">double &w[]) { class="type">int category,first, other ; class="type">class="kw">double distance, top, bottom, loss ; loss = class="num">0.0 ;
近邻边界的距离惩罚怎么写
这段逻辑在做一件事:对 bootstrap 抽出的每个样本,找同类和异类里离它最近的两个点,用两者距离差衡量分类边界清晰度。距离差越大,说明该类样本被推得离异类越远,边界越干净。 外层循环遍历 m_bootsize 个索引,other 是当前样本,category 是它的标签。top 和 bottom 先置为 DBL_MAX,准备分别承接「同类最近距离」和「异类最近距离」。 内层先跳过自身,用加权绝对值差累加 distance:权重 w[v] 乘上两样本在第 v 维的特征差。同类时刷新 top,异类时刷新 bottom,最终 distance = top - bottom。 惩罚项分两段:差大于 30.0 直接加线性距离,否则加 log(1+exp(distance)) 的平滑项。后者在距离接近 0 时梯度温和,避免边界样本被过度拉开。外汇与贵金属行情噪声大,这类边界度量在实盘特征上可能偏向过拟合,开 MT5 把 30.0 阈值和 w[] 权重打印出来比对几组 K 线片段更有感。
for(class="type">int i=class="num">0; i<m_bootsize; i++) { other = m_indices[i] ; category = m_target_bin[other] ; top = bottom = DBL_MAX ; for(class="type">int iother=class="num">0 ; iother<m_bootsize; iother++) { first = m_indices[iother] ; if(first == other) class="kw">continue ; distance = class="num">0.0 ; for(class="type">ulong v=class="num">0 ; v<m_data.Cols()-class="num">1; v++) { distance += w[v] * fabs(m_data[other][v] - m_data[first][v]) ; } if(m_target_bin[first] == category) { if(distance < top) top = distance ; } else { if(distance < bottom) bottom = distance ; } } distance = top - bottom ; if(distance > class="num">30.0) loss += distance ; else loss += log(class="num">1.0 + exp(distance)); } class="kw">return loss ; }