深度学习模型优化,即优化网络权值使得该模型拟合数据的能力达到最优,而最优的一个标准是损失函数较小(兼顾训练数据和测试数据,以及实际应用场景的最优)。PyTorch中有很多损失函数,这里我主要介绍最常用的两种,NLLLoss和CrossEntropyLoss;而实际上CrossEntropyLoss更常用,NLLLoss与其的关系也会在本文中详细介绍。
要介绍上述两个损失函数的关系,得先从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个节点的值,如下图所示。
对output layer的值可以直接做Logsoftmax操作,该操作之后的值作为NLLLoss的输入。
NLLLoss的全称为Negative Log Likelihood Loss,负对数似然损失,是训练多分类问题的常用损失函数2。Likelihood想必大家都熟悉,是似然的意思,而最大似然估计(MLE,maximum likelihood estimation)是一种估计模型参数的方法。NLLLoss,刨去Negative和Log,因为这两个是取负数和取对数的数学操作,剩下的Likelihood Loss,就是这个损失函数的本质了。于是引申出一个问题——似然函数为什么可以作为模型的损失函数(这个问题大部分博文都没有详细讲,我来给大家抛砖引玉,如有理解不对的地方,请大伙儿批评指正)。
似然(函数)这一概念是由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的函数曲线。
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
损失函数用于衡量当前参数(神经网络模型中的weights和biases;高斯混合模型中的均值,方差,权重)下,模型的预测值和真实值(label或数据观测值)的差距。所以,我们希望损失函数越小越好。
我们规定,损失函数为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 N∗C, 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=1∑N∑n=1Nwyn1ln,n=1∑Nln,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。
更直观一点,如下图所示4,“马” 的预测概率值为 0.98 0.98 0.98,非常高,其对应的NLLLoss为 0.02 0.02 0.02,很小。
前文中提到似然函数的时候用抛硬币举例子,而多次抛硬币实际上是一个二项分布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 (1−p)n−k
而多项分布可以理解为掷色子,其概率质量函数为,
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越小,接近零。
看完负log似然损失函数,我们会发现,这个跟交叉熵形式一样啊,没错,实际上就是相通的。同一个事物的不同解释角度,殊途同归。
实在是懒得敲公式了,直接贴官网的图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)=−i∑nP(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)=−i∑np(xi)log(q(xi))
如果对于同一个随机变量 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(p∣∣q)=i=1∑np(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(p∣∣q)=i=1∑np(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(p∣∣q)=i=1∑np(xi)log(q(xi)p(xi))=i∑np(xi)log(p(xi))−i∑np(xi)log(q(xi))=−H(p(x))+[−i∑np(xi)log(q(xi))]
结论就是KL散度 = 交叉熵 -(信息)熵
前文我们说到,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)
'''
官方文档 SOFTMAX ↩︎
官方文档 NLLLoss ↩︎ ↩︎ ↩︎
What is the difference between probability and likelihood, Jason Eisner ↩︎
Understanding softmax and the negative log-likelihood,LJ MIRANDA, 2017 ↩︎ ↩︎
二项分布Wiki ↩︎
官方文档 CrossEntropyLoss ↩︎
相对熵 ↩︎