本文主要总结最近的调研调试结果,介绍通过改进损失来提升语义分割的分割效果;当然还有其他途径,比如蒸馏(提升分割效果)、剪枝(提升fps),之前博客有总结,此处不做介绍。
一、Dice损失
语义分割一般绕不开Dice损失,公式如下:
DiceLoss=1- 2X∩Y+SmoothX+Y+Smooth
集合相似度度量函数,
通常用于计算两个样本的相似度,属于metric learning。X为真实目标mask,Y为预测目标mask,我们总是希望X和Y交集尽可能大,占比尽可能大,但是loss需要逐渐变小,所以在比值前面添加负号。
另外,该损失可以缓解样本中前景背景(面积)不平衡带来的消极影响,前景背景不平衡也就是说图像中大部分区域是不包含目标的,只有一小部分区域包含目标。Dice Loss训练更关注对前景区域的挖掘,即保证有较低的FN,但会存在损失饱和问题,而CE Loss是平等地计算每个像素点的损失。因此单独使用Dice Loss往往并不能取得较好的结果,需要进行组合使用,比如Dice Loss+CELoss或者Dice Loss+Focal Loss等。
如下为Dice_Loss的torch实现:
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]) - tp
fn = torch.sum(temp_target[...,:-1] , axis=[0,1]) - tp
score = ((1 + beta ** 2) * tp + smooth) / ((1 + beta ** 2) * tp + beta ** 2 * fn + fp + smooth)
dice_loss = 1 - torch.mean(score)
return dice_loss
二、Tversky Loss
公式为:
T(X,Y)=X∩YX∩Y+αX-Y+β|Y-X|
其中,|X|表示预测的分割图像,|Y|表示标签的分割图像。Tversky系数是Dice系数和 Jaccard 系数(就是IOU系数)的广义系数。
如果分割任务更加关注召回率(高灵敏度),即真实mask尽可能都被预测出来,不太关注预测mask有没有多预测。Y为真实mask,X为预测mask。 其中α和β可以影响召回率和准确率,若想目标有较高的召回率,那么我们可以选择较高的beta;相反,若想有较高的准确率,则可选择较高的α。
当设置α=β=0.5,此时Tversky系数就是Dice系数。而当设置α=β=1时,此时Tversky系数就是Jaccard系数。其中|X-Y|表示FP(假阳性),|Y-X|表示FN(假阴性),α,β分别控制假阴性和假阳性。通过调整 α 和 β 这两个超参数可以控制这两者之间的权衡,进而影响召回率等指标。
如下为Tiversky代码:
只用了一个参数alpha,另一个参数通过1-alpha来控制。
class tversky(nn.Module):
def __init__(self, smooth=1):
super(tversky, self).__init__()
self.smooth = smooth
def forward(self, logits, label,alpha=0.7):
'''
args: logits: tensor of shape (1, H, W)
args: label: tensor of shape (1, H, W)
'''
probs = torch.sigmoid(logits)
# print("logits:",probs.shape)
# print("label:",label.shape)
true_pos = torch.sum(label * probs)
false_neg = torch.sum(label * (1 - probs))
false_pos = torch.sum((1 - label) * probs)
loss = (true_pos + self.smooth)/(true_pos + alpha*false_neg + (1-alpha)*false_pos + self.smooth)
return 1-loss
三、boundary_loss边界损失
作者实验表明,boundary_loss结合Dice_Loss效果非常好,一个利用距离,一个利用边界。作者对这两个loss的用法是给他们一个权重,训练初期dice loss很高,随着训练进行,Boundary loss比例增加,也就是说越到训练后期越关注边界的准确,边界处理得更细一些。
大致原理为将distance map当做权重来作为某类loss的权重系数训练,详细公式解释请看以下链接:
https://zhuanlan.zhihu.com/p/72783363
源码中,核心部分的distance map变换代码如下:
def one_hot2dist(seg):
res = np.zeros_like(seg)
for i in range(len(seg)):
posmask = seg[i].astype(np.bool)
if posmask.any():
negmask = ~posmask
# print('negmask:', negmask)
# print('distance(negmask):', distance(negmask))
res[i] = distance(negmask) * negmask - (distance(posmask) - 1) * posmask
# print('res[c]', res[c])
return res
假设某一类别的mask如下:
则得到的distance map为:
可以看到,边界处的权重为0,mask内部为负值,背景区域离边界越远权重值越大。
根据上面的distance map的可视化结果看出,如果预测边界小于或者完全符合真实边界并被真实边界包围,这时候loss为负。根据实测,一般训练到最后,Boundary loss会为负值。
如下为我调通的boundary_loss的GitHub链接:
active-boundary-loss/abl.py at main · wangchi95/active-boundary-loss · GitHubOfficial repository for Active Boundary Loss for Semantic Segmentation. - active-boundary-loss/abl.py at main · wangchi95/active-boundary-losshttps://github.com/wangchi95/active-boundary-loss/blob/main/abl.py切记:传入的pred和gt形状,需满足如下要求:
Input:
- pred: the output from model (before softmax)
shape (N, C, H, W)
- gt: ground truth map
shape (N, H, w)
若不满足,需要进行转换。
如下为我自己的调用代码:
Boundary = True
if Boundary:
# 我的输入为:outputs.shape=(n,c,h,w);labels.shape=(n,h,w,c)
n, c, h, w = outputs.size()
nt, ht, wt, ct = labels.size()
# 进行维度转换
temp_labels = torch.flatten(labels[...,:-1].transpose(2, 3).transpose(1, 2), 0, 1) # labels.shape: n h w 4 --> n*3 h w
# print('temp_labels.shape:',temp_labels.shape)
# 调用
abl = ABL()
main_boundaryloss = abl(outputs,temp_labels)
# loss为原损失(dice_loss),在此处也可以加一个系数,来控制dice_loss和边界损失的占比
loss = loss + main_boundaryloss