这里写目录标题

  • 一、Dice评价指标
  • 二、IoU评价指标
  • 三、BCE损失函数
  • 四、Focal Loss
  • 五、Lovász-Softmax


一、Dice评价指标

Dice系数

Dice系数(Dice coefficient)是常见的评价分割效果的方法之一,同样也可以改写成损失函数用来度量prediction和target之间的距离。Dice系数定义如下:

语义分割数据不平衡 语义分割dice_评价指标 式中:语义分割数据不平衡 语义分割dice_损失函数_02表示真实前景(target),语义分割数据不平衡 语义分割dice_召回率_03表示预测前景(prediction)。Dice系数取值范围为语义分割数据不平衡 语义分割dice_深度学习_04,其中值为1时代表预测与真实完全一致。仔细观察,Dice系数与分类评价指标中的F1 score很相似:

语义分割数据不平衡 语义分割dice_深度学习_05

所以,Dice系数不仅在直观上体现了target与prediction的相似程度,同时其本质上还隐含了精确率和召回率两个重要指标。

计算Dice时,将语义分割数据不平衡 语义分割dice_损失函数_06近似为prediction与target对应元素相乘再相加的结果。语义分割数据不平衡 语义分割dice_损失函数_07语义分割数据不平衡 语义分割dice_语义分割数据不平衡_08的计算直接进行简单的元素求和(也有一些做法是取平方求和),如下示例: 语义分割数据不平衡 语义分割dice_评价指标_09

语义分割数据不平衡 语义分割dice_语义分割数据不平衡_10

语义分割数据不平衡 语义分割dice_损失函数_11

Dice Loss

Dice Loss是在V-net模型中被提出应用的,是通过Dice系数转变而来,其实为了能够实现最小化的损失函数,以方便模型训练,以语义分割数据不平衡 语义分割dice_深度学习_12的形式作为损失函数: 语义分割数据不平衡 语义分割dice_语义分割数据不平衡_13 在一些场合还可以添加上Laplace smoothing减少过拟合: 语义分割数据不平衡 语义分割dice_评价指标_14

代码实现:

import numpy as np

def dice(output, target):
    '''计算Dice系数'''
    smooth = 1e-6 # 避免0为除数
    intersection = (output * target).sum()
    return (2. * intersection + smooth) / (output.sum() + target.sum() + smooth)

# 生成随机两个矩阵测试
target = np.random.randint(0, 2, (3, 3))
output = np.random.randint(0, 2, (3, 3))

d = dice(output, target)
# ----------------------------
target = array([[1, 0, 0],
       			[0, 1, 1],
			    [0, 0, 1]])
output = array([[1, 0, 1],
       			[0, 1, 0],
       			[0, 0, 0]])
d = 0.5714286326530524

二、IoU评价指标

IoU(intersection over union)指标就是常说的交并比,不仅在语义分割评价中经常被使用,在目标检测中也是常用的评价指标。顾名思义,交并比就是指target与prediction两者之间交集与并集的比值: 语义分割数据不平衡 语义分割dice_召回率_15 仍然以人物前景分割为例,如下图,其IoU的计算就是使用语义分割数据不平衡 语义分割dice_语义分割数据不平衡_16

代码实现:

def iou_score(output, target):
    '''计算IoU指标'''
	  intersection = np.logical_and(target, output) 
    union = np.logical_or(target, output) 
    return np.sum(intersection) / np.sum(union)

# 生成随机两个矩阵测试
target = np.random.randint(0, 2, (3, 3))
output = np.random.randint(0, 2, (3, 3))

d = iou_score(output, target)
# ----------------------------
target = array([[1, 0, 0],
       			[0, 1, 1],
			    [0, 0, 1]])
output = array([[1, 0, 1],
       			[0, 1, 0],
       			[0, 0, 0]])
d = 0.4

三、BCE损失函数

BCE损失函数(Binary Cross-Entropy Loss)是交叉熵损失函数(Cross-Entropy Loss)的一种特例,BCE Loss只应用在二分类任务中。针对分类问题,单样本的交叉熵损失为: 语义分割数据不平衡 语义分割dice_深度学习_17 式中,语义分割数据不平衡 语义分割dice_评价指标_18,其中语义分割数据不平衡 语义分割dice_损失函数_19是非0即1的数字,代表了是否属于第语义分割数据不平衡 语义分割dice_深度学习_20类,为真实值;语义分割数据不平衡 语义分割dice_评价指标_21代表属于第i类的概率,为预测值。可以看出,交叉熵损失考虑了多类别情况,针对每一种类别都求了损失。针对二分类问题,上述公式可以改写为: 语义分割数据不平衡 语义分割dice_深度学习_22 式中,语义分割数据不平衡 语义分割dice_评价指标_23为真实值,非1即0;语义分割数据不平衡 语义分割dice_深度学习_24为所属此类的概率值,为预测值。这个公式也就是BCE损失函数,即二分类任务时的交叉熵损失。值得强调的是,公式中的语义分割数据不平衡 语义分割dice_深度学习_24为概率分布形式,因此在使用BCE损失前,都应该将预测出来的结果转变成概率值,一般为sigmoid激活之后的输出。

代码实现:

import torch
import torch.nn as nn

bce = nn.BCELoss()
bce_sig = nn.BCEWithLogitsLoss()

input = torch.randn(5, 1, requires_grad=True)
target = torch.empty(5, 1).random_(2)
pre = nn.Sigmoid()(input)

loss_bce = bce(pre, target)
loss_bce_sig = bce_sig(input, target)

# ------------------------
input = tensor([[-0.2296],
        		[-0.6389],
        		[-0.2405],
        		[ 1.3451],
        		[ 0.7580]], requires_grad=True)
output = tensor([[1.],
        		 [0.],
        		 [0.],
        		 [1.],
        		 [1.]])
pre = tensor([[0.4428],
        	  [0.3455],
        	  [0.4402],
        	  [0.7933],
        	  [0.6809]], grad_fn=<SigmoidBackward>)

print(loss_bce)
tensor(0.4869, grad_fn=<BinaryCrossEntropyBackward>)

print(loss_bce_sig)
tensor(0.4869, grad_fn=<BinaryCrossEntropyWithLogitsBackward>)

四、Focal Loss

Focal loss最初是出现在目标检测领域,主要是为了解决正负样本比例失调的问题。那么对于分割任务来说,如果存在数据不均衡的情况,也可以借用focal loss来进行缓解。Focal loss函数公式如下所示:

语义分割数据不平衡 语义分割dice_语义分割数据不平衡_26 仔细观察就不难发现,它其实是BCE扩展而来,对比BCE其实就多了个 语义分割数据不平衡 语义分割dice_深度学习_27 为什么多了这个就能缓解正负样本不均衡的问题呢?见下图:

语义分割数据不平衡 语义分割dice_语义分割数据不平衡_28


简单来说:语义分割数据不平衡 语义分割dice_损失函数_29解决样本不平衡问题,语义分割数据不平衡 语义分割dice_损失函数_30解决样本难易问题。

也就是说,当数据不均衡时,可以根据比例设置合适的语义分割数据不平衡 语义分割dice_损失函数_29,这个很好理解,为了能够使得正负样本得到的损失能够均衡,因此对loss前面加上一定的权重,其中负样本数量多,因此占用的权重可以设置的小一点;正样本数量少,就对正样本产生的损失的权重设的高一点。

那γ具体怎么起作用呢?以图中语义分割数据不平衡 语义分割dice_损失函数_32曲线为例,假设语义分割数据不平衡 语义分割dice_深度学习_33类别为1,当模型预测结果为1的概率语义分割数据不平衡 语义分割dice_召回率_34比较大时,我们认为模型预测的比较准确,也就是说这个样本比较简单。而对于比较简单的样本,我们希望提供的loss小一些而让模型主要学习难一些的样本,也就是语义分割数据不平衡 语义分割dice_召回率_35则loss接近于0,既不用再特别学习;当分类错误时,语义分割数据不平衡 语义分割dice_深度学习_36则loss正常产生,继续学习。对比图中蓝色和绿色曲线,可以看到,γ值越大,当模型预测结果比较准确的时候能提供更小的loss,符合我们为简单样本降低loss的预期。

代码实现:

import torch.nn as nn
import torch
import torch.nn.functional as F

class FocalLoss(nn.Module):
    def __init__(self, alpha=1, gamma=2, logits=False, reduce=True):
        super(FocalLoss, self).__init__()
        self.alpha = alpha
        self.gamma = gamma
        self.logits = logits	# 如果BEC带logits则损失函数在计算BECloss之前会自动计算softmax/sigmoid将其映射到[0,1]
        self.reduce = reduce

    def forward(self, inputs, targets):
        if self.logits:
            BCE_loss = F.binary_cross_entropy_with_logits(inputs, targets, reduce=False)
        else:
            BCE_loss = F.binary_cross_entropy(inputs, targets, reduce=False)
        pt = torch.exp(-BCE_loss)
        F_loss = self.alpha * (1-pt)**self.gamma * BCE_loss

        if self.reduce:
            return torch.mean(F_loss)
        else:
            return F_loss

# ------------------------

FL1 = FocalLoss(logits=False)
FL2 = FocalLoss(logits=True)

inputs = torch.randn(5, 1, requires_grad=True)
targets = torch.empty(5, 1).random_(2)
pre = nn.Sigmoid()(inputs)

f_loss_1 = FL1(pre, targets)
f_loss_2 = FL2(inputs, targets)

# ------------------------

print('inputs:', inputs)
inputs: tensor([[-1.3521],
        [ 0.4975],
        [-1.0178],
        [-0.3859],
        [-0.2923]], requires_grad=True)
    
print('targets:', targets)
targets: tensor([[1.],
        [1.],
        [0.],
        [1.],
        [1.]])
    
print('pre:', pre)
pre: tensor([[0.2055],
        [0.6219],
        [0.2655],
        [0.4047],
        [0.4274]], grad_fn=<SigmoidBackward>)
    
print('f_loss_1:', f_loss_1)
f_loss_1: tensor(0.3375, grad_fn=<MeanBackward0>)
    
print('f_loss_2', f_loss_2)
f_loss_2 tensor(0.3375, grad_fn=<MeanBackward0>)

五、Lovász-Softmax

IoU是评价分割模型分割结果质量的重要指标,因此很自然想到能否用语义分割数据不平衡 语义分割dice_深度学习_37(即Jaccard loss)来做损失函数,但是它是一个离散的loss,不能直接求导,所以无法直接用来作为损失函数。为了克服这个离散的问题,可以采用lLovász extension将离散的Jaccard loss 变得连续,从而可以直接求导,使得其作为分割网络的loss function。Lovász-Softmax相比于交叉熵函数具有更好的效果。

首先明确定义,在语义分割任务中,给定真实像素标签向量语义分割数据不平衡 语义分割dice_语义分割数据不平衡_38和预测像素标签语义分割数据不平衡 语义分割dice_损失函数_39,则所属类别语义分割数据不平衡 语义分割dice_语义分割数据不平衡_40的IoU(也称为Jaccard index)如下,其取值范围为语义分割数据不平衡 语义分割dice_深度学习_04,并规定语义分割数据不平衡 语义分割dice_语义分割数据不平衡_42语义分割数据不平衡 语义分割dice_评价指标_43 则Jaccard loss为: 语义分割数据不平衡 语义分割dice_语义分割数据不平衡_44 针对类别语义分割数据不平衡 语义分割dice_语义分割数据不平衡_40,所有未被正确预测的像素集合定义为: 语义分割数据不平衡 语义分割dice_语义分割数据不平衡_46 则可将Jaccard loss改写为关于语义分割数据不平衡 语义分割dice_深度学习_47的子模集合函数(submodular set functions): 语义分割数据不平衡 语义分割dice_召回率_48 方便理解,此处可以把语义分割数据不平衡 语义分割dice_语义分割数据不平衡_49理解成如图像mask展开成离散一维向量的形式。

Lovász extension可以求解子模最小化问题,并且子模的Lovász extension是凸函数,可以高效实现最小化。在论文中作者对语义分割数据不平衡 语义分割dice_语义分割数据不平衡_50(集合函数)和语义分割数据不平衡 语义分割dice_召回率_51(集合函数的Lovász extension)进行了定义,为不涉及过多概念以方便理解,此处不再过多讨论。我们可以将语义分割数据不平衡 语义分割dice_召回率_51理解为一个线性插值函数,可以将语义分割数据不平衡 语义分割dice_语义分割数据不平衡_49这种离散向量连续化,主要是为了方便后续反向传播、求梯度等等。因此我们可以通过这个线性插值函数得到语义分割数据不平衡 语义分割dice_损失函数_54的Lovász extension语义分割数据不平衡 语义分割dice_评价指标_55

在具有语义分割数据不平衡 语义分割dice_语义分割数据不平衡_56个类别的语义分割任务中,我们使用Softmax函数将模型的输出映射到概率分布形式,类似传统交叉熵损失函数所进行的操作: 语义分割数据不平衡 语义分割dice_召回率_57 式中,语义分割数据不平衡 语义分割dice_召回率_58表示了像素语义分割数据不平衡 语义分割dice_深度学习_20所属类别语义分割数据不平衡 语义分割dice_语义分割数据不平衡_40的概率。通过上式可以构建每个像素产生的误差m( c )

语义分割数据不平衡 语义分割dice_评价指标_61

可知,对于一张图像中所有像素则误差向量为语义分割数据不平衡 语义分割dice_召回率_62,则可以建立关于语义分割数据不平衡 语义分割dice_语义分割数据不平衡_63的代理损失函数: 语义分割数据不平衡 语义分割dice_评价指标_64 当我们考虑整个数据集是,一般会使用mIoU进行度量,因此我们对上述损失也进行平均化处理,则定义的Lovász-Softmax损失函数为: 语义分割数据不平衡 语义分割dice_召回率_65

import torch
from torch.autograd import Variable
import torch.nn.functional as F
import numpy as np
try:
    from itertools import  ifilterfalse
except ImportError: # py3k
    from itertools import  filterfalse as ifilterfalse
    
# --------------------------- MULTICLASS LOSSES ---------------------------
def lovasz_softmax(probas, labels, classes='present', per_image=False, ignore=None):
    """
    Multi-class Lovasz-Softmax loss
      probas: [B, C, H, W] Variable, class probabilities at each prediction (between 0 and 1).
              Interpreted as binary (sigmoid) output with outputs of size [B, H, W].
      labels: [B, H, W] Tensor, ground truth labels (between 0 and C - 1)
      classes: 'all' for all, 'present' for classes present in labels, or a list of classes to average.
      per_image: compute the loss per image instead of per batch
      ignore: void class labels
    """
    if per_image:
        loss = mean(lovasz_softmax_flat(*flatten_probas(prob.unsqueeze(0), lab.unsqueeze(0), ignore), classes=classes)
                          for prob, lab in zip(probas, labels))
    else:
        loss = lovasz_softmax_flat(*flatten_probas(probas, labels, ignore), classes=classes)
    return loss


def lovasz_softmax_flat(probas, labels, classes='present'):
    """
    Multi-class Lovasz-Softmax loss
      probas: [P, C] Variable, class probabilities at each prediction (between 0 and 1)
      labels: [P] Tensor, ground truth labels (between 0 and C - 1)
      classes: 'all' for all, 'present' for classes present in labels, or a list of classes to average.
    """
    if probas.numel() == 0:
        # only void pixels, the gradients should be 0
        return probas * 0.
    C = probas.size(1)
    losses = []
    class_to_sum = list(range(C)) if classes in ['all', 'present'] else classes
    for c in class_to_sum:
        fg = (labels == c).float() # foreground for class c
        if (classes is 'present' and fg.sum() == 0):
            continue
        if C == 1:
            if len(classes) > 1:
                raise ValueError('Sigmoid output possible only with 1 class')
            class_pred = probas[:, 0]
        else:
            class_pred = probas[:, c]
        errors = (Variable(fg) - class_pred).abs()
        errors_sorted, perm = torch.sort(errors, 0, descending=True)
        perm = perm.data
        fg_sorted = fg[perm]
        losses.append(torch.dot(errors_sorted, Variable(lovasz_grad(fg_sorted))))
    return mean(losses)


def flatten_probas(probas, labels, ignore=None):
    """
    Flattens predictions in the batch
    """
    if probas.dim() == 3:
        # assumes output of a sigmoid layer
        B, H, W = probas.size()
        probas = probas.view(B, 1, H, W)
    B, C, H, W = probas.size()
    probas = probas.permute(0, 2, 3, 1).contiguous().view(-1, C)  # B * H * W, C = P, C
    labels = labels.view(-1)
    if ignore is None:
        return probas, labels
    valid = (labels != ignore)
    vprobas = probas[valid.nonzero().squeeze()]
    vlabels = labels[valid]
    return vprobas, vlabels


def xloss(logits, labels, ignore=None):
    """
    Cross entropy loss
    """
    return F.cross_entropy(logits, Variable(labels), ignore_index=255)

# --------------------------- HELPER FUNCTIONS ---------------------------
def isnan(x):
    return x != x
    
def mean(l, ignore_nan=False, empty=0):
    """
    nanmean compatible with generators.
    """
    l = iter(l)
    if ignore_nan:
        l = ifilterfalse(isnan, l)
    try:
        n = 1
        acc = next(l)
    except StopIteration:
        if empty == 'raise':
            raise ValueError('Empty mean')
        return empty
    for n, v in enumerate(l, 2):
        acc += v
    if n == 1:
        return acc
    return acc / n