三、基分类器
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、基分类器一定要选择弱分类器。