用于二进制分类。每个人都喜欢曲线(AUC)指标下的区域,但没有人直接以其损失功能为目标。相反,人们使用二进制交叉熵(BCE)的代理函数。
大多数时候,这种工作都很好。但是我们遇到了一个令人不安的问题:我们能获得更高的分数,而损失功能本质上更接近AUC?
由于公元前与AUC的关系很少,因此似乎很可能。已经有许多尝试找到更直接针对AUC的损失函数的尝试。 (一种常见的策略是某种形式的排名损失功能,例如铰链排名。)但是,实际上,从未出现过明显的赢家。公元前没有严重的挑战。
还有超出表现的考虑。由于BCE与AUC基本不同,因此BCE在最后的训练中倾向于表现不佳,我们试图将其引导到最高的AUC分数。
大量AUC优化实际上最终发生在超参数的调整中。早期停止成为不舒服的必要性,因为该模型可能随时与其高分相比急剧差异。
我们希望损失功能能给我们带来更高的分数和更少的麻烦。
我们在这里介绍这样的功能。
我最喜欢的AUC工作定义是:让我们称二进制类标签为“黑色”(0)和“白色”(1)。随机选择一个黑色元素,让X为其预测值。现在选择一个带有值y的随机白色元素。然后,
auc =元素处于正确顺序的概率。也就是说, x < y 。
就是这样。对于诸如训练集之类的任何给定的点,我们可以通过进行蛮力计算来获得此概率。扫描所有可能的黑/白对的集合,并计算正确排序的部分。
我们可以看到,AUC分数没有可区分的(相对于任何单个X或y 。 AUC保持不变。一旦点确实越过邻居,我们就有机会翻转X <y比较之一 - 改变了AUC。因此,AUC没有平稳的过渡。
这是神经网的问题,我们需要一个可区分的损失功能。
因此,我们着手找到与AUC尽可能接近的可区分函数。
我挖掘了现有文献,没有发现在实践中起作用。最终,我遇到了一个好奇的代码,有人检查了Tflearn代码库。
没有大张旗鼓,它承诺以新的损失函数的形式与BCE的可区分释放。
(不要尝试,它会炸毁。):http://tflearn.org/objectives/#roc-auc-score
def roc_auc_score(y_pred, y_true):
"""Bad code, do not use.
ROC AUC Score.
Approximates the Area Under Curve score, using approximation based on
the Wilcoxon-Mann-Whitney U statistic.
Yan, L., Dodier, R., Mozer, M. C., & Wolniewicz, R. (2003).
Optimizing Classifier Performance via an Approximation to the Wilcoxon-Mann-Whitney Statistic.
Measures overall performance for a full range of threshold levels.
Arguments:
y_pred: `Tensor`. Predicted values.
y_true: `Tensor` . Targets (labels), a probability distribution.
"""
with tf.name_scope("RocAucScore"):
pos = tf.boolean_mask(y_pred, tf.cast(y_true, tf.bool))
neg = tf.boolean_mask(y_pred, ~tf.cast(y_true, tf.bool))
.
.
more bad code)
它根本不起作用。 (炸毁实际上是其最少的问题)。但它提到了基于的论文。
即使该论文很古老,可以追溯到2003年,但我发现,有了一些工作 - 数学和仔细编码的延伸 - 实际上是有效的。它是Uber-fast,速度与BCE相当(对于GPU/MPP而言,也可以矢量化) *。在我的测试中,它给出的AUC得分高于BCE,对学习率的敏感(避免在我的测试中需要调度程序),并且完全消除了早期停止的需求。
好的,让我们转到原始论文:通过近似Wilcoxon -Mann -Whitney统计来优化分类器性能。
作者, Yan等。 al ,通过以特定形式编写AUC分数来激励讨论。回想一下我们的示例,我们通过对可能的黑/白对进行蛮力计算来计算AUC,以找到正确排序的部分。令b为黑色值和w白色值集。所有可能的对由笛卡尔产品B x W给出。为了计算我们写的右顺序对:
这实际上只是说“计算右顺序对”的数学符号。如果我们将该总和除以对的总数,| b | * | W |,我们完全获得了AUC度量。 (从历史上看,这被称为标准化的Wilcoxon-Mann-Whitney(WMW)统计。)
为了从中产生损失函数,我们可以将x <y比较与x> y进行翻转,以便对错误排序的对进行惩罚。当然,问题是当X越过y时,不连续的跳跃。
Yan等。 Al调查 - 然后拒绝 - 使用连续近似的步骤(Heviside)函数(例如Sigmoid曲线)进行了工作。然后,他们从帽子中拉出它:
Yann通过对WMW应用一系列更改来获得这个论坛:
我们会依次通过这些。 1很清楚。除了眼睛,还有2到2。直观的有意义的是,与分离宽的订购的对订购的对应该给予更大的损失。但是,随着分离接近0,也正在发生一些有趣的事情。损失是线性的,而不是阶跃功能。因此,我们摆脱了不连续性。
实际上,如果p为1,而γ为0,那么损失将只是我们的老朋友relu(xy)。但是在这种情况下,我们注意到打ic,揭示了对指数p的需求。 relu在0时并不可区分。这在Relu更习惯于激活函数的角色中并不是什么问题,但是出于我们的目的,我们最感兴趣的是,奇异性直接落在我们最感兴趣的东西上:白色和黑色元素的点互相传递。
幸运的是,提高依赖能力可以解决此问题。与p> 1的relu^p无处不在。好的,p> 1。
现在回到γ:γ提供了一个“填充”,该填充物在两个点之间实施。我们不仅惩罚了错误的订购对,还惩罚了太近的右顺序对。如果一对右顺序太近,则其元素可能会因随机神经网的随机跳动而在将来被交换。这个想法是让他们分开,直到达到舒适的距离。
这就是论文中概述的基本思想。现在,我们对γ和p进行了一些改进。
在这里,我们用纸有点打破。 Yan等。 Al在选择γ和P的主题上似乎有些矛盾,仅提供p = 2或p = 3似乎很好,并且γ应该在0.10到0.70之间。 Yan基本上希望我们能用这些参数和弓箭运气。
首先,我们永久修复p = 2,因为任何自尊损失函数都应是一个平方之和。 (原因之一是它确保损失函数不仅可区分,而且是凸)
第二个也是更重要的是,让我们看一下γ。 “从0.10到0.70”的启发式镜头看起来很奇怪;即使将预测标准化为0 <x <1,该指南似乎过于宽敞,对基础分布无动于衷,而且很奇怪。
我们将从训练集中得出γ。
考虑训练套件及其黑色/白对, b x w 。有| b || W |这组成对。其中,| b | | W | AUC是正确的。因此,错误排序的对的数量为(1-AUC)| b | | W |
当γ为零时,只有这些错误排序的对就开始运动(具有正损失。)正γ会扩大移动对的集合,以包括一些对正确排序但太近的对。我们不必担心γ的数值值,而是要指定要在运动中设置多少个太近的对:
我们定义一个常数δ,该δ固定了过于近距离对的比例与错误排序的对。
我们在整个训练中修复此δ并更新γ以符合它。对于给定的δ,我们发现γ使得
在我们的实验中,我们发现δ可以在0.5到2.0范围内,而1.0是一个很好的默认选择。
因此,我们将δ设置为1,p至2,而完全忘记了γ,
我们的损失功能(1)看起来很昂贵。它要求我们为每个单独的预测扫描整个培训集。
我们通过性能调整绕过这个问题:
假设我们正在计算给定的白色数据点x的损耗函数。要计算(3),我们需要将X与黑色预测的整个训练集进行比较。我们进行短剪切,并使用黑色数据点的随机子样本。如果我们将子样本的大小设置为1000-我们将获得一个非常(非常)与真实损失函数的近似值。 [1]
类似的推理适用于黑色数据点的损失函数。我们使用所有白色训练元素的随机子样本。
这样,白色和黑色子样品很容易适合GPU内存。通过在给定的批次中重复使用相同的子样本,我们可以将操作分批平行。我们最终获得了公元前差不多的损失功能。
这是Pytorch中的批处理功能:
def roc_star_loss( _y_true, y_pred, gamma, _epoch_true, epoch_pred):
"""
Nearly direct loss function for AUC.
See article,
C. Reiss, "Roc-star : An objective function for ROC-AUC that actually works."
https://github.com/iridiumblue/articles/blob/master/roc_star.md
_y_true: `Tensor`. Targets (labels). Float either 0.0 or 1.0 .
y_pred: `Tensor` . Predictions.
gamma : `Float` Gamma, as derived from last epoch.
_epoch_true: `Tensor`. Targets (labels) from last epoch.
epoch_pred : `Tensor`. Predicions from last epoch.
"""
#convert labels to boolean
y_true = (_y_true>=0.50)
epoch_true = (_epoch_true>=0.50)
# if batch is either all true or false return small random stub value.
if torch.sum(y_true)==0 or torch.sum(y_true) == y_true.shape[0]: return torch.sum(y_pred)*1e-8
pos = y_pred[y_true]
neg = y_pred[~y_true]
epoch_pos = epoch_pred[epoch_true]
epoch_neg = epoch_pred[~epoch_true]
# Take random subsamples of the training set, both positive and negative.
max_pos = 1000 # Max number of positive training samples
max_neg = 1000 # Max number of positive training samples
cap_pos = epoch_pos.shape[0]
cap_neg = epoch_neg.shape[0]
epoch_pos = epoch_pos[torch.rand_like(epoch_pos) < max_pos/cap_pos]
epoch_neg = epoch_neg[torch.rand_like(epoch_neg) < max_neg/cap_pos]
ln_pos = pos.shape[0]
ln_neg = neg.shape[0]
# sum positive batch elements agaionst (subsampled) negative elements
if ln_pos>0 :
pos_expand = pos.view(-1,1).expand(-1,epoch_neg.shape[0]).reshape(-1)
neg_expand = epoch_neg.repeat(ln_pos)
diff2 = neg_expand - pos_expand + gamma
l2 = diff2[diff2>0]
m2 = l2 * l2
len2 = l2.shape[0]
else:
m2 = torch.tensor([0], dtype=torch.float).cuda()
len2 = 0
# Similarly, compare negative batch elements against (subsampled) positive elements
if ln_neg>0 :
pos_expand = epoch_pos.view(-1,1).expand(-1, ln_neg).reshape(-1)
neg_expand = neg.repeat(epoch_pos.shape[0])
diff3 = neg_expand - pos_expand + gamma
l3 = diff3[diff3>0]
m3 = l3*l3
len3 = l3.shape[0]
else:
m3 = torch.tensor([0], dtype=torch.float).cuda()
len3=0
if (torch.sum(m2)+torch.sum(m3))!=0 :
res2 = torch.sum(m2)/max_pos+torch.sum(m3)/max_neg
#code.interact(local=dict(globals(), **locals()))
else:
res2 = torch.sum(m2)+torch.sum(m3)
res2 = torch.where(torch.isnan(res2), torch.zeros_like(res2), res2)
return res2
请注意,有一些额外的参数。我们正在上一个时代的训练集中。由于整个训练集从一个时期变为另一个时期的变化不大,因此损失功能可以再次比较每个预测略有过时的训练集。这简化了调试,并且似乎受益于性能,因为“背景”时代并未从一批变为下一个。
同样,γ是一个昂贵的计算。我们再次使用子采样技巧,但将子样本的大小增加到约10,000,以确保准确的估计值。为了保持性能剪辑,我们每个时期仅重新计算一次该值一次。这是这样做的功能:
def epoch_update_gamma(y_true,y_pred, epoch=-1,delta=2):
"""
Calculate gamma from last epoch's targets and predictions.
Gamma is updated at the end of each epoch.
y_true: `Tensor`. Targets (labels). Float either 0.0 or 1.0 .
y_pred: `Tensor` . Predictions.
"""
DELTA = delta
SUB_SAMPLE_SIZE = 2000.0
pos = y_pred[y_true==1]
neg = y_pred[y_true==0] # yo pytorch, no boolean tensors or operators? Wassap?
# subsample the training set for performance
cap_pos = pos.shape[0]
cap_neg = neg.shape[0]
pos = pos[torch.rand_like(pos) < SUB_SAMPLE_SIZE/cap_pos]
neg = neg[torch.rand_like(neg) < SUB_SAMPLE_SIZE/cap_neg]
ln_pos = pos.shape[0]
ln_neg = neg.shape[0]
pos_expand = pos.view(-1,1).expand(-1,ln_neg).reshape(-1)
neg_expand = neg.repeat(ln_pos)
diff = neg_expand - pos_expand
ln_All = diff.shape[0]
Lp = diff[diff>0] # because we're taking positive diffs, we got pos and neg flipped.
ln_Lp = Lp.shape[0]-1
diff_neg = -1.0 * diff[diff<0]
diff_neg = diff_neg.sort()[0]
ln_neg = diff_neg.shape[0]-1
ln_neg = max([ln_neg, 0])
left_wing = int(ln_Lp*DELTA)
left_wing = max([0,left_wing])
left_wing = min([ln_neg,left_wing])
default_gamma=torch.tensor(0.2, dtype=torch.float).cuda()
if diff_neg.shape[0] > 0 :
gamma = diff_neg[left_wing]
else:
gamma = default_gamma # default=torch.tensor(0.2, dtype=torch.float).cuda() #zoink
L1 = diff[diff>-1.0*gamma]
ln_L1 = L1.shape[0]
if epoch > -1 :
return gamma
else :
return default_gamma
这是直升机视图,显示了如何在时期循环时使用两个功能,然后在批处理上使用:
train_ds = CatDogDataset(train_files, transform)
train_dl = DataLoader(train_ds, batch_size=BATCH_SIZE)
#initialize last epoch with random values
last_epoch_y_pred = torch.tensor( 1.0-numpy.random.rand(len(train_ds))/2.0, dtype=torch.float).cuda()
last_epoch_y_t = torch.tensor([o for o in train_tt],dtype=torch.float).cuda()
epoch_gamma = 0.20
for epoch in range(epoches):
epoch_y_pred=[]
epoch_y_t=[]
for X, y in train_dl:
preds = model(X)
# .
# .
loss = roc_star_loss(y,preds,epoch_gamma, last_epoch_y_t, last_epoch_y_pred)
# .
# .
epoch_y_pred.extend(preds)
epoch_y_t.extend(y)
last_epoch_y_pred = torch.tensor(epoch_y_pred).cuda()
last_epoch_y_t = torch.tensor(epoch_y_t).cuda()
epoch_gamma = epoch_update_gamma(last_epoch_y_t, last_epoch_y_pred, epoch)
#...
可以在这里找到一个完整的工作示例,示例。
在下面,我们使用BCE在同一模型上绘制了ROC-Star的性能。经验表明,ROC-Star通常可以使用BCE简单地将任何型号换成任何模型,从而有机会提高性能。