数据科学和机器学习(第 19 部分):利用 AdaBoost 为您的 AI 模型增压·进阶篇
🚀

数据科学和机器学习(第 19 部分):利用 AdaBoost 为您的 AI 模型增压·进阶篇

(2/3)· 从决策树桩到加权融合,手把手把 MetaTrader 5 上的弱模型拼成强预测器

实战向 第 2/3 篇

不少交易者把单棵决策树直接丢进策略测试器,以为这就是机器学习,结果样本外表现稀碎。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 的预测离散度。外汇与贵金属杠杆高,模型回测表现不代表实盘概率,验证时请用小仓位。

MQL5 / C++
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 棵弱树且随机种子锁死,回测结果可复现。

MQL5 / C++
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 默认开启,采样带放回;若你怀疑近期样本代表性差,可关掉它强制全量训练,但过拟合风险会上升。

MQL5 / C++
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 以下,再决定要不要上实盘。外汇和贵金属波动大,这套准确率只是样本内现象,实盘失效概率不低。

MQL5 / C++
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 的出现频率,能直观判断你的特征矩阵在随机化后有多少比例不可分。外汇与贵金属行情噪声大,这类集成模型过拟合风险高,参数改动前务必用历史数据交叉验证。

MQL5 / C++
  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 前先用自己的品种历史复算一遍。

MQL5 / C++
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
把模型诊断交给小布盯盘
这些 AdaBoost 弱学习器的权重曲线和误分类率漂移,小布盯盘的 AIGC 已内置监控,打开对应品种页即可看到,你只管判断信号是否还值得跟。

常见问题

通常不会,但树桩深度一致更利于权重稳定;浅层树若深度跳跃大,可能导致后续迭代对噪声敏感,实盘前建议固定弱学习器结构做对照测试。
算法本身通过提高误分类样本权重来缓解不平衡,但极端行情下仍可能偏向多数类,倾向配合阈值修正或代价敏感学习,贵金属与外汇杠杆品种属高风险,需样本外验证。
小布盯盘目前内置的是训练后的模型诊断与信号观测,不替代你在 MQL5 环境的训练代码;把重复劳动交给小布,你专注决策与参数边界。
分类用加权投票,回归倾向取弱学习器预测的加权均值;AdaBoost 原版以分类为重,回归变体需注意异常值放大,可能需截尾处理。
先确认弱学习器数量与每帧调用频率,决策树桩虽轻量,但数百个叠加仍占计算;建议仅在新柱触发时重算,避免每 tick 重跑全部融合。