Weka算法Classifier-meta-AdaBoostM1源码分析(二)


三、基分类器

AdaBoostM1使用的默认基分类器是weka.classifiers.trees.DecisionStump,名字直译过来就是决策桩(这什么名字?!),其分类方法类似于ID3算法的节点分裂算法,如果是枚举型的,遍历所有属性,选出其中一个属性,使使用该属性进行分类后的熵增益最大,如果是数值型的,选择一个节点做二分,使分类后方差最小。

但和决策树不同的是,并不做递归的树生长,只做一次节点选择并分裂(所以叫桩而不是树)。

这个基分类器大体思路就是这样,代码较简单并且没有什么巧妙的算法思想,故不做具体分析。


四、buildClassifierWithWeight

上篇文章分析主流程的时候,可以看到最后主流程最后把训练过程委派给了buildClassifierWithWeight和buildClassifierUsingResampling,先来分析一下buildClassifierWithWeight

 protected void buildClassifierWithWeights(Instances data) 
    throws Exception {

    Instances trainData, training;
    double epsilon, reweight;
    Evaluation evaluation;
    int numInstances = data.numInstances();
    Random randomInstance = new Random(m_Seed);

    // //初始化
    m_Betas = new double [m_Classifiers.length];
    m_NumIterationsPerformed = 0;

    // 从直观认识上讲,算法训练模型的时候,不应该改变训练数据或者把训练集弄脏,因此需要做一个Instances的深度拷贝。
    training = new Instances(data, 0, numInstances);
    
    // 主循环,boosting使用的基分类器数量就是m_Classifiers数组长度。
    for (m_NumIterationsPerformed = 0; m_NumIterationsPerformed < m_Classifiers.length; 
	 m_NumIterationsPerformed++) {
      if (m_Debug) {
	System.err.println("Training classifier " + (m_NumIterationsPerformed + 1));
      }
      // 这里可以设置一个百分数,代表权重分位数,低于该分位数的实例会被过滤掉,这里是体现了boosting的“每次迭代着重考虑之前分错的实例”这种思想。
      if (m_WeightThreshold < 100) {
	trainData = selectWeightQuantile(training, 
					 (double)m_WeightThreshold / 100);
      } else {
	trainData = new Instances(training, 0, numInstances);
      }

      //训练基分类器,如果是一个不稳定的随机分类器,先设置一个种子。
      if (m_Classifiers[m_NumIterationsPerformed] instanceof Randomizable)
	((Randomizable) m_Classifiers[m_NumIterationsPerformed]).setSeed(randomInstance.nextInt());
      m_Classifiers[m_NumIterationsPerformed].buildClassifier(trainData);

      // 进行模型准确度的估计,枚举型返回准确率,数值型返回方差,但因为AdaBoostM1不支持数值型,所以这里可以认为返回准确率。需要注意的是这里的估计方法就是把training里的每一个Instance放到基分类器里进行预测,相当于训练集和测试集是同一个集合,因此对于AdaBoostM1,<span style="color:#ff0000;">必须选择弱分类器</span>,可以预见到,如果是一个强分类器(如REPTree和不剪枝的J48),导致数据过拟合,这里得到的errorRate就为0,进而直接退出迭代。
      evaluation = new Evaluation(data);
      evaluation.evaluateModel(m_Classifiers[m_NumIterationsPerformed], training);
      epsilon = evaluation.errorRate();

      // 如果错误率大于0.5(那还分个毛。。。),或者错误率是0(完全分类),就退出。
      if (Utils.grOrEq(epsilon, 0.5) || Utils.eq(epsilon, 0)) {
	if (m_NumIterationsPerformed == 0) {
	  m_NumIterationsPerformed = 1; // If we're the first we have to to use it
	}
	break;
      }
      // 设置此基分类器的权重,并且重新设置整个训练集中各用例的权重
      m_Betas[m_NumIterationsPerformed] = Math.log((1 - epsilon) / epsilon);
      reweight = (1 - epsilon) / epsilon;
      if (m_Debug) {
	System.err.println("\terror rate = " + epsilon
			   +"  beta = " + m_Betas[m_NumIterationsPerformed]);
      }
 
      // 下面这个函数做了两个事:1、对于training里分错的对象,让其权重乘以reweight,即提高了权重 2、du对训练集所有权重进行归一化,使之和为1(即稍微同比缩小一点)
      setWeights(training, reweight);
    }
  }
可以看到,此函数并没有对训练集根据权重进行重抽样,因为基分类器自身就是权重敏感的分类器。

但是从设计的角度来吐个槽的话,个人认为这并不是一个非常好的设计,AdaBoostM1类作为一个基分类器的wrapper,必须对整个训练及分类结果负责,并不能因为基分类器“仅仅实现了权重敏感接口”就完全信任其权重敏感的操作,再从算法本身的角度来讲,实现“权重敏感”的逻辑本身就是AdaBoostM1算法的一部分,不应委派给基分类器去做。


五、buildClassifierUsingResampling

protected void buildClassifierUsingResampling(Instances data) 
    throws Exception {

    Instances trainData, sample, training;
    double epsilon, reweight, sumProbs;
    Evaluation evaluation;
    int numInstances = data.numInstances();
    Random randomInstance = new Random(m_Seed);
    int resamplingIterations = 0;

    // 初始化所有基分类器的权重数组,迭代次数等。
    m_Betas = new double [m_Classifiers.length];
    m_NumIterationsPerformed = 0;
    // 深度拷贝训练集
    training = new Instances(data, 0, numInstances);
    sumProbs = training.sumOfWeights();
    for (int i = 0; i < training.numInstances(); i++) {
      training.instance(i).setWeight(training.instance(i).
				      weight() / sumProbs);//初始化权重为均值。
    }
    
    // 主循环
    for (m_NumIterationsPerformed = 0; m_NumIterationsPerformed < m_Classifiers.length; 
	 m_NumIterationsPerformed++) {
      if (m_Debug) {
	System.err.println("Training classifier " + (m_NumIterationsPerformed + 1));
      }

      // 低权重过滤
      if (m_WeightThreshold < 100) {
	trainData = selectWeightQuantile(training, 
					 (double)m_WeightThreshold / 100);
      } else {
	trainData = new Instances(training);
      }
      
      // 重采样
      resamplingIterations = 0;
      double[] weights = new double[trainData.numInstances()];
      for (int i = 0; i < weights.length; i++) {
	weights[i] = trainData.instance(i).weight();
      }
      do {
	sample = trainData.resampleWithWeights(randomInstance, weights);//根据权重重采样,算法是Walker's method, see pp. 232 of "Stochastic Simulation" by B.D. Ripley,我完全找不到出处,并且原代码可读性比较差,如果有知道该算法的读者望留个言,谢谢了。

	// 训练基分类器
	m_Classifiers[m_NumIterationsPerformed].buildClassifier(sample);
	evaluation = new Evaluation(data);
	evaluation.evaluateModel(m_Classifiers[m_NumIterationsPerformed], 
				 training);
	epsilon = evaluation.errorRate();
	resamplingIterations++;
      } while (Utils.eq(epsilon, 0) && 
	      (resamplingIterations < MAX_NUM_RESAMPLING_ITERATIONS));//因为重采样的不稳定性,所以这里会训练多次,MAX_NUM_RESAMPLING_ITERATIONS默认为10。如果只训练一次,或许会出现epsilon大于0.5直接导致迭代退出的情况。
      	
      // 退出条件,上一个函数说过了
      if (Utils.grOrEq(epsilon, 0.5) || Utils.eq(epsilon, 0)) {
	if (m_NumIterationsPerformed == 0) {
	  m_NumIterationsPerformed = 1; // If we're the first we have to to use it
	}
	break;
      }
      
      //权重调整,上个函数也说过了
      m_Betas[m_NumIterationsPerformed] = Math.log((1 - epsilon) / epsilon);
      reweight = (1 - epsilon) / epsilon;
      if (m_Debug) {
	System.err.println("\terror rate = " + epsilon
			   +"  beta = " + m_Betas[m_NumIterationsPerformed]);
      }
      setWeights(training, reweight);
    }
  }


六、总结

训练好了分类器之后,在针对具体instance分类时,会根据mBeta对各基分类器得到的结果进行算数加权,进而得到最终结果,相关代码叫见到不再分析。


总结实在不知道该写些啥,简单说两点吧

1、Weka的AdaBoostM1基本按照标准算法实现得来,唯一的不同之处就是部分权重resampling会委派给基分类器进行实现。

2、基分类器一定要选择弱分类器。



你可能感兴趣的:(源码,算法,机器学习,weka,分类器)