【Pytorch】 Dice系数与Dice Loss损失函数实现

本文深入探讨了PyTorch中Dice系数的计算方法,对比分析了不同平滑参数对结果的影响,并提供了两种不同的Dice损失函数实现,适用于二分类和多分类任务。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

由于 Dice系数是图像分割中常用的指标,而在Pytoch中没有官方的实现,下面通过自己的想法并结合网上的一些参考进行详细实现。

先来看一个我在网上看到的一个版本。

def diceCoeff(pred, gt, smooth=1, activation='sigmoid'):
    r""" computational formula:
        dice = (2 * (pred ∩ gt)) / (pred ∪ gt)
    """

    if activation is None or activation == "none":
        activation_fn = lambda x: x
    elif activation == "sigmoid":
        activation_fn = nn.Sigmoid()
    elif activation == "softmax2d":
        activation_fn = nn.Softmax2d()
    else:
        raise NotImplementedError("Activation implemented for sigmoid and softmax2d 激活函数的操作")

    pred = activation_fn(pred)

    N = gt.size(0)
    pred_flat = pred.view(N, -1)
    gt_flat = gt.view(N, -1)

    intersection = (pred_flat * gt_flat).sum(1)
    unionset = pred_flat.sum(1) + gt_flat.sum(1)
    loss = 2 * (intersection + smooth) / (unionset + smooth)

    return loss.sum() / N

整体思路就是运用dice的计算公式 (2 * A∩B) / (A∪B)。下面来分析一下可能存在的问题:

smooth参数是用来防止分母除0的,但是如果smooth=1的话,会使得dice的计算结果略微偏高,看下面的测试代码。

第一种情况:预测和标签完全一样

# shape = torch.Size([1, 3, 4, 4])
'''
1 0 0= bladder
0 1 0 = tumor
0 0 1= background 
'''
pred = torch.Tensor([[
        [[0, 1, 1, 0],
         [1, 0, 0, 1],
         [1, 0, 0, 1],
         [0, 1, 1, 0]],
        [[0, 0, 0, 0],
         [0, 0, 0, 0],
         [0, 0, 0, 0],
         [0, 0, 0, 0]],
        [[1, 0, 0, 1],
         [0, 1, 1, 0],
         [0, 1, 1, 0],
         [1, 0, 0, 1]]]])
    
gt = torch.Tensor([[
        [[0, 1, 1, 0],
         [1, 0, 0, 1],
         [1, 0, 0, 1],
         [0, 1, 1, 0]],
        [[0, 0, 0, 0],
         [0, 0, 0, 0],
         [0, 0, 0, 0],
         [0, 0, 0, 0]],
        [[1, 0, 0, 1],
         [0, 1, 1, 0],
         [0, 1, 1, 0],
         [1, 0, 0, 1]]]])


dice_baldder1 = diceCoeff(pred[:, 0:1, :], gt[:, 0:1, :], smooth=1, activation=None)
dice_baldder2 = diceCoeff(pred[:, 0:1, :], gt[:, 0:1, :], smooth=1e-5, activation=None)
print('smooth=1 : dice={:.4}'.format(dice_baldder1.item()))
print('smooth=1e-5 : dice={:.4}'.format(dice_baldder2.item()))

# 输出结果
smooth=1 : dice=1.050
smooth=1e-5 : dice=1.0

我们最后预测的是一个3分类的分割图,第一类是baldder, 第二类是tumor, 第三类是背景。我们先假设bladder的预测pred和gt一样,计算bladder的dice值,发现当smooth=1的时候,dice偏高, 而smooth=1e-5时dice比较合理。

解决办法:我想这里应该更改代码的实现方式,用下面的计算公式替换之前的,因为之前加smooth的位置有问题。

# loss = 2 * (intersection + smooth) / (unionset + smooth)  # 之前的

loss = (2 * intersection + smooth) / (unionset + smooth)

替换后的dice如下:

def diceCoeff(pred, gt, smooth=1e-5, activation='sigmoid'):
    r""" computational formula:
        dice = (2 * (pred ∩ gt)) / (pred ∪ gt)
    """

    if activation is None or activation == "none":
        activation_fn = lambda x: x
    elif activation == "sigmoid":
        activation_fn = nn.Sigmoid()
    elif activation == "softmax2d":
        activation_fn = nn.Softmax2d()
    else:
        raise NotImplementedError("Activation implemented for sigmoid and softmax2d 激活函数的操作")

    pred = activation_fn(pred)

    N = gt.size(0)
    pred_flat = pred.view(N, -1)
    gt_flat = gt.view(N, -1)

    intersection = (pred_flat * gt_flat).sum(1)
    unionset = pred_flat.sum(1) + gt_flat.sum(1)
    loss = (2 * intersection + smooth) / (unionset + smooth)

    return loss.sum() / N

上面用到的测试数据进行验证结果如下:dice计算正确 

# smooth=1 : dice=1.0
# smooth=1e-5 : dice=1.0

第二种情况:预测的结果不在标签中 

如下面的代码,我们假设预测的pred中有一部分bladder,但gt中没有bladder,看计算出的dice值如何。

'''
    1 0 0= bladder
    0 1 0 = tumor
    0 0 1= background 
    '''
    pred = torch.Tensor([[
        [[0, 1, 1, 0],
         [0, 0, 0, 0],
         [0, 0, 0, 0],
         [0, 0, 0, 0]],
        [[0, 0, 0, 0],
         [0, 0, 0, 0],
         [0, 0, 0, 0],
         [0, 0, 0, 0]],
        [[1, 0, 0, 1],
         [1, 1, 1, 1],
         [1, 1, 1, 1],
         [1, 1, 1, 1]]]])

    gt = torch.Tensor([[
        [[0, 0, 0, 0],
         [0, 0, 0, 0],
         [0, 0, 0, 0],
         [0, 0, 0, 0]],
        [[0, 0, 0, 0],
         [0, 0, 0, 0],
         [0, 0, 0, 0],
         [0, 0, 0, 0]],
        [[1, 1, 1, 1],
         [1, 1, 1, 1],
         [1, 1, 1, 1],
         [1, 1, 1, 1]]]])

    dice_baldder1 = diceCoeff(pred[:, 0:1, :], gt[:, 0:1, :], smooth=1, activation=None)
    dice_baldder2 = diceCoeff(pred[:, 0:1, :], gt[:, 0:1, :], smooth=1e-5, activation=None)
    print('smooth=1 : dice={:.4}'.format(dice_baldder1.item()))
    print('smooth=1e-5 : dice={:.4}'.format(dice_baldder2.item()))

# 输出结果
smooth=1 : dice=0.3333
smooth=1e-5 : dice=5e-06

从结果可以看到,smooth=1时的dice值为0.3333;而 smooth=1e-5时的dice值接近于0,较为合理。

dice的另一种计算方式:这里参考肾脏肿瘤挑战赛提供的dice计算方法。

def diceCoeffv2(pred, gt, eps=1e-5, activation='sigmoid'):
    r""" computational formula:
        dice = (2 * tp) / (2 * tp + fp + fn)
    """

    if activation is None or activation == "none":
        activation_fn = lambda x: x
    elif activation == "sigmoid":
        activation_fn = nn.Sigmoid()
    elif activation == "softmax2d":
        activation_fn = nn.Softmax2d()
    else:
        raise NotImplementedError("Activation implemented for sigmoid and softmax2d 激活函数的操作")

    pred = activation_fn(pred)

    N = gt.size(0)
    pred_flat = pred.view(N, -1)
    gt_flat = gt.view(N, -1)

    tp = torch.sum(gt_flat * pred_flat, dim=1)
    fp = torch.sum(pred_flat, dim=1) - tp
    fn = torch.sum(gt_flat, dim=1) - tp
    loss = (2 * tp + eps) / (2 * tp + fp + fn + eps)
    return loss.sum() / N

整理代码

def diceCoeff(pred, gt, smooth=1e-5, activation='sigmoid'):
    r""" computational formula:
        dice = (2 * (pred ∩ gt)) / (pred ∪ gt)
    """

    if activation is None or activation == "none":
        activation_fn = lambda x: x
    elif activation == "sigmoid":
        activation_fn = nn.Sigmoid()
    elif activation == "softmax2d":
        activation_fn = nn.Softmax2d()
    else:
        raise NotImplementedError("Activation implemented for sigmoid and softmax2d 激活函数的操作")

    pred = activation_fn(pred)

    N = gt.size(0)
    pred_flat = pred.view(N, -1)
    gt_flat = gt.view(N, -1)

    intersection = (pred_flat * gt_flat).sum(1)
    unionset = pred_flat.sum(1) + gt_flat.sum(1)
    loss = (2 * intersection + smooth) / (unionset + smooth)

    return loss.sum() / N



def diceCoeffv2(pred, gt, eps=1e-5, activation='sigmoid'):
    r""" computational formula:
        dice = (2 * tp) / (2 * tp + fp + fn)
    """

    if activation is None or activation == "none":
        activation_fn = lambda x: x
    elif activation == "sigmoid":
        activation_fn = nn.Sigmoid()
    elif activation == "softmax2d":
        activation_fn = nn.Softmax2d()
    else:
        raise NotImplementedError("Activation implemented for sigmoid and softmax2d 激活函数的操作")

    pred = activation_fn(pred)

    N = gt.size(0)
    pred_flat = pred.view(N, -1)
    gt_flat = gt.view(N, -1)

    tp = torch.sum(gt_flat * pred_flat, dim=1)
    fp = torch.sum(pred_flat, dim=1) - tp
    fn = torch.sum(gt_flat, dim=1) - tp
    loss = (2 * tp + eps) / (2 * tp + fp + fn + eps)
    return loss.sum() / N


# v2的另一种代码写法
def diceCoeffv3(pred, gt, eps=1e-5, activation='sigmoid'):
    r""" computational formula:
        dice = (2 * tp) / (2 * tp + fp + fn)
    """

    if activation is None or activation == "none":
        activation_fn = lambda x: x
    elif activation == "sigmoid":
        activation_fn = nn.Sigmoid()
    elif activation == "softmax2d":
        activation_fn = nn.Softmax2d()
    else:
        raise NotImplementedError("Activation implemented for sigmoid and softmax2d")

    pred = activation_fn(pred)

    N = gt.size(0)
    pred_flat = pred.view(N, -1)
    gt_flat = gt.view(N, -1)

    tp = torch.sum((pred_flat != 0) * (gt_flat != 0), dim=1)
    fp = torch.sum((pred_flat != 0) * (gt_flat == 0), dim=1)
    fn = torch.sum((pred_flat == 0) * (gt_flat != 0), dim=1)
    # 转为float,以防long类型之间相除结果为0
    loss = (2 * tp + eps).float() / (2 * tp + fp + fn + eps).float()

    return loss.sum() / N

基于上面的dice指标实现dice loss:

class SoftDiceLoss(nn.Module):
    __name__ = 'dice_loss'

    def __init__(self, activation='sigmoid'):
        super(SoftDiceLoss, self).__init__()
        self.activation = activation

    def forward(self, y_pr, y_gt):
        return 1 - diceCoeffv2(y_pr, y_gt, activation=self.activation)

代码测试:

if __name__ == '__main__':
    
    # shape = torch.Size([2, 3, 4, 4])
    # 模拟batch_size = 2
    '''
    1 0 0= bladder
    0 1 0 = tumor
    0 0 1= background 
    '''
    pred = torch.Tensor([[
        [[0, 1, 0, 0],
         [1, 0, 0, 1],
         [1, 0, 0, 1],
         [0, 1, 1, 0]],
        [[0, 0, 0, 0],
         [0, 0, 0, 0],
         [0, 1, 1, 0],
         [0, 0, 0, 0]],
        [[1, 0, 1, 1],
         [0, 1, 1, 0],
         [0, 0, 0, 0],
         [1, 0, 0, 1]]],
        [
            [[0, 1, 0, 0],
             [1, 0, 0, 1],
             [1, 0, 0, 1],
             [0, 1, 1, 0]],
            [[0, 0, 0, 0],
             [0, 0, 0, 0],
             [0, 1, 1, 0],
             [0, 0, 0, 0]],
            [[1, 0, 1, 1],
             [0, 1, 1, 0],
             [0, 0, 0, 0],
             [1, 0, 0, 1]]]
    ])

    gt = torch.Tensor([[
        [[0, 1, 1, 0],
         [1, 0, 0, 1],
         [1, 0, 0, 1],
         [0, 1, 1, 0]],
        [[0, 0, 0, 0],
         [0, 0, 0, 0],
         [0, 1, 1, 0],
         [0, 0, 0, 0]],
        [[1, 0, 0, 1],
         [0, 1, 1, 0],
         [0, 0, 0, 0],
         [1, 0, 0, 1]]],
        [
            [[0, 1, 1, 0],
             [1, 0, 0, 1],
             [1, 0, 0, 1],
             [0, 1, 1, 0]],
            [[0, 0, 0, 0],
             [0, 0, 0, 0],
             [0, 1, 1, 0],
             [0, 0, 0, 0]],
            [[1, 0, 0, 1],
             [0, 1, 1, 0],
             [0, 0, 0, 0],
             [1, 0, 0, 1]]]
    ])


    dice1 = diceCoeff(pred[:, 0:1, :], gt[:, 0:1, :], activation=None)
    dice2 = diceCoeffv2(pred[:, 0:1, :], gt[:, 0:1, :], activation=None)
    dice3 = diceCoeffv3(pred[:, 0:1, :], gt[:, 0:1, :], activation=None)
    print(dice1, dice2, dice3)

# 输出
tensor(0.9333) tensor(0.9333) tensor(0.9333)

总结:上面是这几天对dice以及dice loss的一些思考和实现。

2020/6/8更新:SoftDiceLoss的计算方式修改。在实际项目种训练发现之前的loss计算方式不够准确,现在按类别计算dice,求平均之后得到loss。

class SoftDiceLossV2(_Loss):
    __name__ = 'dice_loss'

    def __init__(self, num_classes, activation='sigmoid', reduction='mean'):
        super(SoftDiceLossV2, self).__init__()
        self.activation = activation
        self.num_classes = num_classes

    def forward(self, y_pred, y_true):
        class_dice = []
        for i in range(1, self.num_classes):
            class_dice.append(diceCoeff(y_pred[:, i:i + 1, :], y_true[:, i:i + 1, :], activation=self.activation))
        mean_dice = sum(class_dice) / len(class_dice)
        return 1 - mean_dice

最后总结:通过一些实战发现,之前实现的SoftDiceLoss适合用来作二分类的loss(最后输出的特征图通道类似[N, 1, H, W]这种),而SoftDiceLossV2则适合用来作多分类的loss(最后输出的特征图通道类似[N, C, H, W]这种,C>2)。

<think>好的,用户想在YOLOv11中添加Dice Loss损失函数,需要提供实现方法和示例代码。首先,我需要回顾一下Dice Loss的基本原理和在图像分割中的应用。根据引用[2]和[3],Dice Loss在处理类别不平衡和小目标问题时表现较好,常用于医学图像分割。不过YOLO系列主要用于目标检测,所以需要确认Dice Loss是否适用于检测任务,或者用户是否将其用于分割分支。 接下来,我需要考虑YOLO的损失函数结构。通常YOLO的损失包括分类、置信度和定位损失(如CIoU)。添加Dice Loss可能需要将其整合到现有的损失计算中,或者替换某个部分。用户提到的YOLOv11可能是指某个特定版本或自定义版本,所以需要保持代码的兼容性。 然后,查找如何在PyTorch实现Dice LossDice系数是衡量重叠度的指标,公式为$Dice = \frac{2|X \cap Y|}{|X| + |Y|}$,对应的损失是$1 - Dice$。需要注意的是,二分类和多分类的实现方式不同。用户的应用场景如果是目标检测,可能需要处理每个锚框或像素的预测。 接下来,考虑如何将Dice Loss嵌入到YOLO的损失计算中。通常,YOLO的损失函数在utils/loss.py文件中,需要修改ComputeLoss类。可能需要添加Dice Loss的计算部分,并调整总损失为各部分的加权和。同时,需要处理数据格式,比如将预测和目标转换为二值形式,应用sigmoid或softmax激活函数。 还要注意梯度计算的问题,避免因为Dice Loss的稳定性问题导致训练不稳定。可能需要加入平滑项epsilon,防止分母为零。此外,Dice Loss的数值范围是否其他损失项(如CIoU)匹配,是否需要调整权重系数。 最后,编写示例代码,展示如何定义Dice Loss类,并在ComputeLoss的__init__和__call__方法中整合。需要测试代码的正确性,确保张量形状匹配,计算设备一致(如CUDA)。另外,用户可能还需要调整数据加载部分,确保标签格式适合Dice Loss计算,比如是否需要one-hot编码等。 可能遇到的问题包括:Dice Loss在检测任务中的效果不如传统损失,需要实验验证;多任务学习中损失权重的调整;以及训练过程中可能出现的数值不稳定。需要提醒用户注意这些点,并根据实际情况调整参数。</think>在YOLOv11中引入Dice Loss需要修改损失函数计算模块,以下是分步实现指南: ### 一、Dice Loss原理 Dice系数衡量预测真实标签的重叠度: $$Dice = \frac{2|X \cap Y|}{|X| + |Y|}$$ 对应的损失函数为$1 - Dice$,特别适用于类别不平衡场景[^2][^3] ### 二、代码实现步骤 #### 1. 创建Dice Loss类 ```python import torch import torch.nn as nn class DiceLoss(nn.Module): def __init__(self, eps=1e-6): super(DiceLoss, self).__init__() self.eps = eps # 防止除零 def forward(self, pred, target): # 转换格式:pred需经过sigmoid激活 pred = pred.sigmoid() # 展平张量 [batch, channels, H, W] -> [batch, -1] pred_flat = pred.view(pred.size(0), -1) target_flat = target.view(target.size(0), -1) intersection = (pred_flat * target_flat).sum(1) union = pred_flat.sum(1) + target_flat.sum(1) dice_score = (2. * intersection + self.eps) / (union + self.eps) return 1 - dice_score.mean() ``` #### 2. 修改YOLO损失计算模块 在`utils/loss.py`中找到`ComputeLoss`类进行修改: ```python class ComputeLoss: def __init__(self, model, autobalance=False): # ...原有参数初始化... self.dice_loss = DiceLoss() # 添加Dice损失 def __call__(self, p, targets): # ...原有损失计算... # 添加Dice Loss计算(示例应用于分类分支) dice_loss = self.dice_loss(tensor_pred_class, tensor_target_class) # 总损失组合(需调整权重系数) total_loss = (loss_box * box_weight + loss_obj * obj_weight + loss_cls * cls_weight + dice_loss * 0.5) # dice权重设为0.5 return total_loss * batch_size ``` ### 三、关键实现细节 1. **输入格式适配**: - YOLO的预测输出需转换为$[B, C, H, W]$格式 - 目标标签需进行one-hot编码(分类任务)或保持二值化(分割任务) 2. **多任务平衡**: - 建议初始权重设置:Dice Loss权重0.5,CIoU Loss权重1.0,分类权重0.5 - 使用自动平衡策略时可添加: ```python self.autobalance = autobalance # 引用YOLO原有平衡机制[^4] ``` 3. **梯度控制**: - 添加梯度裁剪防止数值不稳定: ```python torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=10.0) ``` ### 四、验证方法 ```python # 单元测试示例 pred = torch.rand(2, 3, 640, 640).cuda() target = torch.randint(0, 1, (2, 3, 640, 640)).float().cuda() loss = DiceLoss()(pred, target) print(f"Dice Loss值: {loss.item():.4f}") # 预期输出0.3~0.7之间 ```
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值