理解ROC和AUC

分类器各种各样,如何评价这些分类器的性能呢?(这里只考虑二元分类器,分类器的输出为概率值)

方法一:概率定义法

从正样本中随机选取元素记为x,从负样本中随机选取元素记为y,x的置信度大于y的概率

计算方法可以描述为

s=0
for x in 正例:
  s+=1/正例总数×置信度小于x的负例所占比例
return s

概率是用来定义问题的利器,如基尼系数。

方法二:正样本排名法

对全部样本按照置信度从高到低进行排序,排名依次记做1,2,3......全部正例的排名之和记为R,R越小表明分类器越准。

方法三:复杂定义法

我提出一种评判指标:点评测法。
给定一个阈值,就可以将分类器的概率值确定化为0、1值。现在只考虑你认为值为1的样本,这些样本占“实际假”样本的比例越小越好,这些样本占“实际为真”的样本的比例越大越好,于是可以得到二元组(这些样本占“实际假”的比例,这些样本占“实际真”的比例)。把这个二元组画在平面直角坐标系中,得到一个点,这个点离(0,1)越近越好!
上面说的是给定了一个阈值,大多数时候,阈值是不好确定的,那就采用“动态阈值”,让阈值从0到1变化就会得到一个点序列,也就是一条曲线。这条曲线就是传说中的ROC。

ROC是一种折线,它是一种评价二值分类器的性能指标
AUC是求折线下面的面积,折线不一定是ROC折线

00:我说0,实际也为0
01:我说0,实际为1
10:我说1,实际为0
11:我说1,实际为1

那么,x坐标可以表示为10/(00+10),y坐标可以表示为11/(01+11)
也就是说,x坐标可以表示为10/x0,y坐标表示为11/x1
也就是说,只看“我说1”的那一部分,该部分在全部“实际0”的部分所占比例为x轴,该部分在“实际1”部分所占比例为y轴。

描述起来多么复杂!但是上面三种方法得到的结果是一模一样的,虽然定义上差很多,但最终说的是一回事!

AUC的直观认识

  • AUC对于置信度不敏感,它只关注名次而不关注具体置信度
  • 从AUC角度看,好的分类器就是一个好的排序器。分类器在某种程度上就是排序器。
  • AUC对于样本正负样本不均衡的现象完全不敏感,从方法一定义上可以看出来。
import sklearn.metrics as metrics
import numpy as np


def roc(y_true, y_mine):
    a = [(y_mine[i], y_true[i]) for i in range(len(y_true))]
    a = sorted(a, key=lambda x: -x[0])
    thresh = sorted(list(set([0, 1] + [i[0] for i in a])), key=lambda x: -x)
    cnt = [np.count_nonzero(y_true == 0), np.count_nonzero(y_true == 1), 0, 0]
    x = []
    y = []
    j = 0
    for i in thresh:
        while j < len(a) and a[j][0] >= i:
            if a[j][1]:
                cnt[0b11] += 1
                cnt[0b01] -= 1
            else:
                cnt[0b10] += 1
                cnt[0b00] -= 1
            j += 1
        x.append(cnt[0b10] / (cnt[0b00] + cnt[0b10]))
        y.append(cnt[0b11] / (cnt[0b11] + cnt[0b01]))
    return x, y, thresh


def auc(x, y):
    a = sorted([(x[i], y[i]) for i in range(len(x))], key=lambda x: x[0])
    area = 0
    for i in range(1, len(a)):
        area += (a[i - 1][1] + a[i][1]) * (a[i][0] - a[i - 1][0]) / 2
    return area


def auc2(y_mine, y_true):
    """
    auc的物理意义在于:随机正样本比随机负样本得分高的概率
    基于这种直观的思路可以更快速的计算auc
    :param y_mine:
    :param y_true:
    :return:
    """
    a = sorted([(y_mine[i], y_true[i]) for i in range(len(y_mine))], key=lambda x: -x[0])
    s = 0
    true_count = len([1 for i in y_true if i])
    false_count = len(y_true) - true_count
    total_false = false_count
    for i in a:
        if i[1]:
            s += 1 / true_count * false_count / total_false
        else:
            false_count -= 1
    return s


y_mine = np.random.rand(10)
y_true = np.random.randint(0, 2, 10)
x, y, t = roc(y_true, y_mine)
print(auc(x, y))
print(metrics.auc(x, y))
print(auc2(y_mine, y_true))

其实,auc还可以推导一下得到更简单一点的公式:

def auc2(y_mine, y_true):
    """
    auc的物理意义在于:随机正样本比随机负样本得分高的概率
    基于这种直观的思路可以更快速的计算auc
    本函数只适用于计算得分各不相同的样本,如果存在并列情况,此函数结果错误
    :param y_mine:
    :param y_true:
    :return:
    """
    a = sorted(zip(y_mine, y_true), key=lambda x: -x[0])
    T = np.count_nonzero(y_true)
    F = len(y_true) - T
    R = sum(i + 1 for i in range(len(a)) if a[i][1] == 1)
    return 1 + (T + 1) / (2 * F) - R / (T * F)

知乎上大神的答案,很高级。

从AUC到真实类别(label)?
最开始思考这个问题是做一个网上的比赛,二分类问题,每次提交一组预测系统会计算出一个AUC值,因为这个比赛只有5000样本,并且系统显示的AUC值有小数点后面7、8位,所以我想是否可以通过可能通过AUC值计算出样本的真实label来。也许并没有实际价值,但是问题本身是很有趣的,像是在破解密码。
一个naive但是可行但是效率很低的办法, 就是每次生成一组预测值,里面只有一个样本预测为1,其余都是0,然后提交预测计算AUC,然后根据这个AUC来判断此样本的label,但是这样效率太低了,5000个样本,需要5000次提交。
思考了很久,最后发现可以通过AUC的另一个计算公式入手。也就是第一部分说的U statistic。

3.1 根据一个AUC值计算样本中0,1的数目
根据AUC与U statistic的联系,我们可以用下面的代码计算AUC:
auc=(sum(rank(c(p(label==1),p(label==0)))[1:n1])-n1*(n1+1)/2)/n0/n1
上面label表示样本的真实类别,p表示预测为1的概率,rank是R的内置函数,n0表示真实数据中0的数目,n1表示1的数目,n0+n1=n表示数据所有样本数目,根据这个简单的一行代码,我们可以不用任何包来计算AUC。
上面公式很有趣,n0,n1还有label都是固定的,p不同导致auc不一致,观察sum里面,可以发现这个sum本质是什么?就是计算pred里面对应的真实label为1的那些预测值的rank的和。
继续使用第一部分的例子,5个样本的预测值的rank:
rank(c(0.5,0.6,0.55,0.4,0.7))[1] 2 4 3 1 5
其中真实为1的样本(1,2,5)的对应rank是2,4,5,这三个rank值的和2+4+5=11,n0=2,n1=3,于是根据上面的AUC公式:(11-3*(3+1)/2)/2/3=5/6=0.83333,这个结果与我们在第一部分用AUC定义算的值完全一样。
所以说,真正影响auc的,就是预测值里面对本来是1的样本的预测的rank的和。
要破解真实label,第一部要做的是找到样本里面0和1的数目,也就是n0和n1等于多少。这个问题不复杂。要知道相同预测值的rank也一致,就是说如果所有样本的预测只取0或者1,那对应的rank只有两个unique数值。
再观察AUC公式里面的sum:
sum(rank(c(pred(label==1),pred(label==0)))[1:n1])
这个sum是n1个数值的和,这n1个数值,当我们的pred只有两个不同取值的时候,仅包括两个unique的数值。继续用上面例子,一共有5个样本,我们生成第一组预测p如下:
> p=c(1,1,1,0,0)> rank(p)[1] 4.0 4.0 4.0 1.5 1.5

可见p的rank只有两个不同取值,1.5和4,这是因为预测概率p也只有两个不同取值。
然后我们还知道sum是n1个数的sum,我们不知道的是这n1个数,里面有几个1.5,几个4,假设其中有k1个1.5,k2个4,可以列出一个方程:
k1+k2=n1
k1*1.5+k2*4=sum(rank(c(p(label==1),p(label==0)))[1:n1])=auc*n0*n1+n1*(1+n1)/2
所以最终得到下面的方程组:
k1+k2=n1
k1*1.5+k2*4=0.833333*n0*n1+n1*(1+n1)/2
n0+n1=5
其中k1,k2和n1未知,两个方程,3个未知数,属于不定方程组,但是要知道k1,k2,和n1都是整数,并且有取值范围,要借出来很简单,写一个for 循环,1秒中就可以找到一组满足3个方程多k1,k2以及n1。
如果我们变更p,比如p=c(rep(1,m),rep(0,5-m)),通过一样的算法,可以计算出来前m个样本中1的数量。
通过这个算法,我可以算出来这个贷款预测比赛测试数据中有509个1和4491个0。
做到这里,差点就放弃了,但是后来突然又有了灵感,找到了下面的算法:
3.2 根据AUC破解样本的真实label
这里就省略思考过程了, 直接来破解算法:
对于一组总数为n的测试样本,我们先来计算
m=floor(log(n,base=2))+1
这个m表示我们通过两次auc计算可以计算出至少多少个样本的真实label,比如n=5000,那么m=13
也就是说通过我们两次提交,可以最少得到13个样本的label。这13个样本是可以自己随便指定的,比如我们对前13个样本感兴趣,那么具体做法如下:
fix1=2^c(0:12)fix2=c(fix1[-1],fix1[1])unfixed=setdiff(1:5000,fix1)p1=c(fix1,unfixed)#第1组预测p2=c(fix2,unfixed)#第2组预测
使用上面的两组预测p1和p2分别计算AUC,得到auc1和auc2,根据上面给出的auc算法:
sum(rank(c(p1(label==1),p1(label==0)))[1:n1])-n1*(1+n1)/2=auc1*n0*n1sum(rank(c(p2(label==1),p2(label==0)))[1:n1])-n1*(1+n1)/2=auc2*n0*n1
两个公式相减:
sum(rank(c(p1(label==1),p1(label==0)))[1:n1])-sum(rank(c(p2(label==1),p2(label==0)))[1:n1])-n1*(1+n1)/2=(auc1-auc2)*n0*n1
得到的这个等式里,我们已经通过上面的方法找到了n0和n1,auc1和auc2是已知,所以等式右面值可以算出,那么等式左面呢,因为我们两个预测结果p1和p2只有前三个点的预测之不一样,其余点的预测值一样,rank也一样,那么等式左面的两个sum的差,其实只由前13个样本的真实label决定,具体来说:
sum1-sum2=y1*(fix1[1]-fix2[1])+y2*(fix1[2]-fix2[2])+...+y13*(fix1[13]-fix2[13])=y1*(-1)+y2*(-2)+...+y12*(-2048)+y13*(4095)

上面的方程里面yi代表样本i的真实label,有且只有唯一解,以为这个方程本质上就是10进制数用2进制表达。所以通过两次auc计算,我们可以找到13个点的真实标签。比如对上面提到的贷款预测比赛,选定前13个label,auc1=0.50220853,auc2= 0.5017588,然后就可以算出来前13个test样本只有第三个样本是0,其余都是1。
但是13并不是上限,我有一些更好的结果,比较复杂,在这就不展开说了。

参考资料

https://www.zhihu.com/question/39840928

原文地址:https://www.cnblogs.com/weiyinfu/p/7659807.html