算術編碼簡介


上一篇講了LZW編碼,本篇討論另一種不同的編碼算法,算數編碼。和哈夫曼編碼一樣,算數編碼是熵編碼的一種,是基於數據中字符出現的概率,給不同字符以不同的編碼。本文也會對這兩種編碼方式的相似和不同點進行比較。

編碼原理

算數編碼的原理我個人感覺其實並不太容易用三言兩語直觀地表達出來,其背后的數學思想則更是深刻。當然在這里我還是盡可能地將它表述,並着重結合例子來詳細講解它的原理。

簡單來說,算數編碼做了這樣一件事情:

  1. 假設有一段數據需要編碼,統計里面所有的字符和出現的次數。
  2. 將區間 [0,1) 連續划分成多個子區間,每個子區間代表一個上述字符, 區間的大小正比於這個字符在文中出現的概率 p。概率越大,則區間越大。所有的子區間加起來正好是 [0,1)。
  3. 編碼從一個初始區間 [0,1) 開始,設置:low = 0,high = 1low=0high=1
  4. 不斷讀入原始數據的字符,找到這個字符所在的區間,比如 [ LH ),更新:

low = low + (high - low) * L \\\ high = low + (high - low) * Hlow=low+(highlow)L high=low+(highlow)H

  1. 最后將得到的區間 [low, high)中任意一個小數以二進制形式輸出即得到編碼的數據。

乍一看這些數學和公式很難給人直觀理解,所以我們還是看例子。例如有一段非常簡單的原始數據:

ARBER

統計它們出現的次數和概率:

Symbol Times P
A 1 0.2
B 1 0.2
E 1 0.2
R 2 0.4

將這幾個字符的區間在 [0,1) 上按照概率大小連續一字排開,我們得到一個划分好的 [0,1)區間:
圖片描述

 

 

開始編碼,初始區間是 [0,1)。注意這里又用了區間這個詞,不過這個區間不同於上面代表各個字符的概率區間 [0,1)。這里我們可以稱之為編碼區間,這個區間是會變化的,確切來說是不斷變小。我們將編碼過程用下圖完整地表示出來:
圖片描述

 

 

 

 

拆解開來一步一步看:

  1. 剛開始編碼區間是 [0,1),即low = 0\\\ high = 1low=0 high=1
  2. 第一個字符A的概率區間是 [0,0.2),則 L = 0,H = 0.2,更新

low = low + (high - low)* L=0\\\quad high = low + (high - low)* H=0.2low=low+highlowL=0high=low+highlowH=0.2

  1. 第二個字符R的概率區間是 [0.6,1),則 L = 0.6,H = 1,更新

low = low + (high - low)* L=0.12\\high = low + (high - low)* H=0.2low=low+highlowL=0.12high=low+highlowH=0.2

  1. 第三個字符B的概率區間是 [0.2,0.4),則 L = 0.2,H = 0.4,更新

low = low + (high - low)* L=0.136\\\ \ high = low + (high - low)* H=0.152low=low+highlowL=0.136  high=low+highlowH=0.152

  1. ......

上面的圖已經非常清楚地展現了算數編碼的思想,我們可以看到一個不斷變化的小數編碼區間。每次編碼一個字符,就在現有的編碼區間上,按照概率比例取出這個字符對應的子區間。例如一開始A落在0到0.2上,因此編碼區間縮小為 [0,0.2),第二個字符是R,則在 [0,0.2)上按比例取出R對應的子區間 [0.12,0.2),以此類推。每次得到的新的區間都能精確無誤地確定當前字符,並且保留了之前所有字符的信息,因為新的編碼區間永遠是在之前的子區間。最后我們會得到一個長長的小數,這個小數即神奇地包含了所有的原始數據,不得不說這真是一種非常精彩的思想。

解碼

如果你理解了編碼的原理,則解碼的方法顯而易見,就是編碼過程的逆推。從編碼得到的小數開始,不斷地尋找小數落在了哪個概率區間,就能將原來的字符一個個地找出來。例如得到的小數是0.14432,則第一個字符顯然是A,因為它落在了 [0,0.2)上,接下來再看0.14432落在了 [0,0.2)區間的哪一個相對子區間,發現是 [0.6,1), 就能找到第二個字符是R,依此類推。在這里就不贅述解碼的具體步驟了。

編程實現

算數編碼的原理簡潔而又精致,理解起來也不很困難,但具體的編程實現其實並不是想象的那么容易,主要是因為小數的問題。雖然我們在講解原理時非常容易地不斷計算,但如果真的用編程實現,例如C++,並且不借助第三方數學庫,我們不可能簡單地用一個double類型去表示和計算這個小數,因為數據和編碼可以任意長,小數也會到達小數點后成千上萬位。

怎么辦?其實也很容易,小數點是可以挪動的。給定一個編碼區間,例如從上面例子里最后的區間 [0.14432,0.1456)開始,假定還有新的數據進來要繼續編碼。現有區間小數點后的高位0.14其實是確定的,那么實際上14已經可以輸出了,小數點可以向后移動兩位,區間變成 [0.432,0.56),在這個區間上繼續計算后面的子區間。這樣編碼區間永遠保持在一個有限的精度要求上。

上述是基於十進制的,實際數字是用二進制表示的,當然原理是一樣的,用十進制只是為了表述方便。算數編碼/解碼的編程實現其實還有很多tricky的東西和corner case,我當時寫的時候debug了好久,因此我也建議讀者自己動手寫一遍,相信會有收獲。

算數編碼 vs 哈夫曼編碼

這其實是我想重點探討的一個部分。在這里默認你已經懂哈夫曼編碼,因為這是一種最基本的壓縮編碼,算法課都會講。哈夫曼編碼和算數編碼都屬於熵編碼,仔細分析它們的原理,這兩種編碼是十分類似的,但也有微妙的不同之處,這也導致算數編碼的壓縮率通常比哈夫曼編碼略高,這些我們都會加以探討。

不過我們首先要了解什么是熵編碼,熵是借用了物理上的一個概念,簡單來說表示的是物質的無序度,混亂度。信息學里的熵表示數據的無序度,熵越高,則包含的信息越多。其實這個概念還是很抽象,舉個最簡單的例子,假如一段文字全是字母A,則它的熵就是0,因為根本沒有任何變化。如果有一半A一半B,則它可以包含的信息就多了,熵也就高。如果有90%的A和10%的B,則熵比剛才的一半A一半B要低,因為大多數字母都是A。

熵編碼就是根據數據中不同字符出現的概率,用不同長度的編碼來表示不同字符。出現概率越高的字符,則用越短的編碼表示;出現概率地的字符,可以用比較長的編碼表示。這種思想在哈夫曼編碼中其實已經很清晰地體現出來了。那么給定一段數據,用二進制表示,最少需要多少bit才能編碼呢?或者說平均每個字符需要幾個bit表示?其實這就是信息熵的概念,如果從數學上理論分析,香農天才地給出了如下公式:
H(x) = -\sum_{i=1}^{n}p(x_{i})\log_{2}p(x_{i})H(x)=i=1np(xi)log2p(xi)
其中 p (xi) 表示每個字符出現的概率。log對數計算的是每一個字符需要多少bit表示,對它們進行概率加權求和,可以理解為是求數學期望值,最后的結果即表示最少平均每個字符需要多少bit表示,即信息熵,它給出了編碼率的極限。

算數編碼和哈夫曼編碼的比較

在這里我們不對信息熵和背后的理論做過多分析,只是為了幫助理解算數編碼和哈夫曼編碼的本質思想。為了比較這兩種編碼的異同點,我們首先回顧哈夫曼編碼,例如給定一段數據,統計里面字符的出現次數,生成哈夫曼樹,我們可以得到字符編碼集:

Symbol Times Encoding
a 3 00
b 3 01
c 2 10
d 1 110
e 2 111

圖片描述

 

 

仔細觀察編碼所表示的小數,從0.0到0.111,其實就是構成了算數編碼中的各個概率區間,並且概率越大,所用的bit數越少,區間則反而越大。如果用哈夫曼編碼一段數據abcde,則得到:

00 01 10 110 111

如果點上小數點,把它也看成一個小數,其實和算數編碼的形式很類似,不斷地讀入字符,找到它應該落在當前區間的哪一個子區間,整個編碼過程形成一個不斷收攏變小的區間。

由此我們可以看到這兩種編碼,或者說熵編碼的本質。概率越小的字符,用更多的bit去表示,這反映到概率區間上就是,概率小的字符所對應的區間也小,因此這個區間的上下邊際值的差值越小,為了唯一確定當前這個區間,則需要更多的數字去表示它。我們仍以十進制來說明,例如大區間0.2到0.3,我們需要0.2來確定,一位足以表示;但如果是小的區間0.11112到0.11113,則需要0.11112才能確定這個區間,編碼時就需要5位才能將這個字符確定。其實編碼一個字符需要的bit數就等於 -log ( p ),這里是十進制,所以log應以10為底,在二進制下以2為底,也就是香農公式里的形式。

-------
哈夫曼編碼的不同之處就在於,它所划分出來的子區間並不是嚴格按照概率的大小等比例划分的。例如上面的d和e,概率其實是不同的,但卻得到了相同的子區間大小0.125;再例如c,和d,e構成的子樹,c應該比d,e的區間之和要小,但實際上它們是一樣的都是0.25。我們可以將哈夫曼編碼和算術編碼在這個例子里的概率區間做個對比:
圖片描述

 

 

這說明哈夫曼編碼可以看作是對算數編碼的一種近似,它並不是完美地呈現原始數據中字符的概率分布。也正是因為這一點微小的偏差,使得哈夫曼編碼的壓縮率通常比算數編碼略低一些。或者說,算數編碼能更逼近香農給出的理論熵值。

為了更好地理解這一點,我們舉一個最簡單的例子,比如有一段數據,A出現的概率是0.8,B出現的概率是0.2,現在要編碼數據:

AAA...........AAABBB...BBB (800個A,200個B)

如果用哈夫曼編碼,顯然A會被編成0,B會被編成1,如果表示在概率區間上,則A是 [0, 0.5),B是 [0.5, 1)。為了編碼800個A和200個B,哈夫曼會用到800個0,然后跟200個1:

0.000......000111...111 (800個0,200個1)

在編碼800個A的過程中,如果我們試圖去觀察編碼區間的變化,它是不斷地以0.5進行指數遞減,最后形成一個 [0, 0.5^800) 的編碼區間,然后開始B的編碼。

但是如果是算數編碼呢?因為A的概率是0.8,所以算數編碼會使用區間 [0, 0.8) 來編碼A,800個A則會形成一個區間 [0, 0.8^800),顯然這個區間比 [0, 0.5^800) 大得多,也就是說800個A,哈夫曼編碼用了整整800個0,而算數編碼只需要不到800個0,更少的bit數就能表示。

當然對B而言,哈夫曼編碼的區間大小是0.5,算數編碼是0.2,算數編碼會用到更多的bit數,但因為B的出現概率比A小得多,總體而言,算術編碼”犧牲“B而“照顧”A,最終平均需要的bit數就會比哈夫曼編碼少。而哈夫曼編碼,由於其算法的特點,只能“不合理”地使用0.5和0.5的概率分布。這樣的結果是,出現概率很高的A,和出現概率低的B使用了相同的編碼長度1。兩者相比,顯然算術編碼能更好地實現熵編碼的思想。

---
從另外一個角度來看,在哈夫曼編碼下,整個bit流可以清晰地分割出原始字符串:

 

 


圖片描述

而在算數編碼下,每一個字符並不是嚴格地對應整數個bit的,有些字符與字符之間的邊界可能是模糊的,或者說是重疊的,所以它的壓縮率會略高:
圖片描述

 

 

 

當然這樣的解釋並不完全嚴格,如果一定要究其原因,那必須從數學上進行證明,算數編碼的區間分割是更接近於信息熵的結果的,這就不在本文的討論范圍了。在這里我只是試圖用更直觀地方式解釋算數編碼和哈夫曼編碼之間微妙的區別,以及它們同屬於熵編碼的本質性原理。

總結

算數編碼的講解就到這里。說實話我非常喜歡這種編碼以及它所蘊含的思想,那種觸及了數學本質的美感。如果說哈夫曼編碼只是直觀地基於概率,優化了字符編碼長度實現壓縮,那么算術編碼是真正地從信息熵的本質,展現了信息究竟是以怎樣的形式進行無損壓縮,以及它的極限是什么。在討論算術編碼時,總是要提及哈夫曼編碼,並與之進行比較,我們必須認識到它們之間的關系,才能對熵編碼有一個完整的理解


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM