信息量、熵、交叉熵、KL散度、JS散度杂谈及代码实现
信息量
任何事件都会承载着一定的信息量,包括已经发生的事件和未发生的事件,只是它们承载的信息量会有所不同。如昨天下雨这个已知事件,因为已经发生,既定事实,那么它的信息量就为 0。如明天会下雨这个事件,因为未有发生,那么这个事件的信息量就大。
从上面例子可以看出信息量是一个与事件发生概率相关的概念,而且可以得出,事件发生的概率越小,其信息量越大。这也很好理解,比如某明星被爆出轨、逃税等,这种事件信息量就很大,我们在口语中也会称这种新闻 “信息量很大” ,因为是小概率事件,然而近几年看起来好像已经不是小概率事件了。而对于我是我妈生的,这种事件,信息量就几乎为零,因为这是确定事件。
另外,独立时间的信息量是可叠加的,比如 ”a. 张三喝了阿萨姆红茶,b. 李四喝了英式红茶“ 的信息量就应该是a和b的信息量之和,如果张三李四喝什么茶是两个独立事件。
我们已知某个事件的信息量是与它发生的概率有关,那我们可以通过如下公式计算信息量:假设 XXX 是一个离散型随机变量,其取值集合为 xxx ,概率分布函数 P(x)=Pr(X=x),x∈XP(x)=Pr(X=x),x\in XP(x)=Pr(X=x),x∈X ,则定义事件 x=x0x=x_0x=x0 的信息量为:
I(x0)=−logP(x0)I(x_0)=-logP(x_0) I(x0)=−logP(x0)
我们可以看到,当某个时间的概率为1时,即其一定会发生,则此时它的信息量为0:I=−log1=0I=-log1=0I=−log1=0。
熵
熵就是信息量的期望,即:
S(x)=−∑iP(xi)logP(xi)S(x)=-\sum_iP(x_i)logP(x_i) S(x)=−i∑P(xi)logP(xi)
可以认为,熵表征的是随机事件的不确定性的大小,即事件可能性越多、越不确定,熵越大。这从公式中也可以看出来:
- P(xi)P(x_i)P(xi) 作为事件的概率肯定小于 1,取对数肯定小于 0,再取负号肯定大于 0。也就是说,熵累加的每一项都是正数,因此:可能性越多,熵越大。
- 再看每一项的确定性,当 P(xi)=1P(x_i)=1P(xi)=1 或 000 ,分别代表必定发生或必定不发生,这两种情况事件的确定性最小,从式子中也可以看出 P(xi)=1P(x_i)=1P(xi)=1 或 000 ,该项都为 0。而当 P(xi)P(x_i)P(xi) 的值在 0-1 中间某点时,事件非常不确定,此时该项值较大。因此说,越不确定,熵越大。
KL散度
以上说的都是某一个事件的信息量,如果我们另有一个独立的随机事件,我们该怎么计算事件A和事件B的差别?我们先介绍默认的计算方法:KL散度,有时又称相对熵、KL距离,实际上,虽然可以叫做KL距离,但它绝不是严格意义上的距离,因为KL散度不具有对称性,即A对B的KL散度 和 B对A的KL散度是不一样的。
KL散度的数学定义:对于离散事件:
DKL=(A∣∣B)=∑iPA(xi)log(PA(xi)PB(xi))=∑iPA(xi)logPA(xi)−PA(xi)logPB(xi)D_{KL}=(A||B)=\sum_iP_A(x_i)log(\frac{P_A(x_i)}{P_B(x_i)})=\sum_iP_A(x_i)logP_A(x_i)-P_A(x_i)logP_B(x_i) DKL=(A∣∣B)=i∑PA(xi)log(PB(xi)PA(xi))=i∑PA(xi)logPA(xi)−PA(xi)logPB(xi)
而对于连续事件,我们把求和改成积分即可。
可以看到,KL散度有这么几个特点:
- 当两个事件完全相同时,即 PA=PBP_A=P_BPA=PB 时,DKL=0D_{KL}=0DKL=0。
- 如果把 A、BA、BA、B 的位置互换,则上式右边的值也会不同,KL散度不具有对称性,即 DKL(A∣∣B)≠DKL(B∣∣A)D_{KL}(A||B) \ne D_{KL}(B||A)DKL(A∣∣B)=DKL(B∣∣A)。
- 可以发现,上式右边的左半部分 ∑iPA(xi)logPA(xi)\sum_i P_A(x_i)logP_A(x_i)∑iPA(xi)logPA(xi) 其实就是事件 A 的熵,这点也是非常重要发现。
有这样的结论: A对B的KL散度由A自己的熵值和B在A上的期望共同决定,当使用A对B的KL散度来衡量两个分布的差异时,就是求A、B的对数差在A上的期望值。
交叉熵
终于来到了我们最熟悉的交叉熵,它是分类任务的最常用的损失函数,每个炼丹师在最开始拿MNIST手写数字分类做Hello World时,应该都使用的交叉熵损失函数。
可是我们上面已经介绍了:KL散度可以用来衡量两个分布的差异,那为什么不直接拿KL散度来做损失函数呢?我们先来对比一下A的熵、A对B的KL散度和A对B的交叉熵三者的公式:
- A的熵:S(A)=−∑iPA(xi)logPA(xi)S(A)=-\sum_iP_A(x_i)logP_A(x_i)S(A)=−∑iPA(xi)logPA(xi)
- A对B的KL散度:DKL(A∣∣B)=∑iPA(xi)logPA(xi)−PA(xi)logPB(xi)D_{KL}(A||B)=\sum_iP_A(x_i)logP_A(x_i)-P_A(x_i)logP_B(x_i)DKL(A∣∣B)=∑iPA(xi)logPA(xi)−PA(xi)logPB(xi)
- A对B的交叉熵:H(A,B)=−∑iPA(xi)logPB(xi)H(A,B)=-\sum_iP_A(x_i)logP_B(x_i)H(A,B)=−∑iPA(xi)logPB(xi)
是不是越看越感觉这哥仨的某一部分有些像,它们之间应该是存在某种关系的。的确是这样的,有:
DKL(A∣∣B)=H(A,B)−I(A)D_{KL}(A||B)=H(A,B)-I(A) DKL(A∣∣B)=H(A,B)−I(A)
即 KL散度=交叉熵-熵。
也就是说,KL散度与交叉熵的不同就体现在A本身的熵值上,如果A本身的熵值是一个常量,那么优化KL散度和优化交叉熵实质上就是等价的了。
再补充两个交叉熵的性质:
- 自己与自己的交叉熵即自己的熵值,即 H(A,A)=S(A)H(A,A)=S(A)H(A,A)=S(A)。
- 与KL散度类似,交叉熵也不具有对称性,即 H(A,B)≠H(B,A)H(A,B)\ne H(B,A)H(A,B)=H(B,A)。
将交叉熵作为损失函数
在我们训练优化机器学习模型时,我们希望学到的是真实数据的分布 PReal(x)P_{Real}(x)PReal(x),但这只是理想情况,我们不可能得到理想的真实分布。我们实际中一般都是通过在大规模的有标注训练数据分布 PLabel(x)P_{Label}(x)PLabel(x) 上来近似 PReal(x)P_{Real}(x)PReal(x)。
在训练中,如果我们用 KL 散度来表示分布之间的差异,那么我们希望的就是模型学习到的分布 PModel(x)P_{Model}(x)PModel(x) 能够与标注训练数据的分布差异越小越好,也就是说我们要最小化这样的KL散度:DKL(Label∣∣Model)D_{KL}(Label||Model)DKL(Label∣∣Model)。
可以看到,这里的 PLabelP_{Label}PLabel 就相当于上面的 PAP_APA ,而 PModelP_{Model}PModel 就相当于 PBP_BPB。而我们的标签的分布 PLabelP_{Label}PLabel 的熵值还真就是个常量,更准确地说:标签的分布对于待更新的模型参数来说,是常量。因为标签值不对模型参数进行求导,它的分布是固定的,是不受模型参数变化的影响的。那么我们要最小化 DKL(Label∣∣Model)D_{KL}(Label||Model)DKL(Label∣∣Model) 也就等价与计算交叉熵 $H(Label,Model) $了。而交叉熵算起来又更为方便,因此我们一般就将交叉熵作为损失函数了。
JS散度
JS散度又是怎么回事呢?我们看到,上面的KL散度和交叉熵都是不对称的形式,这在很多问题中是不合适的。因此JS散度的提出,解决了KL散度的不对称性的问题。
JS散度的公式如下:
JS(A∣∣B)=12KL(A∣∣A+B2)+12KL(B∣∣A+B2)JS(A||B)=\frac{1}{2}KL(A||\frac{A+B}{2})+\frac{1}{2}KL(B||\frac{A+B}{2}) JS(A∣∣B)=21KL(A∣∣2A+B)+21KL(B∣∣2A+B)
JS散度有这样的性质:
- 其值域范围是 [0,1][0,1][0,1]
- 其具有对称性,即 JS(A∣∣B)=JS(B∣∣A))JS(A||B)=JS(B||A))JS(A∣∣B)=JS(B∣∣A)),这从公式里也能看出来,A、BA、BA、B 互换还是一样的。
代码实现及测试
以下根据公式手动实现了熵、KL 散度、JS 散度的计算,并与 scipy 库中的实现进行了对比。
import numpy as np
from scipy.stats import entropy
from scipy.spatial.distance import jensenshannondef calc_entropy(probs):# probs.shape: [n, ]log_probs = np.log2(probs)ent = -1 * np.sum(probs * log_probs)return entdef calc_KL_divergence(pa, pb):log_pa = np.log2(pa)log_pb = np.log2(pb)kl = np.sum(pa * log_pa - pa * log_pb)return kldef calc_JS_divergence(pa, pb):pc = 0.5 * (pa + pb)js = 0.5 * calc_KL_divergence(pa, pc) + 0.5 * calc_KL_divergence(pb, pc)return jsif __name__ == '__main__':a = np.array( [0.1, 0.2, 0.7] )b = np.array( [0.2, 0.3, 0.5] )sa = calc_entropy(a)sa_ = entropy(a, base=2)sb = calc_entropy(b)sb_ = entropy(b, base=2)print(f"entropy of a: {sa}, {sa_}")print(f"entropy of b: {sb}, {sb_}")kl_ab = calc_KL_divergence(a, b)kl_ab_ = entropy(a, b, base=2)kl_ba = calc_KL_divergence(b, a)kl_ba_ = entropy(b, a, base=2)print(f"kl of a->b: {kl_ab}, {kl_ab_}")print(f"kl of b->a: {kl_ba}, {kl_ba_}")js_ab = calc_JS_divergence(a, b)js_ab_ = jensenshannon(a, b, base=2)print(f"js of a, b: {js_ab}, {js_ab}")
Ref
https://blog.csdn.net/qq_36552489/article/details/103793667
https://www.zhihu.com/question/65288314
https://blog.csdn.net/FrankieHello/article/details/80614422