深度学习中的损失函数

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 将如下图所示:

深度学习中的损失函数

Typical ground truth for objects.

同时假设我们开发了一个深度学习模型,它预测了以下输出:

深度学习中的损失函数

首先,我们将使用标准损失函数进行语义分割,即如下所示的分类交叉熵:

深度学习中的损失函数

这里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 is missing for circle.

深度学习中的损失函数

Prediction output

在另一个例子中,第一个对象(椭圆)缺少了ground truth:

深度学习中的损失函数

Ground truth is missing for ellipse.

深度学习中的损失函数

预测输出

在这些情况下,如果我们仍然使用标准的损失函数,我们可能会错误地惩罚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在损失值中的任何贡献。换句话说,自定义损失函数可以写如下:

深度学习中的损失函数

Custom loss function

为此,我们考虑两个条件。首先,我们找到缺少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。

相关推荐