DiceLoss,由2016年的论文《V-Net: Fully Convolutional Neural Networks for Volumetric Medical Image Segmentation》中首次被提出。它是旨在应对语义分割中正负样本强烈不平衡的场景

本文将详细介绍DiceLoss的基本概念、思想原理,并提供PyTorch的实现代码,帮助大家去更好的理解和使用。


Dice coefficient

DiceLoss追其源头,是来自Dice coefficient,由Thorvald Sørensen和Lee Raymond Dice于1945年提出,是一种用于度量两个集合的相似程度的度量函数。其取值范围在0到1之间,取值越大表示越相似

Dice coefficient有个别名是F1 score,二者是等价的。其定义如下:

D i c e = 2 ∣ X ⋂ Y ∣ ∣ X ∣ + ∣ Y ∣ Dice=\frac{2|X\bigcap Y|}{|X| + |Y|} Dice=X+Y2∣XY

其中, ∣ X ⋂ Y ∣ |X\bigcap Y| XY表示交集的元素个数, ∣ X ∣ |X| X ∣ Y ∣ |Y| Y表示其各自的元素个数。从集合的角度上考虑一下:

  • X X X Y Y Y集合是完全没有交集的情况下,此时 ∣ X ⋂ Y ∣ = 0 |X\bigcap Y|=0 XY=0,此时Dice为0

  • X X X Y Y Y集合完全一致的情况下,此时 2 ∣ X ⋂ Y ∣ 2|X\bigcap Y| 2∣XY ∣ X ∣ + ∣ Y ∣ |X|+ |Y| X+Y都是表示的两个的集合,此时Dice为1

也就是说,Dice的取值范围就是在0到1之间的。

对于分类问题,我们通常可以这样描述预测值:

  • TP:预测positive,预测对了

  • TN:预测negative,预测对了

  • FP:预测positive,预测错了

  • FN:预测negative,预测错了

根据模型预测的一些结果,我们对Dice作一些简单基础的化简:

D i c e = 2 T P 2 T P + F P + F N Dice=\frac{2TP}{2TP+FP+FN} Dice=2TP+FP+FN2TP

根据recall和precision的定义,可得:

r e c a l l = T P T P + F N p r e c i s i o n = T P T P + F P \begin{aligned}recall&=\frac{TP}{TP+FN}\\precision&=\frac{TP}{TP+FP}\end{aligned} recallprecision=TP+FNTP=TP+FPTP

因此,可以得到:

2 ⋅ p r e c i s i o n ⋅ r e c a l l p r e c i s i o n + r e c a l l = 2 T P 2 T P + F P + F N \frac{2\cdot precision\cdot recall}{precision + recall}=\frac{2TP}{2TP+FP+FN} precision+recall2precisionrecall=2TP+FP+FN2TP

而等式左边就是F1 score,等式右边就是Dice。此时,可以得出两者等价的结论。

由此可见,Dice coefficient是等同F1 score,直观上Dice coefficient是计算 X X X Y Y Y的相似性,本质上则同时隐含precision和recall两个指标


DiceLoss的基本概念

通过对Dice的认知,我们很容易就得到了DiceLoss的定义:

D i c e L o s s = 1 − D i c e = 1 − 2 ∣ X ⋂ Y ∣ ∣ X ∣ + ∣ Y ∣ DiceLoss=1-Dice=1-\frac{2|X\bigcap Y|}{|X| + |Y|} DiceLoss=1Dice=1X+Y2∣XY

其中, X X X表示模型预测结果, Y Y Y表示target标签结果。

但是,Dice里面的 ∣ . . . ∣ |...| ∣...∣被理解成元素个数,这导致它是离散的。因此,如果DiceLoss单纯的用 1 − D i c e 1-Dice 1Dice表达,且不对Dice的计算进行一定程度上的替换,是不能作为神经网络的优化目标(要能够被神经网络优化,必须得是连续的,可导的)

通用计算方式

我们可以将,Dice以模型输出的概率值进行替代计算,从而使得Dice和DiceLoss都获得连续性。具体为:

  • 将离散的交集 ∣ X ⋂ Y ∣ |X\bigcap Y| XY,替换为连续的点积

  • 将离散的集合大小 ∣ X ∣ |X| X ∣ Y ∣ |Y| Y,替换为连续的求和

而DiceLoss 的公式本质上是一个分数,分子和分母都可以通过简单的加法和乘法连续化。这种连续化是直接的,并且公式中的操作(加法、乘法、除法)都是可微的,因此可以直接用于梯度下降优化。

这里讨论一个通用的计算方式,并且保证连续性,定义:

I = ∣ X ⋂ Y ∣ = ∑ i = 1 N t i m i U = ∣ X ∣ + ∣ Y ∣ = ∑ i = 1 N t i + ∑ i = 1 N m i \begin{aligned}I&=|X\bigcap Y|=\sum_{i=1}^N t_im_i\\U&=|X|+|Y|=\sum_{i=1}^Nt_i+\sum_{i=1}^Nm_i\end{aligned} IU=XY=i=1Ntimi=X+Y=i=1Nti+i=1Nmi

其中, m i m_i mi是模型预测值,是经过sigmoid或者softmax之后的结果,取值在0到1之间; t i t_i ti是target标签,是经过one hot编码后的结果,取值非0即1

那么,DiceLoss可以表达为:

D i c e L o s s = 1 − 2 ⋅ I U DiceLoss=1-\frac{2\cdot I}{U} DiceLoss=1U2I

原论文将DiceLoss做了一点修改,表达为:

D i c e L o s s = 1 − I U − I DiceLoss=1-\frac{I}{U-I} DiceLoss=1UII

甚至,还会有论文里面会将 U U U的进行方式进行一些修改

U = ∑ i = 1 N t i 2 + ∑ i = 1 N m i 2 U=\sum_{i=1}^Nt_i^2+\sum_{i=1}^Nm_i^2 U=i=1Nti2+i=1Nmi2

当把 m i m_i mi限制到0到1之间, t i t_i ti限制到one hot编码形式。无论是上面的哪种计算形式:

  • 当模型预测结果和target标签结果越一致,Dice越接近于1,此时DiceLoss就越小

  • 当模型预测结果和target标签结果差别越大,Dice越接近0,此时DiceLoss就越大

可以看出,DiceLoss的范围也是0到1之间,并且是符合损失函数的预期的。

本文将会只使用第一种的计算形式。

一般情况下,对于DiceLoss的分数形式,我们常常更偏向于,在分子分母同时加上一个极小数 ε \varepsilon ε,一般称其为平滑系数,有两个作用:

  1. 防止分母为0。值得说明的是,一般分割网络输出经过sigmoid 或 softmax,是不存在输出为绝对0的情况。这里加平滑系数主要防止一些极端情况,输出位数太小而导致编译器丢失数位的情况

  2. 平滑系数,可以起到平滑loss和梯度的操作

二分类计算方式

假设, X X X表示模型预测为正样本的概率,一般为 s i g m o i d sigmoid sigmoid 激活函数的输出值; Y Y Y表示目标标签,正样本为1,负样本为0。

此时,对于分母, ∣ X ∣ |X| X表示所有样本的正样本概率和, ∣ Y ∣ |Y| Y表示所有的目标标签的和;对于分子, ∣ X ⋂ Y ∣ |X\bigcap Y| XY表示每个样本的正样本概率与其对应的目标标签的积,再对所有样本求和。

例如,此时的 X = [ 0.2 0.1 0.9 0.8 0.9 ] X=\begin{bmatrix}0.2&0.1&0.9&0.8&0.9\end{bmatrix} X=[0.20.10.90.80.9] Y = [ 0 0 1 1 1 ] Y=\begin{bmatrix}0&0&1&1&1\end{bmatrix} Y=[00111]。那么, ∣ X ⋂ Y ∣ = 0.2 ⋅ 0 + 0.1 ⋅ 0 + 0.9 ⋅ 1 + 0.8 ⋅ 1 + 0.9 ⋅ 1 = 2.6 |X\bigcap Y|=0.2\cdot 0+0.1\cdot 0+0.9\cdot 1 +0.8\cdot 1+0.9\cdot 1=2.6 XY=0.20+0.10+0.91+0.81+0.91=2.6 ∣ X ∣ = 0.2 + 0.1 + 0.9 + 0.8 + 0.9 = 2.9 |X|=0.2+0.1+0.9+0.8+0.9=2.9 X=0.2+0.1+0.9+0.8+0.9=2.9 ∣ Y ∣ = 0 + 0 + 1 + 1 + 1 = 3 |Y|=0+0+1+1+1=3 Y=0+0+1+1+1=3,此时 D i c e l o s s = 1 − 2 ⋅ 2.6 2.9 + 3 = 0.1186 Diceloss=1-\frac{2\cdot 2.6}{2.9+3}=0.1186 Diceloss=12.9+322.6=0.1186

如果调整模型输出 X = [ 0.2 0.1 0.9 0.8 0.1 ] X=\begin{bmatrix}0.2&0.1&0.9&0.8&0.1\end{bmatrix} X=[0.20.10.90.80.1]。那么, ∣ X ⋂ Y ∣ = 0.2 ⋅ 0 + 0.1 ⋅ 0 + 0.9 ⋅ 1 + 0.8 ⋅ 1 + 0.1 ⋅ 1 = 1.8 |X\bigcap Y|=0.2\cdot 0+0.1\cdot 0+0.9\cdot 1 +0.8\cdot 1+0.1\cdot 1=1.8 XY=0.20+0.10+0.91+0.81+0.11=1.8 ∣ X ∣ = 0.2 + 0.1 + 0.9 + 0.8 + 0.1 = 2.1 |X|=0.2+0.1+0.9+0.8+0.1=2.1 X=0.2+0.1+0.9+0.8+0.1=2.1 ∣ Y ∣ = 0 + 0 + 1 + 1 + 1 = 3 |Y|=0+0+1+1+1=3 Y=0+0+1+1+1=3,此时 D i c e l o s s = 1 − 2 ⋅ 1.8 2.1 + 3 = 0.2941 Diceloss=1-\frac{2\cdot 1.8}{2.1+3}=0.2941 Diceloss=12.1+321.8=0.2941

可以看出,此时将 X X X的最后一个输出概率从0.9降低到0.1,但是该样本的target标签是1,此时Loss应该会提高一些。从结果上看,也是符合这个预期的。

那么,再分析下数值变化:

  • 增加正样本的概率:分子 ∣ X ⋂ Y ∣ |X\bigcap Y| XY变大,分母 ∣ X ∣ |X| X变大,分母 ∣ Y ∣ |Y| Y不变,根据糖水不等式,此时比值变大,Loss变小

  • 增加负样本的概率:分子 ∣ X ⋂ Y ∣ |X\bigcap Y| XY不变,分母 ∣ X ∣ |X| X变大,分母 ∣ Y ∣ |Y| Y不变,此时比值变小,Loss变大

  • 减小正样本的概率:分子 ∣ X ⋂ Y ∣ |X\bigcap Y| XY变小,分母 ∣ X ∣ |X| X变小,分母 ∣ Y ∣ |Y| Y不变,根据糖水不等式,此时比值变小,Loss变大

  • 减小负样本的概率:分子 ∣ X ⋂ Y ∣ |X\bigcap Y| XY不变,分母 ∣ X ∣ |X| X变小,分母 ∣ Y ∣ |Y| Y不变,此时比值变大,Loss变小

这么看来,DiceLoss按照这种计算方式确实是符合预期的。

多分类计算方式

假设, X X X表示模型预测为target标签的概率,一般为经过 s o f t m a x softmax softmax 之后的输出值; Y Y Y表示目标标签,采用 one-hot 编码表示,即target标签对应的位置为 1,其他位置为 0。

此时,对于分母, ∣ X ∣ |X| X表示所有样本的预测为target标签的概率和, ∣ Y ∣ |Y| Y表示所有的目标标签的和;对于分子, ∣ X ⋂ Y ∣ |X\bigcap Y| XY表示每个样本的target标签概率与其对应的目标标签的积,再对所有样本求和。

可以看出,此时 Y Y Y的每一位都是1,假设一共有 N N N个样本,那么 ∣ Y ∣ = N |Y|=N Y=N。同时 ∣ X ⋂ Y ∣ |X\bigcap Y| XY的计算值和 ∣ X ∣ |X| X的计算值是一样的。

那么,再分析下数值变化:

  • 增加模型预测为target标签的概率:分子 ∣ X ⋂ Y ∣ |X\bigcap Y| XY变大,分母 ∣ X ∣ |X| X变大,分母 ∣ Y ∣ |Y| Y不变,根据糖水不等式,此时比值变大,Loss变小

  • 减小模型预测为target标签的概率:分子 ∣ X ⋂ Y ∣ |X\bigcap Y| XY变小,分母 ∣ X ∣ |X| X变小,分母 ∣ Y ∣ |Y| Y不变,根据糖水不等式,此时比值变小,Loss变大

这么看来,DiceLoss按照这种计算方式确实也还是符合预期的。


DiceLoss的深度剖析

DiceLoss梯度分析

从DiceLoss的定义可以看出,DiceLoss是一种区域相关的loss。意味着某像素点的loss以及梯度值不仅和该点的target以及预测值相关,和其他点的target以及预测值也相关,这点和CE(交叉熵CrossEntropy)不同

因此,对于梯度的分析起来比较麻烦。

这里我们简化一下,只分析一下单点输出二分类的情形,即只有一个样本,并且是二分类模型。对于多点输出多分类的情形,是基于这种简化模型的推广,本质是一样的,本文就不继续分析了。

假设, m m m为模型预测正样本的概率值, t t t为target的标签值(正样本为1、负样本为0),则DiceLoss可以近似表达为:

D i c e L o s s = 1 − 2 ⋅ m ⋅ t + ε m + t + ε DiceLoss=1-\frac{2\cdot m\cdot t+\varepsilon}{m+ t+\varepsilon} DiceLoss=1m+t+ε2mt+ε

同时, m m m又是经过sigmoid之后的结果:

m = s i g m o i d ( x ) = 1 1 + e − x = e x 1 + e x m=sigmoid(x)=\frac{1}{1+e^{-x}}=\frac{e^x}{1+e^x} m=sigmoid(x)=1+ex1=1+exex

那么,DiceLoss的梯度为:

∂ ( D i c e L o s s ) ∂ m = − 2 ⋅ t 2 + 2 ⋅ t ε − ε ( m + t + ε ) 2 ∂ m ∂ x = e − x ( 1 + e − x ) 2 \begin{aligned}\frac{\partial(DiceLoss)}{\partial m}&=-\frac{2\cdot t^2+2\cdot t\varepsilon-\varepsilon}{(m+t+\varepsilon)^2}\\ \frac{\partial m}{\partial x}&= \frac{e^{-x}}{(1+e^{-x})^2}\end{aligned} m(DiceLoss)xm=(m+t+ε)22t2+2ε=(1+ex)2ex

t = 0 t=0 t=0时,

∂ ( D i c e L o s s ) ∂ m = ε ( m + ε ) 2 \frac{\partial(DiceLoss)}{\partial m}=\frac{\varepsilon}{(m+\varepsilon)^2} m(DiceLoss)=(m+ε)2ε

t = 1 t=1 t=1时,

∂ ( D i c e L o s s ) ∂ m = − 2 + ε ( 1 + m + ε ) 2 \frac{\partial(DiceLoss)}{\partial m}=-\frac{2+\varepsilon}{(1+m+\varepsilon)^2} m(DiceLoss)=(1+m+ε)22+ε

此时,根据链式求导法则,求得 ∂ ( D i c e L o s s ) ∂ x = ∂ ( D i c e L o s s ) ∂ m ⋅ ∂ m ∂ x \frac{\partial(DiceLoss)}{\partial x}=\frac{\partial(DiceLoss)}{\partial m}\cdot \frac{\partial m}{\partial x} x(DiceLoss)=m(DiceLoss)xm

如果绘制上面的 x x x的梯度的图形,最终可以得出结论:

  • t = 0 t=0 t=0 时,可以知道, x x x的梯度值接近0 。实际上,由于平滑系数的存在,该梯度不为0,而是一个非常小的值 。该值过于小,对网络的贡献也非常有限

  • t = 1 t=1 t=1 时, x x x 的梯度在0点附近存在一个峰值,此时 m m m 接近0.5,梯度接近0.33。随着预测值 m m m 越接近1或0,梯度越小,出现梯度饱和的现象

一般神经网络训练之前都会采取权重初始化,不管是Xavier初始化还是Kaiming初始化(或者其他初始化的方法),输出 x x x是接近于0的。

再回到上面的结论,可见此时正样本( t = 1 t=1 t=1)的监督是远远大于负样本( t = 0 t=0 t=0)的监督,可以认为网络前期会重点挖掘正样本。而交叉熵(CE)是平等对待两种样本的

DiceLoss的作用

DiceLoss为何能够解决正负样本不平衡问题?

因为DiceLoss是一个区域相关的loss。区域相关的意思就是,当前像素的loss不光和当前像素的预测值相关,和其他点的值也相关

DiceLoss的求交的形式可以理解为mask掩码操作,因此不管图片有多大,固定大小的正样本的区域(前景)计算的loss是一样的,对网络起到的监督贡献不会随着图片的大小而变化。因此,训练更倾向于挖掘前景区域,正负样本不平衡的情况就是前景占比较小。而交叉熵CE则会公平处理正负样本,当出现正样本占比较小时,就会被更多的负样本淹没。

DiceLoss背景区域能否起到监督作用?

可以的,但是会小于前景区域。说到底,是因为DiceLoss的分子依赖于前景区域,分母依赖于前景区域和背景区域。

DiceLoss为何训练会很不稳定?

在使用DiceLoss时,一般正样本为小目标时会产生严重的震荡,极端情况甚至会导致梯度饱和现象。因为,在只有前景和背景的情况下,小目标(前景)一旦有部分像素预测错误,那么就会导致loss值大幅度的变动,从而导致梯度变化剧烈。可以假设极端情况,只有一个像素为正样本(前景),如果该像素预测正确了,不管其他像素预测如何,DiceLoss就接近0,预测错误了,DiceLoss接近1。而对于交叉熵CE,loss的值是总体求平均的,更多会依赖负样本的地方。

因此,在实际使用的时候,损失函数并不会单纯使用Dice Loss,通常都会和其他Loss结合起来用,会给其他Loss和Dice Loss分别上不同的权重作为损失函数


代码实战

二分类问题

class BinaryDiceLoss(nn.Module):
  """
  二分类DiceLoss
  """

  def __init__(self):
      super(BinaryDiceLoss, self).__init__()

  def forward(self, pred, target):
      """
      pred: sigmoid的输出结果
      target: 标签, 1为正样本, 0为负样本
      """
      # 计算交集
      inter = 2 * torch.sum(pred * target) + ep
      # 计算并集
      denominator = torch.sum(pred) + torch.sum(target) + ep
      # 计算DiceLoss
      dice_loss = 1 - inter / denominator
      return dice_loss

多分类问题:采用one-hot编码

class DiceLoss(nn.Module):
  """
  多分类DiceLoss
  """

  def __init__(self):
      super(DiceLoss, self).__init__()

  def forward(self, pred, target):
      """
      pred: 模型的输出, 未经过 Softmax, 形状为 [B, C, H, W] (批次大小、类别数、图像高度、图像宽度)
      target: 标签, 形状为 [B, H, W], 取值范围为0到C-1
      """
      # 将目标标签转换为 one-hot 编码
      # F.one_hot将目标标签 target (B, H, W)转换为 one-hot 编码,形状为 (B, H, W, C)
      # torch.permute调整维度顺序,将 one-hot 编码的形状从 (B, H, W, C) 转换为 (B, C, H, W),与 pred 的形状一致
      target_one_hot = torch.permute(
          F.one_hot(target, num_classes=pred.shape[1]),
          (0, 3, 1, 2),
      ).float()
      # 对 pred 的类别维度(dim=1)应用 Softmax 激活
      pred_prob = F.softmax(pred, dim=1)
      # 计算交集
      inter = (pred_prob * target_one_hot).sum()
      # 计算并集
      denominator = pred_prob.sum() + target_one_hot.sum() + self.eps
      # 计算DiceLoss
      dice_loss = 1 - 2 * inter / denominator
      return dice_loss

这边对代码torch.permute(F.one_hot(target, num_classes=pred.shape[1]),(0, 3, 1, 2),).float()分析:

import torch
import torch.nn.functional as F

# 假设 target 是目标标签,形状为 [B, H, W]
target = torch.tensor([
    [0, 1, 2],  # 图像第1行标签
    [1, 0, 2]   # 图像第2行标签
]).unsqueeze(0)  # 添加批次维度,形状变为 [1, H, W]

# 将 target 转换为 one-hot 编码
target_one_hot = torch.permute(
    F.one_hot(target, num_classes=3),  # 形状 [B, H, W, C]
    (0, 3, 1, 2)  # 调整维度顺序为 [B, C, H, W]
).float()

print("目标标签 (target):")
print(target)
print("\nOne-hot 编码 (target_one_hot):")
print(target_one_hot)
print("\nOne-hot 编码的形状:")
print(target_one_hot.shape)

输出结果:

目标标签 (target):
tensor([[[0, 1, 2],
         [1, 0, 2]]])

One-hot 编码 (target_one_hot):
tensor([[[[1., 0., 0.],     # 类别 0 的 one-hot 编码
          [0., 1., 0.]],    # 类别 1 的 one-hot 编码

         [[0., 1., 0.],     # 类别 1 的 one-hot 编码
          [1., 0., 0.]],    # 类别 0 的 one-hot 编码

         [[0., 0., 1.],     # 类别 2 的 one-hot 编码
          [0., 0., 1.]]]])  # 类别 2 的 one-hot 编码

One-hot 编码的形状:
torch.Size([1, 3, 2, 3])

需要注意的是,这边的代码写法应该与输入的shape、参数的shape有关系的,需要针对于具体的情形进行适配和修改。


Dice和Iou

根据Dice的定义:

D i c e = 2 T P 2 T P + F P + F N Dice=\frac{2TP}{2TP+FP+FN} Dice=2TP+FP+FN2TP

莫名得会和另一个进行比较,就是Iou。那么看看Iou得定义:

I o u = T P T P + F P + F N Iou=\frac{TP}{TP+FP+FN} Iou=TP+FP+FNTP

那么可以确定两者之间的关系了:

I o u = D i c e 2 − D i c e Iou=\frac{Dice}{2-Dice} Iou=2DiceDice

可以看出,Dice和Iou的区间都是在0到1之间,此时Dice的值会比Iou略高一些


相关阅读

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐