本文将整理常用的Loss函数,包括BinaryCrossentropy、CategoricalCrossentropy、SparseCategoricalCrossentropy、KLDivergence、Focal Loss、Circle Loss,结合数学公式以及代码实现层面,深入理解Loss函数及实现原理。
几句闲话
之前学习的时候,只从数学公式上理解。比如交叉熵为H(p) = -\sum p \log \hat p,其中p为样本的概率分布,\hat p为神经网络输出的概率分布,最小化交叉熵可以让模型的输出概率分布与样本的概率分布最接近,于是训练loss的过程就是让两个概率分布的距离逐渐减小。
但是深度学习框架通常给出的是y_true,y_pred
。开始学习的时候,只会依葫芦画瓢调用交叉熵的接口,于是构建模型开始训练,没有考虑具体的细节。看到苏神自定义Loss函数,写出来的代码当时根本看不懂,公式中的字母与代码关联不上,我才意识到,以前看似理解的问题,其实只是浮于表面。
这里说几句闲话的意思是学习算法,从原理入手,先学基本概念,从专业的教材开始看,之后再动手用框架搭建模型,有了理论基础,其实框架的接口基本上一目了然,感兴趣可以再看看实现原理,这样不容易走弯路。说了这么多,每个人的学习方法不同,这里只是个人建议而已。
常用的几种交叉熵
这里结合Keras来说明。其实理解了公式,选择哪个框架没有那么重要,Keras的简洁让我非常喜欢。keras的losses函数都在这里,下面也给出了例子,开始学一定要手算一遍,确保与接口计算的结果一致。至于后面两种loss只能参考论文以及别人实现的代码。
在交叉熵接口几乎都有这样的参数from_logits
,默认False,意思是模型的输出没有经过sigmoid或者softmax归一化。True表示输出概率的归一化了。
二分类交叉熵
BinaryCrossentropy在考虑二分类问题时,最后一层通常采用Dense层,网络的个数为1,不使用激活函数.于是输出的结果记为s,对应的标签为z。
X | 正例 | 反例 |
---|---|---|
\hat p | \sigma (s) | 1-\sigma (s) |
p | z | 1-z |
根据交叉熵计算
L = – \sum p(x_i) \log \hat p(x_i) = -z\log \sigma(s) – (1-z)\log (1-\sigma (s))
接下来看看例子:
>>> # Example 1: (batch_size = 1, number of samples = 4)
>>> y_true = [0, 1, 0, 0]
>>> y_pred = [-18.6, 0.51, 2.94, -12.8]
>>> bce = tf.keras.losses.BinaryCrossentropy(from_logits=True)
>>> bce(y_true, y_pred).numpy()
0.865
手动计算交叉熵的结果:
>>>from math import exp, log
>>>sigma = lambda x: 1 / (1+exp(-x))
>>>-log(1-sigma(-18.6)) - log(sigma(0.51)) - log(1-sigma(2.94)) - log(1-sigma(-12.8))
3.461831799124391
>>>3.461831799124391 / 4
0.8654579497810978
对于from_logits=False
默认情况,可以自行尝试。
多分类交叉熵
CategoricalCrossentropy只考虑单标签的多分类问题。对于单标签的N分类问题,神经的网络输出N个标签的打分为s_1,s_2,\cdots,s_t,\cdots,s_n,最后的Dense层使用softmax作为激活函数,则输出的结果为模型的概率分布\hat p_{model} = (p_1,p_2,\cdots,p_t,\cdots,p_n),数据的概率分布为p_{data}=(0,0,\cdots,1\cdots,0),假定第t个位置为目标类。计算交叉熵如下:
L = – \sum p(x_i) \log \hat p(x_i) = – \sum_i \log p_t ^ i
上面是使用softmax作为激活函数,如果不使用激活函数,需要自己实现交叉熵Loss,则需要在Loss中完成softmax的计算,这些基础的函数框架内部已经实现了(tf.nn.softmax),这里在上式的基础上继续往下推导:
L=- \sum_i \log p_t ^ i = – \sum_i \log \frac{\mathrm e^{s_t^i}}{\sum_j \mathrm e^{s_j^i}}
接下来看看keras接口的实现代码,这里考虑from_logits=False
的情况。激活函数采用softmax。
>>> y_true = [[0, 1, 0], [0, 0, 1]]
>>> y_pred = [[0.05, 0.95, 0], [0.1, 0.8, 0.1]] //概率归一化
>>> # Using 'auto'/'sum_over_batch_size' reduction type.
>>> cce = tf.keras.losses.CategoricalCrossentropy()
>>> cce(y_true, y_pred).numpy()
1.177
>>> from math import log
>>> (-log(0.95) - log(0.1)) / 2 //验证结果
1.176939193690798
稀疏化的多分类交叉熵
SparseCategoricalCrossentropy,稀疏化的多分类交叉熵跟CategoricalCrossentropy基本一样,只不过y_{true}不需要转化为one-hot
形式的概率分布,框架内部会使用稀疏矩阵存储数据的概率分布,其余计算完全相同。
Focal Loss
普通的多分类交叉熵为-\log p_t,其实暗含对于分类正确的样本不贡献Loss值,显然分类完成正确,则目标分类概率为p_t=1其对数值为0,所有对Loss的积累没有贡献,相当于忽视了预测完全正确的样本。越是easy sample,对loss的贡献就越小,交叉熵“重点关注”hard sample。什么是hard sample呢?例如对于3分类的的样本做预测,输出的概率分布为(0.4,0.3,0.3)而样本概率分布为(1,0,0)虽然这个分类是结果是正确的,但最大的分类概率才0.4,只能算是险胜,这样的样本hard sample。那有没有办法让交叉熵进一步关注那些hard sample呢?本质上就是给越hard的sample 分配的权重越大答案就是focal loss了。
从focal loss 的论文1中可以看到,\gamma=0退化为普通的交叉熵,\gamma越大,越忽略简单样本的贡献,也就意味着更加关注困难样本和分类错误的样本。理论上可以有效缓解样本不均衡的问题。因为样本不均衡,某个类的样本很多,以至于算法相对容易学到分类模式,大量正确分类正确的样本对loss影响很小,所以可以缓解样本不均衡。论文指出一般情况\gamma=2效果比较好。
tensorflow官网的扩展addons给出了sigmoid的focal loss,没有多分类的focal loss,不过开源项目有很多这样的实现。比如基于keras的focal loss2,其实写一个也是比较容易的。
Circle Loss
上面的交叉熵都是单标签的二分类和多分类的,对于多标签的分类问题没有给出交叉熵,之前解决多标签的分类问题采用多个二分类问题,比如在信息抽取中,其中关系识别,识别出文本中包含的关系类别,通常是在N种关系中,识别出一种或者K种关系,将这个任务转化为N个二分类问题。交叉熵使用这N个二分类的loss之和。
苏神在将“softmax+交叉熵”推广到多标签分类问题3中详细推导了交叉熵的计算,并且给出了代码实现。我也是看了苏神的博客,对比了原论文,感受到Circle Loss确实非常漂亮,笔者觉得有两个地方可圈可点。
第一处就是对普通的loss变形,得出固定K个分类的统一loss形式。
-\log p_t = -\log \frac{\mathrm e^{s_t}}{\sum_j \mathrm e^{s_j}}=\log (1+\sum_{j\neq t}\mathrm e^{s_j-s_t})
分类的目标就是让每个非目标类都小于目标类的得分,于是固定多个标签的分类可以表示为:
-\log p_t =\log (1+\sum_{j\in \Omega_{neg} i\in\Omega_{pos}}\mathrm e^{s_j-s_i}) = \log (1+\sum_{j\in \Omega_{neg}}\mathrm e^{s_j} \sum_{i\in \Omega_{pos}} \mathrm e^{-s_i})
实际上就是把单标签的得分-s_t变为求和,将目标类得分作为整个求和的目标。上式可以用于固定多标签分类,也就是假定文本中都存在相同的分类个数,如果少于分类个数可以用0来填充。当然最好的办法是找到阈值,分类以后直接取大于这个阈值作为正例,小于阈值就是反例。于是往上式中注入阈值s_0,这里是第二处巧妙的地方。
\log (1+\sum_{j\in \Omega_{neg}}\mathrm e^{s_j} \sum_{i\in \Omega_{pos}} \mathrm e^{-s_i}+ \sum_{j\in \Omega_{neg}} \mathrm e^{s_j-s_0} + \sum_{i\in \Omega_{pos}} \mathrm e^{s_0-s_i})\\ =\log(\sum_{i\in\Omega_{pos}}\mathrm e^{s_0-s_i}+1) + \log(\sum_{j\in \Omega_{neg}} \mathrm e^{s_j} + \mathrm e^{s_0})
令阈值s_0=0得到最终的多标签分类的交叉熵:
\log(\sum_{i\in\Omega_{pos}}\mathrm e^{-s_i}+1) + \log(\sum_{j\in \Omega_{neg}} \mathrm e^{s_j} + 1)
这里需要注意y_{pred}模型输出不能使用激活函数,以保证输出取值整个实数范围,softmax的计算已经放在loss函数了。
总结
本文总结了常用的几种Loss函数,不仅需要看懂交叉熵的数学公式,也要跟实现的代码联系上来,才能算是真正做到理解了。focal loss看似简单,在普通的交叉熵基础上加了权重,以至于更加“专注”;而circle loss也是在普通交叉熵基础上,得到了softmax分类适用于多标签的情况,推导过程非常漂亮。笔者之前由于没有动手推到和实现代码,理解的深度不够。如果要做算法的工作,还得动手才能真正理解算法的精髓。
参考
- Focal Loss ↩︎
- SigmoidFocalCrossEntropy ↩︎
- 苏剑林. (Apr. 25, 2020). 《将“softmax+交叉熵”推广到多标签分类问题 》[Blog post]. Retrieved from https://kexue.fm/archives/7359 ↩︎