PyTorch踩坑指南(2)损失函数nn.NLLLoss()和nn.CrossEntropyLoss()

前言

深度学习模型优化,即优化网络权值使得该模型拟合数据的能力达到最优,而最优的一个标准是损失函数较小(兼顾训练数据和测试数据,以及实际应用场景的最优)。PyTorch中有很多损失函数,这里我主要介绍最常用的两种,NLLLoss和CrossEntropyLoss;而实际上CrossEntropyLoss更常用,NLLLoss与其的关系也会在本文中详细介绍。

1. Softmax

要介绍上述两个损失函数的关系,得先从Softmax说起。Softmax函数是一个非线性转换函数,通常用在网络输出的最后一层,Softmax处理之后的输出的是归一化的概率分布,即各个类别的概率值在 [ 0 , 1 ] [0,1] [0,1]之间且概率和为1)1。如在多分类问题中,Softmax输出每个类别或节点对应的概率。其计算公式如下, σ ( z ⃗ ) i = e z i ∑ j = 1 K e z j \sigma(\vec{z})_i = \frac{e^{z_i}}{\sum_{j=1}^Ke^{z_j}} σ(z )i=j=1Kezjezi其中, z ⃗ \vec{z} z 是输入向量(神经网络output layer的输出向量), z i z_i zi为输入向量第 i i i个节点的值,如下图所示。
PyTorch踩坑指南(2)损失函数nn.NLLLoss()和nn.CrossEntropyLoss()_第1张图片
对output layer的值可以直接做Logsoftmax操作,该操作之后的值作为NLLLoss的输入。

2. NLLLoss

NLLLoss的全称为Negative Log Likelihood Loss,负对数似然损失,是训练多分类问题的常用损失函数2。Likelihood想必大家都熟悉,是似然的意思,而最大似然估计(MLE,maximum likelihood estimation)是一种估计模型参数的方法。NLLLoss,刨去Negative和Log,因为这两个是取负数和取对数的数学操作,剩下的Likelihood Loss,就是这个损失函数的本质了。于是引申出一个问题——似然函数为什么可以作为模型的损失函数(这个问题大部分博文都没有详细讲,我来给大家抛砖引玉,如有理解不对的地方,请大伙儿批评指正)。

2.1 似然函数

似然(函数)这一概念是由Fisher提出。当我们有一系列观测数据 x x x,我们使用该观测数据进行模型参数 θ \theta θ估计,就用到了似然函数, L ( θ ∣ x ) L(\theta|x) L(θx)

这里插一个对概率的解释。 L ( θ ∣ x ) = f ( x ∣ θ ) L(\theta|x) = f(x|\theta) L(θx)=f(xθ) ,左边表示likelihood,右边表示probability—— It can be called the likelihood of θ (given that x was observed) or the probability of x (given θ) 3——这个等式表示的是对于同一件事件发生的两种思考角度,核心意思为给定一个 θ \theta θ和观测数据 x x x的情况下,整个事件发生的可能性。

统计学观点认为样本的出现是基于一个分布。那么我们先假设这个分布为 f f f,参数为 θ \theta θ。不同的 θ \theta θ,样本分布不一样,即出现 x x x的概率也不一样。 L ( θ ∣ x ) L(\theta|x) L(θx) 表示的是在给定样本 x x x的时候,参数 θ \theta θ使得 x x x出现的可能性多大。

所以,似然函数实际上表示的是参数 θ \theta θ的函数,而最大似然估计的意思是寻找一个 θ \theta θ使得该函数值最大。我们拿抛硬币举例,正面向上的概率为 θ \theta θ,反面向上的概率为 1 − θ 1-\theta 1θ。假如我们抛 N N N次,其中 N 1 N_1 N1次正面朝上, N 2 N_2 N2次反面朝上,那么 L ( θ ∣ x ) = θ N 1 ( 1 − θ ) N 2 L(\theta|x) = {\theta}^{N_1} (1-\theta) ^{N_2} L(θx)=θN1(1θ)N2
可以基于此表达式画出 L L L的函数曲线。
PyTorch踩坑指南(2)损失函数nn.NLLLoss()和nn.CrossEntropyLoss()_第2张图片

import matplotlib.pyplot as plt
import numpy as np

N = 100
N1 = 60
N2 = N - N1

theta = np.arange(0.10, 0.90, 0.05)
L = np.zeros(theta.size)

for i in range(theta.size):
    L[i] =  np.power(theta[i], N1) * np.power(1-theta[i], N2)

# find the theta makes the L funtion maximum
value = np.max(L)
ind = np.where(L==value)
# draw the Likelihood function
plt.figure()
plt.plot(theta, L)
plt.text(theta[ind],value,(theta[ind],value),color='r')
plt.show()

可以看出,当100次抛硬币,60次正面朝上的情况下, θ \theta θ的最大似然估计值为 0.6 0.6 0.6

2.2 似然损失

损失函数用于衡量当前参数(神经网络模型中的weights和biases;高斯混合模型中的均值,方差,权重)下,模型的预测值和真实值(label或数据观测值)的差距。所以,我们希望损失函数越小越好。

(1) 从损失函数设计直接解释

我们规定,损失函数为2 l ( x , y ) = L = l 1 , . . . , l N , l n = − w y n x n , y n l(x,y) = L = {l_1,...,l_N}, l_n = -w_{y_n}x_{n,y_n} l(x,y)=L=l1,...,lN,ln=wynxn,yn
对于个数为 N N N的batch数据,每个 x x x的大小为 N ∗ C N*C NC C C C为类别数,且 x x x为——神经网络的output layer,经过LogSoftmax之后的值;而 x n x_n xn为该batch中,第 n n n个向量; y n y_n yn表示该batch中,第 n n n个向量的label或者target。我们取向量 x n x_n xn中,第target位置的值,然后乘以权重(如果有,一般情况下为 1 1 1),取负号,可得第 n n n个数据的损失。该batch的综合损失为2 l ( x , y ) = { ∑ n = 1 N 1 ∑ n = 1 N w y n l n , i f   r e d u c t i o n = ′ m e a n ′ ∑ n = 1 N l n , i f   r e d u c t i o n = ′ s u m ′ l(x,y)=\left\{ \begin{aligned} & \sum_{n=1}^N \frac{1}{\sum_{n=1}^Nw_{y_n}} l_n, &if \ reduction & = 'mean' \\ & \sum_{n=1}^N l_n, &if \ reduction &= 'sum' \end{aligned} \right. l(x,y)=n=1Nn=1Nwyn1ln,n=1Nln,if reductionif reduction=mean=sum
很显然,一个是求和,一个是求平均。
当预测值越接近真值(label或者target)的时候,也就是说概率值在target这个位置越大,说明这个模型就越准确。概率 P ( x ) P(x) P(x)的值为 [ 0 , 1 ] [0,1] [0,1](softmax操作),取对数后为 ( − ∞ , 0 ] (-\infty ,0] (,0](Logsoftmax操作),在前面加个符号,变成 [ 0 , ∞ ) [0,\infty) [0,),为损失函数的取值范围。换句话说,概率越接近 1 1 1,损失函数越小,越接近零。符合我们优化的目标,如下图所示4
PyTorch踩坑指南(2)损失函数nn.NLLLoss()和nn.CrossEntropyLoss()_第3张图片
更直观一点,如下图所示4“马” 的预测概率值为 0.98 0.98 0.98,非常高,其对应的NLLLoss为 0.02 0.02 0.02,很小。
PyTorch踩坑指南(2)损失函数nn.NLLLoss()和nn.CrossEntropyLoss()_第4张图片

(2) 从多项分布理解损失函数设计

前文中提到似然函数的时候用抛硬币举例子,而多次抛硬币实际上是一个二项分布5,单次实验为伯努利实验, n n n为抛硬币次数, k k k为正面朝上的次数, p p p为正面朝上的概率,其概率质量函数为 f ( k , n , p ) = P r ( X = k ) = ( n k )   p k   ( 1 − p ) n − k f(k,n,p) = Pr(X = k) = \left( \begin{matrix} n \\ k \end{matrix} \right) \ p^k \ (1-p)^{n-k} f(k,n,p)=Pr(X=k)=(nk) pk (1p)nk
而多项分布可以理解为掷色子,其概率质量函数为,
f ( x 1 , . . . , x k ; n , p 1 , . . . , p k ) = P r ( X 1 = x 1 , X 2 = x 2 , . . . , X k = x k ) = n ! x 1 ! . . . x k ! p 1 x 1 × . . . × p k x k \begin{aligned} f(x_1,...,x_k;n,p1,...,pk) &=Pr(X1=x_1, X_2=x_2,...,X_k = x_k) \\ &= \frac{n!}{x_1!...x_k!}p_1^{x_1} \times ...\times p_k^{x_k} \end{aligned} f(x1,...,xk;n,p1,...,pk)=Pr(X1=x1,X2=x2,...,Xk=xk)=x1!...xk!n!p1x1×...×pkxk
对于每次模型的输出概率,即label的分布(如 k k k个类别),可以理解为多项式分布。更直观一点,输入同一张图片——还是以上图中的“马”为例子——100次,它的被模型预测到正确label的次数为98次,概率为 0.98 0.98 0.98,模型预测输出,统计下来为 p = [ 0.02 , 0.00 , 0.98 ] p=[0.02, 0.00, 0.98] p=[0.02,0.00,0.98]。而这张图的label,实际上就是我们“要求”模型预测出现的次数100次,则 x = [ 0 , 0 , 1 ] x=[0, 0, 1] x=[0,0,1]。接下来,我们就有了 f ( p , x ) = 0.0 2 0 × 0.0 0 0 × 0.9 8 1 f(p, x) = 0.02^0 \times 0.00^{0} \times 0.98^{1} f(p,x)=0.020×0.000×0.981最大化似然函数,可知, p = x p=x p=x的时候,得到最大值1。log不改变单调性,也可使得上述乘法变成加法 l o g ( f ( p , x ) ) = ∑ x × l o g ( p ) log(f(p,x)) = \sum x \times log(p) log(f(p,x))=x×log(p)最大化似然,进一步就变成了最小化负的log似然 l o s s ( p , x ) = − ∑ x × l o g ( p ) loss(p, x) = - \sum x \times log(p) loss(p,x)=x×log(p)如此看来,负log似然损失可以作为模型训练的损失函数——模型输入的预测概率越接近label,loss越小,接近零。

3. CrossEntropyLoss

看完负log似然损失函数,我们会发现,这个跟交叉熵形式一样啊,没错,实际上就是相通的。同一个事物的不同解释角度,殊途同归。

3.1 交叉熵

实在是懒得敲公式了,直接贴官网的图6——吐槽一下CSDN的latex接口实在是不太友好。实际上,pytorch的这个计算公式里面,log里面就是一个softmax,然后加上一个NLLLoss。对于标签来讲,除了 0 0 0就是 1 1 1,就变成了下面这个形式,标签为 1 1 1,就是乘以 1 1 1,在公式形式上也省了,也就是后面说的P(X)省了——样本真实分布,只剩下Q(X)——模型预测输出。
在这里插入图片描述
交叉熵从字面意思理解就是交叉+。熵,是信息函数关于概率分布P的期望,这个期望值就是熵,公式如下 H ( X ) = − ∑ i n P ( X = x i ) l o g ( P ( X = x i ) ) H(X) = - \sum_i^nP(X=x_i)log\big(P(X=x_i)\big) H(X)=inP(X=xi)log(P(X=xi))那cross就是真实分布 p p p和模型预测 q q q进行cross了 H ( p , q ) = − ∑ i n p ( x i ) l o g ( q ( x i ) ) H(p, q) = - \sum_i^np(x_i)log\big(\bf{q(x_i)}\big) H(p,q)=inp(xi)log(q(xi))

3.2 KL散度7

如果对于同一个随机变量 X X X有两个单独的概率分布 P ( x ) P(x) P(x) Q ( x ) Q(x) Q(x),则我们可使用KL算的来衡量这两个概率分布的差异。 D K L ( p ∣ ∣ q ) = ∑ i = 1 n p ( x i ) l o g ( p ( x i ) q ( x i ) ) D_{KL}(p||q) = \sum_{i=1}^n p(x_i)log(\frac{p(x_i)}{q(x_i)}) DKL(pq)=i=1np(xi)log(q(xi)p(xi))深度学习中, P ( x ) P(x) P(x)样本真实分布, Q ( x ) Q(x) Q(x)表示模型预测输出,还拿上面的猫,狗,马分类为例,第二张马的照片,真实分布 P ( X ) = [ 0 , 0 , 1 ] P(X) =[0, 0, 1] P(X)=[0,0,1],预测分布 Q ( X ) = [ 0.02 , 0.00 , 0.98 ] Q(X) = [0.02, 0.00, 0.98] Q(X)=[0.02,0.00,0.98],计算KL散度 D K L ( p ∣ ∣ q ) = ∑ i = 1 n p ( x i ) l o g ( p ( x i ) q ( x i ) ) = p ( x 1 ) l o g ( p ( x 1 ) q ( x 1 ) ) + p ( x 2 ) l o g ( p ( x 2 ) q ( x 2 ) ) + p ( x 3 ) l o g ( p ( x 3 ) q ( x 3 ) ) = 1 × l o g ( 1 0.98 ) = 0.0088 \begin{aligned} D_{KL}(p||q) &= \sum_{i=1}^n p(x_i)log(\frac{p(x_i)}{q(x_i)})\\ &=p(x_1)log \big(\frac{p(x_1)}{q(x_1)}\big)+ p(x_2)log \big(\frac{p(x_2)}{q(x_2)}\big) + p(x_3)log \big(\frac{p(x_3)}{q(x_3)}\big)\\ &= 1 \times log \big(\frac{1}{0.98}\big) \\ &= 0.0088 \end{aligned} DKL(pq)=i=1np(xi)log(q(xi)p(xi))=p(x1)log(q(x1)p(x1))+p(x2)log(q(x2)p(x2))+p(x3)log(q(x3)p(x3))=1×log(0.981)=0.0088
KL散度越小,表示两个分布越接近。为啥要讲这个KL散度,因为KL散度可以拆成交叉熵和信息熵,而信息熵实际是个常量(lable是固定的)。交叉熵就是个简化的KL散度呀。我们实际上就是用的KL散度——简化成交叉熵——来训练的神经网络,让输出的分布接近真实分布(标签)。我来拆给大家看 D K L ( p ∣ ∣ q ) = ∑ i = 1 n p ( x i ) l o g ( p ( x i ) q ( x i ) ) = ∑ i n p ( x i ) l o g ( p ( x i ) ) − ∑ i n p ( x i ) l o g ( q ( x i ) ) = − H ( p ( x ) ) + [ − ∑ i n p ( x i ) l o g ( q ( x i ) ) ] \begin{aligned} D_{KL}(p||q) &= \sum_{i=1}^n p(x_i)log(\frac{p(x_i)}{q(x_i)})\\ &=\sum_i^n p(x_i)log \big(p(x_i)\big) - \sum_i^n p(x_i)log \big(q(x_i)\big) \\ &= -H(p(x)) + \big[- \sum_i^n p(x_i)log \big(q(x_i)\big)\big] \end{aligned} DKL(pq)=i=1np(xi)log(q(xi)p(xi))=inp(xi)log(p(xi))inp(xi)log(q(xi))=H(p(x))+[inp(xi)log(q(xi))]
结论就是KL散度 = 交叉熵 -(信息)熵

Show me the codes

前文我们说到,pytorch中的CrossEntroyLoss是LogSoftmax + NLLLoss。我们用代码验证一下

import torch.nn as nn
import torch
import math

def softmax_(input_x):
    x_exp = [math.exp(i) for i in input_x]
    sum_x_exp = sum(x_exp)
    # softmax_result = [round(i / sum_x_exp, 4) for i in x_exp]
    softmax_result = [(i / sum_x_exp) for i in x_exp]
    # convert to tensor
    softmax_result = torch.tensor(softmax_result, dtype = torch.float)
    return softmax_result
    
def log_softmax_(input_x):
    softmax_value = softmax_(input_x)
    log_softmax_result = [math.log(i) for i in softmax_value]
    # convert to tensor
    log_softmax_result = torch.tensor(log_softmax_result, dtype = torch.float)
    return log_softmax_result

def NLLLoss_(input_x, target):
    # NLLLoss needs target and its log likelihood values
    # target is the label (or index) of input_x, choose that value
    return -input_x[0][target]

def printHead(Head):
    print('=================================================')
    print('=================='+Head)
    print('=================================================')
    
if __name__=="__main__":

    input_x = list(range(1,4))
    input_x_tensor = torch.reshape(torch.tensor(input_x, dtype = torch.float),(1,len(input_x)))
    
    # softmax by define
    output_x = softmax_(input_x)
    
    # softmax in torch
    softmax_torch = nn.Softmax(dim=1)
    output_x_tensor = softmax_torch(input_x_tensor)
    printHead('Result of softmax compare: ')
    print('mine: ', output_x)
    print('pytorch: ', output_x_tensor)
    print('\n')
    # log softmax by define
    output_x = log_softmax_(input_x)
    
    # log softmax in torch
    logsoftmax_torch = nn.LogSoftmax(dim = 1)
    output_x_tensor = logsoftmax_torch(input_x_tensor)
    printHead('Result of logsoftmax compare: ')
    print('mine: ', output_x)
    print('pytorch: ', output_x_tensor)
    print('\n')
    
    # NLLLoss by define
    target =  torch.empty(1, dtype=torch.long).random_(len(input_x))
    # print(target, len(output_x_tensor))
    NLLLoss_value = NLLLoss_(output_x_tensor, target)
    
    # NLLLoss by torch
    loss = nn.NLLLoss()
    output  = loss(output_x_tensor, target)
    printHead('Result of NLLLoss compare: ')
    print('mine NLLLoss: ', NLLLoss_value)
    print('pytorch NLLLoss: ', output)
    
    # crossentropy
    m3 = nn.CrossEntropyLoss()
    o3 = m3(input_x_tensor, target)
    print('CrossEntropyLoss Result: ', o3)
    
    ''' minibatch input 
    N = 2
    C = 3

    input = torch.randn(2,3) # N*C
    target = torch.empty(N, dtype=torch.long).random_(C)
    
    o3 = m3(input, target)
    '''

Reference


  1. 官方文档 SOFTMAX ↩︎

  2. 官方文档 NLLLoss ↩︎ ↩︎ ↩︎

  3. What is the difference between probability and likelihood, Jason Eisner ↩︎

  4. Understanding softmax and the negative log-likelihood,LJ MIRANDA, 2017 ↩︎ ↩︎

  5. 二项分布Wiki ↩︎

  6. 官方文档 CrossEntropyLoss ↩︎

  7. 相对熵 ↩︎

你可能感兴趣的:(pytorch)