以 MQL5 实现强化分类任务的融汇方法·进阶篇
(2/3)·组件分类器只吐离散决策时,数字组合为何失灵,以及排位输出怎么救场
「用均匀噪声扰动输出并做归一化选优」
这段逻辑在模型推理后做一层轻量随机扰动,目的是避免每次都卡在同一个输出索引上。核心做法是遍历输出数组 m_output,对每个元素加 0.999 倍的均匀随机数,再记录扰动后最大的那个索引。 循环里 temp = m_output[i] + 0.999 * CAlglib::HQRndUniformR(state) 这行决定了噪声强度,系数 0.999 意味着扰动幅度接近原输出量级,而非微小抖动。若 i==0 或 temp 超过当前 best,就更新 best 与 ibest,同时把原值累加进 sum。 循环结束后,当 sum>0.0 时对 m_output 整体除以 sum 做归一化,最后返回 ibest。外汇与贵金属市场高风险,该扰动可能让选出的信号在样本外更分散,但绝不保证盈利。 把系数从 0.999 调到 0.1 或 2.0,在 MT5 里跑同一段,能直接看出输出分布是被压窄还是被拉散。
for(class="type">ulong i=class="num">0 ; i<m_output.Size() ; i++) { temp = m_output[i] + class="num">0.999 * CAlglib::HQRndUniformR(state); if((i == class="num">0) || (temp > best)) { best = temp ; ibest = i ; } sum += m_output[i] ; } if(sum>class="num">0.0) m_output/=sum; class="kw">return ibest; }
把组件模型输出当数字因子来均化
当每个组件模型吐出的数值既有意义、又能跨模型比较时,直接把这些量值合并,往往比多数投票或 Borda 计数更能榨干数据。后者丢掉了大部分可用信息,而均化思路是取每个类别在所有组件模型里输出的中位数——模型数量固定时,这在数学上等价于把输出全加起来。本质上,它是把每个分类模型当成数值预测器,用最朴素的 averaging 拼到一起,最后看哪类聚合输出最高就判给谁。 分类任务和数值预测在这里有个坑:数值模型通常共用训练目标,输出天然对齐;分类模型往往只有排位有意义,原始输出可能根本不可比。这种错位会让合并悄悄变成隐式加权均值,而不是真平均,个别模型可能对总和产生不成比例的影响,反过来伤了融汇效果。所以上 MT5 前,先验证各组件输出尺度是否一致,是保住元模型完整性的前提。 有人假设组件输出就是概率,于是引出乘积规则——把加法换成连乘。听着优雅,实则脆弱:只要有一个模型把某类概率严重低估到接近零,乘进去后这类就被不可逆地罚死了。这种对概率假设违反的极度敏感,让乘积规则在实盘里基本不实用,更适合当反面教材。 下面的 CAvgClass 就是均化规则的进化实现,结构镜像 CMajority。classify() 从所有组件收预测并累加,最终类取累积得分最高者。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| full resolution&class="macro">#x27; version of majority rule. | class=class="str">"cmt">//+------------------------------------------------------------------+ class CAvgClass { class="kw">private: class="type">ulong m_outputs; class="type">ulong m_inputs; vector m_output; class="kw">public: CAvgClass(class="type">void); ~CAvgClass(class="type">void); class="type">ulong classify(vector &inputs, IClassify* &models[]); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| make classification with consensus model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">ulong CAvgClass::classify(vector &inputs, IClassify* &models[]) { m_output=vector::Zeros(models[class="num">0].getNumOutputs()); vector model_classification; for(class="type">uint i =class="num">0 ; i<models.Size(); i++) { model_classification = models[i].classify(inputs); m_output+=model_classification; } class="type">class="kw">double sum = m_output.Sum(); class="type">ulong min = m_output.ArgMax(); m_output/=sum; class="kw">return min; }
◍ 用排位中值压住极端预测
多模型集成时,均值融汇会把异常预测直接拉偏,尤其外汇与贵金属这类高波动品种,单根极端 K 线就可能让某个子模型输出离群值,进而污染整体信号。中位数融汇的思路是:先对每个模型的输出独立排位,再取各类排位的中值,极端值即便存在也只影响排位边缘,不会主导结果。 这种基于排位的转换在保留类间相对次序的同时,削弱了对异常值的敏感度。实测中,当预测分布偏斜或不对称时,中值法比简单平均更稳,代价仅是信息利用率略有下降。 ensemble 里的 CMedian 类把这套逻辑封装好了,核心在 classify 方法:先把各模型输出按列塞进矩阵,再逐行取中值。下面代码可直接拷进 MT5 看结构。 别把正态当圣经 真实盘面里预测分布常带厚尾,中值融汇不是万灵药,但是当你的用例里『偶尔极端预测』比『每次都用满信息』更致命时,它往往更划算。外汇与贵金属交易杠杆高、风险大,任何集成方案都只是概率倾斜,不等于方向锁定。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| median of predications | class=class="str">"cmt">//+------------------------------------------------------------------+ class CMedian { class="kw">private: class="type">ulong m_outputs; class="type">ulong m_inputs; vector m_output; matrix m_out; class="kw">public: CMedian(class="type">void); ~CMedian(class="type">void); class="type">ulong classify(vector &inputs, IClassify* &models[]); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CMedian::CMedian(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CMedian::~CMedian(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| consensus classification | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">ulong CMedian::classify(vector &inputs,IClassify *&models[]) { m_out = matrix::Zeros(models[class="num">0].getNumOutputs(),models.Size()); vector model_classification; for(class="type">uint i = class="num">0;i<models.Size(); i++) { model_classification = models[i].classify(inputs); if(!m_out.Col(model_classification,i)) { Print(__FUNCTION__, " ", __LINE__, " failed row insertion ", GetLastError()); class="kw">return ULONG_MAX; } } m_output = vector::Zeros(models[class="num">0].getNumOutputs()); for(class="type">ulong i = class="num">0;i<m_output.Size(); i++) { vector row = m_out.Row(i); if(!row.Size()) { Print(__FUNCTION__," ", __LINE__," empty vector ");
「中位数合成与概率归一的实现尾段」
这段收尾逻辑把每行排序后的中位数塞进 m_output 数组,再对全体输出求和并用 ArgMax 定位权重最大的列索引。若 sum 大于 0,则整体除以 sum 完成归一,使各列输出转化为相对概率分布,最后返回概率最高的索引。 外汇与贵金属市场的高风险在于:这种归一结果只反映历史样本的统计倾向,实盘里信号失效的概率并不低,任何一次调用都应先在 MT5 策略测试器里跑多品种回测。 直接把上面这段贴在自定义指标的最后,改一行 row.Size() 的入参就能验证不同窗口长度下中位数的稳定性。
class="kw">return ULONG_MAX; } qsortd(class="num">0,row.Size()-class="num">1,row); m_output[i] = row.Median(); } class="type">class="kw">double sum = m_output.Sum(); class="type">ulong mx = m_output.ArgMax(); if(sum>class="num">0.0) m_output/=sum; class="kw">return mx; }
模型专精与排错:MaxMax 与 MaxMin 的取舍
组件模型融汇里,单个模型常被训练成只吃透某几类样本。在它擅长的领域,输出置信度会明显冲高;碰到专业外的类,往往吐出 0.4~0.6 这种不上不下的中间值,信息量很低。 MaxMax 规则按「所有模型中对每个类的最大输出」来定类,等于只听最自信的那一声。它适合模型高度专精、中等输出纯属噪音的架构,但二次输出本身带分析价值的场景会失效——这点容易被忽略。 反过来,有些模型专长是「排除」而非「认出」。某类样本进来时,融汇里至少有一个模型会对错误类给出极低分,等于帮你在矩阵里划掉干扰项。MaxMin 规则取每个类在所有模型里的最小输出做归属判断,正好利用这种异常排错能力。 落地前先摸清楚自己的融汇形态:用 MaxMax 就确认模型真有专业分工、中等输出确为噪音、且类别覆盖完整;用 MaxMin 则核对异常排错能否覆盖全部误分类可能,别留缝隙。 下方是 MaxMax 类的核心实现,classify 里先建零向量,再逐模型取各类最大输出,最后 ArgMax 出最优类、Sum 归一。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Compute the maximum of the predictions | class=class="str">"cmt">//+------------------------------------------------------------------+ class CMaxMax { class="kw">private: class="type">ulong m_outputs; class="type">ulong m_inputs; vector m_output; matrix m_out; class="kw">public: CMaxMax(class="type">void); ~CMaxMax(class="type">void); class="type">ulong classify(vector &inputs, IClassify* &models[]); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CMaxMax::CMaxMax(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CMaxMax::~CMaxMax(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ensemble classification | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">ulong CMaxMax::classify(vector &inputs,IClassify *&models[]) { class="type">class="kw">double sum; class="type">ulong ibest; m_output = vector::Zeros(models[class="num">0].getNumOutputs()); for(class="type">uint i = class="num">0; i<models.Size(); i++) { vector classification = models[i].classify(inputs); for(class="type">ulong j = class="num">0; j<classification.Size(); j++) { if(classification[j] > m_output[j]) m_output[j] = classification[j]; } } ibest = m_output.ArgMax(); sum = m_output.Sum(); if(sum>class="num">0.0) m_output/=sum; class="kw">return ibest; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Compute the minimum of the predictions | class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 集成分类里的极小投票实现
把多个分类模型的结果做集成,不一定非要取平均。CMaxMin 这个类走的是另一条路:对每一个类别,只保留所有模型给出的最小置信度,相当于一种保守的『极小投票』机制。 核心在 classify 方法里。它先拿第一个模型的分类向量初始化 m_output,之后遍历其余模型,逐元素取 min,所以 m_output[j] 最终等于所有模型在第 j 类上的最低打分。 得到合成向量后,用 ArgMax 挑出置信最低的类别里相对最高的那个作为 ibest 返回;若总和大于 0 再做一次归一化。这种写法在模型间分歧大时倾向压低整体置信,外汇与贵金属行情受突发事件扰动频繁,此类保守集成可能降低假信号概率,但同样会拖慢响应,实盘前请在 MT5 策略测试器用历史数据验证分歧样本下的表现。 下面的代码可直接拷进 MQ5 文件编译,注意 IClassify 是外部模型接口,需你自行实现 classify(vector&) 返回各类别打分向量。
class CMaxMin { class="kw">private: class="type">ulong m_outputs; class="type">ulong m_inputs; vector m_output; matrix m_out; class="kw">public: CMaxMin(class="type">void); ~CMaxMin(class="type">void); class="type">ulong classify(vector &inputs, IClassify* &models[]); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CMaxMin::CMaxMin(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CMaxMin::~CMaxMin(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ensemble classification | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">ulong CMaxMin::classify(vector &inputs,IClassify *&models[]) { class="type">class="kw">double sum; class="type">ulong ibest; for(class="type">uint i = class="num">0; i<models.Size(); i++) { vector classification = models[i].classify(inputs); if(i == class="num">0) m_output = classification; else { for(class="type">ulong j = class="num">0; j<classification.Size(); j++) if(classification[j] < m_output[j]) m_output[j] = classification[j]; } } ibest = m_output.ArgMax(); sum = m_output.Sum(); if(sum>class="num">0.0) m_output/=sum; class="kw">return ibest; }