paddlepaddle 25 Boundary Loss的实现(Boundary F1 loss)

目前有两篇学术中共有两篇论文以不同的形式提出了boundary loss,分别是论文1: Boundary Loss for Remote Sensing Imagery Semantic Segmentation 与论文2:Boundary loss for highly unbalanced segmentation

论文1所提出的boundary loss即最小化label边缘与pred边缘的f-score(也就是dice loss),其项目地址如下所示。代码为pytorch版本,稍后博主会分享改写的paddle版本代码。GitHub - yiskw713/boundary_loss_for_remote_sensing: Pytorch re-implementation of boundary loss, proposed in "Boundary Loss for Remote Sensing Imagery Semantic Segmentation"Pytorch re-implementation of boundary loss, proposed in "Boundary Loss for Remote Sensing Imagery Semantic Segmentation" - GitHub - yiskw713/boundary_loss_for_remote_sensing: Pytorch re-implementation of boundary loss, proposed in "Boundary Loss for Remote Sensing Imagery Semantic Segmentation"https://github.com/yiskw713/boundary_loss_for_remote_sensing/

论文2所提出的boundary loss为最小化label边缘与pred边缘异区域,即最小化Dist(G,S),具体如图1所示

图1 论文2中的boundary loss 

论文2所实现的boundary loss代码如下所示,是pytorch的,个人感觉是跟上面的示意图毫无关联,只是单纯的将label与pred进行了一个点乘,然后最小化。如果有精通此道的朋友,可以去github上阅读作者的项目,然后在评论区留下您的点评。boundary-loss/losses.py at master · LIVIAETS/boundary-loss · GitHubhttps://github.com/LIVIAETS/boundary-loss/blob/master/losses.py

class SurfaceLoss():
    def __init__(self, **kwargs):
        # Self.idc is used to filter out some classes of the target mask. Use fancy indexing
        self.idc: List[int] = kwargs["idc"]
        print(f"Initialized {self.__class__.__name__} with {kwargs}")

    def __call__(self, probs: Tensor, dist_maps: Tensor) -> Tensor:
        assert simplex(probs)
        assert not one_hot(dist_maps)

        pc = probs[:, self.idc, ...].type(torch.float32)
        dc = dist_maps[:, self.idc, ...].type(torch.float32)

        multipled = einsum("bkwh,bkwh->bkwh", pc, dc)

        loss = multipled.mean()

        return loss


BoundaryLoss = SurfaceLoss

这里主要讲述的是paddle下论文1的boundary loss的实现。

1、boundary loss的计算流程

论文1中boundary loss的计算分3步实现,具体如图2所示,首先对label与pred求边缘区域,然后对边缘区域进行膨胀,最后计算膨胀后的label边缘与pred边缘的f_score,用1 - f_score即得到了boundary loss。

图2 论文1中boundary loss的计算流程

这里大家可能会好奇,在神经网络中怎么对label求边缘?通过maxpool操作可以对图像进行腐蚀、膨胀、开运算和闭运算等形态学操作。在论文1中是通过膨胀的结果减原图得到了图像的边缘,其中有一个关键技巧就是,要将label转换为独热码。因为在进行maxpool操作时,label值的大小会影响结果。但转换为独热码后,不同的label值处于不同的chanel上且值为1,这就不会影响maxpool操作。pytorch 20 基于pytorch实现腐蚀膨胀、开运算闭运算等形态学操作_万里鹏程转瞬至的博客-CSDN博客_pytorch 腐蚀和膨胀在一些图像处理项目中,有时需要对图像进行腐蚀膨胀和各种值处理,引入opencv处理后,数据无法进行快速的值处理,如值域截取、使用mask、where操作等。因此,可以使用torch实现腐蚀膨胀,从而避免对数据结构的修改,同时保证了在一个模型内实现所有操作。(当然,opencv中的值域截取也是可以很方便的,具体可以参考以下链接中的第七节c++上opencv的常用数组操作_a486259的博客-CSDN博客)但是,基于torch实现的腐蚀膨胀,有一定的假设前提:所有的结构元素都为方形算子。实现后可以作用于Chttps://blog.csdn.net/a486259/article/details/122931576

 2、paddle下的实现

代码改自作者所提供的pytorch版本,然后针对paddle进行了适配。其中有必要说明的是作者提供的F1计算方式为 (2 * P * R) / (P + R + smooth),这会导致在P与R同时为0时(label中不存在特定类别,预测结果中也没有特定类别),F1为0,使boundary loss为1。各位可以按照自己的理解将代码改成(2 * P * R + smooth) / (P + R + smooth),这样子改动后P与R同时为0时,boundary loss为0

#https://github.com/yiskw713/boundary_loss_for_remote_sensing/blob/master/boundary_loss.py
def one_hot(label, n_classes, requires_grad=True):
    b,w,h=label.shape
    one_hot_label = paddle.eye(n_classes)[label.flatten()]#
    #n_classes作为最后补充的维度,因此在最后面。但独热码需要的格式为b,n_classes,w,h,所以调用transpose
    one_hot_label = one_hot_label.reshape((b,w,h,n_classes)).transpose((0,3,1,2))
    one_hot_label.stop_gradient=False

    return one_hot_label


class BoundaryLoss(nn.Layer):
    """Boundary Loss proposed in:
    Alexey Bokhovkin et al., Boundary Loss for Remote Sensing Imagery Semantic Segmentation
    https://arxiv.org/abs/1905.07852
    """
    def __init__(self, theta0=3, theta=5):
        super().__init__()

        self.theta0 = theta0
        self.theta = theta
    def __str__(self):
        return "BoundaryLoss(theta0:%s,theta1:%s)"%(self.theta0,self.theta)
    def forward(self, pred, gt):
        n, c, _, _ = pred.shape

        #将预测结果转化为概率值
        pred = F.softmax(pred, axis=1)

        #转换为独热码,将label值分配到chanel,避免在max_pool中产生的极大值抑制
        one_hot_gt = one_hot(gt, c)

        #求label的边界。本质:对1-label进行膨胀,然后用膨胀的结果减去(1-label)
        gt_b = F.max_pool2d(
            1 - one_hot_gt, kernel_size=self.theta0, stride=1, padding=(self.theta0 - 1) // 2)
        gt_b -= 1 - one_hot_gt
        
        #求pred的边界
        pred_b = F.max_pool2d(
            1 - pred, kernel_size=self.theta0, stride=1, padding=(self.theta0 - 1) // 2)
        pred_b -= 1 - pred
        

        #对边界进行扩展===>该操作使softmax下的非极大值区域,在次要通道得到激活并产生loss引导
        gt_b_ext = F.max_pool2d(
            gt_b, kernel_size=self.theta, stride=1, padding=(self.theta - 1) // 2)
        pred_b_ext = F.max_pool2d(
            pred_b, kernel_size=self.theta, stride=1, padding=(self.theta - 1) // 2)
        
        #print((gt_b_ext-pred_b_ext).abs().sum())
        #print((gt_b-pred_b).abs().sum())

        #进行reshape操作
        gt_b = gt_b.reshape((n, c, -1))
        pred_b = pred_b.reshape((n, c, -1))
        gt_b_ext = gt_b_ext.reshape((n, c, -1))
        pred_b_ext = pred_b_ext.reshape((n, c, -1))
        
        #print((gt_b_ext-pred_b_ext).abs().sum())
        #print((gt_b-pred_b).abs().sum())

        # Precision, Recall
        P = paddle.sum(pred_b * gt_b_ext, axis=2) / (paddle.sum(pred_b, axis=2) + 1e-7)
        R = paddle.sum(pred_b_ext * gt_b, axis=2) / (paddle.sum(gt_b, axis=2) + 1e-7)
        # Boundary F1 Score
        smooth=1e-7
        #BF1 = (2 * P * R) / (P + R + smooth)
        BF1 = (2 * P * R +smooth ) / (P + R + smooth)

        # summing BF1 Score for each class and average over mini-batch
        loss = paddle.mean(1 - BF1)
        #print(P,"\n",R,"\n",BF1,'\n',1 - BF1)
        return loss

3、boundary loss的使用

boundary loss只对边缘区域进行监督,因此需要配合其他loss使用。就如作者在使用中就是配合了交叉熵、IOU loss来使用的。

if __name__ == "__main__":
    label=paddle.zeros((1,100,100),dtype=paddle.int64)
    label[:,30:50,30:50]=3
    pred=one_hot(label,9)
    label[:,30:50,30:50]=3

    print(pred.shape,label.shape)
    ceriter=BoundaryLoss()
    loss=ceriter(pred,label)
    print(loss)
    loss.backward()

猜你喜欢

转载自blog.csdn.net/a486259/article/details/124434717