对AUC和它计算方法的解释可以看这位大佬的文章:https://www.cnblogs.com/peizhe123/p/5081559.html,本文主要讲tensorflow中计算AUC的API实现。
tf.contrib.metrics.streaming_auc & tf.metrics.auc
在tf1.x中有两个计算AUC的API: tf.contrib.metrics.streaming_auc()
和tf.metrics.auc()
,在使用tf.contrib.metrics.streaming_auc()
的时候会有warning提示这个API将要被删除,建议用另外一个。在tf1.14源码中可以看到两个API的实现是完全一样的:
@deprecated(None, 'Please switch to tf.metrics.auc. Note that the order of '
'the labels and predictions arguments has been switched.')
def streaming_auc(predictions,
labels,
weights=None,
num_thresholds=200,
metrics_collections=None,
updates_collections=None,
curve='ROC',
name=None):
# 中间省略一万行注释
return metrics.auc(
predictions=predictions,
labels=labels,
weights=weights,
metrics_collections=metrics_collections,
num_thresholds=num_thresholds,
curve=curve,
updates_collections=updates_collections,
name=name)
计算方法
以下是对API源码思路的一个简略版解释,有很多地方不严谨(比如thresholds其实是在(0,1)采样n-2个值,加上头尾的-1e-7和1+1e-7一共得到n个值),所以还是推荐大家有余力的话去看源码。
我们日常手写代码计算AUC的时候可能会用它的等价公式来做:
A
U
C
=
∑
i
∈
p
o
s
i
t
i
v
e
C
l
a
s
s
r
a
n
k
i
−
M
(
1
+
M
)
2
M
×
N
AUC = \frac{\sum_{i \in positiveClass}rank_i-\frac{M(1+M)}{2}}{M \times N}
AUC=M×N∑i∈positiveClassranki−2M(1+M)
tensorflow中没有采用这种方法,而是“画”出了ROC曲线,然后用某种近似方法求出曲线下的面积得到AUC。API默认的面积求法是梯形法。
tensorflow “画”ROC曲线的方法也很简单,就是得到了一系列曲线上点的坐标。例如下面这张偷来的 ROC曲线图,得到图上20个点的坐标后,就很容易用梯形法计算出曲线下的面积,得到近似的AUC。
那么ROC曲线上点的坐标是怎么画出来的呢?首先tensorflow会在[0, 1]范围内均匀采样n个值,将这n个值作为预测正负样本的阈值。例如采样20个值,得到的阈值集合就是{0.05, 0.1, 0.15, …, 1},对每个阈值分别计算出当前阈值下的tpr和fpr,(tpr, fpr)就是ROC曲线上的点了。
在调用metrics.auc()
时,采样的阈值个数,也就是ROC曲线上点的个数,是作为参数传入的,即num_thresholds
。num_thresholds
默认值为200,这个值越大,“画”出的点就越多,ROC曲线就越精细,得到的AUC值就越精确。
使用中的问题
一开始促使我去翻源码的是我在用tf.contrib.metrics.streaming_auc()
这个API的时候发现的一个奇怪的现象。我的模型训练过程中loss的波动很大,但是训练集的AUC一直在稳步上升,稳到从来没有波动的那种。再看到streaming_auc这个名字,就猜想tf计算AUC是一个累计的过程,翻了下源码确实是这样。
如前所述,tf在计算AUC前,会先计算在每个threshold下的tpr和fpr,而计算tpr和fpr需要通过prediction和label得到混淆矩阵,累计就发生在计算混淆矩阵时。
以混淆矩阵中的true positive为例:
values = {}
update_ops = {}
if 'tp' in includes:
true_p = metric_variable(
[num_thresholds], dtypes.float32, name='true_positives')
is_true_positive = math_ops.to_float(
math_ops.logical_and(label_is_pos, pred_is_pos))
if weights_tiled is not None:
is_true_positive *= weights_tiled
update_ops['tp'] = state_ops.assign_add(true_p,
math_ops.reduce_sum(
is_true_positive, 1))
values['tp'] = true_p
计算true positive的过程中,tf维护一个不参与训练的局部变量true_p
和一个opupdate_ops['tp']
,true_p
返回给metrics.auc()
用来进行tpr和fpr的计算,而update_ops
同时也是metrics.auc()
的返回值之一,用来对每个threshold下的true_p
进行累加。所以metrics.auc()
的计算结果并不是每个batch的AUC的均值,而是在每个step运行时,把当前batch和以前所有batch的tp, fp, fn, tn进行累加,重进计算了tpr和fpr,也就是每个step都会重新“画”一遍ROC曲线。
这也就引出了使用中的几个问题:
- 因为在计算混淆矩阵时定义了局部变量,所以在运行前必须进行初始化,也就是调用
tf.local_variables_initializer()
。 metrics.auc()
的返回值有两个,auc
和update_ops
,在运行时要把update_ops
也加进去,否则tf不会更新混淆矩阵的值,计算出的AUC一直为0。- 在一次代码运行过程中AUC的值会一直累计,所以如果你的代码在同一个session里跑训练集和测试集的话,那么测试集的AUC还是会在训练集的基础上进行累计。对此可以在跑完训练集或者跑完一个epoch或者whatever之后重新对局部变量进行初始化,但前提是要确保你的代码其他地方没有用到局部变量。有大佬专门写了个解决方案指路。
关于这个问题,个人觉得,还是算出测试集的prediction之后,把session关掉,用sklearn算AUC,比较香。 - tf中局部变量在
saver.save()
的时候是不写入checkpoint的,所以模型restore之后计算AUC不会再累计。这个属实也没什么办法……如果你的模型训练得比较好那新的epoch或者增量的时候重新开始计算AUC虽然会和累计的有差距,但也不会很差所以还是要保持一颗平常心。