信息量、熵、交叉熵、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)∑i?PA?(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)=?∑i?PA?(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)=∑i?PA?(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)=?∑i?PA?(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)=21?KL(A∣∣2A+B?)+21?KL(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