交叉熵的前生今世

做深度学习的同学肯定对交叉熵不会感到陌生,因为我们在训练网络的时候经常会用到交叉熵作为损失函数,往大的方向看,大概有两种交叉熵函数:

  • 普通交叉熵函数:在TensorFlowKeras框架中又叫 Categorical Cross Entropy,主要用于多分类任务(Multi-Class);
  • 二元交叉熵函数:在TensorFlowKeras框架中又叫 Binary Cross Entropy,主要用于多标签任务(Multi-Label);

对多分类和多标签不熟悉的同学,多分类就是给你一张照片,里面可能是太阳,可能是月亮,可能是云,但它可以且只可以是其中的一种,也就是说各个标签之间是互斥的;而多标签任务,就是给你一张照片,要求检测其中存在的太阳月亮和云,这时候太阳、月亮、云在照片中的存在的相互独立的,比如,可能只有云,可能只有太阳,也可能两个都有。



但是,究竟什么是交叉熵?或者说,为什么叫交叉熵?这就涉及到信息论中的信息熵,这是克劳德·香农姥爷在1948年提出的概念,旨在对一个随机事件的信息量进行量化。香农老爷子人称信息论之父,如果你不是读通信的可能只是听说过他的名字,对他的工作并不熟悉,但他在信息论中的贡献对人类文明的进步是有重大意义的。我读大学的时候的学院院长是香农的小迷弟,他曾经说过香农是凭一己之力把人类文明推前了20年,现在想来毫不夸张。今天刚好是香农的死祭,我们也来了解一下香农的工作。

信息熵

前面提到,香农的许多重要贡献之一是1948年发表的一篇文章,他在其中定义了信息熵(Entropy)的概念。一个随机事件 X 的信息熵的数学表达式如下:$$ Entropy = \mathbb{E}_{x \sim P}[-\log P(x)] = -1 \times \sum P(x_i) \log P(x_i)$$ 其中$P(x)$是变量 X 的概率分布,$-\log P(x)$是 X 为 x 时的理论最小编码长度,当 $\log$函数的底数为2时,该物理量的单位为比特(Bit)。也就是说,一个随机事件的信息熵是它的理论最小编码长度的期望值。只要我们知道一个随机事件的概率分布,我们就可以计算它的信息熵。

我们来深入搞清楚一些概念,首先是信息熵的单位比特(Bit)。比特的意思是 Binary Digit,来自于计算信息熵时的log函数的底数2。事实上,你也可以用3或者4或者任意一个数作为底数,当然这样的话信息熵的单位就要对应的改变,上面的公式才有意义。比方说我们用3作为底数,套用公式算出来的信息熵单位就可以叫做Tet,Tenary digit。我们之所以在日常生活中常见用Bit做单位而不是Tet,是因为我们的数字电路设计是基于布尔逻辑的,布尔逻辑中只有两种状态。当然了,用电子开关模拟布尔逻辑运算的现代电子电路的基本思路也是香农的研究结果(膜拜大佬)。

其次,信息熵代表的是一个随机事件的理论最小编码长度的期望值。这句话是什么意思?举个例子,我们有一个随机事件$\mathbb A$,它的概率为$ {0.5, 0.25, 0.25} $。由于它有三个可能,在二进制世界里至少要用两个二进制位来代表它,如果我们采用${00, 01, 10}$ 的方式对其进行编码,那么它的平均编码长度为 $$ 0.5 \times 2 + 0.25 \times 2 + 0.25 \times 2 = 2$$ 但是,根据香农的信息理论,该随机事件的最小平均编码长度为 $$ - (0.5 \times \log_{2} 0.5 + 0.25 \times \log_{2} 0.25 + 0.25 \times \log_{2} 0.25) = 1.5 $$ 那么,怎么样编码才能达到最小平均编码长度呢?一种常见的方法是霍夫曼编码,这也是一位大佬,具体编码方法详见wiki。在我们的例子中,我们可以把概率为0.5的情况用二进制$0$表示,其他两种情况分别用${10, 11}$表示,这样我们的平均编码长度就是 $$ 0.5 \times 1 + 0.25 \times 2 + 0.25 \times 2 = 1.5$$ 香农的理论不仅告诉了我们随机事件$\mathbb A$可以用平均编码长度为1.5的编码方式编码,还告诉了我们这就是它的最小平均编码长度,不能再小了,就问你服不服。

事件 发生概率 编码1 编码2
$a_1$ 0.5 00 0
$a_2$ 0.25 01 10
$a_3$ 0.25 10 11

交叉熵

看到这里,你们可能会想,这跟深度学习有什么关系?Emmm,好像半毛钱关系都没有,对不起,你们白看了。但下面就要说到交叉熵了,观众老爷们看完再走嘛,反正来都来了。。。哎



上面提到,只要我们知道一个随机事件的概率分布,我们就能计算它的信息熵。但是,如果我们不知道它的概率分布呢?我们就只能估算它的概率分布,然后用估算的概率分布来设计它的编码方式了。还是用回我们的老朋友事件$\mathbb A$。假设我们不知道它实际的概率分布,只能估测它的概率分布为$ {0.5, 0.25, 0.25} $,据此设计的最优编码如上所述,它的平均编码长度是1.5比特。

但是,如果实际上它的概率分布是$ {0.4, 0.25, 0.35} $,那么它的实际平均编码长度其实是$ 0.4 \times 1 + 0.25 \times 2 + 0.35 \times 2 = 1.6$ 比特,比我们设想的平均编码长度多了0.1比特。这就是交叉熵的概念了。在现实生活中,对大部分有意义的随机事件我们不可能知道它的实际的概率分布 $P$,只能估测它的概率分布为$Q$,并根据$Q$进行编码,那么实际的平均编码长度就是交叉熵 $$ CrossEntropy = \mathbb{E}_{x \sim P}[-\log Q(x)] = -1 \times \sum P(x_i) \log Q(x_i) $$ 根据这个公式,我们可以证明,估测的概率分布$Q$越接近实际概率分布$P$,交叉熵就越小,当$Q=P$时,交叉熵达到最小值,也就是该随机事件的信息熵。

这和深度学习中的损失函数的意义是吻合的。在深度学习中,实际概率分布就是我们的实际标签(Ground Truth,gt),估测概率分布就是模型预测出来的标签(Prediction,pred),当pred越接近gt时,损失函数的值越小,模型性能越好。看到这里,你应该再也不会记不住交叉熵损失的公式了,不要再问gt到底是在log里面还是外面了。因为gt表示的是真值,是实际的概率,它必须是在log外面的。但是,如果你问交叉熵损失函数和信息论有什么联系,在我看来是一点关系都没有的,只不过刚好都用了同一套数学原理罢了。(再一次给老爷们道歉,就这样把你们骗进来了,对不起!!)

Keras中的两种交叉熵损失函数

知道了交叉熵的定义,我们来看一下交叉熵的分类。前面提到,常见的有两种交叉熵损失函数,Categorical Cross EntropyBinary Cross Entropy,一般Categorical Cross Entropy用于多分类任务,并且网络最后一层的输出一般使用softmax激活函数,因为softmax保证了最后一层输出的数值在0到1之间且和为1;而Binary Cross Entropy一般用于多标签任务,网络最后一层输出使用sigmoid激活函数,因为sigmoid保证了最后一层输出的数值在0到1之间且相互独立。

这两种损失函数在 TensorFlow 和 Keras 中的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import tensorflow as tf
import tensorflow.keras.backend as K
import numpy as np

def sigmoid(x):
return 1 / (1 + np.exp(-1 * x))

def softmax(x):
d = np.exp(x)
return d / np.sum(d)

'''
binary crossentropy的定义,用numpy实现
'''
def binary_crossentropy(y_true, y_pred, from_logits=False):
if from_logits:
y_pred = sigmoid(y_pred)

b1 = -1 * y_true * np.log(y_pred)
b2 = -1 * (1 - y_true) * np.log(1 - y_pred)
return b1 + b2

'''
categorical crossentropy的定义,用numpy实现
'''
def categorical_crossentropy(y_true, y_pred, from_logits=False):
if from_logits:
ce = -1 * y_true * np.log(softmax(y_pred))
else:
ce = -1 * y_true * np.log(y_pred / np.sum(y_pred))
return np.sum(ce)

'''
对比确认上面的两个函数和框架里面的是一样的
'''
if __name__ == '__main__':
pred = np.array([0.1, 0.3, 0.5, 0.7, 0.85, 0.97], dtype='float32')
gt = np.array([0.2, 0.3, 0.6, 0.67, 0.88, 0.65], dtype='float32')

graph1 = tf.Graph()
with graph1.as_default():
pred_t = tf.constant(pred)
gt_t = tf.constant(gt)
bce_loss = K.binary_crossentropy(gt_t, pred_t, from_logits=True) # note from_logits

graph2 = tf.Graph()
with graph2.as_default():
pred_t = tf.constant(pred)
gt_t = tf.constant(gt)
cce_loss = K.categorical_crossentropy(gt_t, pred_t, from_logits=True) # note from_logits

# keras's definition of binary and categorical cross entropy
with tf.Session(graph=graph1) as sess:
keras_bce = sess.run(bce_loss)

with tf.Session(graph=graph2) as sess:
keras_cce = sess.run(cce_loss)


# binary and categorical cross entropy calculated with numpy
numpy_bce = binary_crossentropy(gt, pred, from_logits=True) # note from_logits
numpy_cce = categorical_crossentropy(gt, pred, from_logits=True) # note from_logits