交互分割实战知识笔记2

深度学习中训练网络,必定要考虑的问题之一就是损失函数如何选取。近年来分割中Focal Loss等十分火热,但是很多项目使用的仍然是基础的Dice,CrossEntropy等基础损失函数,效果相差也并不惊人,可见传统的Loss当中仍然有许多值得学习的地方。

本文主要针对分割分类问题中的Loss函数进行一个handbook式的分析,对于我所了解的不同Loss进行特点说明,重点研究实际计算方式,以求直观地理解他们的含义。

实验基于的版本是当前的stable版本pytorch-1.7.1

目录

计算公式细节

总的loss计算公式都满足$L(x,y)=func\\{l_1, l_2, ..., l_N\\}^\\top$,所以下文的公式只写其中的$l_n$的计算部分。

nn.L1Loss

就是MAE(mean absolute error),计算公式为
$$l_n = |x_n-y_n|$$
有mean和sum两种模式选,通过reduction控制。

例子

target = torch.tensor([1,1,0,1,0]).float()
output = torch.tensor([1,0,0,0,0]).float()

loss_fn = torch.nn.L1Loss(reduction='mean')
loss = loss_fn(output, target)
print(loss)

loss_fn = torch.nn.L1Loss(reduction='sum')
loss = loss_fn(output, target)
print(loss)

结果

tensor(0.4000)
tensor(2.)

nn.MSELoss

如其名,mean squared error,也就是L2正则项,计算公式为
$$l_n = (x_n-y_n)^2$$
有mean和sum两种模式选,通过reduction控制。

例子

target = torch.tensor([1,0,0,1,0]).float()
output = torch.tensor([1,2,0,0,0]).float()

loss_fn = torch.nn.MSELoss(reduction='mean')
loss = loss_fn(output, target)
print(loss)

loss_fn = torch.nn.MSELoss(reduction='sum')
loss = loss_fn(output, target)
print(loss)

结果

tensor(1.)
tensor(5.)

nn.SmoothL1Loss

对L1做了一点平滑,比起MSELoss,对于outlier更加不敏感。

$$ l_{n}=\left\{\begin{array}{ll} 0.5\left(x_{n}-y_{n}\right)^{2} / \text {beta}, & \text { if }\left|x_{n}-y_{n}\right|<\text {beta} \\ \left|x_{n}-y_{n}\right|-0.5 * \text {beta}, & \text { otherwise } \end{array}\right. $$

在Fast-RCNN中使用以避免梯度爆炸。

nn.NLLLoss

negative log likelihood loss, 用于训练n类分类器,
对于不平衡数据集,可以给类别添加weight,计算公式为
$$l_n = -w_{y_n}x_{n,y_n}, -w_c=weight[c]\cdot1$$

预期输入形状$(N,C)和(N)$,其中$N$为batch大小,C为类别数;

计算每个case的target对应类别的概率的负值,然后求取平均/和,一般与一个LogSoftMax连用从而获得对数概率。

例子

target = torch.tensor([1,0,3])
output = torch.randn(3,5)
print(output)

loss_fn = torch.nn.NLLLoss()
loss = loss_fn(output, target)
print(loss)

结果

tensor([[ 0.1684, -0.2378, -0.5189,  1.5398, -1.1828],
        [-0.4370,  0.3035,  1.3718, -0.2823, -0.4714],
        [ 0.2863, -0.3008,  0.8902,  0.4902, -0.4487]])
tensor(0.0615)

结果 $0.0615 \\times 3 = -(-0.2378)-(-0.4370)-(0.4902)$

nn.CrossEntropyLoss

经典Loss, 计算公式为:

$$ weight[class](-\log \left(\frac {\exp (x[\text {class}])}{\sum_{j} \exp (x[j])}\right))=weight[class](-x[\text {class}]+\log \left(\sum_{j} \exp (x[j])\right)) $$

相当于先将输出值通过softmax映射到每个值在$[0,1]$,和为1的空间上。
希望正确的class对应的loss越小越好,所以对$\\left(\\frac {\\exp (x[\\text {class}])}{\\sum_{j} \\exp (x[j])}\\right)$求取$-log()$, 把$[0,1]$映射到$[0,+\\infty]$上,正确项的概率占比越大,整体损失就越小。

torch里的CrossEntropyLoss(x) 等价于 NLLLoss(LogSoftmax(x))

预期输入未normalize过的score,输入形状和NLL一样,为$(N,C)和(N)$

例子1

target = torch.tensor([1,0,3])
output = torch.randn(3,5)
print(output)

loss_fn = torch.nn.CrossEntropyLoss()
loss = loss_fn(output, target)
print(loss)

结果

tensor([[-0.6324,  0.1134,  0.0695, -1.6937, -0.3634],
        [ 1.2044,  2.0876, -1.6558, -0.4869, -0.8516],
        [-0.7290, -0.4808,  0.8488, -0.3595, -1.3598]])
tensor(1.4465)

例子2-用numpy实现的CrossEntropyLoss

target = torch.tensor([1,0,3])
output = torch.randn(3,5)
print(output)

result = np.array([0.0, 0.0, 0.0])
for ix in range(3):
    log_sum = 0.0
    for iy in range(5):
        if(iy==target[ix]): result[ix] += -output[ix, iy]
        log_sum += exp(output[ix, iy])
    result[ix] += log(log_sum)
print(result)
print(np.mean(result))

loss_fn = torch.nn.CrossEntropyLoss()
loss = loss_fn(output, target)
print(loss)

结果

tensor([[ 1.6021,  0.5762, -1.9105, -1.0844, -0.0256],
        [ 1.0483,  0.8033,  1.1037, -1.2296,  1.2662],
        [ 0.7592, -2.6041, -1.6092, -0.2643,  1.2362]])
[1.52833433 1.43165374 2.15453246]
1.704840179536648
tensor(1.7048)

nn.BCELoss 以及 nn.BCEWithLogitsLoss

Binary Cross Entropy,公式如下:

$$ l_{n}=-w_{n}\left[y_{n} \cdot \log x_{n}+\left(1-y_{n}\right) \cdot \log \left(1-x_{n}\right)\right] $$

双向的交叉熵,相当于交叉熵公式的二分类简化版,可以用于分类不互斥的多分类任务。

BCELoss需要先手动对输入sigmoid,然后每一个位置如果分类是1则加$-log(exp(x))$否则加$-log(exp(1-x))$,最后求取平均。

BCEWithLogitsLoss则不需要sigmoid,其他都完全一样。

例子

target = torch.tensor([[1,0,1],[0,1,1]]).float()
raw_output = torch.randn(2,3)
output = torch.sigmoid(raw_output)
print(output)

result = np.zeros((2,3), dtype=np.float)
for ix in range(2):
    for iy in range(3):
        if(target[ix, iy]==1): 
            result[ix, iy] += -log(output[ix, iy])
        elif(target[ix, iy]==0): 
            result[ix, iy] += -log(1-output[ix, iy])

print(result)
print(np.mean(result))

loss_fn = torch.nn.BCELoss(reduction='none')
print(loss_fn(output, target))
loss_fn = torch.nn.BCELoss(reduction='mean')
print(loss_fn(output, target))
loss_fn = torch.nn.BCEWithLogitsLoss(reduction='mean')
print(loss_fn(raw_output, target))

结果

tensor([[0.3370, 0.2463, 0.4499],
        [0.2124, 0.3505, 0.7828]])
[[1.08756434 0.28280236 0.79866814]
 [0.23878274 1.04849163 0.24483089]]
0.6168566833989618
tensor([[1.0876, 0.2828, 0.7987],
        [0.2388, 1.0485, 0.2448]])
tensor(0.6169)
tensor(0.6169)

nn.MultiLabelMarginLoss

multi-class multi-classification hinge loss
与将问题转换为2分类的BCELoss不同,这个loss就是为了不互斥的多分类(多类别多分类)设计的,

$$ \operatorname{loss}(x, y)=\sum_{i j} \frac {\max (0,1-(x[y[j]]-x[i]))}{\mathrm{x} \cdot \operatorname{size}(0)} $$

HingeLoss的常见形式为
$$l_n = \mathrm{max}(0, 1-x_ny_n)$$
其中$x_n$为预测,$y_n$为真实值。
如果$x_n$和$y_n(+1/-1)$符号一致,则$|x_n|$越大,loss越小,到0为止
如果符号不一样,则loss必大于1,且$|x_n|$越大,loss越大。

总的来说,这种Loss函数训练的目标是拟合一堆$\\pm1$标签,使得输出最后根据正负号确定结果。

nn.MultiLabelSoftMarginLoss

$$ \operatorname{loss}(x, y)=-\frac {1}{C} * \sum_{i} y[i] * \log \left((1+\exp (-x[i]))^{-1}\right)+(1-y[i]) * \log \left(\frac{\exp (-x[i])}{(1+\exp (-x[i]))}\right) $$

$(1+exp(-x[i]))^-1$的值域为$(0,1)$, 计算方式类似于BCE,就是把$(1+exp(-x[i]))^-1$填到了BCE的$x_n$当中。文档里说适用于多分类(互斥)的问题当中,这个式子是基于最大熵计算的。
BCELoss公式如下

$$ l_{n}=-w_{n}\left[y_{n} \cdot \log x_{n}+\left(1-y_{n}\right) \cdot \log \left(1-x_{n}\right)\right] $$

nn.MultiMarginLoss

公式如下:

$$ \operatorname{loss}(x, y)=\frac {\left.\sum_{i} \max (0, \operatorname{margin}-x[y]+x[i])\right)^{p}}{\operatorname{x} \cdot \operatorname{size}(0)} $$

和MultiLabelMarginLoss公式非常像,仔细一看发现就是相同函数的不同接口,只是nn.MultiMarginLoss不支持多标签多分类,所以输入的y_true应当为$[0,3,5,2]$这种,直接给出多分类的类别,格式为$(N,C)$和$(N)$。

nn.HingeEmbeddingLoss

公式如下:

$$ l_{n}=\left\{\begin{array}{ll} x_{n}, & \text { if } y_{n}=1 \\ \max \left\{0, \Delta-x_{n}\right\}, & \text { if } y_{n}=-1 \end{array}\right. $$

同nn.MultiLabelMarginLoss的标准HingeLoss形式类似,希望拟合的标签为$\\pm 1$,其中$\\Delta$是指定的margin,默认为1.0;$x_n$实际上是$|x_n-y_n|$。

常用于非线性的embedding或者半监督中。

nn.PoissonNLLLoss

NLL的泊松分布版本,输入形状变成了$(N,*)$和$(N,*)$,
公式为
$$target∼Poisson(input)$$
$$loss(input,target)=input−target∗log(input)+log(target!)$$

$target$被认为符合$\\lambda=input$的泊松分布,没太用过这种Loss,网上也没啥相关的。

nn.KLDivLoss

KL散度,也就是相对熵,用来比较两个分布之间的信息损失,
计算公式为:

$$ l_n = y_n \cdot (\mathrm{log} y_n-\mathrm{log} x_n) $$

此处补充,普通的信息熵计算公式为:

$$ y_n \cdot - \mathrm{log} y_n $$

交叉熵计算公式为:

$$ y_n \cdot - \mathrm{log} x_n $$

很显然,KL散度直接计算两个分布间的自信息(-log项)差距在y分布上的期望,不能直接理解为距离(因为KLDiv(x,y)!=KLDiv(y,x)),可以理解为用x去拟合y所损失的信息量

要点总结

  • 最基础的L1,MSE,SmoothL1没啥好说的,就是评价序列相似性,哪里都能用
  • NLL, CrossEntropy, BCE(WithLogits)就是一套基础Loss,分类问题里到处用,个人目前感觉差别不大,BCE可能会更敏感一些吧。
  • MultiLabelMargin,MultiLabelSoftMargin,MultiMarginLoss是标准的HingeLoss的不同场景实现,多分类问题的通用Loss,不过我目前论文看得太少了,还不太清楚哪些网络用过。

blueyo0
1 声望0 粉丝