【深度学习】语义分割损失函数之Dice Loss
DiceLoss,由2016年的论文《》中首次被提出。它是旨在应对语义分割中正负样本强烈不平衡的场景。本文将详细介绍DiceLoss的基本概念、思想原理,并提供PyTorch的实现代码,帮助大家去更好的理解和使用。
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∣+∣Y∣2∣X⋂Y∣
其中, ∣ X ⋂ Y ∣ |X\bigcap Y| ∣X⋂Y∣表示交集的元素个数, ∣ X ∣ |X| ∣X∣、 ∣ Y ∣ |Y| ∣Y∣表示其各自的元素个数。从集合的角度上考虑一下:
-
当 X X X和 Y Y Y集合是完全没有交集的情况下,此时 ∣ X ⋂ Y ∣ = 0 |X\bigcap Y|=0 ∣X⋂Y∣=0,此时Dice为0
-
当 X X X和 Y Y Y集合完全一致的情况下,此时 2 ∣ X ⋂ Y ∣ 2|X\bigcap Y| 2∣X⋂Y∣和 ∣ 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+recall2⋅precision⋅recall=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=1−Dice=1−∣X∣+∣Y∣2∣X⋂Y∣
其中, X X X表示模型预测结果, Y Y Y表示target标签结果。
但是,Dice里面的 ∣ . . . ∣ |...| ∣...∣被理解成元素个数,这导致它是离散的。因此,如果DiceLoss单纯的用 1 − D i c e 1-Dice 1−Dice表达,且不对Dice的计算进行一定程度上的替换,是不能作为神经网络的优化目标(要能够被神经网络优化,必须得是连续的,可导的)。
通用计算方式
我们可以将,Dice以模型输出的概率值进行替代计算,从而使得Dice和DiceLoss都获得连续性。具体为:
-
将离散的交集 ∣ X ⋂ Y ∣ |X\bigcap Y| ∣X⋂Y∣,替换为连续的点积
-
将离散的集合大小 ∣ 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=∣X⋂Y∣=i=1∑Ntimi=∣X∣+∣Y∣=i=1∑Nti+i=1∑Nmi
其中, 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=1−U2⋅I
原论文将DiceLoss做了一点修改,表达为:
D i c e L o s s = 1 − I U − I DiceLoss=1-\frac{I}{U-I} DiceLoss=1−U−II
甚至,还会有论文里面会将 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=1∑Nti2+i=1∑Nmi2
当把 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 ε,一般称其为平滑系数,有两个作用:
-
防止分母为0。值得说明的是,一般分割网络输出经过sigmoid 或 softmax,是不存在输出为绝对0的情况。这里加平滑系数主要防止一些极端情况,输出位数太小而导致编译器丢失数位的情况
-
平滑系数,可以起到平滑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| ∣X⋂Y∣表示每个样本的正样本概率与其对应的目标标签的积,再对所有样本求和。
例如,此时的 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 ∣X⋂Y∣=0.2⋅0+0.1⋅0+0.9⋅1+0.8⋅1+0.9⋅1=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=1−2.9+32⋅2.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 ∣X⋂Y∣=0.2⋅0+0.1⋅0+0.9⋅1+0.8⋅1+0.1⋅1=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=1−2.1+32⋅1.8=0.2941。
可以看出,此时将 X X X的最后一个输出概率从0.9降低到0.1,但是该样本的target标签是1,此时Loss应该会提高一些。从结果上看,也是符合这个预期的。
那么,再分析下数值变化:
-
增加正样本的概率:分子 ∣ X ⋂ Y ∣ |X\bigcap Y| ∣X⋂Y∣变大,分母 ∣ X ∣ |X| ∣X∣变大,分母 ∣ Y ∣ |Y| ∣Y∣不变,根据糖水不等式,此时比值变大,Loss变小
-
增加负样本的概率:分子 ∣ X ⋂ Y ∣ |X\bigcap Y| ∣X⋂Y∣不变,分母 ∣ X ∣ |X| ∣X∣变大,分母 ∣ Y ∣ |Y| ∣Y∣不变,此时比值变小,Loss变大
-
减小正样本的概率:分子 ∣ X ⋂ Y ∣ |X\bigcap Y| ∣X⋂Y∣变小,分母 ∣ X ∣ |X| ∣X∣变小,分母 ∣ Y ∣ |Y| ∣Y∣不变,根据糖水不等式,此时比值变小,Loss变大
-
减小负样本的概率:分子 ∣ X ⋂ Y ∣ |X\bigcap Y| ∣X⋂Y∣不变,分母 ∣ 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| ∣X⋂Y∣表示每个样本的target标签概率与其对应的目标标签的积,再对所有样本求和。
可以看出,此时 Y Y Y的每一位都是1,假设一共有 N N N个样本,那么 ∣ Y ∣ = N |Y|=N ∣Y∣=N。同时 ∣ X ⋂ Y ∣ |X\bigcap Y| ∣X⋂Y∣的计算值和 ∣ X ∣ |X| ∣X∣的计算值是一样的。
那么,再分析下数值变化:
-
增加模型预测为target标签的概率:分子 ∣ X ⋂ Y ∣ |X\bigcap Y| ∣X⋂Y∣变大,分母 ∣ X ∣ |X| ∣X∣变大,分母 ∣ Y ∣ |Y| ∣Y∣不变,根据糖水不等式,此时比值变大,Loss变小
-
减小模型预测为target标签的概率:分子 ∣ X ⋂ Y ∣ |X\bigcap Y| ∣X⋂Y∣变小,分母 ∣ 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=1−m+t+ε2⋅m⋅t+ε
同时, 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+e−x1=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)∂x∂m=−(m+t+ε)22⋅t2+2⋅tε−ε=(1+e−x)2e−x
当 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)⋅∂x∂m。
如果绘制上面的 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=2−DiceDice
可以看出,Dice和Iou的区间都是在0到1之间,此时Dice的值会比Iou略高一些。
相关阅读
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐

所有评论(0)