首页 » 机器学习实战 » 机器学习实战全文在线阅读

《机器学习实战》7.7 非均衡分类问题

关灯直达底部

在我们结束分类这个主题之前,还必须讨论一个问题。在前面六章的所有分类介绍中,我们都假设所有类别的分类代价是一样的。例如在第5章,我们构建了一个用于检测患疝病的马匹是否存活的系统。在那里,我们构建了分类器,但是并没有对分类后的情形加以讨论。假如某人给我们牵来一匹马,他希望我们能预测这匹马能否生存。我们说马会死,那么他们就可能会对马实施安乐死,而不是通过给马喂药来延缓其不可避免的死亡过程。我们的预测也许是错误的,马本来是可以继续活着的。毕竟,我们的分类器只有80%的精确率(accuracy)。如果我们预测错误,那么我们将会错杀了一个如此昂贵的动物,更不要说人对马还存在情感上的依恋。

如何过滤垃圾邮件呢?如果收件箱中会出现某些垃圾邮件,但合法邮件永远不会扔进垃圾邮件夹中,那么人们是否会满意呢?癌症检测又如何呢?只要患病的人不会得不到治疗,那么再找一个医生来看看会不会更好呢(即情愿误判也不漏判)?

还可以举出很多很多这样的例子,坦白地说,在大多数情况下不同类别的分类代价并不相等。在本节中,我们将会考察一种新的分类器性能度量方法,并通过图像技术来对在上述非均衡问题下不同分类器的性能进行可视化处理。然后,我们考察这两种分类器的变换算法,它们能够将不同决策的代价考虑在内。

7.7.1 其他分类性能度量指标:正确率、召回率及ROC曲线

到现在为止,本书都是基于错误率来衡量分类器任务的成功程度的。错误率指的是在所有测试样例中错分的样例比例。实际上,这样的度量错误掩盖了样例如何被分错的事实。在机器学习中,有一个普遍适用的称为混淆矩阵(confusion matrix)的工具,它可以帮助人们更好地了解分类中的错误。有这样一个关于在房子周围可能发现的动物类型的预测,这个预测的三类问题的混淆矩阵如表7-2所示。

表7-2 一个三类问题的混淆矩阵

利用混淆矩阵就可以更好地理解分类中的错误了。如果矩阵中的非对角元素均为0,就会得到一个完美的分类器。

接下来,我们考虑另外一个混淆矩阵,这次的矩阵只针对一个简单的二类问题。在表7-3中,给出了该混淆矩阵。在这个二类问题中,如果将一个正例判为正例,那么就可以认为产生了一个真正例(True Positive,TP,也称真阳);如果对一个反例正确地判为反例,则认为产生了一个真反例(True Negative,TN,也称真阴)。相应地,另外两种情况则分别称为伪反例(False Negative,FN,也称假阴)和伪正例(False Positive,FP,也称假阳)。这4种情况如表7-3所示。

表7-3 一个二类问题的混淆矩阵,其中的输出采用了不同的类别标签

在分类中,当某个类别的重要性高于其他类别时,我们就可以利用上述定义来定义出多个比错误率更好的新指标。第一个指标是正确率(Precision),它等于TP/(TP+FP),给出的是预测为正例的样本中的真正正例的比例。第二个指标是召回率(Recall),它等于TP/(TP+FN),给出的是预测为正例的真实正例占所有真实正例的比例。在召回率很大的分类器中,真正判错的正例的数目并不多。

我们可以很容易构造一个高正确率或高召回率的分类器,但是很难同时保证两者成立。如果将任何样本都判为正例,那么召回率达到百分之百而此时正确率很低。构建一个同时使正确率和召回率最大的分类器是具有挑战性的。

另一个用于度量分类中的非均衡性的工具是ROC曲线(ROC curve),ROC代表接收者操作特征(receiver operating characteristic),它最早在二战期间由电气工程师构建雷达系统时使用过。图7-3给出了一条ROC曲线的例子。

图7-3 利用10个单层决策树的AdaBoost马疝病检测系统的ROC曲线

在图7-3的ROC曲线中,给出了两条线,一条虚线一条实线。图中的横轴是伪正例的比例(假阳率=FP/(FP+TN)),而纵轴是真正例的比例(真阳率=TP/(TP+FN))。ROC曲线给出的是当阈值变化时假阳率和真阳率的变化情况。左下角的点所对应的是将所有样例判为反例的情况,而右上角的点对应的则是将所有样例判为正例的情况。虚线给出的是随机猜测的结果曲线。

ROC曲线不但可以用于比较分类器,还可以基于成本效益(cost-versus-benefit)分析来做出决策。由于在不同的阈值下,不同的分类器的表现情况可能各不相同,因此以某种方式将它们组合起来或许会更有意义。如果只是简单地观察分类器的错误率,那么我们就难以得到这种更深入的洞察效果了。

在理想的情况下,最佳的分类器应该尽可能地处于左上角,这就意味着分类器在假阳率很低的同时获得了很高的真阳率。例如在垃圾邮件的过滤中,这就相当于过滤了所有的垃圾邮件,但没有将任何合法邮件误识为垃圾邮件而放入垃圾邮件的文件夹中。

对不同的ROC曲线进行比较的一个指标是曲线下的面积(Area Unser the Curve,AUC)。AUC给出的是分类器的平均性能值,当然它并不能完全代替对整条曲线的观察。一个完美分类器的AUC为1.0,而随机猜测的AUC则为0.5。

为了画出ROC曲线,分类器必须提供每个样例被判为阳性或者阴性的可信程度值。尽管大多数分类器都能做到这一点,但是通常情况下,这些值会在最后输出离散分类标签之前被清除。朴素贝叶斯能够提供一个可能性,而在Logistic回归中输入到Sigmoid函数中的是一个数值。在AdaBoost和SVM中,都会计算出一个数值然后输入到sign函数中。所有的这些值都可以用于衡量给定分类器的预测强度。为了创建ROC曲线,首先要将分类样例按照其预测强度排序。先从排名最低的样例开始,所有排名更低的样例都被判为反例,而所有排名更高的样例都被判为正例。该情况的对应点为<1.0,1.0>。然后,将其移到排名次低的样例中去,如果该样例属于正例,那么对真阳率进行修改;如果该样例属于反例,那么对假阴率进行修改。

上述过程听起来有点容易让人混淆,但是如果阅读一下程序清单7-5中的代码,一切就会变得一目了然了。打开adaboost.py文件并加入如下代码。

程序清单7-5 ROC曲线的绘制及AUC计算函数

def plotROC(predStrengths, classLabels):    import matplotlib.pyplot as plt    cur = (1.0,1.0)    ySum = 0.0    numPosClas = sum(array(classLabels)==1.0)    yStep = 1/float(numPosClas)    xStep = 1/float(len(classLabels)-numPosClas)    #❶ 获取排好序的索引      sortedIndicies = predStrengths.argsort    fig = plt.figure    fig.clf    ax = plt.subplot(111)    for index in sortedIndicies.tolist[0]:        if classLabels[index] == 1.0:            delX = 0; delY = yStep;        else:            delX = xStep; delY = 0;            ySum += cur[1]        ax.plot([cur[0],cur[0]-delX],[cur[1],cur[1]-delY], c=/'b/')        cur = (cur[0]-delX,cur[1]-delY)ax.plot([0,1],[0,1],/'b--/')plt.xlabel(/'False Positive Rate/'); plt.ylabel(/'True Positive Rate/')plt.title(/'ROC curve for AdaBoost Horse Colic Detection System/')ax.axis([0,1,0,1])plt.showprint /"the Area Under the Curve is: /",ySum*xStep     

上述程序中的函数有两个输入参数,第一个参数是一个NumPy数组或者一个行向量组成的矩阵。该参数代表的则是分类器的预测强度。在分类器和训练函数将这些数值应用到sign函数之前,它们就已经产生了。尽管很快就可以看到该函数的实际执行效果,但是我们还是要先讨论一下这段代码。函数的第二个输入参数是先前使用过的classLabels。我们首先导入pyplot,然后构建一个浮点数二元组,并将它初始化为(1,0,1.0)。该元组保留的是绘制光标的位置,变量ySum则用于计算AUC的值。接下来,通过数组过滤方式计算正例的数目,并将该值赋给numPosClas。该值先是确定了在y坐标轴上的步进数目,接着我们在x轴和y轴的0.0到1.0区间上绘点,因此y轴上的步长是1.0/numPosClas。类似地,就可以得到x轴的步长了。

接下来,我们得到了排序索引❶,但是这些索引是按照最小到最大的顺序排列的,因此我们需要从点<1.0,1.0>开始绘,一直到<0,0>。跟着的三行代码则是用于构建画笔,并在所有排序值上进行循环。这些值在一个NumPy数组或者矩阵中进行排序,Python则需要一个表来进行迭代循环,因此我们需要调用tolist方法。当遍历表时,每得到一个标签为1.0的类,则要沿着y轴的方向下降一个步长,即不断降低真阳率。类似地,对于每个其他类别的标签,则是在x轴方向上倒退了一个步长(假阴率方向)。上述代码只关注1这个类别标签,因此就无所谓是采用1/0标签还是+1/-1标签。

为了计算AUC,我们需要对多个小矩形的面积进行累加。这些小矩形的宽度是 xStep,因此可以先对所有矩形的高度进行累加,最后再乘以xStep得到其总面积。所有高度的和(ySum)随着x轴的每次移动而渐次增加。一旦决定了是在x轴还是y轴方向上进行移动的,我们就可以在当前点和新点之间画出一条线段。然后,当前点cur更新了。最后,我们就会得到一个像样的绘图并将AUC打印到终端输出。

为了解实际运行效果,我们需要将adaboostTrainDS( )的最后一行代码替换成:

return weakClassArr,aggClassEst  

以得到aggClassEst的值。然后,在Python提示符下键入如下命令:

>>> reload(adaboost)<module /'adaboost/' from /'adaboost.pyc/'>>>> datArr,labelArr = adaboost.loadDataSet(/'horseColicTraining2.txt/')>>> classifierArray,aggClassEst =     adaboost.adaBoostTrainDS(datArr,labelArr,10)>>> adaboost.plotROC(aggClassEst.T,labelArr)the Area Under the Curve is: 0.858296963506   

这时,我们也会得到和图7-3一样的ROC曲线。这是在10个弱分类器下,AdaBoost算法性能的结果。我们还记得,当初我们在50个弱分类器下得到了最优的分类性能,那么这种情况下的ROC曲线会如何呢?这时的AUC是不是更好呢?

7.7.2 基于代价函数的分类器决策控制

除了调节分类器的阈值之外,我们还有一些其他可以用于处理非均衡分类的代价的方法,其中的一种称为代价敏感的学习(cost-sensitive learning)。考虑表7-4中的代价矩阵,第一张表给出的是到目前为止分类器的代价矩阵(代价不是0就是1)。我们可以基于该代价矩阵计算其总代价:TP*0+FN*1+FP*1+TN*0。接下来我们考虑下面的第二张表,基于该代价矩阵的分类代价的计算公式为:TP*(-5)+FN*1+FP*50+TN*0。采用第二张表作为代价矩阵时,两种分类错误的代价是不一样的。类似地,这两种正确分类所得到的收益也不一样。如果在构建分类器时,知道了这些代价值,那么就可以选择付出最小代价的分类器。

表7-4 一个二类问题的代价矩阵

在分类算法中,我们有很多方法可以用来引入代价信息。在AdaBoost中,可以基于代价函数来调整错误权重向量D。在朴素贝叶斯中,可以选择具有最小期望代价而不是最大概率的类别作为最后的结果。在SVM中,可以在代价函数中对于不同的类别选择不同的参数C。上述做法就会给较小类更多的权重,即在训练时,小类当中只允许更少的错误。

7.7.3 处理非均衡问题的数据抽样方法

另外一种针对非均衡问题调节分类器的方法,就是对分类器的训练数据进行改造。这可以通过欠抽样(undersampling)或者过抽样(oversampling)来实现。过抽样意味着复制样例,而欠抽样意味着删除样例。不管采用哪种方式,数据都会从原始形式改造为新形式。抽样过程则可以通过随机方式或者某个预定方式来实现。

通常也会存在某个罕见的类别需要我们来识别,比如在信用卡欺诈当中。如前所述,正例类别属于罕见类别。我们希望对于这种罕见类别能尽可能保留更多的信息,因此,我们应该保留正例类别中的所有样例,而对反例类别进行欠抽样或者样例删除处理。这种方法的一个缺点就在于要确定哪些样例需要进行剔除。但是,在选择剔除的样例中可能携带了剩余样例中并不包含的有价值信息。

上述问题的一种解决办法,就是选择那些离决策边界较远的样例进行删除。假定我们有一个数据集,其中有50例信用卡欺诈交易和5000例合法交易。如果我们想要对合法交易样例进行欠抽样处理,使得这两类数据比较均衡的话,那么我们就需要去掉4950个样例,而这些样例中可能包含很多有价值的信息。这看上去有些极端,因此有一种替代的策略就是使用反例类别的欠抽样和正例类别的过抽样相混合的方法。

要对正例类别进行过抽样,我们可以复制已有样例或者加入与已有样例相似的点。一种方法是加入已有数据点的插值点,但是这种做法可能会导致过拟合的问题。