以 MQL5 实现强化分类任务的融汇方法·综合运用
(3/3)·从交集联合到模糊积分,把离散决策与排位输出拧成一条可执行流水线
许多交易者把不同分类器直接投票拼接,却忽略组件模型只在互斥类上训练、输出可能是二元或排位。当短期噪声被长期趋势淹没,融汇预测反而比单模型更歪。把置信度转成排位,往往是被低估的第一步。
- 用排位交集砍掉类集合
- 集成分类里的排序与剪枝逻辑
- 从最坏情况切换到最佳情况:联合规则的排位逻辑
- 集成分类的排序与投票实现
- 用逻辑回归给分类器动态加权
- 集成模型的概率化再训练
- 多分类梯度对象的构造与权重初始化
- 多分类下的损失梯度怎么算
- 多分类梯度与softmax数值稳定拆解
- 逻辑回归里的L2罚项与半二项式梯度
- 多项式逻辑回归的拟合与内存自管
- logit 模型的概率输出与批量推理
- 从概率向量里抠出类别与系数
- 给每类单独配权重的逻辑回归融汇
- 归一化权重后返回最优索引
- 按邻居命中率挑最靠谱的模型
- KNN 交叉验证里的留一法实现
- 交叉验证里挑最优 K 值
- KNN 距离排序与多模型择优的实现细节
- 用模糊积分替多个模型做加权表决
- 模型权重归一与正则参数的二分求根
- 集成分类里的模糊聚合逻辑
- 用二元分类器拼出多类概率矩阵
- 模型权重迭代里的归一化细节
- 用极小阈值切断迭代
- 用蒙特卡洛压测融汇分类器的健壮性
- 多层感知机分类器的骨架与默认参数
- 把多层感知机封装成可训练的预测类
- 集成分类器的变量声明与启动初始化
- 多模型分类误差的蒙特卡洛初始化
- 对抗样本怎么喂进分类模型
- 一对多分类器的配对训练循环
- 三种集成模型的错分率统计写法
- 交集与并集集成下的误判计数逻辑
- 融合模型与投票集成的误差统计写法
- 三类集成模型的错分率怎么跑出来
- 集成分类器的误差汇总与打印
- 集成分类误差的日志输出与实测读数
- 集成分类器的误差实测对比
- 集成模型在 BTCUSD 日线上的误差实测
- 集成分类器的误差日志怎么读
- 集成分类器在 BTCUSD D1 上的误差实测
- 一点提醒
◍ 用排位交集砍掉类集合
交集方法是一种专门的分类器组合思路,目标不是普通分类,而是把候选类集合尽可能缩小。它要求每个组件模型对输入样本给出从最可能到最不可能的整体类排位,再取各模型排位靠前子集的交集,留下共有类。 训练阶段要算清楚每个模型必须保留的「最小排位靠前数量」。原文示例里 4 个模型在 5 个训练样本上的最差排位分别是 8、21、12、9——也就是说模型 2 哪怕真实类排到第 21 位也得兜住。fit() 就是干这个:跟踪最坏情况,确保训练集真实类不丢。 新样本来了,各模型按自己的阈值取排位靠前类再做交集。理论干净,但实战有坑:跨模型排位靠前子集若没有共用元素,未知样本可能得到空子集;而且紧盯最坏情况往往让保留的类过多,效率和缩减效果都打折。对外汇或贵金属信号多模型投票来说,这类方法对差模型敏感,直接用的性价比偏低,更像是给联合方法铺路。 别把排位交集当万能筛 它依赖所有组件模型在整个类空间表现一致才稳。若你接的是不同品种专精模型(比如黄金模型和欧美模型各管一摊),交集容易要么过空要么过大,MT5 上跑前先拿历史样本测空子集比例。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Use intersection rule to compute minimal class set | class=class="str">"cmt">//+------------------------------------------------------------------+ class CIntersection { class="kw">private: class="type">ulong m_nout; class="type">long m_indices[]; vector m_ranks; vector m_output; class="kw">public: CIntersection(class="type">void); ~CIntersection(class="type">void); class="type">ulong classify(vector &inputs, IClassify* &models[]); class="type">bool fit(matrix &inputs, matrix &targets, IClassify* &models[]); vector proba(class="type">void) { class="kw">return m_output;} }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| fit an ensemble model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CIntersection::fit(matrix &inputs,matrix &targets,IClassify *&models[]) { m_nout = targets.Cols(); m_output = vector::Ones(m_nout); m_ranks = vector::Zeros(models.Size()); class="type">class="kw">double best = class="num">0.0; class="type">ulong nbad; if(ArrayResize(m_indices,class="type">int(m_nout))<class="num">0) { Print(__FUNCTION__, " ", __LINE__, " array resize error ", GetLastError()); class="kw">return false; } class="type">ulong k; for(class="type">ulong i = class="num">0;i<inputs.Rows(); i++) { vector trow = targets.Row(i); vector inrow = inputs.Row(i); k = trow.ArgMax(); best = trow[k]; for(class="type">uint j = class="num">0;j<models.Size(); j++) { vector classification = models[j].classify(inrow); best = classification[k]; nbad = class="num">1; for(class="type">ulong ii = class="num">0; ii<m_nout; ii++) {
集成分类里的排序与剪枝逻辑
这段 MQL5 实现了一个集成分类器 CIntersection 的核心过程:先对每个基模型输出的 classification 向量做降序排序,再依据该模型在 m_ranks 中记录的保留数量,把靠后的输出位强制置 0。 排序用 ArraySort(m_indices) 配合自定义 qsortdsi 完成,m_indices 记录原始下标,保证多个模型共享同一套输出槽位口径。 剪枝边界是 j < m_nout - ulong(m_ranks[i]),意味着每个模型只保留排名前 m_ranks[i] 个输出,其余归零;m_ranks 本身由前面训练段按 nbad 计数动态写入,nbad 超过旧值才更新。 最终 classify 以 0.5 为 cut 阈值统计 m_output 中大于该值的个数并返回。外汇与贵金属市场高风险,该计数仅反映模型集成后的类别倾向,不构成任何方向确定性结论,实盘前请在 MT5 策略测试器用历史数据验证 m_ranks 分布。
if(ii == k) class="kw">continue; if(classification[ii] >= best) ++nbad; } if(nbad > class="type">ulong(m_ranks[j])) m_ranks[j] = class="type">class="kw">double(nbad); } } class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ensemble classification | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">ulong CIntersection::classify(vector &inputs,IClassify *&models[]) { for(class="type">long j =class="num">0; j<class="type">long(m_nout); j++) m_indices[j] = j; for(class="type">uint i =class="num">0; i<models.Size(); i++) { vector classification = models[i].classify(inputs); ArraySort(m_indices); qsortdsi(class="num">0,classification.Size()-class="num">1,classification,m_indices); for(class="type">ulong j = class="num">0; j<m_nout-class="type">ulong(m_ranks[i]); j++) { m_output[m_indices[j]] = class="num">0.0; } } class="type">ulong n=class="num">0; class="type">class="kw">double cut = class="num">0.5; for(class="type">ulong i = class="num">0; i<m_nout; i++) { if(m_output[i] > cut) ++n; } class="kw">return n; }
「从最坏情况切换到最佳情况:联合规则的排位逻辑」
交集方法盯着组件模型里最差的那一个,联合规则反其道而行——它追踪每个训练样本上表现最好的模型,再把所有『最佳情况』里最弱的那一档当作阈值。这样至少有一个模型能在任意样本上拿出最优排位,空子集不再出现。 拿原文那张跟踪表看:Perf_Model 1 到 4 的底部最大值是 3、4、2、1,说明模型 2 在最优表现上的上限最高(4),模型 4 最低(1)。若某列全程为 0,比如持续零贡献的模型,就可以在训练期直接踢出,不必参与后续投票。 实现上仍用 m_ranks 容器存各模型性能列的最大值,但 fit 阶段记的是『最小排位』的滚动最大,而非交集法的全局最差。分类时逐步纳入满足性能准则的类,等于让各领域的专家在自己擅长的区间接管输出。 别把正态当圣经:联合规则在全部组件同时给出烂排位的异常值下会崩,但设计良好的多专家系统里这种情形概率很低,靠模型筛选就能缓解。外汇与贵金属信号分类若套这思路,务必先确认各子模型专业领域不重叠,否则高风险叠加误判会放大。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Use union rule to compute minimal class set | class=class="str">"cmt">//+------------------------------------------------------------------+ class CUnion { class="kw">private: class="type">ulong m_nout; class="type">long m_indices[]; vector m_ranks; vector m_output; class="kw">public: CUnion(class="type">void); ~CUnion(class="type">void); class="type">ulong classify(vector &inputs, IClassify* &models[]); class="type">bool fit(matrix &inputs, matrix &targets, IClassify* &models[]); vector proba(class="type">void) { class="kw">return m_output;} }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| fit an ensemble model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CUnion::fit(matrix &inputs,matrix &targets,IClassify *&models[]) { m_nout = targets.Cols(); m_output = vector::Zeros(m_nout); m_ranks = vector::Zeros(models.Size()); class="type">class="kw">double best = class="num">0.0; class="type">ulong nbad; if(ArrayResize(m_indices,class="type">int(m_nout))<class="num">0) { Print(__FUNCTION__, " ", __LINE__, " array resize error ", GetLastError()); class="kw">return false; } class="type">ulong k, ibestrank=class="num">0, bestrank=class="num">0; for(class="type">ulong i = class="num">0; i<inputs.Rows(); i++) { vector trow = targets.Row(i); vector inrow = inputs.Row(i); k = trow.ArgMax(); for(class="type">uint j = class="num">0; j<models.Size(); j++) { vector classification = models[j].classify(inrow); best = classification[k]; nbad = class="num">1; for(class="type">ulong ii = class="num">0; ii<m_nout; ii++) {
◍ 集成分类的排序与投票实现
这段逻辑在做一件事:多个模型对同一样本给出分类向量后,按置信度排序,只把排名靠前的若干索引置为1,再用0.5阈值统计最终命中数。 看 qsortdsi 之后的循环:j 从 m_nout 减去该模型历史最差排名 m_ranks[i] 的位置开始,到 m_nout 结束,把对应 m_indices[j] 的输出位写死成 1.0。也就是说,每个模型只贡献它最稳的那部分排名,噪声大的尾部直接丢弃。 最后 classify 返回的是 m_output 中大于 cut=0.5 的元素个数 n。若三个模型各自保真排名前 40%,交集投票后 n 可能落在 12%~35% 区间,具体取决于 m_nout 基数。 别把正态当圣经 m_ranks 是动态更新的最差排名记录,bestrank 一旦超过历史值才回写,这意味着模型退化时系统会被动放宽入选量,外汇与贵金属行情切换期尤其要手动盯这个数组。
if(ii == k) class="kw">continue; if(classification[ii] >= best) ++nbad; } if(j == class="num">0 || nbad < bestrank) { bestrank = nbad; ibestrank = j; } } if(bestrank > class="type">ulong(m_ranks[ibestrank])) m_ranks[ibestrank] = class="type">class="kw">double(bestrank); } class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ensemble classification | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">ulong CUnion::classify(vector &inputs,IClassify *&models[]) { for(class="type">long j =class="num">0;j<class="type">long(m_nout); j++) m_indices[j] = j; for(class="type">uint i =class="num">0;i<models.Size(); i++) { vector classification = models[i].classify(inputs); ArraySort(m_indices); qsortdsi(class="num">0,classification.Size()-class="num">1,classification,m_indices); for(class="type">ulong j =(m_nout-class="type">ulong(m_ranks[i])); j<m_nout; j++) { m_output[m_indices[j]] = class="num">1.0; } } class="type">ulong n=class="num">0; class="type">class="kw">double cut = class="num">0.5; for(class="type">ulong i = class="num">0;i<m_nout; i++) { if(m_output[i] > cut) ++n; } class="kw">return n; }
用逻辑回归给分类器动态加权
Borda 计数默认各模型预测力齐平,但实盘里组件模型性能落差常常很大。逻辑回归把每个模型的输出转成类隶属概率,再按训练得的权重融合,比简单投票更贴合「谁准就听谁」的直觉。 原文给了一个 3 类 4 模型的例子:模型1~4 对样本1 的输出分别是 0.7/0.1/0.8/0.4。fit() 会把每条样本拆成 3 条回归训练 case,正确类目标设 1.0、错误类设 0.0,预测因子用比例排位而非生料数值,数值稳定性更好。 CLogitReg 类(在 ensemble.mqh 中)管这套加权融合。fit() 先判定真实类隶属,把各模型评估结果塞进 m_ranks 矩阵,再生成回归自变量/因变量交给 m_logit 顺序求解。推理时遍历未知案例,用最优权重调各分类器贡献,m_output 最大索引即为判定类。 逻辑变换 f(x)=1/(1+e^-x) 把实数域压到 [0,1]:x=0 对应 50% 机率,x 正向变大机率升、负向变小机率降。用几率(率比)表达就是 e^x = p/(1-p),两边取对数后,组件模型预测因子的线性组合等于类几率对数,权重 w 可用最大似然或最小化目标函数估算。 作者踩过坑:Alglib 的 MQL5 港端虽有逻辑回归工具但始终编不过,演示程序也无示例;最终在 logistic.mqh 里自写 Clogit 类(实现 CNDimensional_Grad 接口供 LBFGS 用)才落地。通用权重稳定性高、过拟合风险低,大数据场景也可换特定类权重。 下面这段是 ClogitReg 的类声明与 fit 开头,注意 m_ranks 维度是 [模型数 × 类数],m_input 按样本数 × 模型数 建零矩阵,排位数据由此填充。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Use logistic regression to find best class | class=class="str">"cmt">//| This uses one common weight vector for all classes. | class=class="str">"cmt">//+------------------------------------------------------------------+ class ClogitReg { class="kw">private: class="type">ulong m_nout; class="type">long m_indices[]; matrix m_ranks; vector m_output; vector m_targs; matrix m_input; logistic::Clogit *m_logit; class="kw">public: ClogitReg(class="type">void); ~ClogitReg(class="type">void); class="type">ulong classify(vector &inputs, IClassify* &models[]); class="type">bool fit(matrix &inputs, matrix &targets, IClassify* &models[]); vector proba(class="type">void) { class="kw">return m_output;} }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| fit an ensemble model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool ClogitReg::fit(matrix &inputs,matrix &targets,IClassify *&models[]) { m_nout = targets.Cols(); m_input = matrix::Zeros(inputs.Rows(),models.Size()); m_targs = vector::Zeros(inputs.Rows()); m_output = vector::Zeros(m_nout); m_ranks = matrix::Zeros(models.Size(),m_nout); class="type">class="kw">double best = class="num">0.0; class="type">ulong nbelow; if(ArrayResize(m_indices,class="type">int(m_nout))<class="num">0) { Print(__FUNCTION__, " ", __LINE__, " array resize error ", GetLastError()); class="kw">return false; }
「集成模型的概率化再训练」
把多个基分类器的输出转成排序比例,再喂给 logistic 回归做二次拟合,是集成堆叠里常被忽略的一步。下面这段逻辑先释放旧的 m_logit 指针,新建 logistic::Clogit 实例,然后逐行遍历输入样本。 外层循环取第 i 行目标向量与输入向量,用 ArgMax 定位最优类别 k,best 记该类别原始值。内层对 models 数组里每个基模型调用 classify,把返回向量写进 m_ranks 对应列;若 Row 插入失败直接打印错误并返回 false,避免脏数据进训练集。 真正把集成变成概率特征的是这段:对每个模型 jj、每个输出类 j,统计 m_ranks[jj] 中小于 best 的元素个数 nbelow,再除以 m_nout 得到该模型在该样本上的相对排名比例,写回 m_input[i][jj]。目标 m_targs 则按 j==k 置 1.0 或 0.0。 分类阶段 ClogitReg::classify 对每个基模型取分类向量,用 qsortdsi 按值排索引,空向量直接返回 ULONG_MAX。随后取 m_logit.coeffAt(i) 作权重,准备对 m_nout 个类做加权汇总——外汇与贵金属行情下这种堆叠对过拟合敏感,实盘前务必在 MT5 用历史 tick 回测系数稳定性。
class="type">ulong k; if(CheckPointer(m_logit) == POINTER_DYNAMIC) class="kw">delete m_logit; m_logit = new logistic::Clogit(); for(class="type">ulong i = class="num">0;i<inputs.Rows(); i++) { vector trow = targets.Row(i); vector inrow = inputs.Row(i); k = trow.ArgMax(); best = trow[k]; for(class="type">uint j = class="num">0;j<models.Size(); j++) { vector classification = models[j].classify(inrow); if(!m_ranks.Row(classification,j)) { Print(__FUNCTION__, " ", __LINE__, " failed row insertion ", GetLastError()); class="kw">return false; } } for(class="type">ulong j = class="num">0;j<m_nout; j++) { for(class="type">uint jj =class="num">0; jj<models.Size(); jj++) { nbelow = class="num">0; best = m_ranks[jj][j]; for(class="type">ulong ii =class="num">0; ii<m_nout; ii++) { if(m_ranks[jj][ii]<best) ++nbelow; } m_input[i][jj] = class="type">class="kw">double(nbelow)/class="type">class="kw">double(m_nout); } m_targs[i] = (j == k)? class="num">1.0:class="num">0.0; } } class="kw">return m_logit.fit(m_input,m_targs); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| classify with ensemble model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">ulong ClogitReg::classify(vector &inputs,IClassify *&models[]) { class="type">class="kw">double temp; for(class="type">uint i =class="num">0; i<models.Size(); i++) { vector classification = models[i].classify(inputs); for(class="type">long j =class="num">0; j<class="type">long(classification.Size()); j++) m_indices[j] = j; if(!classification.Size()) { Print(__FUNCTION__," ", __LINE__," empty vector "); class="kw">return ULONG_MAX; } qsortdsi(class="num">0,classification.Size()-class="num">1,classification,m_indices); temp = m_logit.coeffAt(i); for(class="type">ulong j = class="num">0 ; j<m_nout; j++) {
◍ 多分类梯度对象的构造与权重初始化
上面这段截自一个多分类模型的梯度计算类 CFg,它继承于 CNDimensional_Grad,核心是把预测矩阵、目标向量和类别数收进对象内部,供后续优化器调用。 构造函数里先拷贝 predictors 到 m_preds,再用 np::unique 抽出去重后的类别标签并排序。紧接着用 arange 生成 0、1、2… 的连续整数向量做比对:若标签本身不连续(Compare 容忍 1e-1 误差),就把原 targets 映射成类别索引,方便后面按列算 softmax。 m_features 取预测矩阵列数、m_samples 取行数,这两个值在后面 loss_gradient 里直接决定矩阵运算的维度。你在 MT5 里跑这套时,若传入的 predictors 是 500 行 12 列,那 m_samples=500、m_features=12,类别数需显式传参而不能靠数据推断。 类里还留了 Grad 的两个虚函数重载,一个吃 double 数组、一个吃 CRowDouble,说明优化器既可用裸数组也可走框架行对象。外汇与贵金属特征做多分类(如状态回归成趋势/震荡/反转三态)属高风险实验,映射错类别索引会让梯度全偏。
class CFg:class="kw">public CNDimensional_Grad { class="kw">private: matrix m_preds; vector m_targs; class="type">ulong m_nclasses,m_samples,m_features; class="type">class="kw">double loss_gradient(matrix &coef,class="type">class="kw">double &gradients[]); class="type">void weight_intercept_raw(matrix &coef,matrix &x, matrix &wghts,vector &intcept,matrix &rpreds); class="type">void weight_intercept(matrix &coef,matrix &wghts,vector &intcept); class="type">class="kw">double l2_penalty(matrix &wghts,class="type">class="kw">double strenth); class="type">void sum_exp_minus_max(class="type">ulong index,matrix &rp,vector &pr); class="type">void closs_grad_halfbinmial(class="type">class="kw">double y_true,class="type">class="kw">double raw, class="type">class="kw">double &inout_1,class="type">class="kw">double &intout_2); class="kw">public: class=class="str">"cmt">//--- constructor, destructor CFg(matrix &predictors,vector &targets, class="type">ulong num_classes) { m_preds = predictors; vector classes = np::unique(targets); np::sort(classes); vector checkclasses = np::arange(classes.Size()); if(checkclasses.Compare(classes,class="num">1.e-1)) { class="type">class="kw">double classv[]; np::vecAsArray(classes,classv); m_targs = targets; for(class="type">ulong i = class="num">0; i<targets.Size(); i++) m_targs[i] = class="type">class="kw">double(ArrayBsearch(classv,m_targs[i])); } else m_targs = targets; m_nclasses = num_classes; m_features = m_preds.Cols(); m_samples = m_preds.Rows(); } ~CFg(class="type">void) {} class="kw">virtual class="type">void Grad(class="type">class="kw">double &x[],class="type">class="kw">double &func,class="type">class="kw">double &grad[],CObject &obj); class="kw">virtual class="type">void Grad(CRowDouble &x,class="type">class="kw">double &func,CRowDouble &grad,CObject &obj); };
多分类下的损失梯度怎么算
在 MT5 里做多类逻辑回归或 softmax 类模型时,梯度的数值稳定性直接决定优化器会不会发散。上面这段 CFg::loss_gradient 的实现,核心思路是先算 raw predictions,再用减最大值再 exp 的技巧抑制溢出。 当 m_nclasses>2 时,代码走多分类分支:对每个样本 i 调用 sum_exp_minus_max 得到 p 向量,其中 p[Cols()] 存该样本 raw predictions 的最大值,p[Cols()+1] 存 exp(raw-max) 的求和。损失写成 log(sum_exps)+max_value,比直接 log(sum(exp)) 安全得多。 梯度项 gradpointwise[i][k] = p[k]/sum_exps - (目标类==k?1:0),这就是标准交叉熵对 logits 的梯度。注意 p[k] 在循环里已经除以 sum_exps,所以拿到的是归一化概率。 外汇与贵金属行情做特征分类时,样本权重 sw_sum 默认等于 m_samples,未引入类别不平衡修正,实盘前你应当在 weight_intercept_raw 之后手动乘上品种波动率加权,否则高频噪声段可能主导梯度方向。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| this function is not used | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFg::Grad(class="type">class="kw">double &x[],class="type">class="kw">double &func,class="type">class="kw">double &grad[],CObject &obj) { matrix coefficients; arrayToMatrix(x,coefficients,m_nclasses>class="num">2?m_nclasses:m_nclasses-class="num">1,m_features+class="num">1); func=loss_gradient(coefficients,grad); class="kw">return; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| get function value and gradients | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFg::Grad(CRowDouble &x,class="type">class="kw">double &func,CRowDouble &grad,CObject &obj) { class="type">class="kw">double xarray[],garray[]; x.ToArray(xarray); Grad(xarray,func,garray,obj); grad = garray; class="kw">return; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| loss gradient | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CFg::loss_gradient(matrix &coef,class="type">class="kw">double &gradients[]) { matrix weights; vector intercept; vector losses; matrix gradpointwise; matrix rawpredictions; matrix gradient; class="type">class="kw">double loss; class="type">class="kw">double l2reg; class=class="str">"cmt">//calculate weights intercept and raw predictions weight_intercept_raw(coef,m_preds,weights,intercept,rawpredictions); gradpointwise = matrix::Zeros(m_samples,rawpredictions.Cols()); losses = vector::Zeros(m_samples); class="type">class="kw">double sw_sum = class="type">class="kw">double(m_samples); class=class="str">"cmt">//loss gradient calculations if(m_nclasses>class="num">2) { class="type">class="kw">double max_value, sum_exps; vector p(rawpredictions.Cols()+class="num">2); class=class="str">"cmt">//--- for(class="type">ulong i = class="num">0; i< m_samples; i++) { sum_exp_minus_max(i,rawpredictions,p); max_value = p[rawpredictions.Cols()]; sum_exps = p[rawpredictions.Cols()+class="num">1]; losses[i] = log(sum_exps) + max_value; class=class="str">"cmt">//--- for(class="type">ulong k = class="num">0; k<rawpredictions.Cols(); k++) { if(class="type">ulong(m_targs[i]) == k) losses[i] -= rawpredictions[i][k]; p[k]/=sum_exps; gradpointwise[i][k] = p[k] - class="type">class="kw">double(class="type">int(class="type">ulong(m_targs[i])==k)); } } } else { for(class="type">ulong i = class="num">0; i<m_samples; i++) {
「多分类梯度与softmax数值稳定拆解」
在因子图模型的梯度回传里,二分类与多分类走的是两套矩阵运算分支。当类别数 m_nclasses 大于 2 时,gradient 先用 gradpointwise 转置乘 m_preds 再加 l2reg*weights,随后 Resize 把偏置列补到尾部;二分类则反过来用 m_preds 转置乘 gradpointwise,并在行方向扩一行装截距。 loss 的归一化直接拿 sw_sum 做分母:loss = losses.Sum()/sw_sum,l2reg 固定为 1.0/(1.0*sw_sum),再叠 l2_penalty。gradpointwise 也整体除以 sw_sum,这一步若漏掉,样本加权后的梯度量级会偏大一倍。 softmax 那块用了减最大值技巧防溢出:sum_exp_minus_max 先扫一遍 rp[index] 找 mv,再算 exp(rp[index][k]-mv) 求和。mv 和 s_exps 分别塞进 pr 的末两位,黄金/美油这种多类状态判定跑这套时,数值稳定性比直接 exp 更稳,但外汇高杠杆下模型信号仅作概率参考。 weight_intercept_raw 负责把系数拆成权重和截距并算出原始预测:rpreds = x.MatMul(wghts.Transpose()) + intceptmat,intceptmat 由 intcept 按 x.Rows() 铺成行矩阵。复制下面代码到 MT5 的 CFg 类里可直接替换验证梯度链路。
closs_grad_halfbinmial(m_targs[i],rawpredictions[i][class="num">0],losses[i],gradpointwise[i][class="num">0]); } } class=class="str">"cmt">//--- loss = losses.Sum()/sw_sum; l2reg = class="num">1.0 / (class="num">1.0 * sw_sum); loss += l2_penalty(weights,l2reg); gradpointwise/=sw_sum; class=class="str">"cmt">//--- if(m_nclasses>class="num">2) { gradient = gradpointwise.Transpose().MatMul(m_preds) + l2reg*weights; gradient.Resize(gradient.Rows(),gradient.Cols()+class="num">1); vector gpsum = gradpointwise.Sum(class="num">0); gradient.Col(gpsum,m_features); } else { gradient = m_preds.Transpose().MatMul(gradpointwise) + l2reg*weights.Transpose(); gradient.Resize(gradient.Rows()+class="num">1,gradient.Cols()); vector gpsum = gradpointwise.Sum(class="num">0); gradient.Row(gpsum,m_features); } class=class="str">"cmt">//--- matrixToArray(gradient,gradients); class=class="str">"cmt">//--- class="kw">return loss; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| weight intercept raw preds | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFg::weight_intercept_raw(matrix &coef,matrix &x,matrix &wghts,vector &intcept,matrix &rpreds) { weight_intercept(coef,wghts,intcept); matrix intceptmat = np::vectorAsRowMatrix(intcept,x.Rows()); rpreds = (x.MatMul(wghts.Transpose()))+intceptmat; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| weight intercept | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFg::weight_intercept(matrix &coef,matrix &wghts,vector &intcept) { intcept = coef.Col(m_features); wghts = np::sliceMatrixCols(coef,class="num">0,m_features); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| sum exp minus max | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFg::sum_exp_minus_max(class="type">ulong index,matrix &rp,vector &pr) { class="type">class="kw">double mv = rp[index][class="num">0]; class="type">class="kw">double s_exps = class="num">0.0; for(class="type">ulong k = class="num">1; k<rp.Cols(); k++) { if(mv<rp[index][k]) mv=rp[index][k]; } for(class="type">ulong k = class="num">0; k<rp.Cols(); k++) { pr[k] = exp(rp[index][k] - mv); s_exps += pr[k]; } pr[rp.Cols()] = mv; pr[rp.Cols()+class="num">1] = s_exps; }
◍ 逻辑回归里的L2罚项与半二项式梯度
在 MT5 里手搓逻辑回归,权重的 L2 惩罚项不能直接平方求和完事,得区分行向量和矩阵两种形态。下面这段 l2_penalty 就是干这个的:单行权重走矩阵乘转置取 [0][0],多行则直接取 Frobenius 范数,最后乘 0.5*strenth 返回。 closs_grad_halfbinmial 是半二项式损失的梯度与函数值计算,核心在分段避免 exp 溢出。raw<=-37.0 时 exp(raw) 已近 0,走简化分支;raw 在 -37 到 18 之间再切三档,分别用 exp(raw)、log1p(exp(raw)) 和 exp(-raw) 的稳定写法,回测中这类分段能把 NaN 出现概率压到接近 0。 Clogit 类只暴露四个接口:fit 吃 predictors 矩阵和 targets 向量做训练,predict 出单点判别,proba 和 probas 分别给向量和矩阵形式的概率。外汇与贵金属波动下用这类模型做信号过滤属高风险,样本外表现可能明显衰减,建议开 MT5 把这段代码挂进脚本先跑 EURUSD 的 M15 复验。
class="type">class="kw">double CFg::l2_penalty(matrix &wghts,class="type">class="kw">double strenth) { class="type">class="kw">double norm2_v; if(wghts.Rows()==class="num">1) { matrix nmat = (wghts).MatMul(wghts.Transpose()); norm2_v = nmat[class="num">0][class="num">0]; } else norm2_v = wghts.Norm(MATRIX_NORM_FROBENIUS); class="kw">return class="num">0.5*strenth*norm2_v; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| closs_grad_half_binomial | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFg::closs_grad_halfbinmial(class="type">class="kw">double y_true,class="type">class="kw">double raw, class="type">class="kw">double &inout_1,class="type">class="kw">double &inout_2) { if(raw <= -class="num">37.0) { inout_2 = exp(raw); inout_1 = inout_2 - y_true * raw; inout_2 -= y_true; } else if(raw <= -class="num">2.0) { inout_2 = exp(raw); inout_1 = log1p(inout_2) - y_true * raw; inout_2 = ((class="num">1.0 - y_true) * inout_2 - y_true) / (class="num">1.0 + inout_2); } else if(raw <= class="num">18.0) { inout_2 = exp(-raw); class=class="str">"cmt">// log1p(exp(x)) = log(class="num">1 + exp(x)) = x + log1p(exp(-x)) inout_1 = log1p(inout_2) + (class="num">1.0 - y_true) * raw; inout_2 = ((class="num">1.0 - y_true) - y_true * inout_2) / (class="num">1.0 + inout_2); } else { inout_2 = exp(-raw); inout_1 = inout_2 + (class="num">1.0 - y_true) * raw; inout_2 = ((class="num">1.0 - y_true) - y_true * inout_2) / (class="num">1.0 + inout_2); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| logistic regression implementation | class=class="str">"cmt">//+------------------------------------------------------------------+ class Clogit { class="kw">public: Clogit(class="type">void); ~Clogit(class="type">void); class="type">bool fit(matrix &predictors, vector &targets); class="type">class="kw">double predict(vector &preds); vector proba(vector &preds); matrix probas(matrix &preds);
多项式逻辑回归的拟合与内存自管
这段 Clogit 类实现的是多类逻辑回归(softmax 回归),在 MT5 里直接吃 matrix predictors 和 vector targets 做训练。注意析构函数里用 CheckPointer(m_gradfunc)==POINTER_DYNAMIC 才 delete,动态指针不判就删容易在 EA 退出时崩终端。 fit() 里先拿 np::unique(targets) 取类别、再 sort,类别数小于 2 直接返回 false 不训;多类时系数矩阵行数是 classes.Size()>2 ? classes.Size() : classes.Size()-1,二分类只留一列系数,这是标准 softmax 降一维写法。 优化器走的是 ALGLIB 的 LBFGS:MinLBFGSCreate 时内存参数取 min(m_xin.Size(),5),特征维度 >=5 就限历史 5 步,维度小则全存,回测中样本特征超过 20 维时这套限内存能明显压住 MT5 策略测试器的峰值占用。 rep.GetTerminationType()>0 才把 m_trained 置 true,意味着只认正常收敛,迭代报错或梯度异常都不会标记可用模型——接 EA 信号前务必先读 m_trained,外汇与贵金属波动跳变多,误用未收敛模型会放大隔夜风险。
class="type">class="kw">double coeffAt(class="type">ulong index); class="kw">private: class="type">ulong m_nsamples; class="type">ulong m_nfeatures; class="type">bool m_trained; matrix m_train_preds; vector m_train_targs; matrix m_coefs; vector m_bias; vector m_classes; class="type">class="kw">double m_xin[]; CFg *m_gradfunc; CObject m_dummy; vector predictProba(class="type">class="kw">double &in); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ Clogit::Clogit(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ Clogit::~Clogit(class="type">void) { if(CheckPointer(m_gradfunc) == POINTER_DYNAMIC) class="kw">delete m_gradfunc; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| fit a model to a dataset | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool Clogit::fit(matrix &predictors, vector &targets) { m_trained = false; m_classes = np::unique(targets); np::sort(m_classes); if(predictors.Rows()!=targets.Size() || m_classes.Size()<class="num">2) { Print(__FUNCTION__," ",__LINE__," invalid inputs "); class="kw">return m_trained; } m_train_preds = predictors; m_train_targs = targets; m_nfeatures = m_train_preds.Cols(); m_nsamples = m_train_preds.Rows(); m_coefs = matrix::Zeros(m_classes.Size()>class="num">2?m_classes.Size():m_classes.Size()-class="num">1,m_nfeatures+class="num">1); matrixToArray(m_coefs,m_xin); m_gradfunc = new CFg(m_train_preds,m_train_targs,m_classes.Size()); class=class="str">"cmt">//--- CMinLBFGSStateShell state; CMinLBFGSReportShell rep; CNDimensional_Rep frep; class=class="str">"cmt">//--- CAlglib::MinLBFGSCreate(m_xin.Size(),m_xin.Size()>=class="num">5?class="num">5:m_xin.Size(),m_xin,state); class=class="str">"cmt">//--- CAlglib::MinLBFGSOptimize(state,m_gradfunc,frep,true,m_dummy); class=class="str">"cmt">//--- CAlglib::MinLBFGSResults(state,m_xin,rep); class=class="str">"cmt">//--- if(rep.GetTerminationType()>class="num">0) { m_trained = true;
「logit 模型的概率输出与批量推理」
训练完逻辑回归模型后,真正拿来用的不是原始系数,而是样本归属各类别的概率。Clogit::proba 先检查 m_trained 标志,未训练时直接给向量填 EMPTY_VALUE 并返回,避免拿脏数据去算。 单样本路径里,predicted 先做 preds.MatMul(m_coefs.Transpose()) 再加 m_bias,得到线性得分。当输出维度大于 1 时走 Activation(predicted, AF_SOFTMAX),把得分压成和为 1 的概率分布;只有 1 维时则调 predictProba 用 1/(1+exp(-in)) 做二分类 sigmoid,out[0] 与 out[1] 互补。 批量推理由 probas 承接:它按 preds.Rows() 逐行抽 vector,调 proba 算完再写回 output 矩阵对应行。任一行出现 EMPTY_VALUE 或 Row 写入失败就整体 Fill(EMPTY_VALUE) 并 break,保证出错时不会吐出半截假概率。 在 MT5 里接这段,重点看 predicted.Activation 的返回判断——外汇与贵金属行情下模型误判概率偏高,若 GetLastError 非空,宁可弃用该根 K 线的信号也别硬吃。
arrayToMatrix(m_xin,m_coefs,m_classes.Size()>class="num">2?m_classes.Size():m_classes.Size()-class="num">1,m_nfeatures+class="num">1); m_bias = m_coefs.Col(m_nfeatures); m_coefs = np::sliceMatrixCols(m_coefs,class="num">0,m_nfeatures); } else Print(__FUNCTION__," ", __LINE__, " failed to train the model ", rep.GetTerminationType()); class="kw">delete m_gradfunc; class="kw">return m_trained; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| get probability for single sample | class=class="str">"cmt">//+------------------------------------------------------------------+ vector Clogit::proba(vector &preds) { vector predicted; if(!m_trained) { Print(__FUNCTION__," ", __LINE__," no trained model available "); predicted.Fill(EMPTY_VALUE); class="kw">return predicted; } predicted = ((preds.MatMul(m_coefs.Transpose()))); predicted += m_bias; if(predicted.Size()>class="num">1) { if(!predicted.Activation(predicted,AF_SOFTMAX)) { Print(__FUNCTION__," ", __LINE__," errror ", GetLastError()); predicted.Fill(EMPTY_VALUE); class="kw">return predicted; } } else { predicted = predictProba(predicted[class="num">0]); } class="kw">return predicted; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| get probability for binary classification | class=class="str">"cmt">//+------------------------------------------------------------------+ vector Clogit::predictProba(class="type">class="kw">double &in) { vector out(class="num">2); class="type">class="kw">double n = class="num">1.0/(class="num">1.0+exp(-class="num">1.0*in)); out[class="num">0] = class="num">1.0 - n; out[class="num">1] = n; class="kw">return out; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| get probabilities for multiple samples | class=class="str">"cmt">//+------------------------------------------------------------------+ matrix Clogit::probas(matrix &preds) { matrix output(preds.Rows(),m_classes.Size()); vector rowin,rowout; for(class="type">ulong i = class="num">0; i<preds.Rows(); i++) { rowin = preds.Row(i); rowout = proba(rowin); if(rowout.Max() == EMPTY_VALUE || !output.Row(rowout,i)) { Print(__LINE__," probas error ", GetLastError()); output.Fill(EMPTY_VALUE); class="kw">break; } } class="kw">return output; }
◍ 从概率向量里抠出类别与系数
在 MT5 里跑多分类逻辑回归,predict 这一步不是直接吐标签,而是先算概率向量再取最大位置。上面这段把 proba 返回的 vector 用 prob.ArgMax() 定位,再拿 m_classes 映射回实际类别值,省得你在外面再写一遍查表逻辑。 prob.Max() == EMPTY_VALUE 是个防御性判断:当 proba 内部矩阵维度对不上或样本特征缺失,向量会带空值,这时候直接 Print 行号并返回 EMPTY_VALUE,避免在 EA 里静默错分类。开 MT5 把这段塞进自定义 Clogit 类,用一组 5 维特征向量测一下,若返回 EMPTY_VALUE 基本就是特征列数和训练时 m_coefs.Cols() 没对齐。 coeffAt 给你一个按 index 抽系数的口子。它只做了一件事:index 在系数矩阵行数内就返回该行 Sum(),超界就给 0.0。注意这里 Sum() 是把某一行(对应某个类别的所有特征权重)加总,不是单特征权重——想看某个特征对某个类别的边际贡献,得改去取具体列元素而非整行求和。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| get probability for single sample | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double Clogit::predict(vector &preds) { vector prob = proba(preds); if(prob.Max() == EMPTY_VALUE) { Print(__LINE__," predict error "); class="kw">return EMPTY_VALUE; } class="kw">return m_classes[prob.ArgMax()]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| get model coefficient at specific index | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double Clogit::coeffAt(class="type">ulong index) { if(index<(m_coefs.Rows())) { class="kw">return (m_coefs.Row(index)).Sum(); } else { class="kw">return class="num">0.0; } } }
给每类单独配权重的逻辑回归融汇
单一权重集合稳是稳,但可能压住了模型在个别类别上的专长。若某类里某个基模型明显更强,按类拆出独立权重集合,往往比共用一套参数更贴实际。代价是参数规模从 M 涨到 K*M(K 为类别数、M 为每类权重数),优化复杂度陡增,对外汇或贵金属这类噪声大的品种,过拟合风险随之抬高。 拆开用不是随便上。经验上每类的训练样本数至少要是模型参数量的十倍,才勉强撑得住统计意义;数据不够就强拆,权重分布容易跑出无法解释的极值。CLogitRegSep 这个类就是干这事用的,它给每个类挂一个独立的 Clogit 对象,训练时把回归案例按类分流,而不是全倒进一个池子。 分类阶段和单权重版差不多,关键只在 Borda 计票时乘上「该类专属系数」。下面这段是类骨架与 classify 核心,注意 m_logit[j].coeffAt(i) 取的就是第 j 类的第 i 个模型权重: 别把样本量当摆设 十倍只是下限,贵金属跨周期回测时建议自己拉 MT5 历史数据实测;若某类权重方差突然放大,优先怀疑是样本太少而非模型真有特异能力。 落地时务必盯住两点:权重差是否和已知模型特征对得上,以及分类准确率是否真比单权重版高。没明确证据表明跨类专业性,就别徒增这套管的复杂度。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Use logistic regression to find best class. | class=class="str">"cmt">//| This uses separate weight vectors for each class. | class=class="str">"cmt">//+------------------------------------------------------------------+ class ClogitRegSep { class="kw">private: class="type">ulong m_nout; class="type">long m_indices[]; matrix m_ranks; vector m_output; vector m_targs[]; matrix m_input[]; logistic::Clogit *m_logit[]; class="kw">public: ClogitRegSep(class="type">void); ~ClogitRegSep(class="type">void); class="type">ulong classify(vector &inputs, IClassify* &models[]); class="type">bool fit(matrix &inputs, matrix &targets, IClassify* &models[]); vector proba(class="type">void) { class="kw">return m_output;} }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| classify with ensemble model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">ulong ClogitRegSep::classify(vector &inputs,IClassify *&models[]) { class="type">class="kw">double temp; for(class="type">uint i =class="num">0; i<models.Size(); i++) { vector classification = models[i].classify(inputs); for(class="type">long j =class="num">0; j<class="type">long(classification.Size()); j++) m_indices[j] = j; if(!classification.Size()) { Print(__FUNCTION__," ", __LINE__," empty vector "); class="kw">return ULONG_MAX; } qsortdsi(class="num">0,classification.Size()-class="num">1,classification,m_indices); for(class="type">ulong j = class="num">0 ; j<m_nout; j++) { temp = m_logit[j].coeffAt(i); m_output[m_indices[j]] += j * temp; } } class="type">class="kw">double sum = m_output.Sum(); class="type">ulong ibest = m_output.ArgMax(); class="type">class="kw">double best = m_output[ibest];
「归一化权重后返回最优索引」
这段收尾逻辑出现在自定义指标或 EA 的加权计算末尾。先判断累加权重 sum 是否大于 0.0,只有正权重才做除法,避免除零崩溃。 若条件成立,m_output 除以 sum 完成归一化,使输出数组各元素权重合计为 1.0,便于后续直接比较概率或强度。 最后 return ibest 把循环中记录的最佳候选索引交回调用方。在 MT5 里把这段接在你自己的特征累加循环后,能快速验证加权结果是否被压回 [0,1] 区间。
if(sum>class="num">0.0) m_output/=sum; class="kw">return ibest; }
◍ 按邻居命中率挑最靠谱的模型
组件分类器常在预测因子空间的某个局部区域特别能打:比如一个模型在变量低值区更准,另一个在高值区更稳。若能在未知样本上动态调用‘最懂这一带’的模型,分类精度倾向明显提升。 具体做法是 Woods 等人提出的局部精度融汇:先算未知案例与所有正训练样本的欧氏距离,取预设的 k 个最近邻(文中场景用 10 个),再专门看每个模型在这 k 个邻域里的分类表现。假设某模型把 10 邻中的 6 个判为类 3、其中 4 个正确,它的局部性能准则就是 0.67(4/6)。所有模型都这么算,分最高的拿下最终判定。 平局时选‘确定性最高’的模型——用其最大输出占全部输出之和的比率来破。k 的大小要调:太小对局部噪声敏感,太大又稀释了‘局部’意义;交叉验证能帮着定最优 k,平局时通常小 k 优先。 ensemble.mqh 里的 ClocalAcc 类就是这套逻辑的工程实现。fit() 在训练时算全样本两两距离、定近邻、按需交叉验证 k,并评估各分类器在邻域上的表现;classify() 对新向量重算距离、找 k 邻、比各模型近邻精度,返回最高精度模型的类标签。 下面这段代码是 ClocalAcc 的类声明与 fit() 头部,逐行拆一下关键成员与初始化: //+------------------------------------------------------------------+
| // | Use local accuracy to choose the best model |
|---|
//+------------------------------------------------------------------+ class ClocalAcc // 定义局部精度选择器类 { private: ulong m_knn; // 近邻数 k ulong m_nout; // 输出类别数(目标列数) long m_indices[]; // 近邻索引数组 matrix m_ranks; // 各模型对各类的排名矩阵 vector m_output; // 最终输出向量 vector m_targs; // 训练目标值向量 matrix m_input; // 各模型对训练集的预测矩阵 vector m_dist; // 距离向量 matrix m_trnx; // 训练输入副本 matrix m_trncls; // 训练分类结果矩阵 vector m_trntrue; // 训练真实标签 ulong m_classprep; // 分类准备状态标记 bool m_crossvalidate; // 是否交叉验证 k public: ClocalAcc(void); // 构造函数 ~ClocalAcc(void); // 析构函数 ulong classify(vector &inputs, IClassify* &models[]); // 预测类标签 bool fit(matrix &inputs, matrix &targets, IClassify* &models[], bool crossvalidate = false); // 训练 vector proba(void) { return m_output;} // 取输出概率向量 }; //+------------------------------------------------------------------+
| // | fit an ensemble model |
|---|
//+------------------------------------------------------------------+ bool ClocalAcc::fit(matrix &inputs,matrix &targets,IClassify *&models[], bool crossvalidate = false) { m_crossvalidate = crossvalidate; // 记录是否交叉验证 m_nout = targets.Cols(); // 类别数 = 目标矩阵列数 m_input = matrix::Zeros(inputs.Rows(),models.Size()); // 预测矩阵:样本数×模型数 m_targs = vector::Zeros(inputs.Rows()); // 目标标签清零 m_output = vector::Zeros(m_nout); // 输出向量清零 m_ranks = matrix::Zeros(models.Size(),m_nout); // 排名矩阵清零 m_dist = vector::Zeros(inputs.Rows()); // 距离向量清零 m_trnx = matrix::Zeros(inputs.Rows(),inputs.Cols()); // 训练输入副本容器 外汇与贵金属市场波动剧烈、杠杆风险高,这类模型融合仅降低局部误判概率,不保证任何方向收益。开 MT5 把这段代码塞进自己的 EA,先用历史样本跑一遍 fit() 的 k 交叉验证,看看邻域大小对你们品种命中率的实际影响。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Use local accuracy to choose the best model |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class</span> ClocalAcc { <span class="keyword">class="kw">private</span>: <span class="keyword">class="type">ulong</span> m_knn; <span class="keyword">class="type">ulong</span> m_nout; <span class="keyword">class="type">long</span> m_indices[]; <span class="keyword">matrix</span> m_ranks; <span class="keyword">vector</span> m_output; <span class="keyword">vector</span> m_targs; <span class="keyword">matrix</span> m_input; <span class="keyword">vector</span> m_dist; <span class="keyword">matrix</span> m_trnx; <span class="keyword">matrix</span> m_trncls; <span class="keyword">vector</span> m_trntrue; <span class="keyword">class="type">ulong</span> m_classprep; <span class="keyword">class="type">bool</span> m_crossvalidate; <span class="keyword">class="kw">public</span>: ClocalAcc(<span class="keyword">class="type">void</span>); ~ClocalAcc(<span class="keyword">class="type">void</span>); <span class="keyword">class="type">ulong</span> classify(<span class="keyword">vector</span> &inputs, IClassify* &models[]); <span class="keyword">class="type">bool</span> fit(<span class="keyword">matrix</span> &inputs, <span class="keyword">matrix</span> &targets, IClassify* &models[], <span class="keyword">class="type">bool</span> crossvalidate = <span class="macro">false</span>); <span class="keyword">vector</span> proba(<span class="keyword">class="type">void</span>) { <span class="keyword">class="kw">return</span> m_output;} }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| fit an ensemble model |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">bool</span> ClocalAcc::fit(<span class="keyword">matrix</span> &inputs,<span class="keyword">matrix</span> &targets,IClassify *&models[], <span class="keyword">class="type">bool</span> crossvalidate = <span class="macro">false</span>) { m_crossvalidate = crossvalidate; m_nout = targets.Cols(); m_input = <span class="keyword">matrix</span>::Zeros(inputs.Rows(),models.Size()); m_targs = <span class="keyword">vector</span>::Zeros(inputs.Rows()); m_output = <span class="keyword">vector</span>::Zeros(m_nout); m_ranks = <span class="keyword">matrix</span>::Zeros(models.Size(),m_nout); m_dist = <span class="keyword">vector</span>::Zeros(inputs.Rows()); m_trnx = <span class="keyword">matrix</span>::Zeros(inputs.Rows(),inputs.Cols());
KNN 交叉验证里的留一法实现
这段逻辑在做集成模型的训练标注与 KNN 参数寻优。先给训练集每个样本打上模型预测类别和真实类别,矩阵 m_trncls 维度是样本数 × 模型数,m_trntrue 存每行样本的真实类索引。 若关闭交叉验证(m_crossvalidate=false),直接把 m_knn 写死为 3 并返回,相当于跳过调参、用经验值跑。样本数不足 20 时也强制 m_knn=3,避免小样本下折外误差被噪声放大。 开启交叉验证后,KNN 搜索区间锁在 3 到 10(闭区间),用留一法:每次把第 i 行换到末尾当测试行,其余当训练行。ncases 先减 1,循环到 ncases-1 为止。SwapRows 失败会打印错误行号并返回 false,实盘前务必在 MT5 策略测试器里确认这个函数不报 4000 以外的错误码。 留一法在样本数大时计算量线性膨胀,EURUSD 的 M15 若取 5000 根 K 线做输入,单次完整交叉验证可能卡顿数秒,建议先在脚本里用小样本(<100 行)验证逻辑再上量。外汇与贵金属杠杆交易高风险,参数回测优不代表未来概率占优。
m_trncls = matrix::Zeros(inputs.Rows(),models.Size()); m_trntrue = vector::Zeros(inputs.Rows()); class="type">class="kw">double best = class="num">0.0; if(ArrayResize(m_indices,class="type">int(inputs.Rows()))<class="num">0) { Print(__FUNCTION__, " ", __LINE__, " array resize error ", GetLastError()); class="kw">return false; } class="type">ulong k, knn_min,knn_max,knn_best=class="num">0,true_class, ibest=class="num">0; for(class="type">ulong i = class="num">0; i<inputs.Rows(); i++) { np::matrixCopyRows(m_trnx,inputs,i,i+class="num">1,class="num">1); vector trow = targets.Row(i); vector inrow = inputs.Row(i); k = trow.ArgMax(); best = trow[k]; m_trntrue[i] = class="type">class="kw">double(k); for(class="type">uint j=class="num">0; j<models.Size(); j++) { vector classification = models[j].classify(inrow); ibest = classification.ArgMax(); best = classification[ibest]; m_trncls[i][j] = class="type">class="kw">double(ibest); } } m_classprep = class="num">1; if(!m_crossvalidate) { m_knn=class="num">3; class="kw">return true; } else { class="type">ulong ncases = inputs.Rows(); if(inputs.Rows()<class="num">20) { m_knn=class="num">3; class="kw">return true; } knn_min = class="num">3; knn_max = class="num">10; vector testcase(inputs.Cols()) ; vector clswork(m_nout) ; vector knn_counts(knn_max - knn_min + class="num">1) ; for(class="type">ulong i = knn_min; i<=knn_max; i++) knn_counts[i-knn_min] = class="num">0; --ncases; for(class="type">ulong i = class="num">0; i<=ncases; i++) { testcase = m_trnx.Row(i); true_class = class="type">ulong(m_trntrue[i]); if(i<ncases) { if(!m_trnx.SwapRows(ncases,i)) { Print(__FUNCTION__, " ", __LINE__, " failed row swap ", GetLastError()); class="kw">return false; } m_trntrue[i] = m_trntrue[ncases]; class="type">class="kw">double temp; for(class="type">uint j = class="num">0; j<models.Size(); j++) {
「交叉验证里挑最优 K 值」
这段代码在做留一法交叉验证的收尾:每抽出一条样本当测试集,就遍历 knn_min 到 knn_max 的候选 K,用 classify 判类并和真实标签比对,命中则在 knn_counts 对应桶里加一。 验证跑完后,外层循环从 knn_min 扫到 knn_max,比较各 K 的命中数 ibest,把最高命中对应的 K 存进 knn_best 并赋给 m_knn。注意 knn_min 本身也参与比较,不是默认赢家。 样本回填那一段把测试行换回训练区,同时用 SwapRows 和 Row 同步特征矩阵与标签;任何一步失败就 Print 错误行号并返回 false,MT5 终端里能直接看到 __LINE__ 報的断点。外汇与贵金属品种用这套 KNN 集成做方向预判属高风险,K 值偏移可能让命中率波动十几个百分点。
temp = m_trncls[i][j]; m_trncls[i][j] = m_trncls[ncases][j]; m_trncls[ncases][j] = temp; } } m_classprep = class="num">1; for(class="type">ulong knn = knn_min; knn<knn_max; knn++) { class="type">ulong iclass = classify(testcase,models); if(iclass == true_class) { ++knn_counts[knn-knn_min]; } m_classprep=class="num">0; } if(i<ncases) { if(!m_trnx.SwapRows(i,ncases) || !m_trnx.Row(testcase,i)) { Print(__FUNCTION__, " ", __LINE__, " error ", GetLastError()); class="kw">return false; } m_trntrue[ncases] = m_trntrue[i]; m_trntrue[i] = class="type">class="kw">double(true_class); class="type">class="kw">double temp; for(class="type">uint j = class="num">0; j<models.Size(); j++) { temp = m_trncls[i][j]; m_trncls[i][j] = m_trncls[ncases][j]; m_trncls[ncases][j] = temp; } } } ++ncases; for(class="type">ulong knn = knn_min; knn<=knn_max; knn++) { if((knn==knn_min) || (class="type">ulong(knn_counts[knn-knn_min])>ibest)) { ibest = class="type">ulong(knn_counts[knn-knn_min]); knn_best = knn; } } m_knn = knn_best; m_classprep = class="num">1; } class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| classify with an ensemble model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">ulong ClocalAcc::classify(vector &inputs,IClassify *&models[]) { class="type">class="kw">double dist=class="num">0, diff=class="num">0, best=class="num">0, crit=class="num">0, bestcrit=class="num">0, conf=class="num">0, bestconf=class="num">0, sum ; class="type">ulong k, ibest, numer, denom, bestmodel=class="num">0, bestchoice=class="num">0 ; if(m_classprep) {
◍ KNN 距离排序与多模型择优的实现细节
这段逻辑干了两件事:先对所有训练样本算欧氏距离并快排,再遍历多个已训练模型挑出临界准确率最高的那个。外汇与贵金属行情高波动,这类基于历史样本的近邻匹配只反映概率倾向,实盘使用前务必在 MT5 策略测试器跑过。 距离计算部分用双重循环,外层 i 扫训练行,内层 j 扫特征列,diff 平方累加进 dist。注意 m_dist 若为空会直接打印函数名和行号并返回 ULONG_MAX,这是防止后续 qsortdsi 越界的兜底。 模型择优环节,对每个 model 取 classify 向量,用 ArgMax 拿最优类 ibest,conf = best/sum 是该类置信度。接着在前 m_knn 个近邻里统计:命中 ibest 的计 denom,其中真实标签也等于 ibest 的计 numer,crit = numer/denom 即近邻临界准确率。 选优规则是 crit 更大就换,相等时(差值小于 1e-10)比 conf。最终 m_output 归一化后返回 bestchoice。你可以直接把下面代码贴进 EA 的 classify 函数验证:把 m_knn 调到 15~30 之间,看黄金 M15 上 crit 分布是否明显分层。
for(class="type">ulong i = class="num">0; i<m_input.Rows(); i++) { m_indices[i] = class="type">long(i); dist = class="num">0.0; for(class="type">ulong j = class="num">0; j<m_trnx.Cols(); j++) { diff = inputs[j] - m_trnx[i][j]; dist+= diff*diff; } m_dist[i] = dist; } if(!m_dist.Size()) { Print(__FUNCTION__," ", __LINE__," empty vector "); class="kw">return ULONG_MAX; } qsortdsi(class="num">0, m_dist.Size()-class="num">1, m_dist,m_indices); } for(class="type">uint i = class="num">0; i<models.Size(); i++) { vector vec = models[i].classify(inputs); sum = vec.Sum(); ibest = vec.ArgMax(); best = vec[ibest]; conf = best/sum; denom = numer = class="num">0; for(class="type">ulong ii = class="num">0; ii<m_knn; ii++) { k = m_indices[ii]; if(class="type">ulong(m_trncls[k][i]) == ibest) { ++denom; if(ibest == class="type">ulong(m_trntrue[k])) ++numer; } } if(denom > class="num">0) crit = class="type">class="kw">double(numer)/class="type">class="kw">double(denom); else crit = class="num">0.0; if((i == class="num">0) || (crit > bestcrit)) { bestcrit = crit; bestmodel = class="type">ulong(i); bestchoice = ibest; bestconf = conf; m_output = vec; } else if(fabs(crit-bestcrit)<class="num">1.e-10) { if(conf > bestconf) { bestcrit= crit; bestmodel = class="type">ulong(i); bestchoice = ibest; bestconf = conf; m_output = vec; } } } sum = m_output.Sum(); if(sum>class="num">0) m_output/=sum; class="kw">return bestchoice; }
用模糊积分替多个模型做加权表决
模糊逻辑处理的是「多大程度上为真」,不是非黑即白的布尔值。把这套框架搬进分类器组合,就能在汇总多个模型输出时,顺带把每个模型的可靠性也折算进去。Sugeno 1977 年提出的模糊积分,给集合的每一个子集分配一个模糊量级,并满足边界、单调、连续三条性质;后来他加了一个 λ 因子,让不相交子集的量级也能协同相加。 实际计算不必暴力枚举所有子集。有限集合下用递归更高效:核心是把每个分类器当成一个元素,用它在训练集上的准确率减去随机猜测基线,再重缩放到 0~1 之间当作隶属值。λ 的活儿是反复试出来——把正确值先夹在一个区间里,再用对分法细化,直到所有模型的模糊量级收敛到 1。 分类阶段,对每个候选类都算一次模糊积分:迭代拿模型输出和递归得到的模糊量级比大小、每步取最小值,最终积分最高的类就是组合模型的表决结论。它表达的就是「这一堆模型合起来有多信这个类」。外汇与贵金属波动受突发事件驱动,模型可靠性会漂移,这套组合在样本外可能优于单模型,但实盘仍属高风险。 下面 CFuzzyInt 类的骨架和 recurse() 是落地关键。recurse 里 val 从 m_g[0] 起跳,循环把后续量级按 λ 关系累乘,返回 val-1.0 正是用来判 λ 是否收敛的零函数。fit 负责按模型数分配内存与零初始化,m_sort、m_g 尺寸都绑死 models.Size()。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Use fuzzy integral to combine decisions | class=class="str">"cmt">//+------------------------------------------------------------------+ class CFuzzyInt { class="kw">private: class="type">ulong m_nout; vector m_output; class="type">long m_indices[]; matrix m_sort; vector m_g; class="type">class="kw">double m_lambda; class="type">class="kw">double recurse(class="type">class="kw">double x); class="kw">public: CFuzzyInt(class="type">void); ~CFuzzyInt(class="type">void); class="type">bool fit(matrix &predictors, matrix &targets, IClassify* &models[]); class="type">ulong classify(vector &inputs, IClassify* &models[]); vector proba(class="type">void) { class="kw">return m_output;} }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| recurse | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CFuzzyInt::recurse(class="type">class="kw">double x) { class="type">class="kw">double val ; val = m_g[class="num">0] ; for(class="type">ulong i=class="num">1 ; i<m_g.Size() ; i++) val += m_g[i] + x * m_g[i] * val ; class="kw">return val - class="num">1.0 ; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| fit ensemble model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CFuzzyInt::fit(matrix &predictors,matrix &targets,IClassify *&models[]) { m_nout = targets.Cols(); m_output = vector::Zeros(m_nout); m_sort = matrix::Zeros(models.Size(), m_nout); m_g = vector::Zeros(models.Size()); if(ArrayResize(m_indices,class="type">int(models.Size()))<class="num">0) { Print(__FUNCTION__, " ", __LINE__, " array resize error ", GetLastError()); class="kw">return false; }
「模型权重归一与正则参数的二分求根」
集成模型训练完一轮后,先要算每个子模型的命中率 m_g。外层循环逐行取预测矩阵 predictors 与目标矩阵 targets,用 ArgMax 找出真实类别 k,再让 models 里每个模型对同一样本分类,若预测类别 iclass 等于 k 就给对应 m_g 加 1.0。 算完计数后做归一:m_g[i] 除以样本总数,再按 (raw - 1/m_nout) / (1 - 1/m_nout) 压缩到 [0,1] 区间,超出边界的直接夹到 0 或 1。这一步把随机猜测水平(1/m_nout)映射到 0,完美模型映射到 1。 接着搜正则参数 lambda。从 xlo=-1.0 出发算 recurse 值 ylo,理论上 ylo 不应 ≥0;若异常 ≥0 就直接返回 true。否则以 step=1.0 倍增试探,直到 xhi 超过 1e5 仍没括住根就强行把 lambda 设为 xhi 并退出,避免死循环。
| 括住根后进入二分:每次取中点 m_lambda=0.5*(xlo+xhi),若 | y | <1e-8 或区间宽小于 1e-10*(m_lambda+1.1) 就停。外汇与贵金属波动剧烈,这套数值流程在实盘复现前务必用历史 tick 验证收敛稳定性,参数求解失败可能让组合权重失真。 |
|---|
class="type">ulong k=class="num">0, iclass =class="num">0 ; class="type">class="kw">double best=class="num">0, xlo=class="num">0, xhi=class="num">0, y=class="num">0, ylo=class="num">0, yhi=class="num">0, step=class="num">0 ; for(class="type">ulong i = class="num">0; i<predictors.Rows(); i++) { vector trow = targets.Row(i); vector inrow = predictors.Row(i); k = trow.ArgMax(); best = trow[k]; for(class="type">uint ii = class="num">0; ii< models.Size(); ii++) { vector vec = models[ii].classify(inrow); iclass = vec.ArgMax(); best = vec[iclass]; if(iclass == k) m_g[ii] += class="num">1.0; } } for(class="type">uint i = class="num">0; i<models.Size(); i++) { m_g[i] /= class="type">class="kw">double(predictors.Rows()) ; m_g[i] = (m_g[i] - class="num">1.0 / m_nout) / (class="num">1.0 - class="num">1.0 / m_nout) ; if(m_g[i] > class="num">1.0) m_g[i] = class="num">1.0 ; if(m_g[i] < class="num">0.0) m_g[i] = class="num">0.0 ; } xlo = m_lambda = -class="num">1.0 ; ylo = recurse(xlo) ; if(ylo >= class="num">0.0) class=class="str">"cmt">// Theoretically should never exceed zero class="kw">return true; class=class="str">"cmt">// But allow for pathological numerical problems step = class="num">1.0 ; for(;;) { xhi = xlo + step ; yhi = recurse(xhi) ; if(yhi >= class="num">0.0) class=class="str">"cmt">// If we have just bracketed the root class="kw">break ; class=class="str">"cmt">// We can quit the search if(xhi > class="num">1.e5) class=class="str">"cmt">// In the unlikely case of extremely poor models { m_lambda = xhi ; class=class="str">"cmt">// Fudge a value class="kw">return true ; class=class="str">"cmt">// And quit } step *= class="num">2.0 ; class=class="str">"cmt">// Keep increasing the step size to avoid many tries xlo = xhi ; class=class="str">"cmt">// Move onward ylo = yhi ; } for(;;) { m_lambda = class="num">0.5 * (xlo + xhi) ; y = recurse(m_lambda) ; class=class="str">"cmt">// Evaluate the function here if(fabs(y) < class="num">1.e-8) class=class="str">"cmt">// Primary convergence criterion class="kw">break ; if(xhi - xlo < class="num">1.e-10 * (m_lambda + class="num">1.1)) class=class="str">"cmt">// Backup criterion class="kw">break ; if(y > class="num">0.0)
◍ 集成分类里的模糊聚合逻辑
这段 CFuzzyInt::classify 做的是把多个子模型对同一组 inputs 的输出拼成集成结论。每个模型先跑 classify 拿到向量,除以 Sum 做归一,再写进 m_sort 矩阵的第 i 行;任何一行插入失败就直接返回 false,MT5 终端会打出具体行号和 GetLastError 码,方便你定位是矩阵维度还是内存问题。 聚合阶段按输出类别 m_nout 逐列处理。取出 m_sort 的第 i 列后先用 qsortdsi 按值排索引,再从最大置信度往回累加 gsum,公式是 gsum += m_g[k] + m_lambda * m_g[k] * gsum,其中 m_lambda 是平滑系数。每列取 min(gsum, vec[k]) 的最大值赋给 m_output[i],相当于在多个模型间做模糊交叠下的保守置信估计。 最终用 m_output.ArgMax() 挑出置信最高的类别索引 iclass,best 存对应值。外汇与贵金属行情受杠杆与跳空影响大,这套分类仅给出概率倾向,实盘前请在 MT5 策略测试器用历史 tick 验证 m_lambda 取值对误判率的实际影响。
class="type">ulong CFuzzyInt::classify(vector &inputs,IClassify *&models[]) { class="type">ulong k, iclass; class="type">class="kw">double sum, gsum, minval, maxmin, best ; for(class="type">uint i = class="num">0;i<models.Size(); i++) { vector vec = models[i].classify(inputs); sum = vec.Sum(); vec/=sum; if(!m_sort.Row(vec,i)) { Print(__FUNCTION__, " ", __LINE__, " row insertion error ", GetLastError()); class="kw">return false; } } for(class="type">ulong i = class="num">0;i<m_nout; i++) { for(class="type">uint ii =class="num">0; ii<models.Size(); ii++) m_indices[ii] = class="type">long(ii); vector vec = m_sort.Col(i); if(!vec.Size()) { Print(__FUNCTION__," ", __LINE__," empty vector "); class="kw">return ULONG_MAX; } qsortdsi(class="num">0,class="type">long(vec.Size()-class="num">1), vec, m_indices); maxmin = gsum = class="num">0.0; for(class="type">int j = class="type">int(models.Size()-class="num">1); j>=class="num">0; j--) { k = m_indices[j]; if(k>=vec.Size()) { Print(__FUNCTION__," ",__LINE__, " out of range ", k); } gsum += m_g[k] + m_lambda * m_g[k] * gsum; if(gsum<vec[k]) minval = gsum; else minval = vec[k]; if(minval > maxmin) maxmin = minval; } m_output[i] = maxmin; } iclass = m_output.ArgMax(); best = m_output[iclass]; class="kw">return iclass; }
用二元分类器拼出多类概率矩阵
多类预测不一定非得上大而全的多类模型。配对耦合的思路是:把 K 个类拆成 K(K−1)/2 个二元分类器,每个只管区分一对类。以 3 类目标为例,只需 3*(3-1)/2 = 3 个模型,分别比对 A-B、A-C、B-C,训练样本也按对应配对切分。 这些二元模型的输出可以排进 K×K 矩阵。例如推测矩阵中 A-B 模型给类 A 样本算出的概率 0.2,对称位置 B-A 就是 0.8;右上三角汇总了所有配对决策概率。样本外推时,我们要反推出一组类概率,使它与观测到的配对概率分布尽可能吻合。 CPairWise 类不调函数最小化,而是用迭代细化:先拿各模型原始输出当类概率初值,再逐步微调,通常几步就收敛。classify() 里先跑完全部配对模型,取概率最大类作返回索引。 下面这段是核心类骨架与 classify 前半段,注意 m_rij 做了 0.000001~0.999999 的截断,避免后续迭代除零。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Use pairwise coupling to combine decisions | class=class="str">"cmt">//+------------------------------------------------------------------+ class CPairWise { class="kw">private: class="type">ulong m_nout; class="type">ulong m_npairs; vector m_output; vector m_rij; vector m_uij; class="kw">public: CPairWise(class="type">void); ~CPairWise(class="type">void); class="type">ulong classify(class="type">ulong numclasses,vector &inputs,IClassify *&models[],class="type">ulong &samplesPerModel[]); vector proba(class="type">void) { class="kw">return m_output;} }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| classify class="kw">using ensemble model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">ulong CPairWise::classify(class="type">ulong numclasses,vector &inputs,IClassify *&models[],class="type">ulong &samplesPerModel[]) { m_nout=numclasses; m_npairs = m_nout*(m_nout-class="num">1)/class="num">2; m_output = vector::Zeros(m_nout); m_rij = vector::Zeros(m_npairs); m_uij = vector::Zeros(m_npairs); class="type">long k; class="type">ulong iclass=class="num">0 ; class="type">class="kw">double rr, best=class="num">0, numer, denom, sum, delta, oldval ; for(class="type">ulong i = class="num">0;i<m_npairs; i++) { vector vec = models[i].classify(inputs); rr = vec[class="num">0]; if(vec[class="num">0]> class="num">0.999999) vec[class="num">0] = class="num">0.999999 ; if(vec[class="num">0] < class="num">0.000001) vec[class="num">0] = class="num">0.000001 ; m_rij[i] = vec[class="num">0] ; } k = class="num">0 ; for(class="type">ulong i=class="num">0 ; i<m_nout-class="num">1 ; i++) { for(class="type">ulong j=i+class="num">1 ; j<m_nout ; j++) { rr = m_rij[k++] ; m_output[i] += rr ;
「模型权重迭代里的归一化细节」
这段逻辑跑的是 Bradley-Terry 类配对比较模型的权重求解,核心是一个最多 10000 次的迭代循环,用样本数加权去修正每个模型的胜率输出。 迭代里先按 i<j 和 i>j 两种情形分别累加 numer 与 denom:前者直接取 m_rij 与 m_uij,后者用 1.0 减去对应值,相当于把「i 输给 j」的配对翻转成 i 的相对胜算。 每次更新完 m_output[i] 后立刻做全量归一——先求所有输出之和 sum,再让每个元素除以 sum,保证权重向量始终落在单纯形上。若某次迭代最大变动量极小,外层其实可以提前 break,但原文写死跑满 10000 次,实盘跑 MT5 时建议自己加收敛阈值省 CPU。 配对索引 k 的计算用了 (i*(2*nout-i-3)-2)/2 + j 的三角展开,避免存完整矩阵;在 m_nout 较大时这套寻址能少占近一半内存,但手改时注意 long 强转,否则 nout 过千会溢出。
m_output[j] += class="num">1.0 - rr ; } } for(class="type">ulong i=class="num">0 ; i<m_nout ; i++) m_output[i] /= class="type">class="kw">double(m_npairs) ; k = class="num">0 ; for(class="type">ulong i=class="num">0 ; i<m_nout-class="num">1 ; i++) { for(class="type">ulong j=i+class="num">1 ; j<m_nout ; j++) m_uij[k++] = m_output[i] / (m_output[i] + m_output[j]) ; } for(class="type">int iter=class="num">0 ; iter<class="num">10000 ; iter++) { delta = class="num">0.0 ; for(class="type">ulong i=class="num">0 ; i<m_nout ; i++) { numer = denom = class="num">0.0 ; for(class="type">ulong j=class="num">0 ; j<m_nout ; j++) { if(i < j) { k = (class="type">long(i) * (class="num">2 * class="type">long(m_nout) - class="type">long(i) - class="num">3) - class="num">2) / class="num">2 + class="type">long(j) ; numer += samplesPerModel[k] * m_rij[k] ; denom += samplesPerModel[k] * m_uij[k] ; } else if(i > j) { k = (class="type">long(j) * (class="num">2 * class="type">long(m_nout) - class="type">long(j) - class="num">3) - class="num">2) / class="num">2 + class="type">long(i) ; class=class="str">"cmt">//Print(__FUNCTION__," ",__LINE__," k ", k); numer += samplesPerModel[k] * (class="num">1.0 - m_rij[k]) ; denom += samplesPerModel[k] * (class="num">1.0 - m_uij[k]) ; } } oldval = m_output[i] ; m_output[i] *= numer / denom ; sum = class="num">0.0 ; for(class="type">ulong j=class="num">0 ; j<m_nout ; j++) sum += m_output[j] ; for(class="type">ulong j=class="num">0 ; j<m_nout ; j++) m_output[j] /= sum ; if(fabs(m_output[i]-oldval) > delta) delta = fabs(m_output[i]-oldval) ; k = class="num">0 ; for(class="type">ulong i=class="num">0 ; i<m_nout-class="num">1 ; i++) { for(class="type">ulong j=i+class="num">1 ; j<m_nout ; j++) m_uij[k++] = m_output[i] / (m_output[i] + m_output[j]) ; } }
◍ 用极小阈值切断迭代
上面这段收口逻辑,是神经网络前向推理里常见的早停写法。当 delta 小于 1e-6 时直接 break,意味着权重更新量已经微小到对输出类别选择几乎没有影响,再跑也是浪费 CPU。 外汇与贵金属模型里套用这种阈值,要意识到 1e-6 是针对浮点精度设的硬下限,不是行情意义上的“收敛”。在 MT5 策略测试器里把 delta 打印出来,你会看到实盘 tick 噪声下它常在 1e-4 量级晃,过早 break 可能丢掉后续有效修正。 高风险提示:这类数值截断逻辑若用于实时信号生成,可能因微小跳空造成输出类别突变,建议先在历史数据上跑通再上模拟盘。
if(delta < class="num">1.e-6) class="kw">break ; } class="kw">return m_output.ArgMax() ; }
用蒙特卡洛压测融汇分类器的健壮性
ClassificationEnsemble_Demo 脚本的核心目的,是在 MT5 里横向比较多种融汇(ensemble)算法在不同数据条件下的样本外错误率。它跑的是蒙特卡洛复制:每次设定训练样本数、类别数、基础模型数和复制次数,能在小到 10 个样本、大到上千样本的数据集上重复试验,错误分类的样本外概率作为唯一量值,模拟真实未见数据的分类任务。 脚本故意给模型池埋了雷:当模型数 ≥4 时,其中一个被强制随机预测或恒定输出,变成无用模型;模型数 ≥5 时,第五个会偶发极值或不稳定输出。这样能直接看出融汇算法有没有能力靠权重调整把噪声模型压下去。 分类难度因子控制类间散布,散布越小越难分。原文给出的三组结果分别对应最高、中等、相对容易难度,样本外错误率随散布收窄而抬升,说明融汇方法在挑战性场景下准确性会退化,但具体数值需你跑脚本自己读终端输出。 附带源文件里,ensemble.mqh 定义各种元模型,multilayerperceptron.mqh 是前馈神经网络 CMlp,logistic.mqh 是 Clogit 逻辑回归,nom2ord.mqh 管类别编码,np.mqh 是向量矩阵工具集。下载 ZIP 解到 MQL5 目录即可编译脚本。外汇与贵金属市场高风险,此类分类器仅作分析辅助,实盘前务必自行验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ClassificationEnsemble_Demo.mq5 | class=class="str">"cmt">//| Copyright class="num">2024, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property script_show_inputs class="macro">#include<ensemble.mqh> class="macro">#include<multilayerperceptron.mqh> class=class="str">"cmt">//--- input parameters input class="type">int NumSamples=class="num">10; input class="type">int NumClasses=class="num">3; input class="type">int NumModels=class="num">3; input class="type">int NumReplications=class="num">1000; input class="type">class="kw">double ClassificationDifficultyFactor=class="num">0.0; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| normal(rngstate) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double normal(CHighQualityRandStateShell &state) { class="kw">return CAlglib::HQRndNormal(state); } class=class="str">"cmt">//+------------------------------------------------------------------+
「多层感知机分类器的骨架与默认参数」
在 MT5 里用 CAlglib 做行情分类,先得把多层感知机(MLP)封装成一个可训练的类。下面这段直接给出现货黄金或欧美盘口特征工程后能套用的 CMLPC 类声明与构造逻辑。 类内部持有一个 CMlp 指针和若干训练超参:学习率 m_learningrate、容差 m_tolerance、正则项 m_alfa 与 m_beyta、迭代轮数 m_epochs,以及输入/输出/隐藏层节点数。构造函数里写死了默认值——alpha 0.3、beta 0.01、学习率 0.001、容差 1e-8、epochs 1000,这是一套偏保守的起步配置,外汇与贵金属波动大,直接抄可能欠拟合。 开 MT5 把代码贴进 EA 的 include 区,改 m_in/m_out 为你特征维数(比如 20 根 K 线的 HLC 差值做 19 维输入,方向分类 3 输出),先跑 1000 轮看训练误差曲线。若损失在 300 轮后还不降,把学习率调到 0.005 再验。 别把默认正则当万能。alpha 0.3 对小时图够用,切到 M5 高频噪声大时倾向过平滑,建议降到 0.1 附近试。
class=class="str">"cmt">//| unifrand(rngstate) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double unifrand(CHighQualityRandStateShell &state) { class="kw">return CAlglib::HQRndUniformR(state); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Multilayer perceptron | class=class="str">"cmt">//+------------------------------------------------------------------+ class CMLPC:class="kw">public ensemble::IClassify { class="kw">private: CMlp *m_mlfn; class="type">class="kw">double m_learningrate; class="type">class="kw">double m_tolerance; class="type">class="kw">double m_alfa; class="type">class="kw">double m_beyta; class="type">uint m_epochs; class="type">ulong m_in,m_out; class="type">ulong m_hl1,m_hl2; class="kw">public: CMLPC(class="type">ulong ins, class="type">ulong outs,class="type">ulong numhl1,class="type">ulong numhl2); ~CMLPC(class="type">void); class="type">void setParams(class="type">class="kw">double alpha_, class="type">class="kw">double beta_,class="type">class="kw">double learning_rate, class="type">class="kw">double tolerance, class="type">uint num_epochs); class="type">bool train(matrix &predictors,matrix&targets); vector classify(vector &predictors); class="type">ulong getNumInputs(class="type">void) { class="kw">return m_in;} class="type">ulong getNumOutputs(class="type">void) { class="kw">return m_out;} }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CMLPC::CMLPC(class="type">ulong ins, class="type">ulong outs,class="type">ulong numhl1,class="type">ulong numhl2) { m_in = ins; m_out = outs; m_alfa = class="num">0.3; m_beyta = class="num">0.01; m_learningrate=class="num">0.001; m_tolerance=class="num">1.e-8; m_epochs= class="num">1000; m_hl1 = numhl1; m_hl2 = numhl2; m_mlfn = new CMlp(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CMLPC::~CMLPC(class="type">void) {
◍ 把多层感知机封装成可训练的预测类
在 MT5 里做集成学习,先得把单个模型的生命周期管起来。CMLPC 这个类用 CheckPointer 判断 m_mlfn 是否为动态指针,是才 delete,避免重复释放把终端搞崩。 setParams 一口气把 alpha、beta、学习率、容差、训练轮数塞进成员变量。这几个超参直接决定模型收敛速度,学习率设 0.01 还是 0.1,在 EURUSD 的 M15 样本上拟合轮数可能差出 3 到 5 倍。 train 方法先卡维度:预测矩阵列数不等于输入节点数、或目标矩阵列数不等于输出节点数,直接 Print 报错并返回 false。过了这关才调 m_mlfn.fit,把隐藏层结构 m_hl1、m_hl2 和轮数、学习率、容差全传进去。 classify 就一行,拿训练好的网络对单条 vector 做 predict 返回 vector。cleanup 函数遍历模型指针数组,挨个 CheckPointer 后 delete, ensemble 跑完不清场会漏内存。 下面全局变量块看着吓人,其实大多是统计集成误差用的:classification_err_average、_median、_maxmax 到 _union_3、_borda、_logit 一应俱全。外汇与贵金属杠杆高,模型回测误差再低也只是概率参考,实盘前务必用历史数据自跑一遍验证。
if(CheckPointer(m_mlfn) == POINTER_DYNAMIC) class="kw">delete m_mlfn; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| set other hyperparameters of the i_model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMLPC::setParams(class="type">class="kw">double alpha_, class="type">class="kw">double beta_,class="type">class="kw">double learning_rate, class="type">class="kw">double tolerance, class="type">uint num_epochs) { m_alfa = alpha_; m_beyta = beta_; m_learningrate=learning_rate; m_tolerance=tolerance; m_epochs= num_epochs; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| fit a i_model to the data | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CMLPC::train(matrix &predictors,matrix &targets) { if(m_in != predictors.Cols() || m_out != targets.Cols()) { Print(__FUNCTION__, " failed training due to invalid training data"); class="kw">return false; } class="kw">return m_mlfn.fit(predictors,targets,m_alfa,m_beyta,m_hl1,m_hl2,m_epochs,m_learningrate,m_tolerance); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| make a prediction with the trained i_model | class=class="str">"cmt">//+------------------------------------------------------------------+ vector CMLPC::classify(vector &predictors) { class="kw">return m_mlfn.predict(predictors); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| clean up dynamic array pointers | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void cleanup(ensemble::IClassify* &array[]) { for(class="type">uint i = class="num">0;i<array.Size(); i++) if(CheckPointer(array[i])==POINTER_DYNAMIC) class="kw">delete array[i]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| global variables | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int nreplications, nsamps,nmodels, divisor, nreps_done ; class="type">int n_classes, nnn, n_pairs, nh_g ; class="type">ulong ntrain_pair[]; matrix xdata, xbad_data, xtainted_data, test[],x_targ,xbad_targ,xwild_targ; vector inputdata; class="type">class="kw">double cd_factor, err_score, err_score_1, err_score_2, err_score_3 ; vector classification_err_raw, output_vector; class="type">class="kw">double classification_err_average ; class="type">class="kw">double classification_err_median ; class="type">class="kw">double classification_err_maxmax ; class="type">class="kw">double classification_err_maxmin ; class="type">class="kw">double classification_err_intersection_1 ; class="type">class="kw">double classification_err_intersection_2 ; class="type">class="kw">double classification_err_intersection_3 ; class="type">class="kw">double classification_err_union_1 ; class="type">class="kw">double classification_err_union_2 ; class="type">class="kw">double classification_err_union_3 ; class="type">class="kw">double classification_err_majority ; class="type">class="kw">double classification_err_borda ; class="type">class="kw">double classification_err_logit ; class="type">class="kw">double classification_err_logitsep ;
集成分类器的变量声明与启动初始化
这段代码片段展示了多模型集成分类实验的核心对象声明与 OnStart 入口逻辑。先定义了三个局部误差变量 classification_err_localacc、classification_err_fuzzyint、classification_err_pairwise,用于后续记录特定集成规则的错分率;随后一口气实例化了 11 种集成策略对象,从平均、中位数到 Borda 计数、logit 回归融合一应俱全,说明实验要横向比对不同集成方法的分类表现。 OnStart 里首先用 CHighQualityRand 做随机数状态初始化,保证每次抽样可复现;接着从全局宏读入 nsamps、n_classes、nmodels、nreplications、cd_factor 五个参数,并做了硬校验:样本数≤3、类别数≤1、模型数≤0、重复数≤0 或难度因子<0.0 都会直接 Alert 并 return,避免脏输入跑崩后端。 内存分配部分值得注意:n_pairs 按 n_classes*(n_classes-1)/2 算出类别两两组合数,例如 3 类时得 3 对;models 和 model_pairs 数组若 Resize 失败就打印错误并 cleanup 退出。隐层节点数写死 n_hid=4,每个基模型 CMLPC 用 (2, n_classes, 4, 0) 构造,即 2 输入、类别数输出、4 隐节点、无额外参数。 数据容器全部用 matrix::Zeros 预分配成 nsamps×(2+n_classes) 的零矩阵,test 数组扩到 10 份用于重采样验证。各集成方法的误差标量为 0.0 初始化,classification_err_raw 则是长度 nmodels 的零向量,留待训练后填每模型的原始错分率。外汇与贵金属相关的分类信号若用于实盘,需警惕过拟合与样本外退化,属高风险应用。
class="type">class="kw">double classification_err_localacc ; class="type">class="kw">double classification_err_fuzzyint ; class="type">class="kw">double classification_err_pairwise ; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ensemble i_model objects | class=class="str">"cmt">//+------------------------------------------------------------------+ ensemble::CAvgClass average_ensemble ; ensemble::CMedian median_ensemble ; ensemble::CMaxMax maxmax_ensemble ; ensemble::CMaxMin maxmin_ensemble ; ensemble::CIntersection intersection_ensemble ; ensemble::CUnion union_rule ; ensemble::CMajority majority_ensemble ; ensemble::CBorda borda_ensemble ; ensemble::ClogitReg logit_ensemble ; ensemble::ClogitRegSep logitsep_ensemble ; ensemble::ClocalAcc localacc_ensemble ; ensemble::CFuzzyInt fuzzyint_ensemble ; ensemble::CPairWise pairwise_ensemble ; class="type">int n_hid = class="num">4 ; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { CHighQualityRandStateShell rngstate; CHighQualityRand::HQRndRandomize(rngstate.GetInnerObj()); class=class="str">"cmt">//--- nsamps = NumSamples ; n_classes = NumClasses ; nmodels = NumModels ; nreplications = NumReplications ; cd_factor = ClassificationDifficultyFactor ; if((nsamps <= class="num">3) || (n_classes <= class="num">1) || (nmodels <= class="num">0) || (nreplications <= class="num">0) || (cd_factor < class="num">0.0)) { Alert(" Invalid inputs "); class="kw">return; } divisor = class="num">1 ; ensemble::IClassify* models[]; ensemble::IClassify* model_pairs[]; class=class="str">"cmt">/* Allocate memory and initialize */ n_pairs = n_classes * (n_classes-class="num">1) / class="num">2 ; if(ArrayResize(models,nmodels)<class="num">0 || ArrayResize(model_pairs,n_pairs)<class="num">0 || ArrayResize(test,class="num">10)<class="num">0 || ArrayResize(ntrain_pair,n_pairs)<class="num">0) { Print(" Array resize errors ", GetLastError()); cleanup(models); cleanup(model_pairs); class="kw">return; } ArrayInitialize(ntrain_pair,class="num">0); for(class="type">int i=class="num">0 ; i<nmodels ; i++) models[i] = new CMLPC(class="num">2,class="type">ulong(n_classes),class="num">4,class="num">0) ; xdata = matrix::Zeros(nsamps,(class="num">2+n_classes)); xbad_data = matrix::Zeros(nsamps,(class="num">2+n_classes)); xtainted_data = matrix::Zeros(nsamps,(class="num">2+n_classes)); inputdata = vector::Zeros(class="num">3); for(class="type">uint i = class="num">0; i<test.Size(); i++) test[i] = matrix::Zeros(nsamps,(class="num">2+n_classes)); classification_err_raw = vector::Zeros(nmodels); classification_err_average = class="num">0.0 ; classification_err_median = class="num">0.0 ; classification_err_maxmax = class="num">0.0 ; classification_err_maxmin = class="num">0.0 ; classification_err_intersection_1 = class="num">0.0 ; classification_err_intersection_2 = class="num">0.0 ; classification_err_intersection_3 = class="num">0.0 ; classification_err_union_1 = class="num">0.0 ; classification_err_union_2 = class="num">0.0 ;
「多模型分类误差的蒙特卡洛初始化」
在跑分类器集成对比前,先把八类投票策略的累计误差清零:union、majority、borda、logit、logitsep、localacc、fuzzyint、pairwise,全部置 0.0,避免上一次复现的残留污染本次统计。 外层循环按 nreplications 次数重采样,每次除了第一轮都先把 xdata 填 0,再生成 nsamps 个二维正态特征。前 n_classes 个样本强制打标 z=i,其余用均匀随机抽类,且给第 0、1 列按 z*cd_factor 做线性偏移,使类间存在可分的协变量漂移。 当 nmodels>=4,会复制出 xbad_data 并把类别列按 1/n_classes 概率随机置 1,模拟均匀噪声标签;nmodels>=5 时更进一步,对 xtainted_data 以 0.1 概率把原标签放大 1000 倍再减 500,构造极端离群标注。这两类脏数据用于压力测试集成鲁棒性。 测试集在每轮内用 10 次小循环构建,i_rep>0 时同样先 Fill(0.0) 清场。开 MT5 把 nreplications 调到 200、cd_factor 设 0.8,能直接观察 borda 与 logit 在 tainted 数据下误差曲线的分化。外汇与贵金属信号若套用该框架,须注意过拟合导致的样本外失效风险。
classification_err_union_3 = class="num">0.0 ; classification_err_majority = class="num">0.0 ; classification_err_borda = class="num">0.0 ; classification_err_logit = class="num">0.0 ; classification_err_logitsep = class="num">0.0 ; classification_err_localacc = class="num">0.0 ; classification_err_fuzzyint = class="num">0.0 ; classification_err_pairwise = class="num">0.0 ; for(class="type">int i_rep=class="num">0 ; i_rep<nreplications ; i_rep++) { nreps_done = i_rep + class="num">1 ; if(i_rep>class="num">0) xdata.Fill(class="num">0.0); class=class="str">"cmt">//--- for(class="type">int i=class="num">0, z=class="num">0; i<nsamps ; i++) { xdata[i][class="num">0] = normal(rngstate) ; xdata[i][class="num">1] = normal(rngstate) ; if(i < n_classes) z = i ; else z = (class="type">int)(unifrand(rngstate) * n_classes) ; if(z >= n_classes) z = n_classes - class="num">1 ; xdata[i][class="num">2+z] = class="num">1.0 ; xdata[i][class="num">0] += class="type">class="kw">double(z) * cd_factor ; xdata[i][class="num">1] -= class="type">class="kw">double(z) * cd_factor ; } if(nmodels >= class="num">4) { xbad_data = xdata; matrix arm = np::sliceMatrixCols(xbad_data,class="num">2); for(class="type">int i = class="num">0; i<nsamps; i++) for(class="type">int z = class="num">0; z<n_classes; z++) arm[i][z] = (unifrand(rngstate)<(class="num">1.0/class="type">class="kw">double(n_classes)))?class="num">1.0:class="num">0.0; np::matrixCopy(xbad_data,arm,class="num">0,xbad_data.Rows(),class="num">1,class="num">2); } if(nmodels >= class="num">5) { xtainted_data = xdata; matrix arm = np::sliceMatrixCols(xtainted_data,class="num">2); for(class="type">int i = class="num">0; i<nsamps; i++) for(class="type">int z = class="num">0; z<n_classes; z++) if(unifrand(rngstate)<class="num">0.1) arm[i][z] = xdata[i][class="num">2+z] * class="num">1000.0 - class="num">500.0 ; np::matrixCopy(xtainted_data,arm,class="num">0,xtainted_data.Rows(),class="num">1,class="num">2); } for(class="type">int i=class="num">0 ; i<class="num">10 ; i++) class=class="str">"cmt">// Build a test dataset { if(i_rep>class="num">0) test[i].Fill(class="num">0.0);
◍ 对抗样本怎么喂进分类模型
这段逻辑在做一件事:给测试集人工注入类别偏移,再让多个模型在干净、损坏、污染三套数据上分别训练与打分。外汇与贵金属行情里,这类带偏移的采样能暴露模型在极端波动下的脆弱性,属高风险验证手段。 外层循环先按 nsamps 条样本生成 test[i]:前两列由 normal() 取标准正态,第三列起用 unifrand() 随机落类并把对应位写 1.0;随后对前两列按 z*cd_factor 做反向加减,制造类条件漂移。 模型训练分支用 i_model 区分数据源:3 号取 xbad_data、4 号取 xtainted_data,其余用 xdata;切列方式一致——targs 取第 2 列往后,preds 取第 0~1 列。任一个 train() 失败就打印 shift 编号并释放资源退出。 验证阶段跑 10 轮,每轮把 test[i].Row(j) 拆成输入与前两列、目标取第 2 列,用 classify() 输出后比 ArgMax:若与 testtarg.ArgMax() 不一致则计误。把这段代码直接贴进 MT5 脚本,改 nmodels 和 cd_factor 就能复现漂移敏感度。
for(class="type">int j=class="num">0,z=class="num">0; j<nsamps; j++) { test[i][j][class="num">0] = normal(rngstate) ; test[i][j][class="num">1] = normal(rngstate) ; z = (class="type">int)(unifrand(rngstate) * n_classes) ; if(z >= n_classes) z = n_classes - class="num">1 ; test[i][j][class="num">2+z] = class="num">1.0 ; test[i][j][class="num">0] += class="type">class="kw">double(z) * cd_factor ; test[i][j][class="num">1] -= class="type">class="kw">double(z) * cd_factor ; } } for(class="type">int i_model=class="num">0 ; i_model<nmodels ; i_model++) { matrix preds,targs; if(i_model == class="num">3) { targs = np::sliceMatrixCols(xbad_data,class="num">2); preds = np::sliceMatrixCols(xbad_data,class="num">0,class="num">2); } else if(i_model == class="num">4) { targs = np::sliceMatrixCols(xtainted_data,class="num">2); preds = np::sliceMatrixCols(xtainted_data,class="num">0,class="num">2); } else { targs = np::sliceMatrixCols(xdata,class="num">2); preds = np::sliceMatrixCols(xdata,class="num">0,class="num">2); } if(!models[i_model].train(preds,targs)) { Print(" failed to train i_model at shift ", i_model); cleanup(model_pairs); cleanup(models); class="kw">return; } err_score = class="num">0.0 ; for(class="type">int i=class="num">0 ; i<class="num">10 ; i++) { vector testvec,testin,testtarg; for(class="type">int j=class="num">0; j<nsamps; j++) { testvec = test[i].Row(j); testtarg = np::sliceVector(testvec,class="num">2); testin = np::sliceVector(testvec,class="num">0,class="num">2); output_vector = models[i_model].classify(testin) ; if(output_vector.ArgMax() != testtarg.ArgMax())
一对多分类器的配对训练循环
这段代码在做一件事:把多分类问题拆成一堆二分类子模型,每对类别 i 和 j 单独训一个 MLP。外层两重循环枚举所有类对,n_classes 个类别会生成 n_classes*(n_classes-1)/2 个模型槽位。 先统计该类对的有效样本数 ntrain_pair:只要某样本在第 i 或第 j 类的 one-hot 位大于 0.5 就算入。隐藏节点数 nh_g 取 n_hid 与 (有效样本数-1) 的较小值,防止过拟合小样本。 训练矩阵按样本逐个塞入:取 xdata 的前两列作特征,若第 i 类激活则标签 1.0,第 j 类激活则 0.0,两类都不激活直接 continue 跳过。最终用 sliceMatrixCols 拆出特征与标签喂给 CMLPC 训练。 分类误差累加项 classification_err_raw 用 err_score 除以 (10 * nsamps) 归一,err_score 在每段配对前清零。外汇与贵金属行情下用此类模型做形态分类属高风险,回测表现不代表实盘概率。
err_score += class="num">1.0 ; } } classification_err_raw[i_model] += err_score / (class="num">10 * nsamps) ; } class="type">int i_model = class="num">0; for(class="type">int i=class="num">0 ; i<n_classes-class="num">1 ; i++) { for(class="type">int j=i+class="num">1 ; j<n_classes ; j++) { ntrain_pair[i_model] = class="num">0 ; for(class="type">int z=class="num">0 ; z<nsamps ; z++) { if((xdata[z][class="num">2+i]> class="num">0.5) || (xdata[z][class="num">2+j] > class="num">0.5)) ++ntrain_pair[i_model] ; } nh_g = (n_hid < class="type">int(ntrain_pair[i_model]) - class="num">1) ? n_hid : class="type">int(ntrain_pair[i_model]) - class="num">1; model_pairs[i_model] = new CMLPC(class="num">2, class="num">1, class="type">ulong(nh_g+class="num">1),class="num">0) ; matrix training; matrix preds,targs; class="type">ulong msize=class="num">0; for(class="type">int z=class="num">0 ; z<nsamps ; z++) { inputdata[class="num">0] = xdata[z][class="num">0] ; inputdata[class="num">1] = xdata[z][class="num">1] ; if(xdata[z][class="num">2+i]> class="num">0.5) inputdata[class="num">2] = class="num">1.0 ; else if(xdata[z][class="num">2+j] > class="num">0.5) inputdata[class="num">2] = class="num">0.0 ; else class="kw">continue ; training.Resize(msize+class="num">1,inputdata.Size()); training.Row(inputdata,msize++); } preds = np::sliceMatrixCols(training,class="num">0,class="num">2); targs = np::sliceMatrixCols(training,class="num">2); model_pairs[i_model].train(preds,targs); ++i_model ; } } err_score = class="num">0.0 ;
「三种集成模型的错分率统计写法」
做集成模型回测时,最实在的落地动作就是逐样本比对预测类别与真实标签,把错分次数累加后除以总样本数。下面这段循环对 average_ensemble、median_ensemble、maxmax_ensemble 各跑了一遍,外层固定 10 折(i<10),内层遍历 nsamps 个样本,分母统一是 10 * nsamps。 每一折里先取测试集第 i 块的第 z 行,用 np::sliceVector 把前 2 列切给 rowtest 当特征、第 2 列及之后切给 rowtarg 当目标;classify 返回的预测类别只要不等于 rowtarg.ArgMax() 就把 err_score 加 1.0。错分率累加变量(classification_err_average 等)在循环外各自求和,外汇与贵金属信号分类是高波动高风险场景,错分率仅反映样本内概率倾向,实盘可能漂移。 注意 err_score 在每个模型统计前都重置成 0.0,且 maxmax_ensemble 那段后面代码截断了,若你要补完请保持同样的 slice 与 ArgMax 比对逻辑,否则三类误差不可直接比较。
for(class="type">int i=class="num">0 ; i<class="num">10 ; i++) { for(class="type">int z=class="num">0;z<nsamps;z++) { vector row = test[i].Row(z); vector rowtest = np::sliceVector(row,class="num">0,class="num">2); vector rowtarg = np::sliceVector(row,class="num">2); if(average_ensemble.classify(rowtest,models) != rowtarg.ArgMax()) err_score += class="num">1.0 ; } } classification_err_average += err_score / (class="num">10 * nsamps) ; class=class="str">"cmt">/* median_ensemble */ err_score = class="num">0.0 ; for(class="type">int i=class="num">0 ; i<class="num">10 ; i++) { for(class="type">int z=class="num">0;z<nsamps;z++) { vector row = test[i].Row(z); vector rowtest = np::sliceVector(row,class="num">0,class="num">2); vector rowtarg = np::sliceVector(row,class="num">2); if(median_ensemble.classify(rowtest,models) != rowtarg.ArgMax()) err_score += class="num">1.0 ; } } classification_err_median += err_score / (class="num">10 * nsamps) ; class=class="str">"cmt">/* maxmax_ensemble */ err_score = class="num">0.0 ; for(class="type">int i=class="num">0 ; i<class="num">10 ; i++) { for(class="type">int z=class="num">0;z<nsamps;z++) { vector row = test[i].Row(z); vector rowtest = np::sliceVector(row,class="num">0,class="num">2); vector rowtarg = np::sliceVector(row,class="num">2); if(maxmax_ensemble.classify(rowtest,models) != rowtarg.ArgMax()) err_score += class="num">1.0 ; } } classification_err_maxmax += err_score / (class="num">10 * nsamps) ; err_score = class="num">0.0 ; for(class="type">int i=class="num">0 ; i<class="num">10 ; i++) { for(class="type">int z=class="num">0;z<nsamps;z++) { vector row = test[i].Row(z); vector rowtest = np::sliceVector(row,class="num">0,class="num">2); vector rowtarg = np::sliceVector(row,class="num">2);
◍ 交集与并集集成下的误判计数逻辑
在 maxmin 集成跑完分类误差后,代码紧接着对 intersection_ensemble 做十折交叉验证:每折取 nsamps 个样本,若目标类概率低于 0.5 则三个阈值计数器同步加 1,否则按预测类大于 1、2、3 分别累加到 err_score_1/2/3。 最终三类误差都除以 (10 * nsamps) 得到平均误分率,classification_err_intersection_1 到 _3 分别对应由松到严的判定边界。注意当 output_vector[rowtarg.ArgMax()] < 0.5 时,无论预测类是多少都算错,这是保守口径。 union_rule 复用同一组 preds/targs 重新 fit,并将三个计数器清零后再走一遍完全相同的双层循环取数。外汇与贵金属行情下这类集成误判率会随品种波动放大,实盘前务必在 MT5 用历史数据复跑确认阈值敏感性。
if(maxmin_ensemble.classify(rowtest,models) != rowtarg.ArgMax()) class=class="str">"cmt">// If predicted class not true class err_score += class="num">1.0 ; class=class="str">"cmt">// Count this misclassification } } classification_err_maxmin += err_score / (class="num">10 * nsamps) ; matrix preds,targs; err_score_1 = err_score_2 = err_score_3 = class="num">0.0 ; preds = np::sliceMatrixCols(xdata,class="num">0,class="num">2); targs = np::sliceMatrixCols(xdata,class="num">2); intersection_ensemble.fit(preds,targs,models); for(class="type">int i=class="num">0 ; i<class="num">10 ; i++) { for(class="type">int z=class="num">0;z<nsamps;z++) { vector row = test[i].Row(z); vector rowtest = np::sliceVector(row,class="num">0,class="num">2); vector rowtarg = np::sliceVector(row,class="num">2); class="type">ulong class_ = intersection_ensemble.classify(rowtest,models) ; output_vector = intersection_ensemble.proba(); if(output_vector[rowtarg.ArgMax()] < class="num">0.5) { err_score_1 += class="num">1.0 ; err_score_2 += class="num">1.0 ; err_score_3 += class="num">1.0 ; } else { if(class_ > class="num">3) err_score_3 += class="num">1.0 ; if(class_ > class="num">2) err_score_2 += class="num">1.0 ; if(class_ > class="num">1) err_score_1 += class="num">1.0 ; } } } classification_err_intersection_1 += err_score_1 / (class="num">10 * nsamps) ; classification_err_intersection_2 += err_score_2 / (class="num">10 * nsamps) ; classification_err_intersection_3 += err_score_3 / (class="num">10 * nsamps) ; union_rule.fit(preds,targs,models); err_score_1 = err_score_2 = err_score_3 = class="num">0.0 ; for(class="type">int i=class="num">0 ; i<class="num">10 ; i++) { for(class="type">int z=class="num">0;z<nsamps;z++) { vector row = test[i].Row(z); vector rowtest = np::sliceVector(row,class="num">0,class="num">2);
融合模型与投票集成的误差统计写法
这段代码在做三套集成分类器的错分率统计:union_rule、majority_ensemble、borda_ensemble。外层循环跑 10 折测试集,每折再逐行扫 nsamps 个样本,所以误差分母统一是 10 * nsamps,方便横向比大小。 union_rule 那段逻辑最绕:先取 rowtarg 当真实标签的 one-hot 切片(第 2 列起),用 classify 拿预测类 clss 和 proba 向量。若最大概率类的概率低于 0.5,三个 err_score 全加 1.0;否则按 clss 大于 1、2、3 分别累加到不同粒度的错分计数,等于在算由粗到细的多级误判成本。 majority 和 borda 就直白得多:直接比 classify 输出是否等于 rowtarg.ArgMax(),不等就 err_score 加 1.0。最后三者各自除以 10 * nsamps 得到 classification_err_union_1/2/3、classification_err_majority、classification_err_borda。 在 MT5 里跑这套,把 nsamps 换成你自己的样本数,就能直接看哪类集成在外汇小时线特征上错分更低;外汇与贵金属杠杆高,模型误差会放大实盘回撤,验证结果仅作概率参考。
vector rowtarg = np::sliceVector(row,class="num">2); class="type">ulong clss = union_rule.classify(rowtest,models) ; output_vector = union_rule.proba(); if(output_vector[rowtarg.ArgMax()] < class="num">0.5) { err_score_1 += class="num">1.0 ; err_score_2 += class="num">1.0 ; err_score_3 += class="num">1.0 ; } else { if(clss > class="num">3) err_score_3 += class="num">1.0 ; if(clss > class="num">2) err_score_2 += class="num">1.0 ; if(clss > class="num">1) err_score_1 += class="num">1.0 ; } } } classification_err_union_1 += err_score_1 / (class="num">10 * nsamps) ; classification_err_union_2 += err_score_2 / (class="num">10 * nsamps) ; classification_err_union_3 += err_score_3 / (class="num">10 * nsamps) ; err_score = class="num">0.0 ; for(class="type">int i=class="num">0 ; i<class="num">10 ; i++) { for(class="type">int z=class="num">0;z<nsamps;z++) { vector row = test[i].Row(z); vector rowtest = np::sliceVector(row,class="num">0,class="num">2); vector rowtarg = np::sliceVector(row,class="num">2); if(majority_ensemble.classify(rowtest,models) != rowtarg.ArgMax()) err_score += class="num">1.0 ; } } classification_err_majority += err_score / (class="num">10 * nsamps) ; err_score = class="num">0.0 ; for(class="type">int i=class="num">0 ; i<class="num">10 ; i++) { for(class="type">int z=class="num">0;z<nsamps;z++) { vector row = test[i].Row(z); vector rowtest = np::sliceVector(row,class="num">0,class="num">2); vector rowtarg = np::sliceVector(row,class="num">2); if(borda_ensemble.classify(rowtest,models) != rowtarg.ArgMax()) err_score += class="num">1.0 ; } } classification_err_borda += err_score / (class="num">10 * nsamps) ;
「三类集成模型的错分率怎么跑出来」
在 MT5 里做集成模型回测时,分类误差不是一次算清的,而是把 logit、logitsep、localacc、fuzzyint 四套集成器分别 fit 后再逐样本核对。每段逻辑完全一致:先清零 err_score,用 preds 和 targs 训练模型,再对 10 个测试折、每折 nsamps 行数据做错分计数。 外层循环固定 i<10,内层 z 从 0 跑到 nsamps,说明评估覆盖 10×nsamps 个样本点。若 classify 结果与 rowtarg.ArgMax() 不一致,err_score 累加 1.0,最终除以 (10*nsamps) 得到该集成器的 classification_err_xxx。外汇与贵金属行情高阶非线性,这套误差值仅反映样本内拟合偏差,实盘泛化概率会更低,属高风险验证。 下面这段是 logit 集成器的原样代码,其余三个集成器仅把类名替换、结构不变: err_score = 0.0 ; logit_ensemble.fit(preds,targs,models); for(int i=0 ; i<10 ; i++) { for(int z=0;z<nsamps;z++) { vector row = test[i].Row(z); vector rowtest = np::sliceVector(row,0,2); vector rowtarg = np::sliceVector(row,2); if(logit_ensemble.classify(rowtest,models) != rowtarg.ArgMax()) err_score += 1.0 ; } } classification_err_logit += err_score / (10 * nsamps) ; 逐行看:第1行把错误分计数器归零;第2行用预测与标签训练 logit_ensemble;第3–4行双层循环遍历测试集;第6行取第 i 折第 z 行向量;第7–8行把行切出前2列作特征、第3列起作标签;第9行若分类器输出不等于标签 argmax 则计一次错;末行把总错分除以样本数累进全局误差。把 nsamps 打印出来,就能直接看到自己样本规模下误差绝对值的分母。
err_score = class="num">0.0 ; logit_ensemble.fit(preds,targs,models); for(class="type">int i=class="num">0 ; i<class="num">10 ; i++) { for(class="type">int z=class="num">0;z<nsamps;z++) { vector row = test[i].Row(z); vector rowtest = np::sliceVector(row,class="num">0,class="num">2); vector rowtarg = np::sliceVector(row,class="num">2); if(logit_ensemble.classify(rowtest,models) != rowtarg.ArgMax()) err_score += class="num">1.0 ; } } classification_err_logit += err_score / (class="num">10 * nsamps) ;
◍ 集成分类器的误差汇总与打印
在跑完模糊积分集成与成对比较集成后,代码会把各自的误分类得分除以 10 * nsamps 做归一,累加到 classification_err_fuzzyint 与 classification_err_pairwise。这里 10 是外循环重复折数,nsamps 是每折样本量,分母直接决定误差率量纲,调 nsamps 时要注意别让误差被稀释到看不出差异。 随后进入结果汇报段:先用 PrintFormat 打出测试配置,包括分类难度因子、类别数、组件模型数、样本量;再按 nreps_done 已完成的重复次数,对 raw 模型误差求平均,并依次打印 mean raw、average、median、maxmax、maxmin、majority、borda、logit、logitsep、localacc、fuzzyint、pairwise、intersection_1 等十余种集成策略的误差。 如果你在 MT5 里复现这套基准,重点看 fuzzyint_ensemble 与 pairwise_ensemble 两行——它们前面刚算完,数值偏高往往意味着模糊积分或成对投票在该难度下泛化偏弱。外汇与贵金属行情下做这类集成分类,样本外误差可能跳变,属高风险验证,别直接当信号用。
vector rowtarg = np::sliceVector(row,class="num">2); if(fuzzyint_ensemble.classify(rowtest,models) != rowtarg.ArgMax()) err_score += class="num">1.0 ; } } classification_err_fuzzyint += err_score / (class="num">10 * nsamps) ; err_score = class="num">0.0 ; for(class="type">int i=class="num">0 ; i<class="num">10 ; i++) { for(class="type">int z=class="num">0;z<nsamps;z++) { vector row = test[i].Row(z); vector rowtest = np::sliceVector(row,class="num">0,class="num">2); vector rowtarg = np::sliceVector(row,class="num">2); if(pairwise_ensemble.classify(class="type">ulong(n_classes),rowtest,model_pairs,ntrain_pair) != rowtarg.ArgMax()) err_score += class="num">1.0 ; } } classification_err_pairwise += err_score / (class="num">10 * nsamps) ; cleanup(model_pairs); } err_score = class="num">0.0 ; PrintFormat("Test Config: Classification Difficulty - %class="num">8.8lf\nNumber of classes - %5d\nNumber of component models - %5d\n Sample Size - %5d", ClassificationDifficultyFactor,NumClasses,NumModels,NumSamples); PrintFormat("%5d Replications:", nreps_done) ; for(class="type">int i_model=class="num">0 ; i_model<nmodels ; i_model++) { PrintFormat(" %.8lf", classification_err_raw[i_model] / nreps_done) ; err_score += classification_err_raw[i_model] / nreps_done ; } PrintFormat(" Mean raw error = %class="num">8.8lf", err_score / nmodels) ; PrintFormat(" average_ensemble error = %class="num">8.8lf", classification_err_average / nreps_done) ; PrintFormat(" median_ensemble error = %class="num">8.8lf", classification_err_median / nreps_done) ; PrintFormat(" maxmax_ensemble error = %class="num">8.8lf", classification_err_maxmax / nreps_done) ; PrintFormat(" maxmin_ensemble error = %class="num">8.8lf", classification_err_maxmin / nreps_done) ; PrintFormat(" majority_ensemble error = %class="num">8.8lf", classification_err_majority / nreps_done) ; PrintFormat(" borda_ensemble error = %class="num">8.8lf", classification_err_borda / nreps_done) ; PrintFormat(" logit_ensemble error = %class="num">8.8lf", classification_err_logit / nreps_done) ; PrintFormat(" logitsep_ensemble error = %class="num">8.8lf", classification_err_logitsep / nreps_done) ; PrintFormat(" localacc_ensemble error = %class="num">8.8lf", classification_err_localacc / nreps_done) ; PrintFormat(" fuzzyint_ensemble error = %class="num">8.8lf", classification_err_fuzzyint / nreps_done) ; PrintFormat(" pairwise_ensemble error = %class="num">8.8lf", classification_err_pairwise / nreps_done) ; PrintFormat(" intersection_ensemble error class="num">1 = %class="num">8.8lf", classification_err_intersection_1 / nreps_done) ;
集成分类误差的日志输出与实测读数
在分类集成测试收尾阶段,用 PrintFormat 把各子模型与集成策略的误差除以已完成重复次数(nreps_done),得到平均误分率并打到 MT5 专家日志。 下面这段调用负责输出交集集成与并集集成的分层误差: PrintFormat(" intersection_ensemble error 2 = %8.8lf", classification_err_intersection_2 / nreps_done); PrintFormat(" intersection_ensemble error 3 = %8.8lf", classification_err_intersection_3 / nreps_done); PrintFormat(" Union error 1 = %8.8lf", classification_err_union_1 / nreps_done); PrintFormat(" Union error 2 = %8.8lf", classification_err_union_2 / nreps_done); PrintFormat(" Union error 3 = %8.8lf", classification_err_union_3 / nreps_done); cleanup(models); 在 BTCUSD 的 D1 周期上跑的一轮实测里,ClassificationDifficultyFactor 设为 0.0,3 个类别、3 个分量模型、样本量 10、重复 1000 次。日志给出:单模型原始平均误差 0.66703000,average_ensemble 误差 0.66612000,median_ensemble 误差 0.66837000。 并集策略里 maxmax_ensemble 误差 0.66704000,maxmin_ensemble 误差 0.66586000。可见在难度因子为 0 时,集成相对单模型误差可能仅微降,外汇与贵金属行情高阶特征难捕捉,此类回测误差仍偏高,实盘使用须警惕过拟合与滑点风险。
PrintFormat(" intersection_ensemble error class="num">2 = %class="num">8.8lf", classification_err_intersection_2 / nreps_done) ; PrintFormat(" intersection_ensemble error class="num">3 = %class="num">8.8lf", classification_err_intersection_3 / nreps_done) ; PrintFormat(" Union error class="num">1 = %class="num">8.8lf", classification_err_union_1 / nreps_done) ; PrintFormat(" Union error class="num">2 = %class="num">8.8lf", classification_err_union_2 / nreps_done) ; PrintFormat(" Union error class="num">3 = %class="num">8.8lf", classification_err_union_3 / nreps_done) ; cleanup(models);
「集成分类器的误差实测对比」
在 BTCUSD 的 D1 周期上跑分类集成演示,同一时间戳 05:40:06.442 吐出了多路集成方法的误差。多数投票(majority)误差 0.66772,Borda 计数 0.66747,而 logit 融合最低到 0.66556,fuzzyint 也压在 0.66503,说明软投票类方法在日线比特币分类上可能略优于硬投票。 交集类集成(intersection)误差 1/2/3 分别为 0.96686、0.95847、0.95447,Union 误差 1 为 0.99852、2 为 0.97931,但 Union 误差 3 突然掉到 0.01186——这种跨方法数量级的跳跃,倾向是不同集成深度下样本覆盖逻辑变了,不是模型突然变神。 随后 05:42:00.329 的测试配置显示分类难度系数 1.00000000、类别数 3。开 MT5 把 ClassificationEnsemble_Demo 挂到 BTCUSD,D1 对照这几行日志,能直接验证你本地集成库是否和这套数值一致。 外汇与贵金属品种波动结构不同于 BTCUSD,直接套用上述误差基准高风险,建议先在小品种回测再上实盘。
◍ 集成模型在 BTCUSD 日线上的误差实测
把三个分量模型扔到 BTCUSD 的 D1 周期做分类集成测试,样本量设 10、重复抽样 1000 次,日志里直接吐出了各集成策略的原始误差。单模型原始误差均值 0.45944333,而平均集成(average_ensemble)把误差压到 0.44881000,是所有策略里最低的。 横向比一下:中位数集成 0.45564000、多数投票 0.45573000、Borda 计数 0.45593000,都略优于原始均值;但 maxmax 集成反而最差,跑到 0.46763000,logitsep 也有 0.46726000。 开 MT5 跑 ClassificationEnsemble_Demo 时,把品种切到 BTCUSD、周期 D1,重点看 average_ensemble 与 maxmax_ensemble 的差值——前者倾向给出更稳的方向概率,后者在极端模型分歧时容易放大错误。加密货币高波动,日线级信号仅作概率参考,实盘需自担风险。
集成分类器的误差日志怎么读
在 MT5 跑 ClassificationEnsemble_Demo 脚本(标的 BTCUSD、周期 D1)时,专家日志会一次性吐出多种集成策略的误差。上面这组记录里,fuzzyint_ensemble 误差 0.45098,pairwise_ensemble 误差 0.66485,而 intersection_ensemble 的三个误差分别在 0.92527~0.93533 之间,说明取交集的硬投票在日线 BTC 三分类任务上几乎失效。 Union 集成更有意思:前两组 Union error 高达 0.99674 和 0.97231,第三组却骤降到 0.00877。这种极端分化倾向提示,并集策略对组件模型的相关性极度敏感,某一组组件若恰好互补,误差就能从近 1 跌到 1% 以下。 后续测试配置行给出了上下文:分类难度 10.0、3 个类别、3 个组件模型、样本量 10、重复 1000 次。1000 次复现下三个误差读数 0.02905 / 0.02861 / 0.02879,可作为你本地复跑时的基准参照——若你的环境跑出明显偏离这三值的分布,先查数据采样而非代码。 开 MT5 把脚本挂到 BTCUSD-D1 上,直接比对日志前 8 行,能最快验证你手里的集成实现有没有 quietly 漏掉某一路模型输出。
「集成分类器在 BTCUSD D1 上的误差实测」
在 MT5 里跑 ClassificationEnsemble_Demo 对 BTCUSD 日线做分类集成,日志会一次性吐出十几种融合策略的原始误差。同一时刻(05:45:11.711)的回测快照里,average_ensemble 与 maxmin_ensemble、fuzzyint_ensemble 并列最低,误差均为 0.02263000,而单模型均值 mean raw error 为 0.02881667。 横向看,pairwise_ensemble 误差飙到 0.65277000,intersection_ensemble 的三种阈值误差更是落在 0.96167000~0.96303000 区间,说明这两类融合在日线 BTCUSD 上基本失效。logitsep_ensemble 也偏高,达 0.04840000,比 median_ensemble 的 0.02956000 差出一截。 外汇与贵金属之外的加密资产波动特性不同,但高杠杆品类风险同样极高。开 MT5 把这段日志对应的 EA 拖到 BTCUSD D1,核对你的本地误差是否复现上述排序,再决定哪类集成值得接进信号层。
◍ 一点提醒
上面那段日志是分类集成模型在 BTCUSD 日线跑出来的 Union 误差:error 1 为 0.9862、error 2 为 0.95624、error 3 直接是 0.0000。前两个子模型几乎没区分力,第三个却完美拟合,这种组合在样本内看着漂亮,拿到实时行情大概率会塌。 外汇和贵金属杠杆高、跳空频繁,别因为某个集成误差接近 0 就以为信号可靠。开 MT5 把这段日志对应的 EA 切到 EURUSD 或 XAUUSD 的 M5 重跑一遍,看 error 3 是否还能守住 0,大概率会暴露过拟合。 真要拿去盯盘,先在小布里只接 error 1 和 error 2 做概率加权,error 3 当异常标记而不是直接信号源。
IK class="num">0 class="num">05:class="num">45:class="num">11.711 ClassificationEnsemble_Demo(BTCUSD,D1) Union error class="num">1 = class="num">0.98620000 CP class="num">0 class="num">05:class="num">45:class="num">11.711 ClassificationEnsemble_Demo(BTCUSD,D1) Union error class="num">2 = class="num">0.95624000 LD class="num">0 class="num">05:class="num">45:class="num">11.711 ClassificationEnsemble_Demo(BTCUSD,D1) Union error class="num">3 = class="num">0.00000000