談談熵編碼無損壓縮的原理


轉載請標明出處:http://www.cnblogs.com/zblade/

一、概要

在項目開發中,有引入用到rANS熵編碼壓縮算法,在使用的背后,想看看其運行的基本原理,也算補一下個人的熵編碼知識。這里提到的熵編碼壓縮算法都是無損壓縮。很久沒有寫文章了,太忙了,不知道一年一篇文章算不算年更 :b

二、熵編碼

目前較為成熟的熵編碼是霍夫曼編碼,算術編碼,以及14年Duda提出的ANS(Asymmetric Numeral Systems 非對稱數系)編碼。先解釋一下霍夫曼編碼和算術編碼,然后重點說一下ANS編碼的原理。

2.1 香農熵編碼

熵在編碼中,是對信息的衡量,熵越大,表明所包含的信息越多。對於高頻出現的事件,其本身包含的信息其實是不多的,所以其對應的熵更小。而低頻出現的事件,其包含的信息更多,對應的熵更大。香農的熵編碼理論值計算公式為:
[公式] ...(1)

2.2 霍夫曼編碼

霍夫曼編碼是速度最快的熵編碼,其基本原理是基於統計的頻率,構建二叉樹,最后高頻率的字符用最短的編碼表示,最低頻率的字符用最長的編碼來表示。其基本的操作就是不斷構建二叉樹的過程,借鑒示例用圖1

基本操作就是取頻率最低的2個字符,搭建一顆二叉樹,然后根節點頻率為葉子節點之和,如此遞歸,得到最終的二叉樹,示例中的編碼結果:

a: 0
b: 10
c: 110
d: 111

用編碼替換輸入的字符,即可得到最終的編碼結果。 霍夫曼編碼總結就是2個操作:構建霍夫曼樹,執行霍夫曼編碼。霍夫曼是執行速度最快的熵編碼,但是其不能無限接近熵編碼的理論值。

2.3 算術編碼

算術編碼是一種無限接近熵編碼理論值的編碼,其本質操作就是用一個[0, 1)的小數來表示最終的編碼結果,其基本操作也是基於統計來進行的,用示例圖來表示最直觀[2]:

當前編碼的字符為ABC三種字符,如何編碼“BCCB”這個字符?

1)設定初始頻率值,三種字符均值分布,則均為1/3,划分初步的概率分布;

2)輸入B,其位於[0.333,0.667),則以此區間進行下一次划分,這是各個字符出現的頻率進行更新,分別為1/4, 2/4, 1/4,得到最新的區間划分;

3)依次遞推,最后編碼所在區間為[0.639,0.6501),輸出這個區間內的一個小數,例如0.64,轉換為對應的二進制數即為最終的編碼結果。 算術編碼是一種能夠接近理論值的熵編碼,對應的代價就是算術的過程,速度慢。

三、ANS熵編碼

項目中用到的編碼是最近幾年提出的一種新的熵編碼,本着查看原理的心理去探究了一下這種最新的編碼,很多文章都說的較為晦澀,不是我這樣的小白能夠理解的。在偶然拜讀到一位國外大佬的文章后,通過詳細的推導,總算大致了解了基本的實現原理,這里推薦有時間的可以看這篇英文原文:
Lossless Compression with Asymmetric Numeral Systems 結合這篇文章,我大致講講個人的理解:

3.1 將二進制字符串編碼成自然數

從最簡單的編碼開始,假設一個字符串是以0/1字符串組成,如果用進制轉換,我們都知道如何將其轉換為10進制數。讓我們展開來看:
假設我們已經轉換了二進制字符串[公式], 其對應的數值為[公式],如果我們得到一個新的輸入字符[公式],我們希望基於一個基本的編碼函數來得到輸出的數值,假設為:
[公式] ....(2)
基於離散數學教程,如果還記得的話,這個公式是這樣:
[公式] ...(3)

為了區別"0"、“00”等情況,我們設定初始值:[公式], 反過來,我們可以從一個10進制數轉換成對應的二進制字符串,其對應的函數可以表示為:
[公式]...(4)

舉例來說明:
基於公式3和4,將字符[公式]轉換成一個十進制數,[公式], 那么其轉換操作為:

[公式]
[公式]
[公式]
[公式]
[公式]

其對應的解碼過程為:
[公式]
[公式]
[公式]
[公式]
[公式]
最終“10011”二進制字符串,轉換為十進制數為51,需要用[公式]個bit來表示,相對於理論極限值,多了一位,注意這里的解壓結果相對輸入字符串是倒序的,一般應用的時候,會先將輸入倒序排列,這樣解壓得到結果就是正序的結果。

3.2 編碼函數推導

上面的編碼都是基於0和1字符串是均值分布的前置條件的,實際情況中,是很大可能出現不均值分布的情況的。
引用前面的公式2,[公式], 假設[公式]具有[公式]位的信息,如果我們想把[公式]用理論值編碼[公式]位信息,那么可以推導公式:
[公式] [公式] [公式] [公式] 

所以:
[公式]
所以,我們可以得到這樣的理論編碼函數:[公式] ...(5)

3.3 Uniform Binary Variant(uABS)

現在我們將范圍拓展,假設我們要編碼的數字范圍為[1,N], 用"1" 和"0"來分別表示奇數和偶數,對應的概率為[公式] 和 [公式] , 對應的在N個數字中,偶數出現的次數為[公式], 那么我們可以推導N+1 和 N之間的關系為:
[公式] ...(6)
這里就不再詳細的推證了,公式6等價於:
[公式]...(7)

基於公式7,我們就可以依次編碼非均值分布的二進制數字符串了,其對應的解碼公式為:
[公式] => [公式] [公式]

用實例來演示編碼和解碼的過程:
繼續上面的用例[公式],  設定[公式],其編碼過程為:
[公式]
[公式] [公式] [公式] [公式]
對應的解碼過程為:
[公式] [公式]
[公式] [公式]
[公式] [公式]
[公式] [公式] [公式] [公式]

3.4 Range Variant(rANS)

上面的uABS是針對二進制字符的熵編碼,我們也可以進一步的推廣到非二進制字符的非均值熵編碼。首先我們需要明確的是,公式5是依然生效的,只是在推廣的時候,我們將[公式]推廣為[公式],也就是輸入的二進制字符變成符號[公式]即可。這樣在新增一個字符的時候,對應的等價新增該字符的熵編碼信息,所以公式5是依然生效的。
此外理論上來說,對於字符集,我們是可以有任意的概率分布的(只要字符集任意長),但是實際的時候我們是將其限定在一個量化范圍內的,一般是[公式]的范圍。在這個范圍內,符號[公式]出現的次數為[公式],那么可以得到[公式], 基於這個量化,結合uABS的公式,可以用下面的公式來表示rANS的編碼:
[公式]...(8)
對應的解碼操作公式為:
[公式]....(9)
[公式]...(10)
其中[公式],可以理解為累計分布統計操作。

用示例來解釋一下壓縮和解壓:
對於字符集['a', 'b', 'c'],其量化的 n = 3, 其統計的分布為[公式], 其對應的[公式], 現在我們來編碼字符串"abc",對於初始值[公式], 我們設定為[公式],基於公式8可以得到編碼過程為:
[公式]
[公式]
[公式]
對應的解碼操作為:
[公式] [公式]
[公式] [公式] [公式] [公式]

3.5 量化處理rANS

上面的rANS編碼展示的是短字符的時候的編碼流程,如果一個文件大小有1MB或者更大,這么長的字符串如何編碼?如果直接編碼的話,肯定會超過整數的表示范圍,解決辦法就是移位分解:
當編碼[公式]的時候,得到的結果過大時,將其右移M位來確保得到的結果處於[公式](例如M= 16bits)這個區間,同理在解壓的時候,如果[公式]較小,會將其左移M位,然后在進行處理,這樣就能確保編碼結果能用整形數來表示,其大致操作流程為:

MASK = 2**M -1 BOUND = 2**(2*M) - 1 ##Encoding s = readsymbol() x_test = (x / f[s]) << n + (x % f[s]) + c[s] if(x_test > BOUND): write16bits(x & MASK) x = x >> M x = (x /f[s]) << n + (x % f[s]) + c[s] ##Decoding s = symbol[x & MASK] writeSymbol(s) x = f[s](x >> n) + (x & MASK) -c[s] if(x < 2**M): x = x << M + read16bits()

對於ANS, 還有其他的編碼,例如tANS編碼,這里就不再討論,還沒看到這部分的編碼。在實際的編碼過程中,就是脫胎於上面的編碼理論,進一步的完善編碼上下文內容即可。

引用:

[1]:熵壓縮:信息熵、Huffman編碼、算數編碼、ANS+FSE

[2]:算術編碼_小石_新浪博客


免責聲明!

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



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