CYJ0go 2018-05-01
在这篇文章中,我们将在深度学习应用程序(如语义分割)中开发自定义损失函数。我们使用Python 2.7和Keras 2.x来实现。
损失函数是任何基于学习的算法的核心。我们将学习问题转化为一个优化问题,定义一个损失函数,然后优化算法,使损失函数最小化。
考虑C对象的语义分割。这意味着图像中有需要分割的C对象。我们得到了一组图像和相应的注释,用于训练和开发算法。为了简单起见,让我们假设有C=3个对象,包括一个椭圆、一个矩形和一个圆。我们可以使用下面的简单代码来生成带有三个对象的一些掩码。
from skimage.draw import ellipse,polygon,circle
def genEllipse(r,c,h,w,max_rotate=90):
r_radius=h/6
c_radius=w/6
rot_angle=np.random.randint(max_rotate)
img = np.zeros((h, w), dtype=np.uint8)
rr, cc = ellipse(r, c, r_radius, c_radius, rotation=np.deg2rad(rot_angle))
img[rr, cc] = 1
return img
def genCircle(r,c,h,w):
r_radius=h/6
img = np.zeros((h, w), dtype=np.uint8)
rr, cc = circle(r, c, r_radius)
img[rr, cc] = 1
return img
def genPolygon(r,c,h,w):
r = np.array([r-h/6, r-h/6, r+h/6, r+h/6])
c = np.array([c-h/6, c+h/6, c+h/6, c-h/6])
img = np.zeros((h, w), dtype=np.uint8)
rr, cc = polygon(r, c )
img[rr, cc] = 1
return img
def genMasks(N,C,H,W):
X=np.zeros((N,C,H,W),"uint8")
for n in range(N):
m1=genEllipse(H/4,W/4,H,W)
m2=genPolygon(3*H/4,3*W/4,H,W)
m3=genCircle(2*H/4,2*W/4,H,W)
X[n,0]=m1
X[n,1]=m2
X[n,2]=m3
return X
Y_GT=genMasks(nb_batch,C=3,h,w)
这些物体的典型 ground truth masks 将如下图所示:
同时假设我们开发了一个深度学习模型,它预测了以下输出:
首先,我们将使用标准损失函数进行语义分割,即如下所示的分类交叉熵:
这里C是对象的数量。请注意,i = 0对应于background。将针对每个图像中的所有像素以及该批次中的所有图像计算损失。所有这些值的平均值将作为损失值报告的单个标量值。在分类交叉熵的情况下,理想的损失将为零!
为了能够方便地调试和比较结果,我们开发了两种损失函数,一种使用Numpy作为:
import numpy as np
_EPSILON = 1e-7
nb_class=4 # number of objects plus background
def standard_loss_np(y_true, y_pred):
y_pred=np.asarray(y_pred,"float32")
y_pred[y_pred==0]=_EPSILON
loss=0
for cls in range(0,nb_class):
loss+=y_true[:,:,cls]*np.log(y_pred[:,:,cls])
return -loss
它的等效使用Keras backends的张量函数为:
from keras import backend as K
_EPSILON = K.epsilon()
nb_class=4 # number of objects plus background
def standard_loss_tensor(y_true, y_pred):
y_pred = K.clip(y_pred, _EPSILON, 1.0-_EPSILON)
loss=0
for cls in range(0,nb_class):
loss+=y_true[:,:,cls]*K.log(y_pred[:,:,cls])
return -loss
正如你所看到的,除了使用backend 和numpy之外,两种损失函数之间没有太大的区别。如果我们尝试8次随机注释和预测,则分别从numpy和张量函数获得loss_numpy = 0.256108 245968和loss_tensor = 0.256108。实际上,相同的价值!
现在我们将开发我们自己的自定义损失函数。由于数据和注释的质量问题,可能需要此自定义。让我们看一个具体的例子。
在我们的案例研究中,让我们假设,由于某种原因,缺少了ground truth。例如,在下图中,对象3 (circle)没有ground truth,而deep learning model提供了一个预测。
在另一个例子中,第一个对象(椭圆)缺少了ground truth:
在这些情况下,如果我们仍然使用标准的损失函数,我们可能会错误地惩罚AI模型。原因是,像素属于丢失的ground truth,将被视为background,并乘以-log(p_i),其中p_i是小的预测概率,结果-log(p_i)将会是一个很大的数字。请注意,这是基于我们的假设,即应该有一个ground truth,但无论什么原因,注释者都忽略了它。
再次,如果我们尝试8次注释和预测,这次有两个随机丢失的注释,标准损失值= 0.493853!显然,与所有ground truth可用时相比,这显示出更高的损失价值。
一个简单的解决办法是去掉那些缺少ground truth。这意味着,如果C对象中有一个对象丢失了ground truth,那么我们就必须从训练数据中删除该图像。然而,这意味着训练的数据更少了!
相反,我们也许能够开发出一种聪明的损失函数,避免在缺少ground truth的情况下进行这种惩罚。在这种情况下,我们把损失函数写为:
其中w_i是智能权重。如果w_i=1,则与标准损失函数相同。我们知道,如果一个对象丢失了ground truth,那就意味着它被指定为background。因此,如果我们设置w_0=0,将被探测到的像素作为没有ground truth的对象,我们将删除background在损失值中的任何贡献。换句话说,自定义损失函数可以写如下:
为此,我们考虑两个条件。首先,我们找到缺少ground truth的图像。使用:
K.any(y_true,axis=1,keepdims=True)
接下来,我们使用以下方法找出所有图像的每像素预测类别:
pred=K.argmax(y_pred,axis=-1)
然后,我们检查预测的输出是否实际上等于缺少的对象。这也可以使用:
K.equal(pred,cls)
请注意,在实际实施中,我们使用:
K.not_equal(pred,cls)
因为我们希望这两个条件都是False,因此logical-OR is False.
如果满足这两个条件,我们将background权重设置为零。这将保证如果一个对象缺少ground truth(实际上被错误地标记为background),那么background在损失函数中的贡献为零。最终的自定义损失函数在这里:
from keras import backend as K
_EPSILON = K.epsilon()
nb_class=4 # number of objects plus the background
def custom_loss_tensor(y_true, y_pred):
y_pred = K.clip(y_pred, _EPSILON, 1.0-_EPSILON)
# find predictions
pred=K.argmax(y_pred,axis=-1)
# find missing annotations per class
y_trueAny=K.any(y_true,axis=1,keepdims=True)
#print "missing annotations",K.eval(y_trueAny)
backgroundWeights=1
for cls in range(0,nb_class):
# we use repeat to get the same size as other tensors
y_trueAnyRepeat=K.repeat_elements(y_trueAny[:,:,cls],nb_sample,axis=1)
#print "repeat shape",K.eval(K.shape(y_trueAnyRepeat))
# check for two conditions
# 1- annotation missing
# 2- prediction is equal to missing class/object
backgroundWeights*=K.not_equal(pred,cls)+y_trueAnyRepeat
#print "background weights shape",K.eval(K.shape(backgroundWeights)),K.eval(K.shape(pred))
#print "sum of background weights",cls,K.eval(K.sum(backgroundWeig0.191179hts))
# loss for background
loss=backgroundWeights*y_true[:,:,0]*K.log(y_pred[:,:,0])
for cls in range(1,nb_class):
loss+=y_true[:,:,cls]*K.log(y_pred[:,:,cls])
return -loss
如果我们计算8个注释的损失和两个随机丢失的对象,我们将得到custom_loss= 0.191179。