文章目录
- 1. 常见语义分割损失
- 1.1 Cross Entropy
- 1.2 dice Loss
- 1.2.1 为什么使用Dice loss
- 1.2.2 公式
- 1.2.3 Dice loss 和 F1-score代码
- 1.3 focal loss
- 1.3.1 公式:
- 1.3.2 代码
- 2. 语义分割损失应用
- 参考
语义分割任务实际上是一种
像素层面上的分类
,需要识别图像中存在的内容和位置,同样也存在与分类类似问题-样本类别不平衡
,对于语义分割更多的是前景区域的样本远小于背景区域。针对类别不平衡问题,在loss层面
上有不同的选择。
1. 常见语义分割损失
1.1 Cross Entropy
用于图像语义分割任务的最常用
损失函数是像素级别的交叉熵损失
,这种损失会逐个检查每个像素,将对每个像素类别的预测结果(概率分布向量)与我们的one-ho
t标签向量进行比较。
p i x e l − l o s s = − ∑ c l a s s e s y t r u e l o g ( y p r e d ) pixel-loss=- \sum_{classes}y_{true}log(y_{pred}) pixel−loss=−classes∑ytruelog(ypred)
整个图像的损失就是对每个像素的损失求平均值
Pytorch
中CrossEntropyLoss()
函数的主要是将log_softmax
和NLLLoss
(最小化负对数似然函数
)合并到一块得到的结果
CrossEntropyLoss()=log_softmax() + NLLLoss()
(1)
首先对预测值pred进行softmax计算:其中softmax
函数又称为归一化指数函数,它可以把一个多维向量压缩在(0,1)
之间,并且它们的和为1
(2)
然后对softmax计算的结果,再取log对数。(3)
最后再利用NLLLoss() 计算CrossEntropyLoss
, 其中NLLLoss() 的计算过程为:将经过log_softmax
计算的结果与target
相乘并求和,然后取反。
其中(1),(2)实现的是log_softmax计算,(3)实现的是NLLLoss()
, 经过以上3步计算,得到最终的交叉熵损失的计算结果。
详见: 深度学习loss总结:nn.CrossEntropyLoss,nn.MSELoss,Focal_Loss,nn.KLDivLoss等
1.2 dice Loss
- Dice Loss 最先是在
VNet
这篇文章中被提出,后来被广泛的应用在了医学影像分割
之中。 - Dice Loss,也叫
Soft Dice Coefficient
,是一种广泛用于图像分割任务的损失函数。它基于目标分割图像
与模型输出结果
之间的重叠区域
的比例计算出分数。与交叉熵损失函数相比,它更适合于处理难分割的目标
。 - Dice Loss是由
Dice系数
而得名的,Dice系数是一种用于评估两个样本集合相似性的度量函数,其值越大意味着这两个样本集越相似。
1.2.1 为什么使用Dice loss
Dice Loss在处理类别不平衡
和目标小但多
的图像分割任务时有着很好的性能
。交叉熵损失函数忽略了预测值和目标值之间的相似性,并且对于极端的像素值不够敏感。而Dice Loss是基于相似性的评价指标,它看重相同的像素值,可以很好地处理像素值不平衡的情况。
1.2.2 公式
D i c e = 2 ∣ X ∩ Y ∣ ∣ X ∣ + ∣ Y ∣ Dice =\frac{2|X\cap{Y}|}{|X|+|Y|} Dice=∣X∣+∣Y∣2∣X∩Y∣
其中 ∣ X ∩ Y ∣ |X\cap{Y}| ∣X∩Y∣表示X和Y之间交集元素的个数, ∣ X ∣ |X| ∣X∣ 和 ∣ Y ∣ |Y| ∣Y∣分别表示X、Y中元素的个数。分子乘2为了保证分母重复计算后取值范围在$[0,1]之间, D i c e L o s s Dice Loss DiceLoss表达式如下:
D i c e l o s s = 1 − D i c e = 1 − 2 ∣ X ∩ Y ∣ ∣ X ∣ + ∣ Y ∣ Dice loss= 1-Dice =1-\frac{2|X\cap{Y}|}{|X|+|Y|} Diceloss=1−Dice=1−∣X∣+∣Y∣2∣X∩Y∣
- Dice Loss常用于语义分割问题中,X表示真实分割图像的像素标签,Y表示模型预测分割图像的像素类别, ∣ X ∩ Y ∣ |X\cap Y| ∣X∩Y∣ 近试为预测图像的像素与真实标签图像的像素之间的点乘,并将点乘结果相加, ∣ X ∣ |X| ∣X∣ 和 ∣ Y ∣ |Y| ∣Y∣分别近似为他们各自对应图像像素相加。
D i c e L o s s = 1 − 2 ∑ i = 1 N y i y 1 ^ ∑ i = 1 N y i + ∑ i = 1 N y i ^ DiceLoss = 1-\frac{2\sum_{i=1}^{N}y_{i}\hat{y_1}}{\sum_{i=1}^N y_i +\sum_{i=1}^N \hat{y_i}} DiceLoss=1−∑i=1Nyi+∑i=1Nyi^2∑i=1Nyiy1^
可以说Dice Loss
是直接优化F1 score
而来的,是对F1 score
的高度抽象,可用于多分类分割
问题上。F1 score
就被提出,其公式如下:
F 1 s c o r e = 2 P R P + R = 2 T P 2 R P + F P + F N F1 score = \frac{2PR}{P+R} = \frac {2TP}{2RP+FP+FN} F1score=P+R2PR=2RP+FP+FN2TP
在二分类问题中,Dice系数也可以写成 D i c e = 2 T P 2 T P + F P + F N = F 1 s c o r e Dice = \frac {2TP}{2TP+FP+FN}=F1score Dice=2TP+FP+FN2TP=F1score
D i c e L o s s = 1 − D i c e = 1 − F 1 s c o r e Dice_{Loss} =1- Dice= 1 - F1_{score} DiceLoss=1−Dice=1−F1score
1.2.3 Dice loss 和 F1-score代码
(1) F1-score
def f_score(inputs, target, beta=1, smooth = 1e-5, threhold = 0.5):n, c, h, w = inputs.size()nt, ht, wt, ct = target.size()if h != ht and w != wt:inputs = F.interpolate(inputs, size=(ht, wt), mode="bilinear", align_corners=True)temp_inputs = torch.softmax(inputs.transpose(1, 2).transpose(2, 3).contiguous().view(n, -1, c),-1)temp_target = target.view(n, -1, ct)#--------------------------------------------## 计算dice系数#--------------------------------------------#temp_inputs = torch.gt(temp_inputs, threhold).float()tp = torch.sum(temp_target[...,:-1] * temp_inputs, axis=[0,1])fp = torch.sum(temp_inputs , axis=[0,1]) - tpfn = torch.sum(temp_target[...,:-1] , axis=[0,1]) - tpscore = ((1 + beta ** 2) * tp + smooth) / ((1 + beta ** 2) * tp + beta ** 2 * fn + fp + smooth)score = torch.mean(score)return score
- 上述是
F-score
的代码,F1-Score
是F-score
的一种特殊形式,即当beta=1
, F-score等价于F1-Score
inputs
为分割模型的推理预测输出shape为(n,h,w,c)
, 其中c为cls_nums
,未经过softmax处理, 因此在计算F1-score时,需要进行softmax处理。target
为 为真实的分割标签one-hot
编码格式,shape为(n,h,w,c)
- 可以利用如下代码,将标签
mask
图片png
转为one-hot编码seg_labels
#-------------------------------------------------------#
# 转化成one_hot的形式
# 在这里需要+1是因为voc数据集有些标签具有白边部分
# 我们需要将白边部分进行忽略,+1的目的是方便忽略。
#-------------------------------------------------------#
seg_labels = np.eye(self.num_classes + 1)[png.reshape([-1])]
seg_labels = seg_labels.reshape((int(self.input_shape[0]), int(self.input_shape[1]), self.num_classes + 1))
png.reshape([-1])
得到shape大小为(h*w,1)
, 其中每个元素值为类别的索引, 然后根据类别索引从np.eye(self.num_classes + 1)
取对应的行,这样就将mask转化为one-hot
形式的seg_labels- 然后将
seg_labels
reshape为(h,w,self.num_classes + 1)
- 在这里需要+1是因为voc数据集有些标签具有白边部分, +1的目的是方便忽略
(2) Dice loss
def Dice_loss(inputs, target, beta=1, smooth = 1e-5):n, c, h, w = inputs.size()nt, ht, wt, ct = target.size()if h != ht and w != wt:inputs = F.interpolate(inputs, size=(ht, wt), mode="bilinear", align_corners=True)temp_inputs = torch.softmax(inputs.transpose(1, 2).transpose(2, 3).contiguous().view(n, -1, c),-1)temp_target = target.view(n, -1, ct)#--------------------------------------------## 计算dice loss#--------------------------------------------#tp = torch.sum(temp_target[...,:-1] * temp_inputs, axis=[0,1])fp = torch.sum(temp_inputs , axis=[0,1]) - tpfn = torch.sum(temp_target[...,:-1] , axis=[0,1]) - tpscore = ((1 + beta ** 2) * tp + smooth) / ((1 + beta ** 2) * tp + beta ** 2 * fn + fp + smooth)dice_loss = 1 - torch.mean(score)return dice_loss
-
inputs
为分割模型
的推理预测输出shape为(n,h,w,c)
,target
为 为真实的分割标签one-hot
编码格式shape为(n,h,w,c)
-
可以看到
dice_loss
的实现,跟F1-score基本上是一模一样的, 将torch.mean(score)
求得的F1-soce
, 然后通过dice_loss = 1- F1-score
来实现。
1.3 focal loss
上面针对不同类别的像素数量不均衡提出了改进方法,但有时还需要将像素分为难学习
和容易学习
这两种样本。
容易学习的样本模型可以很轻松地将其预测正确,模型只要将大量容易学习的样本分类正确,loss就可以减小很多,从而导致模型不怎么顾及难学习的样本,所以我们要想办法让模型更加关注难学习的样本
。
1.3.1 公式:
(1) 交叉熵损失:
L c e − ∑ q t l o g 2 ( p t ) = − l o g ( p t ) L_{ce} - \sum q_t log_2(p_t)= -log(p_t) Lce−∑qtlog2(pt)=−log(pt)
- 其中:log一般以
e
或者2
为底都是可以的。参考:KL散度、CrossEntropy详解, - 因为对于
分类或者分割
任务, q t q_t qt为one-hot
编码,只在真实类别处为1
,其他都是0
, 所以交叉熵等效为 − l o g ( p t ) -log(p_t) −log(pt)
(2) Focal Loss
F L ( p t ) = − a t ( 1 − p t ) r l o g ( p t ) FL(p_t)=-a_t(1-p_t)^rlog(p_t) FL(pt)=−at(1−pt)rlog(pt)
- a t a_t at 是用来
平衡正负样本数量
的:基于样本非平衡造成的损失函数倾斜,一个直观的做法就是在损失函数中添加权重因子,提高少数类别在损失函数中的权重,平衡损失函数的分布。 p t p_t pt 表示预测的概率值或者置信度
, p t p_t pt越大说明预测越接近
。focal loss
设置了一个modulating factor
: ( 1 − p t ) r (1-p_t)^r (1−pt)r用来区分样本预测的难易程度
, 当预测的概率 p t p_t pt越接近于1,说明样本容易预测,此时 ( 1 − p t ) r (1-p_t)^r (1−pt)r趋近于0;当预测概率 p t p_t pt越接近于0时,说明样本比较难预测,此时 ( 1 − p t ) r (1-p_t)^r (1−pt)r趋近于1。 整体而言,通过modulating factor
因子的调节,相当于增加了难分样本在损失函数中的权重
。
可以看出Focal Loss
是在交叉熵Cross Entropy
基础上演进而来的,相比于交叉熵损失,多了一个类别平衡系数
a t a_t at,以及区分样本难分程度的因子
modulating factor: ( 1 − p t ) r (1-p_t)^r (1−pt)r
1.3.2 代码
def Focal_Loss(inputs, target, cls_weights, num_classes=21, alpha=0.5, gamma=2):n, c, h, w = inputs.size()nt, ht, wt = target.size()if h != ht and w != wt:inputs = F.interpolate(inputs, size=(ht, wt), mode="bilinear", align_corners=True)temp_inputs = inputs.transpose(1, 2).transpose(2, 3).contiguous().view(-1, c)temp_target = target.view(-1)logpt = -nn.CrossEntropyLoss(weight=cls_weights, ignore_index=num_classes, reduction='none')(temp_inputs, temp_target)pt = torch.exp(logpt)if alpha is not None:logpt *= alphaloss = -((1 - pt) ** gamma) * logptloss = loss.mean()return loss
inputs
为分割模型的推理预测输出shape为(n,h,w,c)
- 由于
nn.CrossEntropyLoss
内部已经包含了Softmax
处理,因此不需要对模型的预测输出inputs
进行Softmax
计算。 - 对于分类、分割任务而言,
focal loss
相比于交叉熵损失CE
: − l o g ( p t ) -log(p_t) −log(pt),多了一个类别平衡系数
a t a_t at,以及区分样本难分程度的因子
modulating factor: ( 1 − p t ) r (1-p_t)^r (1−pt)r,因此可以先求CrossEntropyLoss
,然后乘以类别平衡系数
以及区分样本难分程度的因子
modulating factor: ( 1 − p t ) r (1-p_t)^r (1−pt)r, 就可以求出focal loss
了
logpt = -nn.CrossEntropyLoss(weight=cls_weights, ignore_index=num_classes, reduction='none')(temp_inputs, temp_target)pt = torch.exp(logpt)if alpha is not None:logpt *= alphaloss = -((1 - pt) ** gamma) * logptloss = loss.mean()
cls_weights
: 衡量每个类别的重要程度,cls_weights
数组元素个数与类别数一样。代码中默认设置各个类别重要度一样:
cls_weights = np.ones([num_classes], np.float32)
- 在计算
CrossEntropyLoss
, target可以是one-hot
格式,也可以直接输出类别
,不需要进行one-hot处理
(此时pytorch 内部会自动帮忙进行one-hot编码)
temp_inputs = inputs.transpose(1, 2).transpose(2, 3).contiguous().view(-1, c)
temp_target = target.view(-1)logpt = -nn.CrossEntropyLoss(weight=cls_weights, ignore_index=num_classes, reduction='none')(temp_inputs, temp_target)
CrossEntropyLoss
根据预测输出
和真实的target
,就可以计算出交叉熵损失
, 注意预测的输出需要reshape到(-1,c)
, target需要reshape
到[-1]
(一维),每个元素为像素的类别索引
2. 语义分割损失应用
- 在语义分割中,
focal_loss
和Cross entropy loss
以及dice_loss
,通常需要结合一起使用。代码参考:https://github.com/bubbliiiing/deeplabv3-plus-pytorch/blob/main/train.py - 可以看到作者定义语义分割的损失中,结合了这三类损失,默认只使用的是
Cross entropy loss
, 如果存在素类别不平衡,以及难分
的像素类别,可以将focal_loss
和dice_loss
两个损失一起叠加使用。
dice_loss = False
focal_loss = Fals
cls_weights = np.ones([num_classes], np.float32)for iteration, batch in enumerate(train_dataloader):imgs, pngs, labels = batchwith torch.no_grad():weights = torch.from_numpy(cls_weights)if cuda:imgs = imgs.cuda(local_rank)pngs = pngs.cuda(local_rank)labels = labels.cuda(local_rank)weights = weights.cuda(local_rank)#----------------------## 清零梯度#----------------------#optimizer.zero_grad()outputs = model_train(imgs)#----------------------## 计算损失#----------------------#if focal_loss:loss = Focal_Loss(outputs, pngs, weights, num_classes = num_classes)else:loss = CE_Loss(outputs, pngs, weights, num_classes = num_classes)if dice_loss:main_dice = Dice_loss(outputs, labels)loss = loss + main_dice
- 其中
imgs
为图片数据,shape大小为(n,c,h,w)
, pngs
为mask标签图片,shape大小为(n,h,w)
, 为8位单通道图像,每个像素值对应类别信息
利用imgs
和pngs
可以计算focal_loss 和CE_Loss
labels
为标签pngs
的one-hot形式,shape大小为(n,h,w,c+1)
, 在计算Dice_loss时,需要将标签转换为one_hot
格式。weights
是用来,给各个类别附加权重的,weight数组元素需要和类别数一样
参考
https://github.dev/bubbliiiing/deeplabv3-plus-pytorch
https://zhuanlan.zhihu.com/p/101773544?utm_id=0