数据科学和机器学习(第 19 部分):利用 AdaBoost 为您的 AI 模型增压·进阶篇
(2/3)· 从决策树桩到加权融合,手把手把 MetaTrader 5 上的弱模型拼成强预测器
不少交易者把单棵决策树直接丢进策略测试器,以为这就是机器学习,结果样本外表现稀碎。AdaBoost 的思路恰恰相反:用一堆故意做弱的模型,靠权重迭代把误判样本逼出来,强分类器是这么长出来的。本篇接上篇基础概念,直接进训练与部署的实操层。
◍ Adaboost 训练里的自助采样与权重迭代
融合多个弱分类器时,如果直接拿同一份数据灌进 n 个模型(代码里由 m_estimators 控制数量),每个模型学到的边界会高度重合,最终多数投票也分不出高下。Bootstrapping 在这里不是可选项,而是让基模型产生差异化的前提——没有它,融合就退化成了复制粘贴。 下面这段 fit 函数是第一版实现,只做了实例权重初始化和 alpha 计算,但没做自助采样: void AdaBoost::fit(matrix &x,vector &y) { m_alphas.Resize(m_estimators); classes_in_data = MatrixExtend::Unique(y); //Find the target variables in the class ulong m = x.Rows(), n = x.Cols(); vector weights(m); weights = weights.Fill(1.0) / m; //Initialize instance weights vector preds(m); vector misclassified(m); double error = 0; for (uint i=0; i<m_estimators; i++) { //--- weak_learner = new CDecisionTreeClassifier(this.m_min_split, m_max_depth); weak_learner.fit(x, y); //fitting the randomized data to the i-th weak_learner preds = weak_learner.predict(x); //making predictions for the i-th weak_learner for (ulong j=0; j<m; j++) misclassified[j] = (preds[j] != y[j]); error = (misclassified * weights).Sum() / (double)weights.Sum(); //--- Calculate the weight of a weak learner in the final weak_learner double alpha = 0.5 * log((1-error) / (error + 1e-10)); //--- Update instance weights weights *= exp(-alpha * y* preds); weights /= weights.Sum(); //--- save a weak learner and its weight this.m_alphas[i] = alpha; this.weak_learners[i] = weak_learner; } } 逐行拆解:m_alphas.Resize 先给每个弱学习器留好权重槽位;weights 初始化为 1/m,意味着首轮所有样本等权。循环里每棵决策树都用全量 x、y 拟合,misclassified 向量标记错分样本,error 用加权错分率算出。alpha = 0.5*log((1-error)/error) 是 Adaboost 标准公式,错得少的树权重高。随后 weights *= exp(-alpha*y*preds) 把分对的样本降权、分错的升权,再归一化。 第二版补上了自助采样,差异就在循环前的 concatenate 和循环内的 Randomize: void AdaBoost::fit(matrix &x,vector &y) { m_alphas.Resize(m_estimators); classes_in_data = MatrixExtend::Unique(y); //Find the target variables in the class ulong m = x.Rows(), n = x.Cols(); vector weights(m); weights = weights.Fill(1.0) / m; //Initialize instance weights vector preds(m); vector misclassified(m); //--- matrix data = MatrixExtend::concatenate(x, y); matrix temp_data; matrix x_subset; vector y_subset; double error = 0; for (uint i=0; i<m_estimators; i++) { temp_data = data; MatrixExtend::Randomize(temp_data, this.m_random_state, this.m_boostrapping); if (!MatrixExtend::XandYSplitMatrices(temp_data, x_subset, y_subset)) //Get randomized subsets { ArrayRemove(weak_learners,i,1); //Delete the invalid weak_learner 这里 temp_data 把特征和目标拼一起,Randomize 按 m_boostrapping 开关决定是否做有放回抽样。抽完用 XandYSplitMatrices 拆回子集,若拆分失败就删掉这个弱学习器。开了自助采样后,基模型看到的样本分布各不相同,融合后的泛化能力倾向优于第一版。在 MT5 里把 m_boostrapping 从 false 切到 true,跑同一组 EURUSD 的 H1 特征,能直接对比两份 weak_learners 的预测离散度。外汇与贵金属杠杆高,模型回测表现不代表实盘概率,验证时请用小仓位。
class="type">void AdaBoost::fit(matrix &x,vector &y) { m_alphas.Resize(m_estimators); classes_in_data = MatrixExtend::Unique(y); class=class="str">"cmt">//Find the target variables in the class class="type">ulong m = x.Rows(), n = x.Cols(); vector weights(m); weights = weights.Fill(class="num">1.0) / m; class=class="str">"cmt">//Initialize instance weights vector preds(m); vector misclassified(m); class="type">class="kw">double error = class="num">0; for (class="type">uint i=class="num">0; i<m_estimators; i++) { class=class="str">"cmt">//--- weak_learner = new CDecisionTreeClassifier(this.m_min_split, m_max_depth); weak_learner.fit(x, y); class=class="str">"cmt">//fitting the randomized data to the i-th weak_learner preds = weak_learner.predict(x); class=class="str">"cmt">//making predictions for the i-th weak_learner for (class="type">ulong j=class="num">0; j<m; j++) misclassified[j] = (preds[j] != y[j]); error = (misclassified * weights).Sum() / (class="type">class="kw">double)weights.Sum(); class=class="str">"cmt">//--- Calculate the weight of a weak learner in the final weak_learner class="type">class="kw">double alpha = class="num">0.5 * log((class="num">1-error) / (error + class="num">1e-10)); class=class="str">"cmt">//--- Update instance weights weights *= exp(-alpha * y* preds); weights /= weights.Sum(); class=class="str">"cmt">//--- save a weak learner and its weight this.m_alphas[i] = alpha; this.weak_learners[i] = weak_learner; } } class="type">void AdaBoost::fit(matrix &x,vector &y) { m_alphas.Resize(m_estimators); classes_in_data = MatrixExtend::Unique(y); class=class="str">"cmt">//Find the target variables in the class class="type">ulong m = x.Rows(), n = x.Cols(); vector weights(m); weights = weights.Fill(class="num">1.0) / m; class=class="str">"cmt">//Initialize instance weights vector preds(m); vector misclassified(m); class=class="str">"cmt">//--- matrix data = MatrixExtend::concatenate(x, y); matrix temp_data; matrix x_subset; vector y_subset; class="type">class="kw">double error = class="num">0; for (class="type">uint i=class="num">0; i<m_estimators; i++) { temp_data = data; MatrixExtend::Randomize(temp_data, this.m_random_state, this.m_boostrapping); if (!MatrixExtend::XandYSplitMatrices(temp_data, x_subset, y_subset)) class=class="str">"cmt">//Get randomized subsets { ArrayRemove(weak_learners,i,class="num">1); class=class="str">"cmt">//Delete the invalid weak_learner
「AdaBoost 里单棵弱树怎么算权重」
在集成循环里,每一轮先拿自助采样得到的 x_subset、y_subset 去拟合一棵决策树弱分类器,树的最大深度由 m_max_depth 控制,节点最小拆分样本数走 m_min_split。 弱学习器训完立刻在同源子集上 predict,逐样本比对 preds[j] != y_subset[j] 得到误判掩码 misclassified,再用当前样本权重 weights 做加权平均:error = (misclassified * weights).Sum() / weights.Sum(),这就是该弱分类器在这一轮的加权误分率。 alpha 的赋值用了经典公式 0.5 * log((1-error)/(error+1e-10)),当 error 趋近 0 时 alpha 变大、树在最终投票里话语权更重;error 高于 0.5 则 alpha 转负。 随后样本权重按 exp(-alpha * y_subset * preds) 更新并重新归一化,被错分的样本权重倾向放大,下一棵树被迫关注难样本。 类声明里默认 n_estimators=50、random_state=42、bootstrapping=true,意味着开 MT5 跑这套如果不改参,会固定用 50 棵弱树且随机种子锁死,回测结果可复现。
printf("%s %d Failed to split data",__FUNCTION__,__LINE__); class="kw">continue; } class=class="str">"cmt">//--- weak_learner = new CDecisionTreeClassifier(this.m_min_split, m_max_depth); weak_learner.fit(x_subset, y_subset); class=class="str">"cmt">//fitting the randomized data to the i-th weak_learner preds = weak_learner.predict(x_subset); class=class="str">"cmt">//making predictions for the i-th weak_learner class=class="str">"cmt">//printf("[%d] Accuracy %.3f ",i,Metrics::accuracy_score(y_subset, preds)); for (class="type">ulong j=class="num">0; j<m; j++) misclassified[j] = (preds[j] != y_subset[j]); error = (misclassified * weights).Sum() / (class="type">class="kw">double)weights.Sum(); class=class="str">"cmt">//--- Calculate the weight of a weak learner in the final weak_learner class="type">class="kw">double alpha = class="num">0.5 * log((class="num">1-error) / (error + class="num">1e-10)); class=class="str">"cmt">//--- Update instance weights weights *= exp(-alpha * y_subset * preds); weights /= weights.Sum(); class=class="str">"cmt">//--- save a weak learner and its weight this.m_alphas[i] = alpha; this.weak_learners[i] = weak_learner; } } class AdaBoost { class="kw">protected: vector m_alphas; vector classes_in_data; class="type">int m_random_state; class="type">bool m_boostrapping; class="type">uint m_min_split, m_max_depth; CDecisionTreeClassifier *weak_learners[]; class=class="str">"cmt">//store weak_learner pointers for memory allocation tracking CDecisionTreeClassifier *weak_learner; class="type">uint m_estimators; class="kw">public: AdaBoost(class="type">uint min_split, class="type">uint max_depth, class="type">uint n_estimators=class="num">50, class="type">int random_state=class="num">42, class="type">bool bootstrapping=true); ~AdaBoost(class="type">void); class="type">void fit(matrix &x, vector &y);
AdaBoost 构造器里的数组预分配
在 MT5 里跑集成学习模型,第一步是把弱分类器容器先撑开。上面这段构造器用 ArrayResize(weak_learners, n_estimators) 直接按传入的树数量预留内存,避免后续循环里反复扩数组拖慢回测。 默认参数里 n_estimators=50、random_state=42,意味着不显式改的话,模型会用固定随机种子生成 50 棵弱树,结果可复现。外汇与贵金属行情高波动、高杠杆,集成模型信号仅作概率参考,实盘前务必在策略测试器用历史数据验证稳定性。 bootstrapping 默认开启,采样带放回;若你怀疑近期样本代表性差,可关掉它强制全量训练,但过拟合风险会上升。
class="type">int predict(vector &x); vector predict(matrix &x); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ AdaBoost::AdaBoost(class="type">uint min_split, class="type">uint max_depth, class="type">uint n_estimators=class="num">50, class="type">int random_state=class="num">42, class="type">bool bootstrapping=true) :m_estimators(n_estimators), m_random_state(random_state), m_boostrapping(bootstrapping), m_min_split(min_split), m_max_depth(max_depth) { ArrayResize(weak_learners, n_estimators); class=class="str">"cmt">//Resizing the array to retain the number of base weak_learners }
◍ 多数投票怎么落进预测函数
Adaboost 训练完后,判定逻辑很直接:每个弱学习器先各自给样本打一个类标,再按各自权重做加权和,取加权后得分最高的类作为最终输出。也就是说,被更多高权重弱学习器支持的类别,在下一根 K 线出现的概率倾向更高。 用 iris.csv 只是搭模型和调参用,不代表实盘特征。把 random_state 设成 -1 后,EA 每次启动会用 GetTickCount 当随机种子,等于把训练环境故意打乱,看模型在更随机条件下是否还站得住。 日志里那组 EURUSD H1 的回测打印很有参考价值:同一套 AdaBoost(2,1,10,42) 在五次运行中准确率分别在 0.960、0.947、0.960、0.933、0.967 之间浮动。没有一次满血,说明随机种子一变,边界样本就会被重新划分。 换逻辑回归当弱学习器时,类结构不用动,只在 fit 函数里把底模型换成 LogisticRegression 即可。下面这段 predict 是核心,逐行拆一下: int AdaBoost::predict(vector &x) 定义预测入口,入参是特征向量 x。 vector weak_preds(m_estimators), final_preds(m_estimators); 先开两个容器,weak_preds 存各弱学习器预测。 for(uint i=0;i<this.m_estimators;i++) weak_preds[i]=this.weak_learners[i].predict(x); 遍历所有弱学习器,把每个预测塞进 weak_preds。 return (int)weak_preds[(this.m_alphas*weak_preds).ArgMax()]; 用权重向量 m_alphas 点积预测向量,ArgMax 找最大加权位置,即多数决策类。 真要验证,把 OnInit 里 ReadCsv 的路径换成你自己的特征 CSV,跑完看 Terminal 里打出的 acc 是不是掉到 0.9 以下,再决定要不要上实盘。外汇和贵金属波动大,这套准确率只是样本内现象,实盘失效概率不低。
class="type">int AdaBoost::predict(vector &x) { class=class="str">"cmt">// Combine weak learners using weighted sum vector weak_preds(m_estimators), final_preds(m_estimators); for (class="type">uint i=class="num">0;i<this.m_estimators; i++) weak_preds[i] = this.weak_learners[i].predict(x); class="kw">return (class="type">int)weak_preds[(this.m_alphas*weak_preds).ArgMax()]; class=class="str">"cmt">//Majority decision class } class="macro">#include <MALE5\Ensemble\AdaBoost.mqh> DecisionTree::AdaBoost *ada_boost_tree; LogisticRegression::AdaBoost *ada_boost_logit; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- class="type">class="kw">string headers; matrix data = MatrixExtend::ReadCsv("iris.csv",headers); matrix x; vector y; MatrixExtend::XandYSplitMatrices(data,x,y); ada_boost_tree = new DecisionTree::AdaBoost(class="num">2,class="num">1,class="num">10,class="num">42); ada_boost_tree.fit(x,y); vector predictions = ada_boost_tree.predict(x); printf("Adaboost acc = %.3f",Metrics::accuracy_score(y, predictions)); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class="num">2024.01.class="num">17 class="num">17:class="num">52:class="num">27.914 AdaBoost Test(EURUSD,H1) Adaboost acc = class="num">0.960 QK class="num">0 class="num">17:class="num">52:class="num">27.914 AdaBoost Test(EURUSD,H1) Adaboost acc = class="num">0.960 LL class="num">0 class="num">17:class="num">52:class="num">35.436 AdaBoost Test(EURUSD,H1) Adaboost acc = class="num">0.947 JD class="num">0 class="num">17:class="num">52:class="num">42.806 AdaBoost Test(EURUSD,H1) Adaboost acc = class="num">0.960 IL class="num">0 class="num">17:class="num">52:class="num">50.071 AdaBoost Test(EURUSD,H1) Adaboost acc = class="num">0.933 MD class="num">0 class="num">17:class="num">52:class="num">57.822 AdaBoost Test(EURUSD,H1) Adaboost acc = class="num">0.967 class="type">void AdaBoost::fit(matrix &x,vector &y) { m_alphas.Resize(m_estimators); classes_in_data = MatrixExtend::Unique(y); class=class="str">"cmt">//Find the target variables in the class class="type">ulong m = x.Rows(), n = x.Cols(); vector weights(m); weights = weights.Fill(class="num">1.0) / m; class=class="str">"cmt">//Initialize instance weights vector preds(m); vector misclassified(m); class=class="str">"cmt">//--- matrix data = MatrixExtend::concatenate(x, y); matrix temp_data; matrix x_subset; vector y_subset;
「AdaBoost 里弱分类器的权重迭代」
这段 MT5 代码实现的是集成学习中 AdaBoost 的核心训练循环:对 m_estimators 个弱学习器逐个做自助采样、拟合与权重更新。每次循环先复制原始数据并随机化,若无法切分特征与标签矩阵就直接移除该弱学习器并跳过,避免脏数据污染模型。 弱学习器用的是 CLogisticRegression 逻辑回归,拟合随机子集后立刻在训练集上预测,逐样本标记误分类。误差 error 由误分类掩码乘样本权重求和再归一化得到,反映当前弱学习器在加权分布下的犯错比例。 关键在 alpha 的计算:alpha = 0.5 * log((1-error)/(error+1e-10)),误差越低权重越高,1e-10 防止除零。随后样本权重按 exp(-alpha*y*preds) 调整,分对的样本权重下降、分错的上升,再除以权和归一化,迫使下一棵树聚焦难例。 把这段代码直接丢进 MT5 的 EA 或指标工程里,改 m_estimators 从 10 调到 50,观察日志里 Failed to split data 的出现频率,能直观判断你的特征矩阵在随机化后有多少比例不可分。外汇与贵金属行情噪声大,这类集成模型过拟合风险高,参数改动前务必用历史数据交叉验证。
class="type">class="kw">double error = class="num">0; for (class="type">uint i=class="num">0; i<m_estimators; i++) { temp_data = data; MatrixExtend::Randomize(temp_data, this.m_random_state, this.m_boostrapping); if (!MatrixExtend::XandYSplitMatrices(temp_data, x_subset, y_subset)) class=class="str">"cmt">//Get randomized subsets { ArrayRemove(weak_learners,i,class="num">1); class=class="str">"cmt">//Delete the invalid weak_learner printf("%s %d Failed to split data",__FUNCTION__,__LINE__); class="kw">continue; } class=class="str">"cmt">//--- weak_learner = new CLogisticRegression(); weak_learner.fit(x_subset, y_subset); class=class="str">"cmt">//fitting the randomized data to the i-th weak_learner preds = weak_learner.predict(x_subset); class=class="str">"cmt">//making predictions for the i-th weak_learner for (class="type">ulong j=class="num">0; j<m; j++) misclassified[j] = (preds[j] != y_subset[j]); error = (misclassified * weights).Sum() / (class="type">class="kw">double)weights.Sum(); class=class="str">"cmt">//--- Calculate the weight of a weak learner in the final weak_learner class="type">class="kw">double alpha = class="num">0.5 * log((class="num">1-error) / (error + class="num">1e-10)); class=class="str">"cmt">//--- Update instance weights weights *= exp(-alpha * y_subset * preds); weights /= weights.Sum(); class=class="str">"cmt">//--- save a weak learner and its weight this.m_alphas[i] = alpha; this.weak_learners[i] = weak_learner; }
把 Adaboost 模型丢进策略测试器跑一遍
用开盘价布林带作特征,让模型学习下一根收盘蜡烛的方向:决策树版能分三类——收高于开记 1(多头)、收低于开记 2(空头)、持平记 0;逻辑回归因 sigmoid 本质只做二分类,收高于开为 1、否则为 0。 数据按 70% 训练、30% 测试切分,洗牌状态由随机种子固定。这里有个容易踩的坑:换更高时间帧前,train_bars 要从 1000 降到 100,否则服务端可能没有足够历史柱可拉。
- 年 1 月至 2023 年 2 月,H1 上两种 Adaboost 表现都离「优秀」很远——基于当前柱的信号在 24 根/天的密度下噪声太重。切到 H12 后,同等参数下曲线形态明显改善,说明这类模型在单根代表更大波动的时段才更站得住。
信号统一时记住映射:决策树的 1、2 对应多空,逻辑回归的 1、0 也对应多空,决策树 0(持平)直接丢弃。外汇与贵金属杠杆高,回测改善不等于实盘概率占优,上 MT5 前先用自己的品种历史复算一遍。
class=class="str">"cmt">//--- Data Collection for training the model class=class="str">"cmt">//--- x variables Bollinger band only matrix dataset; indicator_v.CopyIndicatorBuffer(bb_handle,class="num">0,class="num">0,train_bars); class=class="str">"cmt">//Main LINE dataset = MatrixExtend::concatenate(dataset, indicator_v); indicator_v.CopyIndicatorBuffer(bb_handle,class="num">1,class="num">0,train_bars); class=class="str">"cmt">//UPPER BB dataset = MatrixExtend::concatenate(dataset, indicator_v); indicator_v.CopyIndicatorBuffer(bb_handle,class="num">2,class="num">0,train_bars); class=class="str">"cmt">//LOWER BB dataset = MatrixExtend::concatenate(dataset, indicator_v); class=class="str">"cmt">//--- Target Variable class="type">int size = CopyRates(Symbol(),PERIOD_CURRENT,class="num">0,train_bars,rates); vector y(size); class="kw">switch(model) { case DECISION_TREE: { for (class="type">ulong i=class="num">0; i<y.Size(); i++) { if (rates[i].close > rates[i].open) y[i] = class="num">1; class=class="str">"cmt">//buy signal else if (rates[i].close < rates[i].open) y[i] = class="num">2; class=class="str">"cmt">//sell signal else y[i] = class="num">0; class=class="str">"cmt">//Hold signal } } break; case LOGISTIC_REGRESSION: for (class="type">ulong i=class="num">0; i<indicator_v.Size(); i++) { y[i] = (rates[i].close > rates[i].open); class=class="str">"cmt">//if close > open buy else sell } break; } dataset = MatrixExtend::concatenate(dataset, y); class=class="str">"cmt">//Add the target variable to the dataset if (MQLInfoInteger(MQL_DEBUG)) { Print("Data Head"); MatrixExtend::PrintShort(dataset); } MatrixExtend::TrainTestSplitMatrices(dataset,train_x,train_y,test_x,test_y,class="num">0.7,_random_state); class=class="str">"cmt">//--- Training and testing the trained model matrix train_x, test_x; vector train_y, test_y; MatrixExtend::TrainTestSplitMatrices(dataset,train_x,train_y,test_x,test_y,class="num">0.7,_random_state); class=class="str">"cmt">//Train test split data | This function splits the data into training and testing sample given a random state and class="num">70% of data to test while the rest class="num">30% for testing