损失函数串讲

本来是不想起这么大而空的名字的,但是今天确实特别自闭,所以想来个刺激一点标题刺激一下自己,也刺激一下别人。

时至今日,距离上一篇blog更新已经过去快九个月了,距离我在MSRA实习已经快八个月了,这么一看好像我的blog是为了找实习而准备的一样 实际上是公司活儿太多了 ,为了不让blog继续这么僵下去,还是决定来更新点东西。

其实也有很多东西可以写,比如对tf和pytorch的认识,一些对CRF及其decode的理解,一些BERT调参心得,或者Nested NER或GNN方面的survey。之所以确定写这个,一个是好久没碰技术型blog了,一时甚至不知道怎么来写,另一方面纯粹是因为我发现每次要用到交叉熵的时候,都需要跑到别人博客里重新看一遍这到底是什么玩意儿,忘性太大。后来我一想,反正都要看,那我不如自己写个blog,自产自销搞个闭环,增加自己的点击量呢。

是为序。

损失函数及其意义

损失函数,顾名思义就是衡量损失的函数。比如小明家里进了小偷,小明报了警,警察来了以后小明就抹着眼泪和警察说哪些东西不见了,价值是多少。这时候警察脑子里就有个损失函数:电脑没了+5000,电视没了+3000,拖鞋没了+30,肥皂没了+8,最后小明的损失就是8838。在这个参照下,损失函数的计算逻辑是加和,损失的参照对象是被偷之前的小明。

换言之,损失函数实际上是衡量两个分布之间的距离的。其中一个分布应当是原始分布,或者正确的分布(ground truth),而另一个分布则是目前的分布,或者模型拟合的分布(prediction)。在以上情况中,正确的分布是快乐的小明,目前的分布是悲伤的小明。

从机器学习的角度来考虑,假设小明想要训练一个模型进行猫狗的图片分类,在获得了一个自己比较满意的模型后,他兴冲冲地跑去找小红。小红把她家的茶杯犬往模型前面一放,模型认为这只生物有40%的概率是狗,60%的概率是猫(我们假设模型的输出是经过softmax的)。因为这是个分类模型,所以它最后告诉小红这是猫,小红气得当场就哭了。

小明沮丧地跑回家,继续迭代它的模型。


在小明继续他的工作之前,我们先简单总结一下损失函数需要具备的特点:

  • 首先,损失函数是一个能够计算距离的函数,它的输入应该是两个分布,输出应当是一个值,即这两个分布之间的距离(损失)。
  • 在某些情况下,损失函数被期许是一个凸函数:因为凸函数具有唯一的全局最优解,同时不存在局部最优解。换言之,如果你使用梯度下降来试图减少一个凸性损失函数的值,它必将带着你走到最终的全局解。
  • 在某些情况下,损失函数被期许对某些比较难分辨的类别具有更好的识别效果。换言之,对于类别不均衡的问题,这样的损失函数将提升模型在少数类样本上的学习效果。
  • 在某些情况下,损失函数被期许能同时考虑多个类别,而不仅仅计算对某一个类别的距离。例如模型能够识别出一个人既是男性,又是长者,又戴着眼镜,损失函数需要以某种方式聚合这些类的损失。

因此,在这篇blog中 预计 会出现的损失函数有以下几个:

  • 均方误差系列损失函数(MSE / RMSE / MAE / Smooth L1)
  • 适用于多分类问题的交叉熵(Cross Entropy)
  • 适用于多标签分类问题的交叉熵(Sigmoid with CE / BCE)
  • 适用于类别不平衡问题的交叉熵(Focal Loss)

本来想简单说一下模型的评价指标的(confusion matrix / $R^2$ 之类的东西),但是现在已经五点多了,一会儿还有SKT打G2的比赛,不知道能不能写到那时候,干脆就先搁着吧。

小明的模型迭代之旅

1. 三天前的小明与逻辑回归

正如本科阶段的我一样,三天前的小明在听说机器学习和它的应用之后倍加振奋,似乎发现了振兴中华的秘诀。小明想,我是不是可以写一个分类的模型,来帮我判断一个女孩好不好追呢?

拍了拍脑袋,小明先提出了两个可能会影响女孩是否好追的因素:

  1. 身高
  2. 体重

这个模型应该输入一个女孩的身高$x_1$与体重$x_2$,然后输出这个女孩是不是容易追求$y$。这是小明的建模目标。

有了自变量与因变量,小明马上想到了数学老师昨天刚教的二元一次方程:如果我自己定义两个系数$\alpha_1$与$\alpha_2$,构建一个方程$y=\alpha_1x_1+\alpha_2x_2+\beta$,如果y的输出值大于0,我就认为这个女孩比较好追,否则就不好追,这不就是一个最简单的分类模型吗?这个模型简单又实用,可不能告诉别人。

BTW, 我没记错的话,这个模型应该叫 线性回归

小明输入了班级里几位女生的数据 ??? 作为训练样本,兴致勃勃地手动标了她们的标签:对于好追的女孩,她的y值是1,不好追的女孩y值为0。现在这个模型应该输出一个0到1之间的值,来衡量一个女孩的好追程度。但是一个线性函数的因变量与自变量是正相关的,怎么才能 把一个线性函数的输出映射到01之间 呢?

一个简单的方式就是 在外面再套一个映射函数

给出前提:如果这个模型输出大于0的值,我们就认为模型将一个女孩预测为比较好追,如果这个值越趋向于正无穷,则这个女孩就越好追;反之亦然。一个sigmoid函数就能实现这个功能:

上式的$x$就是模型的输出。因变量$y$随着自变量$x$的变化会呈现下图的变化:

Sigmoid

这是一个标准的逻辑回归模型,当模型输出0的时候,输出的取值正好是0.5,即这个女孩介于好追与不好追之间。此外,模型的设计者还能通过增加正则项的方式来手动改变模型的输出大小,即使用一个阈值来控制模型更倾向于将一个女生判断为好追型还是不好追型。

接下来就要设计一个损失函数,以衡量模型的输出结果距离正确值的距离。一个最简单的思路是,直接把模型预测值与正确值相减,取绝对值做累加,所获得的总和取平均就是模型在这组样本上的损失。在此预期下,该损失被称为 L1-norm ,也被称为 MAE (平均绝对误差),它的公式形式为:

其中$\hat{Y}$是模型的输出,$Y$是样本的真实值,样本数量为$m$个。和它一样憨的另一个可选损失函数叫 L2-norm ,也被称为 MSE (均方误差),其公式形式为:

两者的差异仅在于计算真实与预测的差值之后是否要取个平方,看起来似乎相差不大,但是在具体使用中,还要根据情况来选择使用哪一个损失函数:从直观上来讲,MSE的求值方式决定了它的值会更容易被异常点所影响。具体来说:

  • 如果模型的预测值与正确值是 完全一致 的,则相减为0,它不会给这两个损失函数指导下的学习过程带来任何损失;
  • 如果模型的预测值与正确值不一致,但是 相差值不超过1 ,平方之后这个值会被进一步缩小:由此看来MSE会倾向于缩小一个与正确值相差较小的预测值给整个模型带来的损失。
  • 如果模型的预测值与正确值不一致,并且 相差值超过了1 ,则这个差值在MSE中会被进一步放大,极端而言,如果一个离群点与正确值相差很远(当然距离肯定超过了1),即便最终的损失值取了均值,这个值仍会成为其中主要的损失来源。

在此分析基础上,我们可以得出一个结论:如果在模型训练过程中,更期望它能尝试把所有的点尽量分对,那可以选择MSE损失;而如果实际业务中允许忽略一部分离群点,不要求对整体的准确度,则可以选择MAE损失。

我们还能从另一个角度比较一下这两个函数:是否容易求导及求导后的梯度。这里部分使用了来自 知乎文章 此处的图片,以便直接说明(如有侵权,我会马上删掉)。

给出预测值变化下的损失变化图:

Sigmoid

左边是MAE的预测值和误差值的变化关系,右边是MSE的变化。容易观察到,在学习过程中,MAE的导数是始终不变的,即在学习(梯度下降)过程中,无论预测值距离正确值的差距多大,每次的下降方向不会改变。如果我们选择一个固定的学习率,在预测值与正确值比较接近的情况下,很可能会发生振荡。而右图的MSE在预测值趋近与正确值时,它的方向也会趋近平缓,这样的MSE天然具有模型学习上的亲近性。而前者则需要设置动态的学习率。

最后也简单提一句 RMSE (均方根误差)的公式及使用:

和MSE相比,它在公式上只多了一个全局的根号,在名称上也只多了一个根号(R-ROOT)。在使用上,RMSE能够完全保留误差所具有的意义,在需要理解模型误差含义时能够被很好地使用。在好追程度模型上这个意义并不明晰,我们假设小明想要训练一个模型预测人的生存年龄。如果使用了RMSE,小明可以对小红说:我的模型的平均误差现在是6.3年;而如果使用MSE,小明对小红说:我的模型现在平均误差是39.69平方年,小红一想什么玩意儿,就不和小明继续玩了。


在图像处理领域也有一系列其他损失函数的提出,它们或多或少解决了上面所提到部分问题,我们走马观花一下:

FastRCNN 中使用的是 Smooth L1-loss 。与一般的L1-loss(MAE)而言,它对这个损失函数做了一个平滑操作,以使它在0点处的导数更加平滑,不会影响模型的收敛效果:

求导得到:

这个损失函数尽管被称为Smooth L1,实际上更像是对L1和L2的结合:在正负1区间内,使用L2的形式,使得导数平滑;在大于1区间,使用L1的形式,使得损失不至于因为少数离群点而呈现出指数爆炸的形式。

正则化 也是对两种loss进行魔改的一个好用的技巧。简单来说,我们在原本的loss基础上后面跟一个与模型复杂程度相关的正则项,这样能够有效防止因为模型设计得过于复杂,loss很低但在实际应用时却效果很差的过拟合现象。

2. 一天前的小明与卷积神经网络

在尝试了两天手撸模型之后,小明终于放弃了。他上网了解到scikit-learn上已经存在封装好的逻辑回归模型,只需要使用 LogisticRegression 来调用就好了,他顿时对实现这个模型感到兴致缺缺。另外,他也同时意识到一个很严重的问题:正如硬币落地前你就会知道你的选择一样,在标数据的时候,他已经对班级里的女孩做出了想要的筛选。 那我还要这个模型干嘛呢 ,小明这样想。

好追程度分类模型的构建无疾而终,小明觉得自己间接地导致了人类文明无法更进一步,心里十分愧疚,但他很快把注意力转移到了自己标注的数据上。在所有的女孩子里,小红是最不好追的那一个,同时身高与体重也十分符合自己的理想型。

在机器学习引领时代潮流的今天,小明觉得自己有必要搞一个有应用价值的模型来向心仪的女孩宣示自己的学习能力与未来潜力,他看中了一个toy dataset: MNIST。因为逻辑回归在炫技层面的价值实在不值一哂,而CNN family所代表的一系列图像分类模型能够直观地展现出模型的落地价值,他决定依葫芦画瓢,搞一个图像分类模型出来。

这次他已经充分了解到:使用pytorch或keras等深度学习框架,就能像搭乐高一样搭出一个想要的模型来。他写下以下几行:

import torch.nn as nn
nn.Sequential(
  nn.Conv2d(in_channels=3, out_channels=128, kernel_size=3)
  nn.Relu(),
  nn.Dropout(0.3),
  nn.Linear(128, 2)
)

乐高真好玩,他想。

这个模型旨在输入一个$32\times32\times3$的小图像,模型将会输出这个模型属于猫狗之间的哪个类。

又到了选择损失函数的时候,这次小明选择的是 Cross Entropy Loss ,中文称其为 交叉熵 损失。作为一个损失函数,交叉熵(along with Adam optimizer)是最不容易出问题的那种:它可以在多分类问题中被使用,也可以在多标签问题中使用,在特殊情况下也能够手动指定每个类别对最终损失所应该贡献的权重。

import torch.nn as nn
loss = nn.CrossEntropyLoss()

炼金术士的生活,就是这么的枯燥,无聊,且乏味。


让我们把小明丢在一旁,仔细理解一下交叉熵的理论逻辑:

熵是信息论中的一个概念,用来衡量一种事物的混乱程度,也即随机变量的不确定程度。在建模过程中,我们往往需要衡量的是两个分布(正确的分布与预测得到的分布)之间的混乱程度,所以我们需要用到相对熵(relative entropy,也叫KL散度)。

对于一个随机变量,我们怎么衡量它的不确定程度呢?考虑一个模型对猫狗进行分类,输入一个样本之后,它认为这个样本有40%的概率是猫,有60%的概率是狗,模型的形式化输出为:

这是一个一维向量,第一个元素表征它是猫的概率,第二个元素表征它是狗的概率,我们提前保证了输出的所有概率加和为1,这才符合一个分布的现实情况(这往往通过一个softmax函数来实现)。怎么理解这个输出所带有的不确定程度呢?

从零开始放弃建模系列公式一:信息熵

H(x)表示一个分布的熵,也就是这个分布的混乱程度。$p(x_i)$就是每个预测结果所对应的概率值。用这个公式来计算上述例子中的值,可以得到:

即这个分布的熵值是0.673。因为熵表征的是一个分布的混乱程度,所以它的值越高说明分布越混乱,也就是预测的不确定程度越强。

我们假设这个样本的实际类别是猫,也即其正确值是:

这个分布的熵是:

可以看到,一个正确的分布所具有的熵值为0,这符合我们对熵这个概念目前的认知。简单推广一下,即便我们的模型预测的是$n$分类,在正确值的向量中也只会有一个元素是1,因此熵值永远都是0。

直觉上来说,一个最混乱的分布应该是所有的类别拥有相同的概率,即:

其熵值为:

也就是说这个分布最混乱的情况下,熵的上限是0.693(值得注意的是,根据输出类别$n$的不同,这个值上限是$-\text{log}\frac{1}{n}$)。而我们之前预测的信息熵值为0.673,可以看到已经无限接近于这个上限了,这说明这个输出的不确定程度很高,很难作为真正的参考。显而易见的,如果我们以熵来衡量一个分布,我们的建模目标应当是尽可能最小化模型预测结果的熵值。

以上我们了解了如何衡量一个分布的不确定程度,那应该如何衡量两个分布之间的差距,以作为我们的损失函数呢?这里需要引入 相对熵 的概念。

从零开始放弃建模系列公式二:相对熵/KL散度

以下是相对熵的形式化表示:

这里需要明确的一点是,$p(x)$表示真实事件的概率分布,而$q(x)$表示模型预测得到的概率分布。

我们把它按加和情况展开:

根据 从零开始放弃建模系列公式一,这个展开项第一项就是真实分布的信息熵$H(p)$(的负数),也就是定值0(只有在多分类问题中才是0,在其他情况下会是一个其他定值)。由此,在学习过程中我们可以忽略这一项,因为相对熵的值只取决于后面一项:$-\sum_{i=1}^N[p(x_i)\;\text{log}q(x_i)]$。而这一项,就是大家常说的 交叉熵

最后给出交叉熵的形式化公式:

从零开始放弃建模系列公式三:交叉熵

显而易见,如果我们的预测结果$q(x)$与数据的真实分布$p(x)$完全一致,那这个式子就会退化为数据的真实分布,也就是0。而如果是我们上面例子中的情况,即40%猫、60%狗的交叉熵值,则有:

实在是有点大。

另外,也可以观察出,如果是多分类的问题中,其实只有真实值那一个元素所在的输出需要被考虑,因为其他项的系数都是0.0。不过,正如上面所说,我们已经把所有的概率加和控制为1,如果真实值项的概率上升,其他项的概率自然就会下降,所以也不足多虑。

3. 今天的小明与茶杯犬

小明在自己喜欢的女孩面前丢人现眼之后跑回家里,痛定思痛,发现自己模型准确度不高的原因在于训练样本太少,这也正是难倒无数调参英雄汉的难题:标注数据的缺失。在训练自己的模型时,小明拍遍了整条街道的猫猫狗狗,但似乎对于茶杯犬这种形状特意的犬种辨析度不够——因为遍寻整个街道,只有小红家有茶杯犬。

只拍下了茶杯犬随地*时候的照片这一问题,也对模型学习到正确的样本特征提出了挑战。

数据样本少,小明可以使用数据增强,但是对茶杯犬这种形似噪音的样本该怎么学习呢?如何修改loss,才能使这种噪音的样本权重更大呢?求助Bing,小明知道了Focal Loss。


Focal Loss的提出并不旨在直接解决噪音样本的问题,但是他确实在某种程度上起到了类似MSE一样的 放大噪音样本所产生的损失 的作用,而它达到这个目的的方式是 缩小分类正确的样本所产生的损失 。让我们详细了解一下这个损失函数的提出背景。

在Object Detection任务中,一个一步到位(one-stage)的目标检测模型所做的工作是:首先在一张图片中画出很多的方框,这些方框可以根据某种规则或者特征指导下画出很多个,随后我们判断这些方框中,哪些包含了我们需要识别的目标,把包含了识别目标的框拿出来,作为模型预测的目标所在的位置。

用下面的图片来描述这一任务:

One-stage Object Detection

首先我们在图中标出了包括红黄两种颜色在内的许多方框(bounding box),注意黄色方框其实有很多个,图中只是给出了少量作为举例。

在大量的bbox中,只有红色所在的是正确的目标,大量黄色的框都是错误的目标。但是值得注意的是,图中左边两个黄色的框是介于背景图与猫之中的,这两个框比较难以识别,在学习过程中需要重点关注这些框。

然而,在所有的candidates中,占绝大多数数量(超过99.9%)的框都是像右边的黄色框一样,框出了毫无意义的背景图,这种框在学习过程中必须被标注为‘没检测到目标’。

值得庆幸的是,这样的框确实很容易被模型识别为无意义的目标(即负样本),因为它和真实样本之间的差别实在是太大了。但是问题是:这样的框实在太多了! 我们可以直观上理解:即使这样样本被判断为负样本,它仍会提供非常小的损失值(因为模型不可能百分之百的判断这个样本是负样本,只能无限趋近于这个结论),而这么小的损失结合很大的基数,它仍会在整个损失里占到相当大的一部分,甚至影响模型的效果。

出于以上的考虑,我们提出这样的期望:对于一个已经被很笃定判断为一个类别的值,尽管它还没有完全到1,但是我们还是应该降低它所产生的损失值;确切的说,我们希望一个样本的分类结果越趋向于1(同时它确实被分对了)的话,这个样本的损失越小越好。

Focal Loss的提出就是为了解决大量负样本影响整体损失的问题。

我们给出Focal Loss的形式化描述:从零开始放弃建模系列公式四:Focal Loss

为了方便比较,也直接给出刚才所提到的交叉熵公式:

这样就非常好理解Focal Loss与Cross Entropy之间的区别了:它只是在交叉熵损失前面加了两个系数$\alpha$和$\gamma$。我们接下来来理解这两个系数是如何达到上文所说的目的的:

  • 对于 $\alpha$ ,这仅仅只是一个平滑系数,它控制了整个损失应该更突出还是更平滑,也即在图中这个损失的曲线是越陡峭还是越平缓。我对这个系数的理解是用来缓冲后面那个系数太过强烈的控制作用的。
  • $\gamma$ 所在项才是达到目的最重要的项。首先,$(1-p_i)$已经完全达到了上文所说的缩小正确样本损失的目的:一个被分对的样本,它的$p_i$值应当趋近于1,因此$(1-p_i)$这一系数项就能把它的损失给缩小。这个值越趋向于1,这个系数就越小,这个样本所产生的损失也就越小。——但是这样还不够:可能这样设置的系数数值上不太合适当前任务,所以我们需要用一个乘方系数$\gamma$来控制这个系数的大小。比如负样本太多的话,我们可以设置乘方系数为2,由此这个系数会更小,对整体造成更小的损失。
  • 再反过来看 $\alpha$,它和$\gamma$一起影响了前面的系数,而对于较大的$\gamma$,我们需要把$\alpha$调小,来使得整体的损失曲线更加平滑。

BTW, Focal Loss的paper中,作者自己调出来的比较好的设置是$\alpha=0.25,\;\gamma=2.0$。


Focal Loss被用在拥有大量负样本的情况下,但是根据上述的分析,它同样可以被用在我们需要把一些被分得很对的样本的损失调低,从而让模型更好地学习其他比较难以学习的样本的情况下。

小明就使用了这个loss好好调教了一下茶杯犬分类模型,并且选择性调大了狗这一类的损失(在封装好的Cross Entropy Loss中可以通过传入weight来控制每一类的样本的损失),以更好地识别出茶杯犬。而就在这时,他再次意识到了一个很严肃 且关乎炼金术士的操守 的问题:如果我只想在小红 甲方 面前展现更好的模型效果,把茶杯犬 某些样本 分对,我直接拿这个数据来训练,直接整个过拟合的模型出来,岂不是万无一失 蒙混过关

Appendix

从上面的故事中,我们可以得到的结论是:小明是一个不合格的调参工程师 但很真实地反映了我的机器学习入门历程

此外,值得一提的是,Cross Entropy也经常被用于多标签模型的学习中。上文中所说的Object Detection任务就是一个典型的多标签任务:因为一张图片中往往不只有猫,有时也有狗,这样我们的正确标签可能是$(1.0, 1.0)$,这种情况下,我们需要做以下改动:

  • 首先,我们要取消形似softmax的归一化层,因为这些类不是互斥的,让它们的概率加和为1不存在任何意义;
  • 其次,我们仍要把每一类放缩到0到1之间,因为只有这样的值才具备概率的意义;
  • 最后,损失函数应当要把这些损失值全部加起来,而不是像上文一样只考虑正确类别的概率值(事实上Cross Entropy本身就支持这个操作)。

由逻辑回归模型得到的启发,我们可以把拼接在最后的分类结果后面的softmax层取消(在目前大多数的深度学习框架中,softmax已经被集成在Cross Entropy系列的损失函数中 - 如上文中的 nn.CrossEntropy() ,在使用这些损失函数时,一定要注意是否里面有那么一个softmax),随后把每类的概率分别过一个sigmoid层,放缩到01之间,最后使用一个阈值(比如0.5)来判断分类模型所属出的这一类的值应当是0还是1。

在实际的框架中,一般使用sigmoid+BCELoss来实现这个结果。