Managing Gigabytes--文本壓縮

開門見山,文本壓縮可以歸納爲兩大類, 符號方法和字典方法, 下面分別介紹下:


1)符號方法,symbolwise method
普通編碼方式是每個字符都採用相同位數編碼, 比如asc碼, 每個字符都是8位編碼。
那麼現在要壓縮,就是要用更少的位數來表示字符。顯而易見, 我們只須用較小的位數來表示高概率字符, 用較長的位數來表示低概率字符,這樣平均下來就可以實現壓縮。


那麼這裏面就有兩個點:
a)怎麼來確定每個待編碼字符的概率,這就是概率模型問題。
所謂概率模型就是爲編碼器提供的概率分佈函數,我們必須保證編碼器,和解碼器使用相同的模型, 不然解出來的就不對了。
那麼對於一個符號的編碼位數是有個下限的, 和這個符合的信息量I(s)相關。
I(s)= -logPr[s] (Pr[s]就是s符合出現的概率)
所以對於”擲硬幣面向上“這個事實,至少要用-log(1/2)=1位來編碼。
那麼對於字母表中每個符合的平均信息量稱爲”概率分佈的熵(entropy)“, H = sum(Pr[s]*I[s])
假定符合以假設的概率值獨立出現,H爲壓縮下限。即這是個理論極值,實際上不可能達到。
這也就是Claude Shannon著名的編碼原理。

 

回到模型,可分爲每個字符都當獨立的符號來處理的0階模型和僅考慮有限個前驅符號的m階有限上下文模型。
也可分爲靜態模型, 半靜態模型, 和自適應模型。
靜態模型就是不考慮正在編碼的文本,只使用固定的模型,這個模型就適用於文本模式相對固定的任務。
半靜態模型就先爲要壓縮的文件生成模型, 發送給解壓方, 這個方法需要遍歷兩遍被壓縮的文檔, 所以這個solution明顯不太好。
所以比較流行的是自適應模型,adaptive modeling,即以平緩的概率模型開始, 隨着遇到的符號變多, 不斷的調整。
自適應模型往往要解決零頻問題,這個問題解決方法很多, 最簡單的是默認每個字符在剛開始時已出現過一次。
這種模型的缺點是不能對文本進行隨機訪問, 必須從文本的開頭開始解碼,因爲我們必須保證編碼和解碼時的context是相同的。

對於怎麼建立符號概率模型, 也是有很多研究, 比如部分匹配模型(Prediction by Partial Matching, PPM)依據以前的字符做出預測, 動態馬爾可夫模型基於有限狀態機。
這些模型就不詳細描述了, 有興趣可以參看相關文獻。

 

 

b)知道待編碼字符的概率,怎麼給它分配合適的編碼, 這就是編碼算法的問題。

哈夫曼編碼
介紹編碼算法當然先來看看哈夫曼編碼, 這個號稱是作者在研究生時爲了避免參加科目考試而想出的算法,很簡單也很高效。再一次對美國的教育環境表示深深的敬意和嚮往。
這類算法基本想法就是給高概率字符分配較少位數的編碼。
這就有個問題, 如果每個字符的編碼位數不一樣, 那麼我們怎麼知道後一個字符的編碼位數,肯定不能用個len去指定。
哈夫曼的方法是解決編碼前綴的歧義性(ambiguity), 即前綴編碼,就是說每個編碼的前綴都是不同的
那麼怎麼去產生編碼和怎麼去解碼呢?
哈夫曼樹,通過哈夫曼樹,從下到上進行編碼, 從上到下進行解碼。具體怎麼構建哈夫曼樹, 就不具體說了, 很簡單。
再一次感嘆此算法構思的巧妙和簡單。
對於靜態的概率分佈, 哈夫曼算法是很好的, 但對於自適應模型,哈夫曼算法會耗費較多的內存或時間。
因爲自適應模型在同一時間會使用許多不同的概率分佈,依賴於被編碼文本的上下文的不同。那麼這樣就要同時建立多顆哈夫曼樹。

 

算術編碼

那麼對於自適應模型而言算術編碼更受歡迎。
算術編碼相對比較複雜,不過顯著的優勢是算術編碼可以用低於一位的編碼來表示高概率字符, 而哈夫曼編碼對於每個字符至少要用一位來編碼。
由於算術編碼包含了各種不同的概率分佈, 所以比較適合自適應模型。
但對於靜態模型,還是哈夫曼編碼要快許多。

算術編碼的基本思想就是, 在編碼過程中, 隨着各個字符的出現不斷縮小概率區間, 高概率字符不會大幅地縮短區間,而低概率字符會導致產生一個小的多的‘下一個’區間。
算術編碼只有當編碼完成時,纔會一次性輸出編碼,編碼值就是在最終的概率區間內任意選取一個值,區間越小,表示這個值所用的位數越多。
不好理解,就舉個例子:
對於只有3個字符的字符集a,b,c, 對bccb進行編碼
爲解決零頻問題, 開始假設a,b,c個出現過一次
編碼前概率區間爲[0,1],此時abc的概率都是1/3,所以abc各佔概率區間的1/3, 如b的概率區間爲[0.33, 0,66]
開始編碼......
第一個字符b,所以概率區間縮短爲[0.33, 0,66]
此時a:b:c的比例爲1:2:1,所以各個字符的概率區間變爲a = [0.333,0.416], b = [0.416, 0.583], c = [0.583, 0.666]
第二個字符c,所以概率區間縮短爲[0.583, 0.666]
此時a:b:c的比例爲1:2:2,所以各個字符的概率區間變爲a = [0.583,0.600], b = [0.600, 0.633], c = [0.633, 0.666]
第三個字符c,所以概率區間縮短爲[0.633, 0.666]
此時a:b:c的比例爲1:2:3,。。。。。。
最終概率區間縮小至[0.639, 0.650]
所以0.64就可以作爲bccb的編碼。

而解碼的過程就是照着編碼的過程重做一遍即可,
解碼前概率區間爲[0,1],此時abc的概率都是1/3,所以abc各佔概率區間的1/3, 如b的概率區間爲[0.33, 0,66]
所以0.64落在了b的概率區間內, 第一個字符爲b
概率區間縮短爲[0.33, 0,66], 此時a:b:c的比例爲1:2:1,所以各個字符的概率區間變爲a = [0.333,0.416], b = [0.416, 0.583], c = [0.583, 0.666]
所以0.64落在了c的概率區間內, 第二個字符爲c
。。。。。。最終完成解碼。

區間中的數需要的多少位來表示和區間長度的負對數成正比。而最終的區間長度是已編碼符合概率值的乘積(顯而易見)。
而log(AB)= log(A)+log(B), 所以最終的區間長度的對數就等於所有已編碼符號單獨概率值的對數的和。所以具有概率Pr[s]的符號s對輸出編碼的貢獻是-logPr[s], 與符號信息量相同。
所以算術編碼的輸出位數接近最優。


2)字典方法,dictionary method
字典模式很好理解, 用字典裏面的碼字來替換原文,如果這個碼字比原文的位數少,那麼就實現了壓縮, 如果你的字典是獨有的, 不公開的, 那麼就實現了加密。
那麼爲了實現壓縮, 就要基於詞或短語進行編碼, 基於字符壓縮效果肯定不好, 那麼如果這個字典是靜態的,因爲各個領域的詞和短語都不一樣的, 所以不可能適用於所有文本。
也有半靜態的,就是每次對要壓縮的文本生成一個字典,這個方法肯定也不好。
所以就有自適應的字典方案(adaptive dictionary scheme), 這個就有技術含量了怎麼產生自適應的字典了。
當然牛人是很多的,Ziv和Lempel發明了LZ77和LZ88兩種方法。
牛人想出來的方法都是很簡單,很好用的, 這個也不例外,基本原理就是文本的子串被指向其以前出現之處的指針所替換。
說白了,就是字典碼書就是文本本身, 碼字就是指針。個人感覺,在發明這個方法時, 作者可能並沒有想到什麼基於字典模式的方法,應該是後面的學者把它歸到這類了。
我們就以LZ77來了解下這個方法,
LZ77的輸出碼是一系列三元組,(a,b,c), a表示往前回溯多遠, b表示短語多長, c表示要輸入的下一個字符。
爲什麼要有c項,是爲沒有出現過的字符準備的, 這個設計照顧了新字符引進的需要。
以abaabab爲例,輸出碼爲:
(0,0,a) (0,0,b) (2,1,a) (3,2,b)
這個方法需要在之前的文本中找到最大匹配窗口, 這個不能線性查找, 一般採用散列表, 或二分查找樹來實現。

著名的Gzip就是這個算法的一個變體,
Gzip用hash table來定位之前出現的字符串, 用3個連續的字符作爲hash鍵值,鏈表中記錄了這三個字符在窗口中出現的位置信息。
出於效率的考慮,限制了鏈表的長度, 其實也沒有必要記錄過遠的位置, 記較近的幾個位置比較合理。
Gzip比較有意思的是,對指針的偏移值也做了哈夫曼編碼,較常用的值用較少的編碼。同時對字符串匹配長度值, 也採用哈夫曼編碼。
當之前沒有發現匹配的時候, 傳遞一個原始字符(raw character),有意思的是這個原始字符和字符串匹配長度值公用同一個編碼。
也就是說第二項有可能是個字符串匹配長度值, 也有可能是個原始字符,反正前綴編碼, 也不衝突, 用心良苦都是爲了壓縮。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章