作为一个超详细的清单,本文介绍了十多种常见的语义切分的丢失和组合。大部分介绍都附有代码实现,帮助你掌握新的损失函数并加以应用。吉时七夕粉丝福利活动:方士们,你们能解决七夕的算法吗?
当类别数等于时,这个损失就是二元交叉熵,在Pytorch中提供了单独的实现。
交叉熵损失可用于大多数语义分割场景,但它有一个明显的缺点,即当只分割前景和背景时,当前景像素的数量远远小于背景像素的数量,即像素的数量远远大于像素的数量时,损失函数中的分量会占优势,使得模型严重偏向背景,导致结果不佳。
代码实现如下:
#二进制交叉熵,这里输入importtorchimportortorch。nnasnnmortorch.nn .函数式asfnn。bceloss (sigmoid (input),target) #多分类交叉熵,有了这个损失,就不需要再加Softmax层nn了。前面是crosstopyloss(输入,目标)。
可以看出,每个类别在交叉熵损失的基础上只增加了一个权重参数,其中计算公式为:其中代表总像素数,代表GT类别的像素数。这样,与原来的交叉熵损失相比,在样本数不平衡的情况下,可以得到更好的结果。
从010年到1010年,何明凯的团队将FocalLoss引入到RetinaNet的论文中求解难易样本数量不平衡.让我们来回顾一下。我们知道,一级的目标检测器通常产生10k帧,但其中只有少数是正样本,而且正负样本的数量很不平衡。我们计算分类时常用的损失——交叉熵公式如下:
为了解决正负样本数量不平衡,问题,我们经常在二元交叉熵损失前面添加一个参数,即:
虽然正负样本数量均衡,但实际上目标检测中大量的候选目标都是容易分离的样本。这些样本的损耗很低,但由于数量极不平衡,容易划分的样本数量相对过多,最终主导了总损耗。
因此,本文认为易分样本(即,置信度高的样本)对模型的提升效果非常小,模型应该主要关注与那些难分样本.是这样诞生的。一个简单的想法就是,只要我们减少高置信度样本的损失,对吗?即下面的公式:
我们拿等于2来感受一下,如果,那么,损失减少1000倍。最后FocalLoss还结合了公式(2),很好理解。公式(3)解决了疑难样本的不平衡,公式(2)解决了正负样本的不平衡。把公式(2)和公式(3)结合起来,就同时解决了正反难度两个问题!所以最终的无焦点形式如下:
下图显示了当FocalLoss取不同值时损失函数下降的情况。
实验结果表明,什么时候,什么时候,效果最好,所以损失函数训练过程中关注样本的优先级是容易丢失的。
虽然在RetinaNet中是最好的,但并不意味着这个参数在我们的分割任务和其他样本中是最好的。我们需要手动调整这个参数,而FocalLoss似乎是分割任务中的只适合于二分类的情况。
FocalLoss的Pytorch代码实现如下:
classFocalLoss(nn。模块):
def__init__(self,gamma=0,alpha=无,size_average=True):
超级(无焦点,自我)。__init__()
自我=
自我=
ifisinstance(alpha,(float,int,long)):self.alpha=torch .张量([,1-])
ifisinstance(alpha,list):self.alpha=torch .张量()
self.size _ average=size _平均值
向前定义(自身、输入、目标):
ifinput.dim()2:
input=input.view(input.size(0),input.size(1),-1)#N,C,H,W=>N,C,H*W
input=input.transpose(1,2)#N,C,H*W=>N,H*W,C
input=input.contiguous().view(-1,input.size(2))#N,H*W,C=>N*H*W,C
target=target.view(-1,1)
logpt=F.log_softmax(input)
logpt=logpt.gather(1,target)
logpt=logpt.view(-1)
pt=Variable(logpt.data.exp())
ifself.alphaisnotNone:
ifself.alpha.type()!=input.data.type():
self.alpha=self.alpha.type_as(input.data)
at=self.alpha.gather(0,target.data.view(-1))
logpt=logpt*Variable(at)
loss=-1*(1-pt)**self.gamma*logpt
ifself.size_average:returnloss.mean()
else:returnloss.sum()
importtorch.nnasnn
importtorch.nn.functionalasF
classSoftDiceLoss(nn.Module):
def__init__(self,weight=None,size_average=True):
super(SoftDiceLoss,self).__init__()
defforward(self,logits,targets):
num=targets.size(0)
//为了防止除0的发生
smooth=1
probs=F.sigmoid(logits)
m1=probs.view(num,-1)
m2=targets.view(num,-1)
intersection=(m1*m2)
score=2.*(intersection.sum(1)+smooth)/(m1.sum(1)+m2.sum(1)+smooth)
score=1-score.sum()/num
returnscore
IOULoss和DiceLoss一样属于metriclearning的衡量方式,公式定义如下:
它和DiceLoss一样仍然存在训练过程不稳定的问题,IOULoss在分割任务中应该是不怎么用的,如果你要试试的话代码实现非常简单,在上面DiceLoss的基础上改一下分母部分即可,不再赘述了。我们可以看一下将IOUloss应用到FCN上在VOC2010上的实验结果:
可以看到IOULoss是对大多数类别的分割结果有一定改善的,但是对Person类却性能反而下降了。
论文地址为:https://arxiv.org/pdf/1706.05721.pdf。实际上DiceLoss只是Tverskyloss的一种特殊形式而已,我们先来看一下Tversky系数的定义,它是Dice系数和Jaccard系数(就是IOU系数,即)的广义系数,公式为:这里A表示预测值而B表示真实值。当和均为的时候,这个公式就是Dice系数,当和均为的时候,这个公式就是Jaccard系数。其中代表FP(假阳性),代表FN(假阴性),通过调整和这两个超参数可以控制这两者之间的权衡,进而影响召回率等指标。下表展示了对FCN使用TverskyLoss进行病灶分割,并且取不同的和参数获得的结果,其中Sensitivity代表召回率Recall,而Specificity表示准确率Precision:
在极小的病灶下的分割效果图如下:
而在较大的病灶下的分割效果图如下:
Keras代码实现如下:
deftversky(y_true,y_pred):
y_true_pos=K.flatten(y_true)
y_pred_pos=K.flatten(y_pred)
true_pos=K.sum(y_true_pos*y_pred_pos)
false_neg=K.sum(y_true_pos*(1-y_pred_pos))
false_pos=K.sum((1-y_true_pos)*y_pred_pos)
alpha=0.7
return(true_pos+smooth)/(true_pos+alpha*false_neg+(1-alpha)*false_pos+smooth)
deftversky_loss(y_true,y_pred):
return1-tversky(y_true,y_pred)
论文原文全程为:GeneralizedOverlapMeasuresforEvaluationandValidationinMedicalImageAnalysis刚才分析过DiceLoss对小目标的预测是十分不利的,因为一旦小目标有部分像素预测错误,就可能会引起Dice系数大幅度波动,导致梯度变化大训练不稳定。另外从上面的代码实现可以发现,DiceLoss针对的是某一个特定类别的分割的损失。当类似于病灶分割有多个场景的时候一般都会使用多个DiceLoss,所以GeneralizedDiceloss就是将多个类别的DiceLoss进行整合,使用一个指标作为分割结果的量化指标。GDLLoss在类别数为2时公式如下:其中表示类别在第个位置的真实像素类别,而表示相应的预测概率值,表示每个类别的权重。的公式为:。
Keras代码实现:
defgeneralized_dice_coeff(y_true,y_pred):
Ncl=y_pred.shape[-1]
w=K.zeros(shape=(Ncl,))
w=K.sum(y_true,axis=(0,1,2))
w=1/(w**2+0.000001)
#Computegendicecoef:
numerator=y_true*y_pred
numerator=w*K.sum(numerator,(0,1,2,3))
numerator=K.sum(numerator)
denominator=y_true+y_pred
denominator=w*K.sum(denominator,(0,1,2,3))
denominator=K.sum(denominator)
gen_dice_coef=2*numerator/denominator
returngen_dice_coef
defgeneralized_dice_loss(y_true,y_pred):
return1-generalized_dice_coeff(y_true,y_pred)
即将BCELoss和DiceLoss进行组合,在数据较为均衡的情况下有所改善,但是在数据极度不均衡的情况下交叉熵Loss会在迭代几个Epoch之后远远小于DiceLoss,这个组合Loss会退化为DiceLoss。
这个Loss的组合应该最早见于腾讯医疗AI实验室2018年在《MedicalPhysics》上发表的这篇论文:https://arxiv.org/pdf/1808.05238.pdf。论文提出了使用FocalLoss和DiceLoss来处理小器官的分割问题。公式如下:
其中,,分别表示对于类别c的真阳性,假阴性,假阳性。可以看到这里使用FocalLoss的时候,里面的两个参数直接用对于类别c的正样本像素个数来代替。具体实验细节和效果可以去看看原论文。
这个Loss是MICCAI2018的论文3DSegmentationwithExponentialLogarithmicLossforHighlyUnbalancedObjectSizes提出来的,论文地址为:https://arxiv.org/abs/1809.00076。这个Loss结合了FocalLoss以及Diceloss。公式如下:
这里增加了两个参数权重分别为和,而为指数logDice损失,为指数交叉熵损失。公式如下:
其中,表示像素位置,表示类别标签,表示位置处的groundtruth类别,表示经过softmax操作之后的概率值。其中:其中表示标签出现的频率,这个参数可以减小出现频率较高的类别权重。和可以提升函数的非线性,如Figure2所示:
这是今天要介绍的最后一个Loss,Kaggle神器。这篇论文是CVPR2018的,原地址为:https://arxiv.org/pdf/1705.08790.pdf。对原理感兴趣可以去看一下论文,这个损失是对Jaccard(IOU)Loss进行Lovaze扩展,表现更好。因为这篇文章的目的只是简单盘点一下,就不再仔细介绍这个Loss了。之后可能会单独介绍一下这个Loss,论文的官方源码见附录,使用其实不是太难。
在介绍DiceLoss的时候留了一个问题,交叉熵的梯度形式推导,这里给一下推导。
(1)softmax函数首先再来明确一下softmax函数,一般softmax函数是用来做分类任务的输出层。softmax的形式为:其中表示的是第i个神经元的输出,接下来我们定义一个有多个输入,一个输出的神经元。神经元的输出为其中是第个神经元的第个权重,b是偏移值.表示网络的第个输出。给这个输出加上一个softmax函数,可以写成:,其中表示softmax函数的第i个输出值。这个过程可以用下图表示:
(2)损失函数softmax的损失函数一般是选择交叉熵损失函数,交叉熵函数形式为:其中y_i表示真实的标签值(3)需要用到的高数的求导公式
c'=0(c为常数)
(x^a)'=ax^(a-1),a为常数且a≠0
(a^x)'=a^xlna
(e^x)'=e^x
(logax)'=1/(xlna),a>0且a≠1
(lnx)'=1/x
(sinx)'=cosx
(cosx)'=-sinx
(tanx)'=(secx)^2
(secx)'=secxtanx
(cotx)'=-(cscx)^2
(cscx)'=-csxcotx
(arcsinx)'=1/√(1-x^2)
(arccosx)'=-1/√(1-x^2)
(arctanx)'=1/(1+x^2)
(arccotx)'=-1/(1+x^2)
(shx)'=chx
(chx)'=shx
(uv)'=uv'+u'v
(u+v)'=u'+v'
(u/)'=(u'v-uv')/^2
(3)进行推导我们需要求的是loss对于神经元输出的梯度,求出梯度后才可以反向传播,即是求:,根据链式法则(也就是复合函数求导法则),初学的时候这个公式理解了很久,为什么这里是而不是呢?这里我们回忆一下softmax的公示,分母部分包含了所有神经元的输出,所以对于所有输出非i的输出中也包含了,所以所有的a都要参与计算,之后我们会看到计算需要分为和两种情况分别求导数。首先来求前半部分:接下来求第二部分的导数:
如果,如果,。接下来把上面的组合之后得到:。推导完成!(4)对于分类问题来说,我们给定的结果最终只有一个类别是1,其他是0,因此对于分类问题,梯度等于:
最后放一份CS231N的代码实现,帮助进一步理解:
#coding=utf-8
importnumpyasnp
defsoftmax_loss_native(W,X,y,reg):
'''
Softmax_loss的暴力实现,利用了for循环
输入的维度是D,有C个分类类别,并且我们在有N个例子的batch上进行操作
输入:
-W:一个numpyarray,形状是(D,C),代表权重
-X:一个形状为(N,D)为numpyarray,代表输入数据
-y:一个形状为(N,)的numpyarray,代表类别标签
-reg:(float)正则化参数
f返回:
-一个浮点数代表Loss
-和W形状一样的梯度
'''
loss=0.0
dW=np.zeros_like(W)#dW代表W反向传播的梯度
num_classes=W.shape[1]
num_train=X.shape[0]
foriinxrange(num_train):
scores=X[i].dot(W)
shift_scores=scores-max(scores)#防止数值不稳定
loss_i=-shift_scores[y[i]]+np.log(sum(np.exp(shift_scores)))
loss+=loss_i
forjinxrange(num_classes):
softmax_output=np.exp(shift_scores[j])/sum(np.exp(shift_scores))
ifj==y[i]:
dW[:,j]+=(-1+softmax_output)*X[i]
else:
dW[:,j]+=softmax_output*X[i]
loss/=num_train
loss+=0.5*reg*np.sum(W*W)
dW=dW/num_train+reg*W
returnloss,dW
defsoftmax_loss_vectorized(W,X,y,reg):
loss=0.0
dW=np.zeros_like(W)
num_class=W.shape[1]
num_train=X.shape[0]
scores=X.dot(W)
shift_scores=scores-np.max(scores,axis=1).reshape(-1,1)
softmax_output=np.exp(shift_scores)/np.sum(np.exp(shift_scores),axis=1).reshape(-1,1)
loss=-np.sum(np.log(softmax_output[range(num_train),list(y)]))
loss/=num_train
loss+=0.5*reg*np.sum(W*W)
dS=softmax_output.copy()
dS[range(num_train),list(y)]+=-1
dW=(x.T).dot(dS)
dW=dW/num_train+reg*W
returnloss,dW
这篇文章介绍了近些年来算是非常常用的一些语义分割方面的损失函数,希望可以起到一个抛砖引玉的作用,引发大家对分割中的损失函数进一步思考。当然对于工程派和比赛派来讲,掌握一种新Loss并且应用于自己的项目或者比赛也是不错的。
1.https://www.aiuai.cn/aifarm1159.html
2.GeneralizedDiceloss代码实现:Generalizeddicelossformulti-classsegmentation·Issue#9395·keras-team/keras
3.Lovasz-SoftmaxLoss代码:bermanmaxim/LovaszSoftmax
作者丨BBuf@知乎
来源丨https://zhuanlan.zhihu.com/p/103426335