能把交叉熵(Cross-Entropy)和负对数似然(Negative Log-Likelihood)分得清吗

2,853 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第30天,点击查看活动详情

Cross-Entropy 和 Negative Log-Likelihood 是一个

直到最近看了一个自己大家神经网络框架的视频,其中涉及到 pytorch 中关于 cross-Entropy 和 Negative Log-Likelihood 的区别,才知道他们原来并不是同一个概念的两个名称。而的的确确是不同的两个 loss 函数。

从其背后的数学谈起

最大似然估计(Maximum Likelihood Estimation)

首先来考虑一个二分类问题,在给定模型 fθ(xi)f_{\theta}(x_i) 其中 θ\theta 为参数,目标是求解观察到数据的极大似然(Maximum likelihood)的参数θ\theta

y^θ,i=σ(fθ(x))\hat{y}_{\theta,i} = \sigma(f_{\theta}(x))

这里 y^\hat{y} 表示预测为正样本的概率,σ\sigma 是一个可将inf,inf- \inf , \inf 映射到 [0,1][0,1] 的非线性函数,通常都会选择 sigmoid 作为激活函数来使用。

σ(z)=11+ex\sigma(z) = \frac{1}{1 + e^{-x}}
P(Dθ)=i=1ny^θ,iyi(1y^θ,i)1yiP(\cal{D}|\theta) = \prod_{i=1}^n \hat{y}_{\theta,i}^{y_i} (1- \hat{y}_{\theta,i})^{1 - y^i}

对数似然(log-Likehood)

在大多数情况下只关心对于正确标签预测的结果有多好,

logP(Dθ)=i=1n((yilog(y^θ,i)+(1yi)log(1y^θ,i))\log P(\cal{D}|\theta) = \sum_{i=1}^n \left( (y_i \log(\hat{y}_{\theta,i}) +(1-y_i)\log(1- \hat{y}_{\theta,i}) \right)

在二分类问题中,yy 的取值为 1 或者 0,这里用 1 和 0 分别表示两个不同的类别。

  • y^θ,i\hat{y}_{\theta,i} 表示将第 i 数据预测为正例样本的概率
  • 1y^θ,i1 - \hat{y}_{\theta,i} 表示将第 i 数据预测为负例样本的概率 这里所谓正例和负例样本其实就是两个类别,如果我们把为某一个类别设计正例,那么另一个类别就属于负例
print(f"{'Setting up binary case':-^80}")
z = torch.randn(5)
yhat = torch.sigmoid(z)
y = torch.Tensor([0, 1, 1, 0, 1])
print(f"z= {z}\nyhat= {yhat}\ny= {y}\n{'':-^80}")
z= tensor([ 0.2590, -1.0297, -0.5008, 0.2734, -0.9181]) 
yhat= tensor([0.5644, 0.2631, 0.3774, 0.5679, 0.2853]) 
y= tensor([0., 1., 1., 0., 1.]) 

基于正态分布随机生成 5 个,表示 5 样本预测为正例的值,接下来用 sigmoid 函数来将这些值映射到 0 到 1 取值范围,接下我们创建一个标注

最小化负对数似然(Negative Log Likelihood)

因为对数(log)是一个单调函数,随意最最大似然就等同于求最大对数似然。在深度学习,我们通常做法是让损失函数值最小为目标,随意这里对数似然函数前面添加一个负号,让求最大对数似然变为最小化负的对数似然

l(θ)=i=1n((yilog(y^θ,i)+(1yi)log(1y^θ,i))l(\theta) = -\sum_{i=1}^n \left( (y_i \log(\hat{y}_{\theta,i}) +(1-y_i)\log(1- \hat{y}_{\theta,i}) \right)

这里简单地总结一下,我们最初是找到这么θ\theta 让概率分布出现我们观察数据可能性最大,也就是极大似然估计方法,现在最小化负对数的似然其实并没有背离当初的目标,只是为了计算巧妙地进行目标变换而已。

l = -(y * yhat.log() + (1 - y) * (1 - yhat).log())
print(f"{l}")

这是负对数似然实现

交叉熵

交叉熵用于衡量两个离散概率分布之间相似性,如果假设概率分别为 q 和 p,那么交叉熵概率公式如下

H(p,q)=xXp(x)logq(x)H(p,q) = - \sum_{x \in \cal{X}}p(x)\log q(x)

从公式来看这个交叉熵的公式与负对数似然非常相似,其中 p(x)p(x) 对应于真实标注,对于二分类问题取值为 0 或者 1,而 log 对应于预测概率的。

两种损失之间的这种相似性引起了我最初的困惑。如果这两个损失函数是一样的,那么为什么 PyTorch 会有提供函数 CrossEntropyLoss 有提供了关于负对数似然的 NLLLoss?下面通过一个小实验 来演示一下他们之间的区别,其实在 CrossEntrypyLoss 的实现隐含包括了 softmax 激活,然后进行了对数转换,而 NLLLoss 没有包括 softmax 需要我们通过 pytorch 提供的 softmax 来进行处理。

l = -(y * yhat.log() + (1 - y) * (1 - yhat).log())
print(f"{l}")

# Observe that BCELoss and BCEWithLogitsLoss can produce the same results
l_BCELoss_nored = torch.nn.BCELoss(reduction="none")(yhat, y)
l_BCEWithLogitsLoss_nored = torch.nn.BCEWithLogitsLoss(reduction="none")(z, y)
print(f"{l_BCELoss_nored}\n{l_BCEWithLogitsLoss_nored}\n{'':=^80}")

推广到多分类

现在我们来看一看如何将负对数似然推广到多分类问题。

logP(Dθ)=i=1nlog(y^θ,i(yi))\log P(\cal{D}|\theta) = \sum_{i=1}^n \log(\hat{y}_{\theta,i}^{(y_i)})

在多分类中,上标yiy_i 表示对于 C 个概率分布中仅对真实类别位置所对应概率值进行求对数,例如对于二分类就可以改写成这样

y^θ,i(1)=y^θ,iy^θ,i(0)=1y^θ,i\hat{y}_{\theta,i}^{(1)} = \hat{y}_{\theta,i}\\ \hat{y}_{\theta,i}^{(0)} = 1 - \hat{y}_{\theta,i}

其实可以直接应用到多分类上,例如有 C 分类,那么 y 取值就是从 0 到 C-1 ,只要保证每一个y^i\hat{y}_i 是一个 0 到 1 之间概率值,且求和为 1 ,也就是 y^i\hat{y}_i 是服从概率分布的即可。

softmax 激活函数

softmax(zi)=exp(zi)j=1Cexp(zj)softmax(z_i) = \frac{\exp(z_i)}{\sum_{j=1}^C \exp(z_j)}

通过 softmax 函数可以将多维向量变为一个概率分,也就是每一个维度的数值都在 0 到 1 之间,且所有维度求和为 1。

小实验

print(f"{'Setting up multiclass case':-^80}")
z2 = torch.randn(5, 3)
yhat2 = torch.softmax(z2, dim=-1)
y2 = torch.Tensor([0, 2, 1, 1, 0]).long()
print(f"z2={z2}\nyhat2={yhat2}\ny2{y2}\n{'':-^80}")

这里基于正态概率分布生成 5 预测结果,多类别数为 3 也就是每一样会输出 3 维度向量,每一个维度对应于一个类别预测值,然后经过 softmax 激活函数将 3 维度数值变为概率值,和为 1。然后定义一个标签为 [0,2,1,1,0][0, 2, 1, 1, 0] 对应每一个样本正确类别的索引数,

l2 = -yhat2.log()[torch.arange(5), y2] 

注意这里 yhat2 是对预测值进行了 softmax 处理过的服从概率分布的值

-torch.log_softmax(z2, dim=-1)[torch.arange(5), y2]

这里 log_softmaxsoftmaxlog 两个函数合二为一进行处理

l2_NLLLoss_nored = torch.nn.NLLLoss(reduction="none")(yhat2.log(), y2)
l2_CrossEntropyLoss_nored = torch.nn.CrossEntropyLoss(reduction="none")(z2, y2)
print(f"{l2_NLLLoss_nored}\n{l2_CrossEntropyLoss_nored}\n{'':=^80}")

小结

  • 负对数似然最小化是最大似然估计问题的一个代理问题
  • 交叉熵和负对数似然在数学公式非常相似
  • 计算负对数似然的方法,就是真实类对应对数概率相加
  • 在 pytorch 中 CrossEntropyLoss 和 NLLLoss的PyTorch 对输入要求并不相同,其中 CrossEntropyLoss 输入预测结果即可,而 NLLLoss 需要输入概率的对数