损失函数:交叉熵详解

2020年10月17日 作者 yabobet 0

更多精彩尽在这里,详情点击:http://novotimes.com/,NBA波士顿凯尔特人

交叉熵和KL散度在机器学习中非常有用,随处可见。 例如,我们可能希望预测的概率分布接近我们观察到的数据的分布,也就是说,我们希望一种分布(可以是概率向量)与另一种分布接近, 而交叉熵和KL散度为我们提供了一种自然的方法测量两个分布之间的差距,这个差距就可以被当作损失函数。

那么,交叉熵作为损失函数来源于哪里呢?为什么它可以用来计算两个分布之间的差距?本文深入探究一下。其实,介绍交叉熵的文章应该有非常多,我再讲一遍好像多余了。不过,我在最近一年半的时间里面试了不少人,发现还是有很多人对交叉熵只知其然而不知其所以然,一知半解。难道他们没读对东西或者老师没讲清楚?这篇文章就当作是拾遗补缺吧。

我们先从熵的来历讲起,再引出交叉熵以及交叉熵如何成为损失函数。最后举两个例子说明Sklearn里的log_loss( )是如何计算交叉熵的。

话说某君鲍勃非常喜欢动物,他也经常和外地的朋友谈论动物。 假设他只说四个字:“狗”,“猫”,“鱼”和“鸟”,为了降低与朋友之间的通信费用(注:英文的每个字由多个字母构成,每个字母需要用一个字节即8比特来表示),他把这四个动物名编成二进制代码如下:

因为一共有4种动物,所以每一种动物用2位二进制的代码(即2比特)就完全表达了()。每一个代码称为一个码字(codeword),它和一个动物名一一对应。这样每当他要告诉远方的朋友他要谈哪个动物时,他就只发送对应的码字。由于有一一对应的关系,所以在接收端,他的朋友知道如何解码,即把码字还原成动物名。显然,平均每个码字长为2比特。

如果鲍勃非常喜欢“狗”,因而也谈论得多一些,其次他喜欢“猫”,再其次是”鱼“和”鸟“,也就是说,我们知道他每次和朋友交流时提到这四种动物名的频率是不一样的,频率越高的字出现的概率越大,如下图所示。就是动物名在谈话中出现的概率。

那么,我们是否可以利用这个信息来进一步压缩平均每个码字所需要的比特数呢?比如下面这个编码方案:

对于出现频率大的动物名,我们用尽量少的比特数来表示它,但必须保证不出现模棱两可的情况,即码字和动物名必须保持一一对应。我们发送的代码和码字如下:

那么这个编码方案的平均每个码字的长度是多少呢?应该等于每个码字出现的概率乘上该码字的长度,再把所有的码字的这个乘积加起来,即计算它们的加权和。比如上例:

注意,制作出并非唯一可解码的编码方案是很有可能的。 例如,假设0和01都是码字,那么我们就不清楚编码字符串0100111的第一个码字是什么,因为可能是0,也可能是01。 我们想要的属性是,任何码字都不应该是另一个码字的前缀。 这称为前缀属性,遵守该属性的编码称为前缀编码。

考虑前缀属性的一种有用方法是,每个代码字都需要牺牲可能的码字空间。 如果我们使用码字01,则会失去使用其前缀的任何其它码字的能力。 比如我们无法再使用010或011010110,因为会有二义性。那么,当我们确定下来一个码字,我们会丢失多少码字空间呢?

以下图为例。01是我们选定的一个码字,以它为前缀的码字空间占整个码字空间的,这就是我们损失的空间,是我们选择2比特(01)作为码字的成本。其它码字只能用3比特或更多比特的编码了(如果需要编码的词多于4)。总的来说,如果我们选择长度为的码字,那么我们需要付出的代价就是。

假设我们愿意为长度为的代码付出的代价是,那么,。换算一下,长度与付出的代价的关系是。

那么我们愿意或者说应该如何为不同长度的代码分配成本预算从而获得最短平均编码呢?有一种特别自然的方法可以做到这一点:根据事件的普遍程度来分配我们的预算。所以,如果一个事件发生了50%的时间,我们应该花费50%的预算为它买一个简短的代码。但是,如果一个事件只发生1%的时间,我们只花1%的预算,因为我们不太在乎代码是否长。

所以,为一个代码分配的成本预算与该代码出现的概率成正比。我们就让等于这个概率好了。这样,只要知道每个码字出现的概率而无需知道具体的编码方案,我们就可以计算编码的平均长度是:

我们从下图也可以直观地得到编码方案的熵,它就是图中各矩形的面积总和。可以看到,如果不用最佳编码,那么它的熵就比较大。

在信息论里,被信息论的创始人香农定义为事件的自信息,即一个概率为的事件具有的信息量,单位是比特。熵就是所有事件的自信息的加权和,即这些事件的自信息的平均值。

熵也反应了这些事件的不确定度。熵越大,事件越不确定。如果一个事件的发生概率为1,那么它的熵为0,即它是确定性的事件。如果我们确定会发生什么,就完全不必发送消息了!结果越不确定,熵越大,平均而言,当我发现发生了什么时,我获得的信息就越多。

如果有两件事各以50%的概率发生,那么只需发送1比特的消息就够了。 但是,如果有64种不同的事情以相同的概率发生,那么我们将不得不发送6比特的消息(2的6次方等于64)。 概率越集中,我就越能用巧妙的代码编写出平均长度很短的消息。可能性越分散,我的信息就必须越长。

再举个例子。在决策树分类算法里,我们需要计算叶子节点的impurity(非纯洁度)。我们可以用熵来计算非纯洁度,而且比使用基尼指数计算更容易理解:一个叶子越纯,里面的分类越少,确定性越大,它的熵越小。如果一个叶子里的观察值都属于同一类,即它是完全纯的,那么这是一个确定性事件,它的概率等于1,所以它的熵为0。不纯的叶子的熵大于0。

有某女爱丽丝不是爱狗而是个爱猫的人。她和鲍勃说相同的词,只是词的频率不同。 鲍勃一直在谈论狗,而爱丽丝一直在谈论猫。

最初,爱丽丝使用鲍勃的代码发送消息。 不幸的是,她的信息比需要的更长。 Bob的代码针对其概率分布进行了优化。 爱丽丝的概率分布不同,因此代码不理想。 当鲍勃使用自己的代码时,一个码字的平均长度为1.75位,而当爱丽丝使用其代码时,则为2.25位。 如果两者不太相似,那就更糟了!

该长度(把来自一个分布q的消息使用另一个分布p的最佳代码传达的平均消息长度)称为交叉熵。 形式上,我们可以将交叉熵定义为:

那么,为什么要关心交叉熵呢? 这是因为,交叉熵为我们提供了一种表达两种概率分布的差异的方法。和的分布越不相同,相对于的交叉熵将越大于的熵。

真正有趣的是熵和交叉熵之间的差。 这个差可以告诉我们,由于我们使用了针对另一个分布而优化的代码使得我们的消息变长了多少。 如果两个分布相同,则该差异将为零。 差增加,则消息的长度也增加。

我们称这种差异为Kullback-Leibler散度,或简称为KL散度。相对的KL散度可定义为:

KL散度的真正妙处在于它就像两个分布之间的距离,即KL散度可以衡量它们有多不同!

交叉熵常用来作为分类器的损失函数。不过,其它类型的模型也可能用它做损失函数,比如生成式模型。

我们有数据集,其中,是特征值或输入变量;是观察值,也是我们期待的模型的输出,最简单的情况是它只有两个离散值的取值,比如,或者,或者。能根据新的对做出预测的模型就是我们常用的二元分类器(Binary Classifier)。

我刻意没有选用作为例子,是为了避免有人认为我们观察到的数据就是概率向量,因而可以直接套用和模型的输出之间的交叉熵作为损失函数。实际上,我们可以使用交叉熵作为分类器的损失函数的根本原因是我们使用了最大似然法,即我们通过在数据集上施用最大似然法则从而得到了与交叉熵一致的目标函数(或者损失函数)。我们观察原始数据时是看不到概率的,即使,它的取值0或1只是客观上的观察值而已,其概率意义是我们后来人为地加给它的。

先看一下Wikipedia里的似然函数的定义。注意,我用而不是代表观察到的随机变量。

如果由参数向量所决定,那么,在我们得到(或者说观察到)的一些具体取值的集合后,在这些观察值上的似然函数就是:

首先,是的似然函数,当我们固定的时候,它是的函数,即自变量是。不同的对应的也不同,从而在不变时也不同。

特别要注意似然和概率的区别。求概率的时候我们要在固定分布参数时计算,这与求似然值是完全不同的。

最大似然法就是通过最大化获得,或者说寻找使一组固定的观察值最可能出现的,亦即使最大的那个参数。

不过要注意,从模型的角度来看,找到不是最终目的,找的过程(即模型的训练)更重要。模型以找到为目标,不断去拟合数据点,使得它的输出离数据的观察值越来越近,直至近到我们满意为止,这时,模型就算训练好了。然后,这个模型针对任何一个新的输入值,就会产生符合训练数据集的分布的一个输出,此输出值即模型产生的预测值。

对于二元分类(binary classification),观察值的取值是二选一。无论实际观察值是什么,我们都用代替。显然,符合Bernoulli分布,而Bernoulli分布只有一个参数,即:

我们的数据集是。假设这些观察到的数据点都是的,那么它们被观察到的对数似然等于

似然函数就是我们的目标函数。如果在它前面加上负号,它就转变成损失函数。观察上式并对比交叉熵公式就可看出,这个损失函数就是与的交叉熵。

上面这个交叉熵公式也称为binary cross-entropy,即二元交叉熵。从的公式可以看到,它是所有数据点的交叉熵之和,亦即每个数据点的交叉熵是可以独立计算的。这一点很重要。

必须注意,只有当观察值的取值是1或0的时候我们才能获得交叉熵形式的损失函数。当然,我们完全可以为赋以概率意义,这样就可以理解交叉熵损失的含义。

而被数据集训练的模型试图估计出整个数据集之上的和,它对应每个输入数据点的输出是概率分布,比如,这个估计值与数据之间的差距就用它与数据之间的交叉熵来衡量。我们来计算一下它分别与和之差:

注: 如果观察值符合高斯分布,那么由最大似然法则可以推出最小二乘或均方差作为损失函数,经常用在回归类的任务中。有趣的是,使最小二乘最小时的预测值也是当前所有观察值的均值,这与交叉熵是一样的,虽然它们的表达式是那么的不同。

比如在Gradient Boost分类算法里,我们通过不断建新的决策树来拟合上一棵树的预测值与观测到的数据之间的残差(residuals)。 一开始看到这样的优化方法,我感觉它的思路很妙,因为它不是直接拟合数据而是拟合残差。但仔细研究它背后的理论推导,我们就能发现,它的优化残差的策略仍然来自数据的极大似然准则和交叉熵。下面简要解释一下。

是第个样本的观察值(比如观察到的是yes或no,分别被1或0取代,以赋予概率意义)。由于Gradient Boost模型的输出不是直接用预测的概率而是胜率(odds)的对数,即,我们利用和之间的关系:

我们要找新的输出,使得最小。但与一般的算法不同,Gradient Boost并不是一步到位就使最小,而是逐步使它越来越小。具体来说,我们仍然像求最小值那样,计算损失函数对预测值的导数(的下标是指第棵树,是指第个样本或数据点):

Gradient Boost的策略不是像我们一般优化一个函数那样,直接使,从而解出,而是让这里的等于上一次的预测概率,则相当于在上一次预测值处的梯度。接着,继续建新的决策树来拟合,使逐步逼近0,从而在最小化损失函数的同时避免模型过拟合。

而从上式我们看到,是我们观察到的概率(1或0)减去上一次预测的概率,即两者之间的残差。所以,Gradient Boost的优化残差的策略是使用最大似然准则和交叉熵损失函数的自然结果。

另一个以交叉熵为损失函数的例子是在MNIST数据集上的变分自编码器VAE。如果手写体数字图像的每个像素取值只为0或1,那么每个像素的分布就是Bernoulli分布,整个模型的输出就符合多变量Bernoulli分布。因此,解码器的损失函数可以使用输入图像(此时也我们期待的输出,相当于标签)与解码器的实际输出之间的交叉熵:

此处,是输出像素等于1时的概率,是输出数据(即模型的预测)的分布参数,相当于上面的。是一个数据点的维度。MNIST的等于28×28=784。

有时,我们不一定非得把定义成数据的分布参数。比如在logistic regression里,我们把输入向量的权重(即系数)向量当作参数,模型的输出为

然后求对的梯度。由于是一个含有多个参数的向量,我们对每个参数逐一求偏导。下面是模型从输入到输出的计算步骤,大家用导数的链式法则自己推导一下只有一个数据点时的梯度吧:

注:如果每次更新参数时使用全体数据或者一个batch的数据,那也可以是这些数据产生的梯度的均值。

即相当于给每一类一个的编号。我们观察到的的分布是multinomial distribution(多元分布),每个类对应的分布参数是:

如同在Bernoulli分布那样,为了用一个公式表达任意观察值的,我们先定义Indicator函数(有人翻译成”指示函数“):

其中,至中只有一项等于1,其它所有项都等于0。如果我们把这些项放到一个向量里:

那么这个维向量其实就是观察值的one-hot编码。我们在讲二元交叉熵时提到给one-hot编码赋予概率意义,所以这个向量就是观察到的数据的概率向量。

如果我们把的one-hot编码里的记作,那么也应加一个下标,变成,表明它是的概率。这样,的对数似然就可以写成:

在整个式子前面加上负号后,目标函数变身为损失函数,它与交叉熵的形式完全一样。在只看单个数据点时,这个多元交叉熵也叫categorical cross-entropy。是观察到的概率向量,而就是模型输出的概率向量。比如

小结一下。如果我们要找的数据分布符合Bernoulli Distribution或者Multinomial Distribution,那么我们通过最大似然准则可以推导出以交叉熵作为损失函数。不过在实践中,每当我们的数据标签是一个概率向量(一般是one-hot编码过的),模型的输出是同维度的另一个概率向量(一般把模型的输出向量经过Softmax归一化成概率向量),那么我们经常就直接使用两者的交叉熵或者KL散度作为损失函数,而不再理会数据的分布是什么了。这就如同最小二乘准则理论上只在高斯分布下与最大似然准则是等价的,但我们还是经常在非高斯分布下使用它作为最优化的准则,因为在非高斯分布下,最大似然法一般无法得到简洁的解析表达式。当然,这样做在性能上可能比最大似然差一些。

scikit-learn里的log_loss()可以用来计算交叉熵,它的计算公式与多元交叉熵完全一样:

因为参数labels=None,而predicted_label的shape是(3, ),所以log_loss认为样本数是3,即;并且此时只有2个分类,即。具体是哪2类要看true_label里面的值,这里只有0和1,所以2个分类就是类0和类1。根据这些推想得到的配置参数,log_loss开始构建矩阵和矩阵,以便利用上面的公式进行计算。

true_label = [0, 1, 0],看上去是一个一维的向量,log_loss认为它代表3个数据点的实际分类(即标签)。log_loss先把这个向量里的每一个元素(即它们代表的分类)按照编码成one-hot,接着按把这个向量写成矩阵:

predicted_label = [0.1, 0.9, 0]被当作3个数据点对应的正分类的概率预测值(不是单个数据点的属于不同类的概率),即对应分类1的概率;对应的负分类的概率则等于1减正分类的概率。同样,把它写成的矩阵:

我们可以自己编个binary cross-entropy的小程序验证一下这个计算是否是二元交叉熵:

注意,数据的标签targets里允许是其它数值,但只能有2个不同的数,比如8和1,这是因为log_loss对targets做了二值化的处理,把[1, 8, 1]变成[0, 1, 0]。我们的小程序也做了相应的处理。

但是,我们的小程序不支持以字符串表示的二分类,而log_loss是支持的。

true_label里只有一个元素,代表只有一个数据点,其标签属于分类1。predicted_label里面也只能有一个数据点,但它输出的是3个分类的概率,所以这3个元素外面必须有两层方括号,最外层的表示内层方括号里无论有几个元素,它们都属于一个数据点。所以,。因为此时true_label无法提供实际预测的分类信息,log_loss使用显式的预测分类参数labels=[0,1,2],表明predicted_label输出3个概率值[0.1,0.9,0]对应的是分类0,1,2。所以,。和例一描述的过程类似,我们构建两个的矩阵和后就可计算了:

注意,与不同,对于来说,[0.1,0.9,0]是单个数据点的多分类的概率。

最后留一个问题。TensorFlow里定义了很多个用于评价模型性能的metrics,其中有