★★★ 本文源自AlStudio社区精品项目,【点击此处】查看更多精品内容 >>>
论文连接:https://www.mdpi.com/2076-3417/13/2/826

Balanced Loss Function

为了解决标签不平衡、难易样本不平衡和边界不平衡的问题,该文分别提出了三种策略:动态类别加权、截断交叉熵损失和标签混淆抑制。这三个组成部分统一起来作为一个平衡损失函数。

Dynamical Class Weighting

该文通过一种动态加权方案来解决标签不平衡问题,这种方案受到 Cui 等人提出的类别平衡损失的启发。类别平衡损失旨在通过有效示例的数量来加权类别特定的损失。分类的类别平衡损失公式为:

L C B = 1 − β 1 − β N k log ⁡ ( 1 − ϵ ) L_{CB}=\frac{1-\beta}{1-\beta^{N_k}}\log{(1-\epsilon)} LCB=1βNk1βlog(1ϵ)

其中 β ∈ ( 0 , 1 ) \beta \in{(0,1)} β(0,1) 是一个超参数, k k k 是示例的标签, N k N_k Nk 是数据集中类别 k 的出现次数。这种类别平衡损失将在训练时为不太常见的类别的损失分配更高的权重。

在表面缺陷分割中,标签不平衡问题存在于像素级别。该文通过缩放像素标签分布的损失值来将类别平衡损失调整为动态的,这意味着上述公式中的 N i N_i Ni 应表示类别的像素数。假设对于图像的注释 A,标签分布是 { N 1 , N 2 , … , N C } \{N_1,N_2,…,N_C\} {N1,N2,,NC},满足 N 1 + N 2 + … + N C = W ∗ H N_1+N_2+…+N_C=W\ast H N1+N2++NC=WH。该文定义像素加权掩码 M c l a s s M_{class} Mclass,其在点 ( i , j ) (i,j) (i,j)(像素标签为 k k k)处的值为:

M c l a s s i , j = 1 − β 1 − β N k M_{class_{i,j}}=\frac{1-\beta}{1-\beta^{N_k}} Mclassi,j=1βNk1β

该文的方法与原始的类别平衡损失在几个方面不同。首先,它不再与交叉熵损失或其他损失函数相关联;它是像素级别损失值的软掩码,可以进行加权。其次, N k N_k Nk 计算的是像素而不是类别的图像。最后,该文通过图像大小 W × H W\times H W×H 来选择 β \beta β 的值,实验中发现最佳的 β \beta β 值为 0.99。

Truncated Cross-Entropy Loss

大多数基于点的分割损失函数可以解释为对概率预测 y ∈ ( 0 , 1 ) y\in{(0,1)} y(0,1) Y Y Y 的一个条目)给出标签 y ^ ∈ ( 0 , 1 ) \hat{y}\in{(0,1)} y^(0,1) y ^ \hat{y} y^ 的一个条目)的点误差 ϵ \epsilon ϵ 的惩罚:

ϵ = { y , y ^ = 0 1 − y , y ^ = 1 \epsilon=\begin{cases}y,&\hat{y}=0\\1-y,&\hat{y}=1\end{cases} ϵ={y,1y,y^=0y^=1

交叉熵损失使用对数函数来扩大误差的比例:

C E ( ϵ ) = − ln ⁡ ( 1 − ϵ ) CE(\epsilon)=-\ln{(1-\epsilon)} CE(ϵ)=ln(1ϵ)

为了缩小简单和困难示例的损失值,该文提出了一种截断交叉熵损失。首先,设置上限 ϵ s u b ∈ ( 0 , 1 ] \epsilon_{sub}\in{(0,1]} ϵsub(0,1] 和下限 ϵ i n f ∈ [ 0 , 1 ) \epsilon_{inf}\in{[0,1)} ϵinf[0,1) (满足 ϵ s u b > ϵ i n f \epsilon_{sub}>\epsilon_{inf} ϵsub>ϵinf)来识别简单和困难示例。截断交叉熵损失定义如下:

T r u n c C E ( ϵ ) = { 0 , ϵ < ϵ i n f − ln ⁡ ( 1 − ϵ ) − t i n f , ϵ i n f ≤ ϵ ≤ ϵ s u b ϵ − ϵ s u b 1 − ϵ s u b + t s u b − t i n f , ϵ s u b < ϵ TruncCE(\epsilon)=\begin{cases}0,&\epsilon<\epsilon_{inf}\\-\ln(1-\epsilon)-t_{inf},&\epsilon_{inf}\leq\epsilon\leq\epsilon_{sub}\\\frac{\epsilon-\epsilon_{sub}}{1-\epsilon_{sub}}+t_{sub}-t_{inf},&\epsilon_{sub}<\epsilon\end{cases} TruncCE(ϵ)= 0,ln(1ϵ)tinf,1ϵsubϵϵsub+tsubtinf,ϵ<ϵinfϵinfϵϵsubϵsub<ϵ

上述参数中取 ϵ i n f = 0.3 \epsilon_{inf}=0.3 ϵinf=0.3 以及 ϵ s u b = 0.7 \epsilon_{sub}=0.7 ϵsub=0.7 ,并且其中:

t i n f = − ln ⁡ ( 1 − ϵ i n f ) t s u b = − ln ⁡ ( 1 − ϵ s u b ) t_{inf}=-\ln(1-\epsilon_{inf})\\ t_{sub}=-\ln(1-\epsilon_{sub}) tinf=ln(1ϵinf)tsub=ln(1ϵsub)

截断交叉熵损失是一个一致的分段损失函数,其中在 ϵ = ϵ i n f \epsilon=\epsilon_{inf} ϵ=ϵinf ϵ = ϵ s u b \epsilon=\epsilon_{sub} ϵ=ϵsub 处有两个不可微点。为了使其与梯度下降优化兼容,该文将两个奇异点的导数设置为与一侧一致,截断交叉熵函数的导数被形式化为:

d T r u n c C E ( ϵ ) d ϵ = { 0 , ϵ < ϵ i n f 1 1 − ϵ , ϵ i n f ≤ ϵ ≤ ϵ s u b 1 1 − ϵ s u b , ϵ s u b < ϵ \frac{dTruncCE(\epsilon)}{d\epsilon}=\begin{cases}0,&\epsilon<\epsilon_{inf}\\\frac{1}{1-\epsilon},&\epsilon_{inf}\leq\epsilon\leq\epsilon_{sub}\\\frac{1}{1-\epsilon_{sub}},&\epsilon_{sub}<\epsilon\end{cases} dϵdTruncCE(ϵ)= 0,1ϵ1,1ϵsub1,ϵ<ϵinfϵinfϵϵsubϵsub<ϵ

截断交叉熵损失的导数是不一致的,对于简单的样例 ϵ < ϵ i n f \epsilon<\epsilon_{inf} ϵ<ϵinf,导数为零。许多其他经典的损失函数也具有类似的性质。例如,平均绝对误差(MAE)回归损失在 ϵ = 0 \epsilon=0 ϵ=0 处有一个不一致点;Huber损失有两个奇异点,分别分难易样本;Hinge损失也将简单样本的损失值和导数设置为零。在现代基于自动梯度的神经网络实现中,可以通过用户指定的梯度计算来解决损失函数在某些点上的不一致和不光滑问题。

Label Confusion Suppression

该文的解决方案有两个步骤:首先,在每个点测量标签混淆的程度;然后,抑制具有标签混淆的点的损失。

该文提出了一种基于熵的标签混淆度量方法。熵是一种常用的分布方差度量。离散随机变量 X ∈ { x 1 , x 2 , … , x N } X\in\{x_1,x_2,…,x_N\} X{x1,x2,,xN}的熵(以自然常数为底数)为:

H ( X ) = − ∑ i = 1 N P ( x i ) ln ⁡ P ( x i ) H(X)=-\sum^{N}_{i=1}{P(x_i)\ln{P(x_i)}} H(X)=i=1NP(xi)lnP(xi)

该文使用标签熵作为局部区域标签混淆度量。对于以像素注释 A A A为中心的 r × r r\times r r×r r > 1 r>1 r>1且为奇数)正方形区域,记为 l o c a l ( A i , j ) ∈ N r × r local(A_{i,j})\in{\mathbb N^{r\times r}} local(Ai,j)Nr×r,标签熵为:

H ( A i , j ) = − ∑ k = 1 C N i , j , k r 2 ln ⁡ ( N i , j , k r 2 ) H(A_{i,j})=-\sum^{C}_{k=1}{\frac{N_{i,j,k}}{r^2}\ln{(\frac{N_{i,j,k}}{r^2})}} H(Ai,j)=k=1Cr2Ni,j,kln(r2Ni,j,k)

其中, N i , j , k N_{i,j,k} Ni,j,k l o c a l ( A i , j ) local(A_{i,j}) local(Ai,j)内标签k的出现次数。标签熵越高,表示在点 ( i , j ) (i,j) (i,j)处标签混淆越严重。标签熵的最大值为 ln ⁡ ( C ) \ln{(C)} ln(C)

为了抑制标签混淆严重的点,该文计算了一个软掩码 M c o n f u s i o n ∈ R W × H M_{confusion}\in{\mathbb R^{W\times H}} MconfusionRW×H用于损失计算,其条目为:

M c o n f u s i o n i , j = 1 − H ( A i , j ) ln ⁡ ( C ) M_{confusion_{i,j}}=1-\frac{H(A_{i,j})}{\ln{(C)}} Mconfusioni,j=1ln(C)H(Ai,j)

M c o n s u f i o n M_{consufion} Mconsufion被定义为在损失值上的常数软掩码,以抑制标签混淆。标签混淆程度越高(由公式 H ( A i , j ) H(A_{i,j}) H(Ai,j)测量),给定公式 M c o n f u s i o n i , j M_{confusion_{i,j}} Mconfusioni,j的权重就越低。当多个标签混杂在局部区域并且在 ( i , j ) (i,j) (i,j)处造成训练混淆时,混淆掩码将抑制损失值并减少整体损失值的噪声。

The Unified Function

通过截断交叉熵损失函数,可以利用预测值 Y Y Y 和标注 A A A(在计算之前, A A A 被 One-hot 编码为 Y ^ \hat{Y} Y^)来计算一个不平衡的损失函数 L u n b a l a n c e d L_{unbalanced} Lunbalanced。与此同时,还可以从 A A A 中计算动态类别加权掩码 M c l a s s M_{class} Mclass 和标签混淆抑制掩码 M c o n f u s i o n M_{confusion} Mconfusion。最后,通过元素乘积计算平衡损失 L b a l a n c e d L_{balanced} Lbalanced,即 L b a l a n c e d = L u n b a l a n c e d ⊙ M c l a s s ⊙ M c o n f u s i o n L_{balanced} = L_{unbalanced} \odot M_{class} \odot M_{confusion} Lbalanced=LunbalancedMclassMconfusion。最终的损失值可以是 L b a l a n c e d L_{balanced} Lbalanced 的元素和或平均值。

image.png

在实际操作中,由于 M c o n f u s i o n M_{confusion} Mconfusion M c l a s s M_{class} Mclass 不依赖于任何运行时变量,因此它们可以在训练之前进行计算,以避免在训练阶段重复计算。此外,由于 L b a l a n c e d L_{balanced} Lbalanced 的计算是 L u n b a l a n c e d L_{unbalanced} Lunbalanced M c o n f u s i o n M_{confusion} Mconfusion M c l a s s M_{class} Mclass 的元素乘积,因此这个乘积也可以在训练之前计算以节省时间。该文的实现表明,平衡损失的计算速度非常接近于普通的交叉熵损失。

实现

使用PaddlePaddle对其进行实现,由于代码没开源,不确定和原文一样,仅用于学习,其中由于TruncatedCrossEntropyLoss需要自己手动计算backward,所以需要用到PyLayer

import paddle
import paddle.nn as nn
import paddle.nn.functional as F
from paddle.autograd import PyLayer

class TruncatedCrossEntropyLoss(PyLayer):
    @staticmethod
    def forward(ctx, pred, label, epsilon_inf, epsilon_sub):
        ctx.epsilon_inf = paddle.to_tensor(epsilon_inf, 'float32')
        ctx.epsilon_sub = paddle.to_tensor(epsilon_sub, 'float32')
        ctx.t_inf = -paddle.log(1 - ctx.epsilon_inf)
        ctx.t_sub = -paddle.log(1 - ctx.epsilon_sub)
        label = paddle.unsqueeze(label, axis=1)
        pred = F.softmax(pred, axis=1)
        epsilon = paddle.where(label == 0, pred, 1 - pred)
        loss = paddle.where(
            epsilon < ctx.epsilon_inf,
            paddle.to_tensor(0, 'float32'),
            paddle.where(
                epsilon > ctx.epsilon_sub,
                (epsilon - ctx.epsilon_sub) / (1 - ctx.epsilon_sub) + ctx.t_sub - ctx.t_inf,
                -paddle.log(1 - epsilon) - ctx.t_inf
            )
        )
        ctx.save_for_backward(epsilon)
        return loss

    @staticmethod
    def backward(ctx, dy):
        epsilon, = ctx.saved_tensor()
        grad = paddle.where(
            epsilon < ctx.epsilon_inf,
            paddle.to_tensor(0, 'float32'),
            paddle.where(
                epsilon > ctx.epsilon_sub,
                1 / (1 - ctx.epsilon_sub),
                1 / (1 - epsilon)
            )
        )
        return dy * grad

class BalancedLoss(nn.Layer):
    def __init__(self, num_classes=2, beta=0.99, epsilon_inf=0.3, epsilon_sub=0.7, r=7):
        super(BalancedLoss, self).__init__()
        self.num_classes = num_classes
        self.beta = beta
        self.epsilon_inf = epsilon_inf
        self.epsilon_sub = epsilon_sub
        self.r = r
        self.r_square = r ** 2
        self.pad = r // 2
        self.kernel = paddle.ones((num_classes, 1, r, r), 'float32')
        self.lnc = paddle.log(paddle.to_tensor(num_classes, 'float32'))
        self.eps = 1e-12

    def forward(self, pred, label):
        m_confusion = self.label_confusion_suppression(label)
        m_class = self.dynamical_class_weighting(label)
        l_unbalanced = TruncatedCrossEntropyLoss.apply(pred, label, self.epsilon_inf, self.epsilon_sub)
        l_balanced = paddle.unsqueeze(m_confusion, axis=1) * paddle.unsqueeze(m_class, axis=1) * l_unbalanced
        l_balanced = paddle.mean(l_balanced)
        return l_balanced

    def dynamical_class_weighting(self, label):
        m_class = paddle.zeros_like(label, dtype='float32')
        for i in range(self.num_classes):
            mask = (label == i)
            N_i = paddle.sum(mask)
            weight = (1 - self.beta) / (1 - self.beta ** N_i)
            m_class = paddle.where(mask, weight, m_class)
        return m_class

    def label_confusion_suppression(self, label):
        label_one_hot = F.one_hot(label, num_classes=self.num_classes).transpose([0, 3, 1, 2])
        label_padded = F.pad(label_one_hot, [self.pad] * 4, mode='reflect')
        N_ijk = F.conv2d(label_padded, self.kernel, groups=self.num_classes)
        P_xi = N_ijk / self.r_square
        H = - paddle.sum(P_xi * paddle.log(P_xi + self.eps), axis=1)
        m_confusion = 1 - H / self.lnc
        return m_confusion

训练

基于CFD数据进行训练,本来也是在做道路裂缝的检查,刚好看到这个是处理一些裂缝划痕之类的分割的,拿来试试效果。

! pip install -q paddleseg==2.5.0
import paddleseg.transforms as T
from paddleseg.datasets import Dataset

base_lr = 3e-4
train_lens = 60
epochs = 2000
batch_size = 32
iters = epochs * train_lens // batch_size

# 构建训练集
train_transforms = [
    T.RandomHorizontalFlip(),
    T.RandomVerticalFlip(),
    T.RandomDistort(),
    T.Resize(target_size=(480, 320)),
    T.Normalize()  # 归一化
]
train_dataset = Dataset(
    transforms=train_transforms,
    dataset_root="dataset",
    num_classes=2,
    mode="train",
    train_path="dataset/train_list.txt",
    separator=" "
)

# 构建验证集
val_transforms = [
    T.Resize(target_size=(480, 320)),
    T.Normalize()
]
val_dataset = Dataset(
    transforms=val_transforms,
    dataset_root="dataset",
    num_classes=2,
    mode="val",
    val_path="dataset/val_list.txt",
    separator=" "
)
import paddle
from paddleseg.models import HarDNet
from paddleseg.models.losses import BCELoss, DiceLoss, MixedLoss

# 网络
model = HarDNet(
    num_classes=2,
    pretrained="https://bj.bcebos.com/paddleseg/dygraph/cityscapes/hardnet_cityscapes_1024x1024_160k/model.pdparams"
)

# 损失函数
losses = {}
# losses["types"] = [MixedLoss([BCELoss(), DiceLoss()], [1, 1])]
losses["types"] = [MixedLoss([BalancedLoss(), DiceLoss()], [1, 1])]
losses["coef"] = [1]

# 学习率及优化器
lr = paddle.optimizer.lr.PolynomialDecay(base_lr, decay_steps=iters, end_lr=base_lr / 5)
optimizer = paddle.optimizer.AdamW(lr, parameters=model.parameters())
from paddleseg.core import train

train(
    model=model,
    train_dataset=train_dataset,
    val_dataset=val_dataset,
    optimizer=optimizer,
    save_dir="output/BalancedLoss",
    iters=iters,
    batch_size=batch_size,
    save_interval=int(iters/50),
    log_iters=10,
    num_workers=0,
    losses=losses,
    use_vdl=True
)

总结

蓝色是BalancedLossDiceLoss组合的结果,绿色是BCELossDiceLoss组合的结果,感觉还行。

vdl

不过水平有限也不知道是否正确实现了。有看到问题的大佬麻烦批评指正一下。
此文章为转载
原文链接

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐