Label Smoothing是一个帮助多分类模型进行正则化的操作。
"When Does Label Smoothing Help? "这篇文章指出Szegedy et al.提出了Label Smoothing. 因此我们就从Szegedy et al.的文章入手。在这里我们简称Label Smoothing为LS。
标签平滑也可以被简称为LSR(Label-Smoothing Regularization)。
假设我们有一个K分类问题,我们有标签1到K。
假设 z k z_k zk是类 k k k未经softmax的log概率值,
设x是一个训练input
那么我们的模型给我们的每一个类 k k k的概率就是:
p ( k ∣ x ) = e x p ( z k ) ∑ i = 1 k e x p ( z i ) p(k|x) = \frac{exp(z_k)}{\sum_{i=1}^{k}exp(z_i)} p(k∣x)=∑i=1kexp(zi)exp(zk)
假设 q ( k ∣ x ) q(k|x) q(k∣x)是类 k k k的真实概率标签。
那么此处的Cross Entropy Loss就是(此处省去了条件概率后面的 “ ∣ x |x ∣x”):
l = − ∑ k = 1 K log ( p ( k ) ) q ( k ) l = - \sum_{k=1}^{K}\log(p(k)) q(k) l=−k=1∑Klog(p(k))q(k)
这个loss关于 z k z_k zk求导的话:
∂ l ∂ z k = p ( k ) − q ( k ) \frac{\partial l}{\partial z_k} = p(k)-q(k) ∂zk∂l=p(k)−q(k)
这个值是肯定在-1和1之间的
假设K个类别中,只有类别y是正确的。也就是说,只有q(y)=1,
对于其他的q(k),只要 k ≠ y k\neq y k=y, q(k)=0。
此时,使得loss l l l最小,就是要让正确label y的log-likelihood最大。因为把所有的q(k)=0的情况都消掉之后,我们就有
l = − log ( p ( y ) ) q ( y ) = − log ( p ( y ) ) \begin{aligned} l &= - \log(p(y)) q(y)\\ &= -\log(p(y)) \end{aligned} l=−log(p(y))q(y)=−log(p(y))
所以当某训练input x的标签是y时,如果模型的输出能够做到
z y ≫ z k z_y \gg z_k zy≫zk
(for all k ≠ y k\neq y k=y)
那么此时我们就可以让loss达到尽可能小。
LS则是为了避免这种偏激的情况。
既然是正则化,当然在最大化当前训练数据的log-likelihood上面就不会有什么帮助了,
不过可以让模型不那么偏激,且在别的数据上更好地泛化。
本来我们的模型标签分布是这样的:
q ( k ∣ x ) = δ k , y q(k|x) = \delta_{k,y} q(k∣x)=δk,y
此处的 δ k , y \delta_{k,y} δk,y是一个狄拉克delta,意思是除了当k=y时这个值为1之外,其他情况值都为0。
而现在,我们不再用这个分布,而是使用:
q ′ ( k ∣ x ) = ( 1 − ϵ ) δ k , y + ϵ u ( k ) q'(k|x) = (1-\epsilon)\delta_{k,y} + \epsilon u(k) q′(k∣x)=(1−ϵ)δk,y+ϵu(k)
此处:
u(k)是一个关于具体类别k的分布,并且是固定的(与x取值无关)
ϵ \epsilon ϵ是一个平滑参数
这个新的 q ′ ( k ∣ x ) q'(k|x) q′(k∣x)其实是再把原来的分布 q ( k ∣ x ) q(k|x) q(k∣x)和分布 u ( k ) u(k) u(k)做了一个结合。
可以这样理解:
1 − ϵ 1-\epsilon 1−ϵ的比例下我们还是使用原来的绝对标签(0或1)
但 ϵ \epsilon ϵ的比例下我们使用我们的新分布 u ( k ) u(k) u(k)
此处 u ( k ) u(k) u(k)如何定义:
既然是标签的概率分布,那可以从数据出发,看一下每个标签出现概率的分布,把这些值用于 u ( k ) u(k) u(k)这个分布。
原作者在论文中的实验中使用的是 u ( k ) = 1 / K u(k)=1/K u(k)=1/K,那么就有(此处再次省略条件概率后面的 “ ∣ x |x ∣x”):
q ′ ( k ) = ( 1 − ϵ ) δ k , y + ϵ K q'(k) = (1-\epsilon)\delta_{k,y} + \frac{\epsilon}{K} q′(k)=(1−ϵ)δk,y+Kϵ
一开始我们提到,如果不用LS, l = − log ( p ( y ) ) l = -\log(p(y)) l=−log(p(y))这个loss会让类y的q(y)尽量接近1,而其它类的q(k)则接近0.
但现在我们的loss − ∑ k = 1 K log ( p ( k ) ) q ′ ( k ) -\sum_{k=1}^{K}\log(p(k)) q'(k) −∑k=1Klog(p(k))q′(k) 中每个q’都是一个正数,不再是原来的0了。让p(k)太小的话,是不利于 l l l的减小的。
当我们用 q ′ ( k ∣ x ) q'(k|x) q′(k∣x)替换 q ( k ∣ x ) q(k|x) q(k∣x),设此时的Cross Entropy为 H ( q ′ , p ) H(q',p) H(q′,p),原来的是 H ( q , p ) H(q,p) H(q,p),我们做一些推导:
H ( q , p ) = − ∑ k = 1 K log ( p ( k ) ) q ( k ) = − ∑ k = 1 K log ( p ( k ) ) δ k , y \begin{aligned} H(q,p) &= -\sum_{k=1}^{K}\log(p(k)) q(k)\\ &=-\sum_{k=1}^{K}\log(p(k)) \delta_{k,y} \end{aligned} H(q,p)=−k=1∑Klog(p(k))q(k)=−k=1∑Klog(p(k))δk,y
H ( q ′ , p ) = − ∑ k = 1 K log ( p ( k ) ) q ′ ( k ) = − ∑ k = 1 K log ( p ( k ) ) [ ( 1 − ϵ ) δ k , y + ϵ u ( k ) ] = ( 1 − ϵ ) ( − ∑ k = 1 K log ( p ( k ) ) δ k , y ) + ϵ ( − ∑ k = 1 K log ( p ( k ) ) u ( k ) ) = ( 1 − ϵ ) H ( q , p ) + ϵ H ( u , p ) \begin{aligned} H(q',p) &= -\sum_{k=1}^{K}\log(p(k)) q'(k)\\ &=-\sum_{k=1}^{K}\log(p(k))[(1-\epsilon)\delta_{k,y} + \epsilon u(k)] \\ &=(1-\epsilon)(-\sum_{k=1}^{K}\log(p(k)) \delta_{k,y})+\epsilon(-\sum_{k=1}^{K}\log(p(k)) u(k))\\ &=(1-\epsilon)H(q,p) + \epsilon H(u,p) \end{aligned} H(q′,p)=−k=1∑Klog(p(k))q′(k)=−k=1∑Klog(p(k))[(1−ϵ)δk,y+ϵu(k)]=(1−ϵ)(−k=1∑Klog(p(k))δk,y)+ϵ(−k=1∑Klog(p(k))u(k))=(1−ϵ)H(q,p)+ϵH(u,p)
从这个角度理解的话,实际上LS就是把原来的loss H ( q , p ) H(q,p) H(q,p)变成了两个loss H ( q ′ , p ) H(q',p) H(q′,p)和 H ( u , p ) H(u,p) H(u,p)的结合。
其中, H ( u , p ) H(u,p) H(u,p)这个loss的作用就是,当p偏离u这个分布时,要对模型进行惩罚。这个惩罚的相对权重则是 ϵ 1 − ϵ \frac{\epsilon}{1-\epsilon} 1−ϵϵ。
注: H ( u , p ) H(u,p) H(u,p)这个东西换成KL散度也是ok的,因为
H ( u , p ) = D K L ( u ∣ ∣ p ) + H ( u ) H(u,p) = D_{KL}(u||p)+H(u) H(u,p)=DKL(u∣∣p)+H(u)
而H(u)又是不变的。
不像TF,Pytorch内目前应该是没有现成的LS implementation的。但仍然有很多可用的实现方式,此处我们以NVIDIA/DeepLearningExamples的版本为例。
class LabelSmoothing(nn.Module):
"""NLL loss with label smoothing.
"""
def __init__(self, smoothing=0.0):
"""Constructor for the LabelSmoothing module.
:param smoothing: label smoothing factor
"""
super(LabelSmoothing, self).__init__()
self.confidence = 1.0 - smoothing
self.smoothing = smoothing
# 此处的self.smoothing即我们的epsilon平滑参数。
def forward(self, x, target):
logprobs = torch.nn.functional.log_softmax(x, dim=-1)
nll_loss = -logprobs.gather(dim=-1, index=target.unsqueeze(1))
nll_loss = nll_loss.squeeze(1)
smooth_loss = -logprobs.mean(dim=-1)
loss = self.confidence * nll_loss + self.smoothing * smooth_loss
return loss.mean()
这里我先把H(q’,p)化成一个跟这里代码最接近的形式。
H ( q ′ , p ) = − ∑ k = 1 K log ( p ( k ) ) q ′ ( k ) = − ∑ k = 1 K log ( p ( k ) ) [ ( 1 − ϵ ) δ k , y + ϵ u ( k ) ] = − ∑ k = 1 K [ ( 1 − ϵ ) log ( p ( k ) ) δ k , y + ϵ log ( p ( k ) ) u ( k ) ] \begin{aligned} H(q',p) &= -\sum_{k=1}^{K}\log(p(k)) q'(k)\\ &=-\sum_{k=1}^{K}\log(p(k))[(1-\epsilon)\delta_{k,y} + \epsilon u(k)] \\ &=-\sum_{k=1}^{K}[(1-\epsilon)\log(p(k))\delta_{k,y}+\epsilon\log(p(k)) u(k)]\\ \end{aligned} H(q′,p)=−k=1∑Klog(p(k))q′(k)=−k=1∑Klog(p(k))[(1−ϵ)δk,y+ϵu(k)]=−k=1∑K[(1−ϵ)log(p(k))δk,y+ϵlog(p(k))u(k)]
logprobs = torch.nn.functional.log_softmax(x, dim=-1)
此处x的shape应该是(batch size * class数量),所以这里在class数量那个维度做了logsoftmax。
Pytorch中的Logsoftmax的具体实现:
L o g S o f t m a x ( a i ) = l o g ( exp ( a i ) ∑ j exp ( a j ) ) LogSoftmax(a_i) = log(\frac{\exp(a_i)}{\sum_{j}\exp(a_j)}) LogSoftmax(ai)=log(∑jexp(aj)exp(ai))
2. log ( p ( k ) ) δ k , y \log(p(k))\delta_{k,y} log(p(k))δk,y
nll_loss = -logprobs.gather(dim=-1, index=target.unsqueeze(1))
此处的target的shape是(batch size), 应该就是每个training data的数字标签。
这里unsqueeze之后,就是(batch size * 1 )
这里的gather函数的具体理解请参考我的另一篇文章:Pytorch中的torch.gather函数详解,从抽象到具体。
总之,这里会输出一个形状也是(batch size * 1)的张量,第二个维度只取了那些target维度的log-prob值。
举个例子,假设我们的x是[[0.3,0.1,0.6],[0.2,0.3,0.5]],一共两个数据。
标签是[2,2]的话
输出会给到[[0.6],[0.5]]
nll_loss = nll_loss.squeeze(1)
把输出的shape变回(batch size)
smooth_loss = -logprobs.mean(dim=-1)
这里的logprobs的shape是(batch size * class数量)
在第二个维度取均值的话,应该就是对每个x,所有类的logprobs取了平均值。
那不就相当于:
log ( p ( k ) ) ∗ 1 K = log ( p ( k ) ) u ( k ) \log(p(k)) * \frac{1}{K} = \log(p(k)) u(k) log(p(k))∗K1=log(p(k))u(k)
和论文里实验的实现方式是一样的
loss = self.confidence * nll_loss + self.smoothing * smooth_loss
OK了