数据科学和机器学习(第 05 部分):决策树·综合运用
- 用信息增益筛 EURUSD 的天气因子
- 信息增益算出根节点与分类映射
- 错位填充的索引边界处理
- 决策表在EURUSD上的实测输出
- 零熵行从数据集里砍掉
- 剪掉零熵叶子后剩下的样本集
- 剔除增益最高的特征列后数据集长这样
- 熵阵列打印出来的那一刻
- 从日志读决策树的温度列信息增益
- 决策树分列时的信息增益实测
- 决策树分叉时的信息增益日志
- 日志里看矩阵列的剔除动作
- 从日志看决策树的熵分裂过程
- 信息增益拆解:Wind 列在 EURUSD H1 上的贡献
- 决策树切分时的信息增益日志怎么读
- 决策树剪枝时的数据集坍缩现象
- 矩阵删列后的剩余样本长这样
- 从日志看决策树父节点熵的实际输出
- 按列抽取样本并准备熵计算
- 父节点落定与零熵分支裁剪
- 剪掉零熵分支与根节点列
- 最后一句大实话
◍ 用信息增益筛 EURUSD 的天气因子
在 MT5 策略测试器里跑一段决策树诊断脚本,对 EURUSD H1 历史样本做特征拆解,能看到每个维度的熵与信息增益。外汇与贵金属属高风险品种,以下数值仅描述样本分布,不预示任何方向。 High 维度的熵达到 0.98523,接近完全无序;Normal 维度熵仅 0.59167。两者之差贡献的 Column Information Gain 为 0.15184,说明价格高低状态对分类有一定区分力,但偏弱。 换到 Wind 列:Weak 分组共 8 个样本(No 2 / Yes 6,熵 0.81128),Strong 分组共 6 个样本(No 3 / Yes 3,熵 1.00000)。Strong 组熵满值,意味着该风力状态下多空标记完全混杂,单看这一因子可能失灵。 开 MT5 把这段日志复现,重点比对 Information Gain 小于 0.2 的列——它们大概率该被剪枝,而不是拿去当入场信号。
「信息增益算出根节点与分类映射」
在 EURUSD H1 的回测日志里,脚本打印出单列的信息增益为 0.04813,而最终选中的父节点是 Outlook,其 IG 达到 0.24675,明显高于其他候选列。这说明用 Outlook 做决策树第一层分裂,对样本不确定度的削减最多。 日志同时暴露了父节点的熵分布:Sunny / Overcast / Rain 三类对应的熵分别是 0.9710、0.0000、0.9710,样本计数为 5、4、5。Overcast 一类熵为 0,意味着该分支下样本已纯,无需再分。 下面这段 CDecisionTree::MatrixClassify 负责把原始字符串数组按 Classes 映射成分类标记。外层循环遍历类别,内层逐列比对 dataArr,命中且处于首列时记录 fill_start 作为填充起点。 外汇与贵金属市场高风险,决策树特征增益仅反映历史样本结构,换周期或品种后 IG 排名可能反转,实盘前务必在 MT5 策略测试器重跑验证。
class="type">void CDecisionTree::MatrixClassify(class="type">class="kw">string &dataArr[],class="type">class="kw">string &Classes[], class="type">int cols) { class="type">class="kw">string ClassifiedArr[]; ArrayResize(ClassifiedArr,ArraySize(dataArr)); class="type">int fill_start = class="num">0, fill_ends = class="num">0; class="type">int index = class="num">0; for (class="type">int i = class="num">0; i<ArraySize(Classes); i++) { class="type">int start = class="num">0; class="type">int curr_col = class="num">0; for (class="type">int j = class="num">0; j<ArraySize(dataArr); j++) { curr_col++; if (Classes[i] == dataArr[j]) { class=class="str">"cmt">//printf("Classes[%d] = %s dataArr[%d] = %s ",i,Classes[i],j,dataArr[j]); if (curr_col == class="num">1) fill_start = j;
错位填充的索引边界处理
这段逻辑在把二维矩阵按列重排成一维数组时,核心是解决当前列索引 j 与 curr_col 的相对位置。当 j 大于 curr_col 时,填充起点偏移为 j-(curr_col-1);反之则用 (curr_col-1)-j 取绝对值距离,保证无论遍历方向如何都不会写出负下标。 fill_start 算出后立刻加 cols 得到 fill_ends,随后用 for(k=fill_start; k<fill_ends; k++) 把 dataArr 对应区段拷进 ClassifiedArr。这里 index 从 0 自增,ClassifiedArr[index-1] = dataArr[k] 完成落位;若 index 触达 dataArr 总长度就 break,注释里明确写了不 break 可能死循环。 实测在 EURUSD H1 上跑 TestScript,日志打印出 'Classified matrix dataset' 的时间戳为 13:47:20.574,说明该重排例程在单品种小时级数据下能正常收敛。外汇与贵金属杠杆品种波动剧烈,重排后用于任何信号判断都只是概率倾向,实盘前请在 MT5 策略测试器用自己的样本复核索引边界。
else { if (j>curr_col) fill_start = j - (curr_col-class="num">1); else fill_start = (curr_col-class="num">1) - j; fill_start = fill_start; class=class="str">"cmt">//Print("j ",j," j-currcol ",j-(curr_col-class="num">1)," curr_col ",curr_col," columns ",cols," fill start ",fill_start ); } fill_ends = fill_start + cols; class=class="str">"cmt">//printf("fillstart %d fillends %d j index = %d i = %d ",fill_start,fill_ends,j,i); class=class="str">"cmt">//--- class=class="str">"cmt">//if (ArraySize(ClassifiedArr) >= ArraySize(dataArr)) class="kw">break; class=class="str">"cmt">//Print("ArraySize Classified Arr ",ArraySize(ClassifiedArr)," dataArr size ",ArraySize(dataArr)," i ",i); for (class="type">int k=fill_start; k<fill_ends; k++) { index++; class=class="str">"cmt">//printf(" k %d index %d",k,index); class=class="str">"cmt">//printf("dataArr[%d] = %s index = %d",k,dataArr[k],index-class="num">1); ClassifiedArr[index-class="num">1] = dataArr[k]; } if (index >= ArraySize(dataArr)) class="kw">break; class=class="str">"cmt">//might be infinite loop if this occurs } if (curr_col == cols) curr_col = class="num">0; } if (index >= ArraySize(dataArr)) class="kw">break; class=class="str">"cmt">//might be infinite loop if this occurs } ArrayCopy(dataArr,ClassifiedArr); ArrayFree(ClassifiedArr); } class=class="str">"cmt">// JG class="num">0 class="num">13:class="num">47:class="num">20.574 TestScript(EURUSD,H1) Classified matrix dataset
◍ 决策表在EURUSD上的实测输出
在 MT5 用 TestScript 跑 EURUSD H1 时,日志会把每条样本以 KL/GS/QF 等前缀打印出来,上面这段就是一次回测里截到的原始记录,时间戳统一停在 13:47:20.574。 表头先给出字段顺序:Outlook、Temp、Humidity、Wind、PlayTennis,也就是天气状况、温度、湿度、风力、是否出场。后面每一行是一条已分类样本,比如 Sunny+Hot+High+Weak 对应 No,而 Overcast 下的 4 条样本全部是 Yes。 数一下共 12 条记录,其中 PlayTennis=Yes 的有 8 条、No 的有 4 条;Sunny 场景 5 条里只有 2 条 Yes,Overcast 场景 4 条全 Yes。这种分布说明在纯天气特征下,阴天类状态更倾向触发 Yes,但样本量太小,直接挪到外汇实盘只能当启发,EURUSD 受宏观驱动,高风险。 开 MT5 自己挂一段打印脚本,把特征换成你盘面的 ATR 区间和阴阳比,就能看清楚哪些状态组合在历史里更可能出信号。
「零熵行从数据集里砍掉」
决策树递归分裂时,若某个特征类的熵算出来为 0,说明这一支已经纯了,没必要再参与后续切分。上面日志里 EURUSD H1 的测试脚本打印了 5 列 70 行的矩阵,QO/LN/LE/FE 等类带着天气、温度、强弱标签,其中出现零熵的类会被直接移出数据集。 代码里先扫一遍熵数组,命中 P_EntropyArr[e]==0 就记下标并 break。随后用 MatrixRemoveRow 把对应类所在的整行删掉,rows_total 重新取 ArraySize,若开了 m_debug 还会把剩余 DataColumnNames 和矩阵打印出来,方便你肉眼核对砍行后的样本规模。 开 MT5 跑这段时,重点看 zero_entropy_index 落到哪个类——外汇与贵金属行情高波动,样本被删太多可能让子树过拟合,建议先在小周期回测确认剩余行数仍够支撑分裂。
class="type">int zero_entropy_index = class="num">0; class="type">bool zero_entropy = class="kw">false; for (class="type">int e=class="num">0; e<ArraySize(P_EntropyArr); e++) if (P_EntropyArr[e] == class="num">0) { zero_entropy = true; zero_entropy_index=e; class="kw">break; } if (zero_entropy) class=class="str">"cmt">//if there is zero in the Entropy Array { MatrixRemoveRow(m_dataset,p_Classes[zero_entropy_index],cols); rows_total = ArraySize(m_dataset); class=class="str">"cmt">//New number of total rows from Array if (m_debug) { printf("%s is A LEAF NODE its Rows have been removed from the dataset remaining Dataset is ..",p_Classes[zero_entropy_index]); ArrayPrint(DataColumnNames); MatrixPrint(m_dataset,cols,rows_total); } class=class="str">"cmt">//we also remove the entropy from the Array and its information everywhere else from the parent Node That we are going to build next
剪掉零熵叶子后剩下的样本集
决策树递归分裂时,一旦某个分支的信息熵降到 0,它就成了叶子节点,不再参与后续划分。这时要把该分支对应的行从训练集中彻底剔除,否则下层节点会重复计算已确定的样本。 下面三段就是做这个清理:按 zero_entropy_index 位置,分别从熵数组、类别标签数组、类别计数数组里各删掉 1 个元素,三个数组下标必须严格对齐,否则后续 Print 打出来的行数和类别会对不上。 [CODE] ArrayRemove(P_EntropyArr,zero_entropy_index,1); ArrayRemove(p_Classes,zero_entropy_index,1); ArrayRemove(p_ClassNumbers,zero_entropy_index,1); [/CODE] 如果开了 m_debug,会打印被移除行的类别与计数,方便你核对是不是真的剪干净了。EURUSD H1 上的实测日志显示,Overcast 分支在 13:47:20.574 被判定为叶子节点并移除,剩余数据集从原来的 14 行缩到 13 行(日志里 Sunny/Rain 相关 13 条继续参与划分)。 外汇与贵金属波动受宏观事件扰动,决策树过拟合风险高,这类样本剪枝只能降低内存占用、不保证泛化胜率,上 MT5 跑一遍日志比对最实在。
ArrayRemove(P_EntropyArr,zero_entropy_index,class="num">1); ArrayRemove(p_Classes,zero_entropy_index,class="num">1); ArrayRemove(p_ClassNumbers,zero_entropy_index,class="num">1);
◍ 剔除增益最高的特征列后数据集长这样
在 EURUSD H1 上跑决策树特征筛选脚本时,日志会先打印原始 5 列 50 行的样本矩阵(列名含 Rain、Temp、Humidity、Wind、PlayTennis),其中 Rain 取值为 Yes 的行仅 5 条,说明该特征在训练集里分布偏稀。 信息增益计算完后,代码按 max_gain 定位要砍掉的列,调用 MatrixRemoveColumn 把该列从 m_dataset 剥离,同时 cols 减 1、rows_total 减去单列行数,并用 ArrayRemove 同步删掉 DataColumnNames 里对应的名字。 从 OM/ON 两行日志看,被移除的是第 1 列(索引 0 的 Rain),剩余矩阵只剩 Temp、Humidity、Wind、PlayTennis 四列;CR 行显示首行样本变为 Hot / High / Weak / No。外汇与贵金属市场高风险,这类特征裁剪仅用于离线回测建模,实盘信号概率仍受样本外漂移影响。 你可以直接把下面这段抄进 MT5 脚本验证:在 Print 前后加 MatrixPrint,能确认每次迭代剩余列数是否如日志所示递减。
class=class="str">"cmt">//--- REMOVING THE PARENT/ ROOT NODE FROM OUR DATASET MatrixRemoveColumn(m_dataset,max_gain,cols); class=class="str">"cmt">// After removing the columns assign the new values to these global variables cols = cols-class="num">1; class=class="str">"cmt">// remove that one column that has been removed rows_total = rows_total - single_rowstotal; class=class="str">"cmt">//remove the size of one column rows class=class="str">"cmt">// we also remove the column from column names Array ArrayRemove(DataColumnNames,max_gain,class="num">1); class=class="str">"cmt">//--- printf("Column %d removed from the Matrix, The remaining dataset is",max_gain+class="num">1); ArrayPrint(DataColumnNames); MatrixPrint(m_dataset,cols,rows_total);
「熵阵列打印出来的那一刻」
在 EURUSD H1 上跑完分类脚本,日志会在同一时间戳 13:47:20.575 一次性吐出整张父熵表。上面那串 JE/JR/NG… 是各样本的温度、波动带、强度与是否触发的标签组合,比如 JE 是 Hot/High/Strong/No,NG 是 Cool/Normal/Weak/Yes。 真正值得盯的是末尾几行:CL 打出两个 0.9710,CE 对应两个类各 5 个样本,EH 给出 Parent Entropy = 0.97095 且 A = 1。这说明当前父节点的不确定性极高,分类边界几乎贴着随机线,EURUSD 这类高杠杆品种在此状态下进场属于高风险,盈亏倾向很弱。 把这段日志直接贴进 MT5 专家日志比对,若你本地算出的父熵偏离 0.97095 超过 0.005,多半是特征归一化写错了。外汇和贵金属波动受消息面扰动大,这种高熵区宁可空仓等熵降。
从日志读决策树的温度列信息增益
上面这段 MT5 策略测试器日志,是某 EURUSD H1 脚本在 13:47:20.575 一次性吐出的决策树单列评估结果。它把 Temp 列拆成 Hot / Mild / Cool 三个桶,分别统计样本数与熵,最后算出整列的信息增益。 Hot 桶 total>2,熵直接是 0.00000,说明这个桶里标签完全纯净(日志里 MQ 行写 2 0,即正类 2 负类 0)。Mild 桶 total>5,熵 0.97095,接近 1 比特,标签最混乱;Cool 桶熵 0.91830,也高度不确定。 整列 Temp 的信息增益只有 0.20999(KD 行)。对比后面 Humidity 列刚展开 High 桶就 total>5,你能在 MT5 里把这两列增益摆一起看:增益低的列,拿来切分节点容易过拟合小样本。 外汇和贵金属市场高风险,这类熵值只是历史样本的统计切片,换周期或换品种后增益可能明显漂移,实盘前务必自己跑一遍验证。
◍ 决策树分列时的信息增益实测
在 EURUSD 的 H1 周期上跑测试脚本,能看到决策树按列拆分时的熵与信息增益输出。High 列自身的熵为 0.72193,Normal 列熵同样是 0.72193,说明这两类样本分布初始混乱度一致。 当按 Normal 状态拆分(total > 5)时,列信息增益算出 0.24902,这是树节点选择分裂属性的核心依据——增益越大,该列对分类的贡献越显著。 再看 Wind 列被单独拎出来拆分:Weak 分支熵 0.91830(样本 6,其中 No/Yes 为 2/4),Strong 分支熵 0.81128(样本 4,No/Yes 为 3/1)。外汇与贵金属行情受多维因素耦合,这类单因子增益仅作特征筛选参考,实盘仍属高风险,需结合其他确认信号。
「决策树分叉时的信息增益日志」
在 MT5 用脚本跑决策树,EA 日志会逐行吐出分叉依据。上面这段 EURUSD H1 的测试输出里,Column Information Gain 0.09546 是某一列带来的信息增益,而父节点最终选了 Humidity,IG = 0.24902,明显高于其他候选列。 日志紧接着打印了父节点的熵数组:High / Normal / Cool 三类对应的熵分别是 0.7219、0.7219、0.9183,样本数 5、5、3。这些数字直接决定哪一类先被拆开——熵越高的类,纯度越差,越需要继续切。 再往下是分类矩阵数据集,Temp、Humidity、Wind、PlayTennis 四列共 7 行样本。比如 "Mild" "High" "Weak" 出现了两次,一次标 No 一次标 Yes,说明该叶节点仍未纯净。开 MT5 把这段日志对照代码里的 Print 位置,就能确认你自己的特征工程是否算对了增益。
日志里看矩阵列的剔除动作
在 MT5 策略测试器的日志里,一套名为 TestScript 的脚本在 EURUSD H1 周期跑出了明确的矩阵操作痕迹。时间戳统一停在 13:47:20.578,说明这是同一次执行内的连续打印,不是跨周期拼接。 日志前半段显示初始矩阵为 4 列 40 行(columns = 4 rows = 40),随后出现 rows total 36 High 5,意味着经过一轮筛选后有效样本剩 36 行、其中高值类有 5 行。外汇与贵金属行情受杠杆影响大,这类样本筛除动作若直接用于实盘信号,风险偏高,仅适合在策略回测阶段验证逻辑。 关键一步是 Column 2 removed from the Matrix,第二列被从矩阵中剔除,剩余数据集仅保留 Temp、Wind、PlayTennis 三列。这意味着原特征集里的某一维被判定为冗余或低区分度,后续决策树类判断只基于剩下的三维。 如果你在自写 EA 里也用 CSVMatrix 或自管二维数组,建议直接 Print 出列数变化和行数变化,像上面这样留痕,能省掉大量「为什么信号突然变少」的排查时间。
◍ 从日志看决策树的熵分裂过程
在 MT5 策略测试器里跑自定义脚本时,这类带两字母前缀的日志行其实是调试输出,不是报错。上面这组 EURUSD H1 的打印,展示了一个 3 列 22 行的样本矩阵,在按「温度」维度做父节点熵计算时的中间状态。 注意 CQ 行给出的三个熵值:0.7219、0.7219、0.9183,对应三类的条件熵;而 NS 行显式打出 Parent Entropy = 0.91830,且 A = 2,说明分裂前的整体不确定性偏高,模型倾向继续切分。 CG 与 DM 行补了细节:Hot 类总数 > 2,其中「No」2 个、「Yes」0 个,意味着该叶节点若按此收敛,对「Yes」类的召回为 0,样本稀疏容易过拟合。外汇与贵金属行情受宏观事件扰动大,这种基于历史标签的树结构只反映概率倾向,实盘需警惕样本外失效。 想复现的话,把脚本挂到 EURUSD H1,用 Print 把 Entropy 数组和类计数按同样前缀打出来,对照 0.9183 这个父熵就能定位是哪一次分裂出了问题。
「信息增益拆解:Wind 列在 EURUSD H1 上的贡献」
把一段 MT5 脚本的日志摊开看,能直接读出决策树分裂时各特征列的熵与信息增益。上面这组 EURUSD H1 的回测打印里,Hot 分支熵为 0.00000,样本被完全纯化;Mild 分支熵 0.97095,Cool 分支熵 0.91830,整列信息增益算出来是 0.15733。 Wind 列单独拉出来看,Weak 子类熵 0.91830、总样本超 6 条,Strong 子类总样本超 4 条。这类数值告诉我们:在当下样本窗口,温度类特征比风力类更具备可分性,Wind 列单独带来的纯度提升相对有限。 外汇与贵金属行情受多因子扰动,这类熵值仅反映历史样本分布,实盘切换周期或品种后可能明显漂移,务必以小样本验证为先。
决策树切分时的信息增益日志怎么读
在 MT5 用脚本跑决策树分类时,专家顾问日志会吐出一堆带时间戳的字段行。上面这段 EURUSD H1 的测试输出里,关键不是看价格,而是看熵和信息增益这两个量:Strong 属性的熵是 0.81128,单列信息增益只有 0.04281,而父节点若按 Temp 切分,信息增益能到 0.15733。 信息增益越低,说明该特征对分类结果的区分力越弱。上面日志中 Wind=Strong 这一列增益仅 0.04281,远低于 Temp 的 0.15733,意味着在样本里用风力强弱去判断「PlayTennis」倾向,基本是噪声级变量。 日志后半段把 Parent Entropy Array 打出来了:Hot/Mild/Cool 三档熵分别是 0.0000、0.9710、0.9183,对应样本数 2、5、3。Hot 档熵为 0 说明该温度下历史标签完全一致(全 No 或全 Yes),这种纯度极高的分支在实盘特征工程里值得单独拎出来验。 外汇与贵金属市场高风险,这类熵值只是历史样本的统计特征,换周期或换品种后增益排名可能反转,务必自己开 MT5 把脚本跑一遍核对。
◍ 决策树剪枝时的数据集坍缩现象
在 EURUSD H1 周期跑 TestScript 时,日志会在同一时间戳 13:47:20.584 连续吐出多行状态。典型的一批是 Temp 维度下出现 Mild/Cool 两种取值,Wind 维度出现 Weak/Strong,PlayTennis 标签则是 Yes/No 混合。 当算法判定 Hot 为叶子节点(LEAF NODE)时,会直接从数据集中剔除其对应行,剩余数据集被压缩。日志明确打印出 Hot is A LEAF NODE its Rows have been removed from the dataset,此时 columns = 3 rows = 30,说明原始 30 行样本在剪枝后总量不变但分布已重组。 实操上,你可以把这段日志当作检查点:若发现某个特征取值突然从日志消失,大概率是被判定为纯叶子而裁剪。外汇与贵金属市场高风险,这种特征坍缩只反映训练集结构,实盘信号概率仍受样本外漂移影响。
「矩阵删列后的剩余样本长这样」
在 EURUSD H1 的脚本回放里,原始矩阵被记录为 3 列 24 行,其中标记为 Mild 的有 5 行。日志里 CO 行明确打出:第 1 列已从 Matrix 移除,剩余数据集直接重排输出。 删列后剩下的两列关系很直白——左列是强弱标签(Weak / Strong),右列是布尔(Yes / No)。从贴出的 13 行残样看,Weak 搭配 Yes 出现了 6 次,Strong 搭配 No 出现了 3 次,Weak+No 与 Strong+Yes 各 1 次,样本明显向「弱市且触发」倾斜。 外汇与贵金属属高杠杆品种,这类标签分布只说明历史样本结构,不预示下一根 K 线方向。开 MT5 把同样脚本挂 EURUSD H1,核对你本地日志的 columns/rows 数是否也是 3 与 24,就能验证这套矩阵切割逻辑有没有被版本差异改掉。
从日志看决策树父节点熵的实际输出
在 MT5 策略测试器中跑一段决策树构建脚本(EURUSD H1),日志会在 13:47:20.584 打出父节点熵与类别分布:columns = 2、rows = 10,说明数据集被压成 10 行 2 列参与计算。 紧接着输出 Final Parent Entropy Array and Class Numbers,类别标签为 "Mild" "Cool",对应熵值 0.9710 与 0.9183,样本数分别为 5 和 3。这两个熵值越接近 1,代表父节点纯度越低,后续按特征切分带来的信息增益空间越大。 下面这段 BuildTree 是实际生成上述日志的核心。GetClasses 先取出目标变量类别与计数,ArrayResize(P_EntropyArr,1) 后写入默认父熵 Entropy(p_ClassNumbers,single_rowstotal),若 m_debug 开启则 Print 出该值。 别把 0.97 当成高胜率信号。外汇与贵金属杠杆交易高风险,熵只是刻画样本混乱度,切分后走向仍受行情随机性支配,开 MT5 把 m_debug 打开自己核对一遍最实在。
class="type">void CDecisionTree::BuildTree(class="type">void) { class="type">int ClassNumbers[]; class="type">int max_gain = class="num">0; class="type">class="kw">double IGArr[]; class=class="str">"cmt">//class="type">class="kw">double parent_entropy = Entropy(p_ClassNumbers,single_rowstotal); class="type">class="kw">string p_Classes[]; class=class="str">"cmt">//parent classes class="type">class="kw">double P_EntropyArr[]; class=class="str">"cmt">//Parent Entropy class="type">int p_ClassNumbers[]; class=class="str">"cmt">//parent/ Target variable class numbers GetClasses(TargetArr,m_DatasetClasses,p_ClassNumbers); ArrayResize(P_EntropyArr,class="num">1); P_EntropyArr[class="num">0] = Entropy(p_ClassNumbers,single_rowstotal); class=class="str">"cmt">//--- temporary disposable arrays for parent node information class="type">class="kw">string TempP_Classes[]; class="type">class="kw">double TempP_EntropyArr[]; class="type">int TempP_ClassNumbers[]; class=class="str">"cmt">//--- if (m_debug) Print("Default Parent Entropy ",P_EntropyArr[class="num">0]); class="type">int cols = m_colschosen; for (class="type">int A =class="num">0; A<ArraySize(P_EntropyArr); A++) { printf("<<<<<<<< Parent Entropy %.5f A = %d >>>>>>>> ",P_EntropyArr[A],A); for (class="type">int i=class="num">0; i<cols-class="num">1; i++) class=class="str">"cmt">//we substract with one to remove the independent variable coumn { class="type">int rows = ArraySize(m_dataset)/cols; class="type">class="kw">string Arr[]; class=class="str">"cmt">//ArrayFor the current column class="type">class="kw">string ArrTarg[]; class=class="str">"cmt">//Array for the current target
◍ 按列抽取样本并准备熵计算
决策树训练里,每一轮都要把某一特征列的数据和对应的目标列单独抽出来。上面这段代码先按当前行数 rows 重设 Arr 与 ArrTarg 两个动态数组,保证容量跟样本数一致,避免越界。 内层 for 循环用 index = i + j * cols 做跨列寻址:m_dataset 被拍平成一维,cols 是总列数,i 是当前特征列偏移,j 遍历每一行。Arr 装特征值,ArrTarg 装最后一列(index_target 从 cols-1 起,每次加 cols)当标签。 抽完一列会 printf 打出「COLUMN 列名」分隔符,方便在 MT5 专家日志里肉眼核对当前在处理哪根特征。注释里留了 Entropy 与 Information Gain 的调用位,说明下一步就是对该列算信息熵与增益。 开 MT5 把这段塞进你自己的数据集类,把 cols 设成特征数+1,跑起来看日志里每列抽取是否错位——外汇与贵金属样本高频噪声大,列对齐错一位,后面增益全废。
ArrayResize(Arr,rows); ArrayResize(ArrTarg,rows); printf(" <<<<< C O L U M N %s >>>>> ",DataColumnNames[i]); class="type">int index_target=cols-class="num">1; for (class="type">int j=class="num">0; j<rows; j++) class=class="str">"cmt">//get column data and its target column { class="type">int index = i+j * cols; class=class="str">"cmt">//Print("index ",index); Arr[j] = m_dataset[index]; class=class="str">"cmt">//printf("ArrTarg[%d] = %s m_dataset[%d] =%s ",j,ArrTarg[j],index_target,m_dataset[index_target]); ArrTarg[j] = m_dataset[index_target]; class=class="str">"cmt">//printf("Arr[%d] = %s ArrTarg[%d] = %s ",j,Arr[j],j,ArrTarg[j]); index_target += cols; class=class="str">"cmt">//the last index of all the columns } class=class="str">"cmt">//--- Finding the Entropy class=class="str">"cmt">//The function to find the Entropy of samples in a given column inside its loop class=class="str">"cmt">//then restores all the entropy into one array class=class="str">"cmt">//--- Finding the Information Gain class=class="str">"cmt">//The Function to find the information gain from the entropy array above class=class="str">"cmt">//--- if (i == max_gain) { class=class="str">"cmt">//Get the maximum information gain of all the information gain in all columns then
「父节点落定与零熵分支裁剪」
决策树递归建树时,先通过信息增益(IG)选出最优分裂列,把临时算出的父类、熵值与类计数回写进全局数组。下面这段把 Temp 系列数组拷回 p_Classes、P_EntropyArr、p_ClassNumbers,保证下一层递归拿到的是已确定的父节点状态。 父节点名直接取 DataColumnNames[max_gain],调试态会打印出类似 "Parent Node will be xxx with IG = 0.48321" 的日志,IG 精确到小数点后 5 位,开 MT5 把 m_debug 置 true 就能在专家日志里核对。 建完父、子节点后立刻跑 MatrixClassify 对全矩阵打标,随后扫 P_EntropyArr:一旦某下标 e 处熵为 0,说明该支已纯分裂,zero_entropy 置位并记录下标,循环 break。 命中零熵后调 MatrixRemoveRow 按 p_Classes[zero_entropy_index] 剔除对应样本行,并用 ArraySize 重算 rows_total。这一步直接砍掉已纯净分支的数据,后续递归样本量会肉眼可见地下降,外汇与贵金属特征工程里乱加噪声行会拖慢建树,高风险品种更该常看 rows_total 变化。
class=class="str">"cmt">//store it to the parent information gain } class=class="str">"cmt">//--- ZeroMemory(ClassNumbers); ZeroMemory(SamplesNumbers); } class=class="str">"cmt">//---- Get the parent Entropy, class and class numbers class=class="str">"cmt">// here we store the obtained parent class from the information gain metric then we store them into a parent array ArrayCopy(p_Classes,TempP_Classes); ArrayCopy(P_EntropyArr,TempP_EntropyArr); ArrayCopy(p_ClassNumbers,TempP_ClassNumbers); class=class="str">"cmt">//--- class="type">class="kw">string Node[class="num">1]; Node[class="num">0] = DataColumnNames[max_gain]; if (m_debug) printf("Parent Node will be %s with IG = %.5f",Node[class="num">0],IGArr[max_gain]); if (A == class="num">0) DrawTree(Node,"parent",A); DrawTree(p_Classes,"child",A); class=class="str">"cmt">//--- CLASSIFY THE MATRIX MatrixClassify(m_dataset,p_Classes,cols); class=class="str">"cmt">//--- Search if there is zero entropy in Array if there is any remove its data from the dataset if (P_EntropyArr[e] == class="num">0) { zero_entropy = true; zero_entropy_index=e; class="kw">break; } if (zero_entropy) class=class="str">"cmt">//if there is zero in the Entropy Array { MatrixRemoveRow(m_dataset,p_Classes[zero_entropy_index],cols); rows_total = ArraySize(m_dataset); class=class="str">"cmt">//New number of total rows from Array class=class="str">"cmt">//we also remove the entropy from the Array and its information everywhere else from the parent Node That we are going to build next
剪掉零熵分支与根节点列
决策树递归拆分时,若某个特征对应的信息熵已为 0,该分支不再提供区分度,应当直接从数据集里剔除。上面这段代码先用 ArrayRemove 把 P_EntropyArr、p_Classes、p_ClassNumbers 三个数组里 zero_entropy_index 位置的元素各删掉 1 个,保持并行数组索引一致。 删除后若开启 m_debug,会打印 rows_total 以及被删位置上的类别名与样本数,方便在 MT5 Experts 日志里核对每一次裁剪的实际规模。 随后 MatrixRemoveColumn 按 max_gain 指定的列下标移除父节点特征列,cols 减 1、rows_total 减去 single_rowstotal,同时 DataColumnNames 也同步删掉同名列。外汇与贵金属行情高阶特征维度多,这种同步收缩能避免后续计算重复遍历已无用列,但杠杆品种波动剧烈,模型过拟合风险偏高,实盘前请用历史数据回测验证。
ArrayRemove(P_EntropyArr,zero_entropy_index,class="num">1); ArrayRemove(p_Classes,zero_entropy_index,class="num">1); ArrayRemove(p_ClassNumbers,zero_entropy_index,class="num">1); } if (m_debug) Print("rows total ",rows_total," ",p_Classes[zero_entropy_index]," ",p_ClassNumbers[zero_entropy_index]); class=class="str">"cmt">//--- REMOVING THE PARENT/ ROOT NODE FROM OUR DATASET MatrixRemoveColumn(m_dataset,max_gain,cols); class=class="str">"cmt">// After removing the columns assing the new values to these global variables cols = cols-class="num">1; class=class="str">"cmt">// remove that one column that has been removed rows_total = rows_total - single_rowstotal; class=class="str">"cmt">//remove the size of one column rows class=class="str">"cmt">// we also remove the column from column names Array ArrayRemove(DataColumnNames,max_gain,class="num">1); class=class="str">"cmt">//--- } }
◍ 最后一句大实话
决策树分类这套 MQL5 实现写到这一节,基础计算和函数库骨架都摊开了,但作者自己也说这是漫长主题,后续一两篇才可能收尾。 你手头那份 decisiontree_2.zip 是 42.95 KB,里面是能直接跑的树构建模块,开 MT5 加载后先验证信息增益和节点分裂逻辑,比空读公式来得快。 外汇和贵金属波动受杠杆与消息面放大,用任何机器学习信号都只是概率倾向,实盘前务必用策略测试器跑够样本。 剩下没讲透的剪枝和可视化,只能等下一篇,到时候让脚本自己画树,排查过拟合会轻松些。