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