交互分割实战知识笔记2
深度学习中训练网络,必定要考虑的问题之一就是损失函数如何选取。近年来分割中Focal Loss等十分火热,但是很多项目使用的仍然是基础的Dice,CrossEntropy等基础损失函数,效果相差也并不惊人,可见传统的Loss当中仍然有许多值得学习的地方。
本文主要针对分割分类问题中的Loss函数进行一个handbook式的分析,对于我所了解的不同Loss进行特点说明,重点研究实际计算方式,以求直观地理解他们的含义。
实验基于的版本是当前的stable版本pytorch-1.7.1
目录
计算公式细节
nn.L1Loss nn.MSELoss nn.SmoothL1Loss nn.NLLLoss nn.CrossEntropyLoss nn.BCELoss 以及 nn.BCEWithLogitsLoss nn.MultiLabelMarginLoss nn.MultiLabelSoftMarginLoss nn.MultiMarginLoss nn.HingeEmbeddingLoss nn.PoissonNLLLoss nn.KLDivLoss 要点总结计算公式细节
总的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所损失的信息量