KL散度、交叉熵与JS散度数学公式以及代码例子

1.1 KL 散度概述

KL 散度 ,Kullback-Leibler divergence,(也称相对熵,relative entropy)是概率论和信息论中十分重要的一个概念,是两个概率分布(probability distribution)间差异的非对称性度量。

对离散概率分布的 KL 散度 计算公式为:
python实现kl散度求解 kl散度公式_javascript

对连续概率分布的 KL 散度 计算公式为:
python实现kl散度求解 kl散度公式_javascript_02

一般情况下,我们得到的数据都是离散的。

KL 散度

  • KL 散度
  • KL 散度 是非对称的,即 python实现kl散度求解 kl散度公式_机器学习_03

具体内容参考 [1] 《深度学习轻松学》。

1.2 KL 散度计算方法的代码实现

1.2.1 自己编写代码

请结合 公式 (1)

import numpy as np
import math

def KL(p,q):
    # p,q 为两个 list,表示对应取值的概率 且 sum(p) == 1 ,sum(q) == 1
    return sum(_p*math.log(_p/_q) for (_p,_q) in zip(p,q) if _p != 0 )

P = [0.2, 0.4, 0.4]
Q = [0.4, 0.2, 0.4]

print(KL(P,Q))

输出内容为:

0.13862943611198905

计算过程python实现kl散度求解 kl散度公式_python_04

当然如果是 二分类问题 计算过程也是一样的。

import numpy as np
import math

def KL(p,q):
    # p,q 为两个 list,表示对应取值的概率 且 sum(p) == 1 ,sum(q) == 1
    return sum(_p*math.log(_p/_q) for (_p,_q) in zip(p,q) if _p != 0 )

P = [0, 1]
Q = [0.4,0.6]

print(KL(P,Q))

输出结果为:

0.5108256237659907

可以自己复制粘贴填写更多测试数值,需要 保证

  • P 中每一组的各项的和为 1
  • Q 中每一组各项的和为 1
  • Q 中每一组不允许出现 0

注意:

1.2.2 使用已存在的库 scipy

from scipy import stats

P = [0.2, 0.4, 0.4]
Q = [0.4, 0.2, 0.4]
stats.entropy(P,Q)

输出内容为:

0.13862943611198905

和上面的例子一样,可以测试那些数据。

2.1 交叉熵基本概述

交叉熵(Cross Entropy)是 Shannon 信息论中一个重要概念,主要用于度量两个概率分布间的差异性信息。

二分类 任务的 交叉熵

python实现kl散度求解 kl散度公式_python实现kl散度求解_05
其中,python实现kl散度求解 kl散度公式_机器学习_06 可以理解为数据集中的 label,也就是接下来例子中的 python实现kl散度求解 kl散度公式_概率分布_07;而 python实现kl散度求解 kl散度公式_概率分布_08 可以理解与模型的预测标签,也就是接下来的例子中的 python实现kl散度求解 kl散度公式_概率分布_09

注意: 如果二分类问题时,python实现kl散度求解 kl散度公式_机器学习_06 的取值并非 [0,1] 两种这种正常模式,需要分别求和再求均值,这一部分内容将会在后面 2.3

多分类

python实现kl散度求解 kl散度公式_javascript_11
公式 4 可以写成如下格式:

python实现kl散度求解 kl散度公式_python_12

其中, 多分类 任务的计算公式同样适用于 二分类 ,因为二分类任务直接等于两个概率运算的和,所以没必要加 python实现kl散度求解 kl散度公式_python实现kl散度求解_13

2.2 交叉熵计算方法的代码实现

2.2.1 自己编写代码

参考 公式4

import math

def CE(p,q):
    # p,q 为两个 list,表示对应取值的概率 且 sum(p) == 1 ,sum(q) == 1
    return sum(_p*math.log( 1 /_q) for (_p,_q) in zip(p,q) if _q != 0 )

P = [0.,1.]
Q = [0.6,0.4]

print(CE(P,Q))

计算过程python实现kl散度求解 kl散度公式_javascript_14

输出内容为:

0.9162907318741551

注意对比前文中对 KL 的实现的代码,非常相似,除了函数名就改动两个地方。

根据 公式5,代码实现如下:

import math

def CE(p,q):
    # p,q 为两个 list,表示对应取值的概率 且 sum(p) == 1 ,sum(q) == 1
    return -sum(_p*math.log(_q) for (_p,_q) in zip(p,q) if _q != 0 )

P = [0.,1.]
Q = [0.6,0.4]

print(CE(P,Q))

输出结果为:

0.916290731874155

2.2.1 使用 tensorflow2

主要是因为使用 tensorflow 的时候可能会用到这个函数,所以特地在这里介绍一下计算过程。

注意 这里只适合 Binary 这种情况,也就是说标签只是 0 与 1 两种。具体参考 [3]

这里直接上例子,然后解释计算过程:

import tensorflow as tf

y_true = [0., 1.]
y_pred = [0.6, 0.4]
bce = tf.keras.losses.BinaryCrossentropy()
bce(y_true, y_pred).numpy()

输出内容为:

0.9162905

在官方文档中给出的例子有两组数据,代码如下:

import tensorflow as tf

y_true = [[0., 1.], [0., 1.]]
y_pred = [[0.6, 0.4], [0.4, 0.6]]
# Using 'auto'/'sum_over_batch_size' reduction type.
bce = tf.keras.losses.BinaryCrossentropy()
bce(y_true, y_pred).numpy()

输出内容为:

0.71355796

这种情况只是分别求两组结果,再进行平均即可。

2.3 当测试数据为全0或全1 的二分类时

上面的公式以及自己编写的代码中,都没有考虑到当测试数据为全0 或全 1 这种情况,接下来在这里讨论,在二分类问题中,应该如何计算结果。

python实现kl散度求解 kl散度公式_python实现kl散度求解_05
公式3 所示,在 输入数据的 python实现kl散度求解 kl散度公式_机器学习_06 为 [0,1] 两种时,使用 公式3,4,5 计算都可以。但是如果输入数据的 python实现kl散度求解 kl散度公式_机器学习_06

这个时候需要使用 公式3 进行计算,并且同样需要做一次求均值,如 公式6

python实现kl散度求解 kl散度公式_python实现kl散度求解_18

对应的代码实现如下:

import math

def CE(p,q):
    return -sum((_p*math.log(_q)+(1-_p)*math.log(1-_q) )for (_p,_q) in zip(p,q) if _q != 0 )/len(p)

P = [0.,0.]
Q = [0.6,0.4]

print(CE(P,Q))

输出结果为:

0.7135581778200728

计算过程python实现kl散度求解 kl散度公式_概率分布_19

对应的 tensorflow2 代码如下:

import tensorflow as tf

y_true =  [0., 0.]
y_pred = [0.4, 0.6]

bce = tf.keras.losses.BinaryCrossentropy()
bce(y_true, y_pred).numpy()

输出结果为:

0.71355796

如果测试数据为 [1.,1.] 的时候,输出结果是相同的。

3.1 JS 散度概述

JS 散度 Jensen-Shannon divergence 用于描述两个概率分布的相似程度。和上面的 KL 的描述一致的话,JS 散度是两个概率分布间差异的对称性度量。

JS 散度的求解公式如下:
python实现kl散度求解 kl散度公式_javascript_20

很明显,等式是对称成立的,也就是说 python实现kl散度求解 kl散度公式_python实现kl散度求解_21

3.2 JS散度计算方法的代码实现

公式7

实验 1

import math

def KL(p,q):
    # p,q 为两个 list,表示对应取值的概率 且 sum(p) == 1 ,sum(q) == 1
    return sum(_p*math.log(_p/_q) for (_p,_q) in zip(p,q) if _p != 0 )

def JS(p,q):
    M = [0.5*(_p +_q) for (_p,_q) in zip(p,q)]
    return 0.5*(KL(p,M)+KL(q,M))

P = [0.,1.]
Q = [0.6,0.4]

print(JS(P,Q))
print(JS(Q,P))

输出内容为:

0.27435846855026524
0.27435846855026524

可以看出 python实现kl散度求解 kl散度公式_概率分布_22python实现kl散度求解 kl散度公式_javascript_23


实验 2

接下来看一下两个概率分布差异大小在 JS 值上的直观反映:

import math

def KL(p,q):
    # p,q 为两个 list,表示对应取值的概率 且 sum(p) == 1 ,sum(q) == 1
    return sum(_p*math.log(_p/_q) for (_p,_q) in zip(p,q) if _p != 0 )

def JS(p,q):
    M = [0.5*(_p +_q) for (_p,_q) in zip(p,q)]
    return 0.5*(KL(p,M)+KL(q,M))

P = [0.,1.]
Q = [0.01,0.99]

print(JS(P,Q))

P = [0.,1.]
Q = [0.1,0.9]

print(JS(P,Q))


P = [0.,1.]
Q = [0.5,0.5]

print(JS(P,Q))

P = [0.,1.]
Q = [0.9,0.1]

print(JS(P,Q))

P = [0.,1.]
Q = [0.99,0.01]

print(JS(P,Q))

输出内容如下:

0.003478298769743019
0.03597375665014844
0.21576155433883565
0.5255973270178643
0.665096412549155

可以看出,最开始的数据两个概率分布是非常接近的,因为 JS 值比较小,接着两个概率分布差异越来越多,JS 值也越来越大。


实验 3

接着使用相同的数据,测试一下 KL 值的大小与概率分布的关系,因为 KL 计算是非对称的,因此每次需要输出两个结果。

import math

def KL(p,q):
    # p,q 为两个 list,表示对应取值的概率 且 sum(p) == 1 ,sum(q) == 1
    return sum(_p*math.log(_p/_q) for (_p,_q) in zip(p,q) if _p != 0 )


P = [0.1,0.9]
Q = [0.01,0.99]

print(KL(P,Q),KL(Q,P))

P = [0.1,0.9]
Q = [0.1,0.9]

print(KL(P,Q),KL(Q,P))

P = [0.1,0.9]
Q = [0.5,0.5]

print(KL(P,Q),KL(Q,P))

P = [0.1,0.9]
Q = [0.9,0.1]

print(KL(P,Q),KL(Q,P))

P = [0.1,0.9]
Q = [0.99,0.01]

print(KL(P,Q),KL(Q,P))

输出结果为:

0.14447934747551233 0.07133122707634103
0.0 0.0
0.3680642071684971 0.5108256237659907
1.7577796618689758 1.7577796618689758
3.8205752275831846 2.224611312865836

可以看出,当 P 和 Q 相同分布时,KL 散度为 0;KL 散度随着分布差异的增大而增大,随着分布差异的减小而减小。


小结

根据以上实验,可以看出:

  • 随着两个概率分布差异的增大,KL 散度与 JS 散度的数值将增大;反之亦然。
  • 随着两个概率分布差异的增大,KL 散度的增大时不均匀的,而 JS 散度的增大时均匀的。
  • KL 散度的不对称性可能带来一些潜在的问题。

4. 总结

本文总结了 KL 散度、交叉熵以及 JS 散度数学公式以及一些性质,并且通过 python 代码实现。在阅读论文或实际编码中,如果忘记了这方面的内容,可以考虑参考一下。

如有任何疑问,欢迎留言评论!