|
データ圧縮の基礎
データ圧縮に関する情報を広く浅く解説する文章を目指しています。比較的新しいアルゴリズムも取り上げています。間違いや不明な点がありましたら、遠慮なくメールや掲示板に書き込んでください。この文章を読んで、一人でも多くの日本人が圧縮アルゴリズムに興味を持ってくれたら幸いです。
| ●可逆と非可逆 (Lossless and Lossy) |
|
データ圧縮には大きく2種類の圧縮方式があります。1つは、データの内容を省いて圧縮する方式。(データを省くので、完全に元の状態には戻らない方式。)もう一つは、データの内容を省かずに完全に圧縮する方式(完全に元の状態に戻る方式)です。
|
| 圧縮アルゴリズム |
| 非可逆(Lossy) |
JPEG, MP3 など |
| 可逆(Lossless) |
LZH, ZIP
など |
|
データの内容を省くというのは、主に、画像や音声データのように、人の目や耳では判別不可能なデータを省くことをいいます。具体的な圧縮ファイルには JPEG や MP3 があります。
これらのデータを省くタイプを「非可逆」や「ロッシー(Lossy)」と呼び、逆に、完全に圧縮/復元するタイプを「可逆」や「ロスレス(Lossless)」と呼んでいます。可逆タイプの具体的な圧縮ファイルには、LZH
や ZIP
があります。
以下、データ圧縮の基礎では、圧縮アルゴリズムの基本ともいえる可逆(ロスレス)タイプについて解説します。非可逆タイプは、この可逆タイプの応用になると思ってください。まずは基本の可逆タイプをおさえましょう。
* yz1 と yz2 は可逆タイプです。LZH や ZIP
と同じです。画像や音声などのノイズの多いデータの圧縮には不向きです。
可逆タイプの(非可逆でも)圧縮アルゴリズムは、一般的に2段ロケット形式です。ここでは、一段目を「モデル化」。二段目を「符号化」と呼びます。
|
| 圧縮アルゴリズム |
| 一段目「モデル化」 |
LZ77, LZ78, BlockSorting, ランレングスなど |
| 二段目「符号化」 |
エントロピー符号(Huffman符号, 算術符号,
RangeCoderなど) |
|
一段目の「モデル化」では、データの特徴を抽出するデータ変換を行います。この段階でデータ量を減らします。一般に人の作成したデータには不必要なデータが多く含まれています。例えば、空白(スペース)であったり、同じ言葉の繰り返しであったり、データの種類にもよりますが、必ずといえるほどそういうデータが含まれています。この不必要なデータを「モデル化」の部分でコンパクトにまとめるデータ変換を行うのです。「モデル化」の具体的なアルゴリズム名では、LZ77,
LZ78 や BlockSorting などがあります。ランレングスなども、このモデル化に入ると思います。
二段目の「符号化」では、一段目で変換したデータを bit
データにします。この段階では情報量は減りません。本来の情報量に近づけるわけです。本来の情報量とは、例えば、あなたが「お茶を飲みますか?」という問いの答えがほしかったとします。この時の答えは「飲む」か「飲まない」の2通りしか考えられません。この時の情報量は2通りなので、1bit
(2進数で1桁)であらわすことができます。これを本来の情報量といいます。この情報量の bit
化のことをエントロピー符号とも言います。「符号化」の具体的なアルゴリズム名では、Huffman符号や、算術符号が有名です。最近では RangeCoder
が注目されています。
* LZH や ZIP は LZ77 + Huffman符号の2段構成です。yz1
も同じですが、yz2 では LZ77 + RangeCoder の2段構成です。
まずは、一般的に圧縮の後段階である、符号化について説明します。
|
| 符号化 |
| ・単純な符号化 |
単純な方法。他との比較のために列挙しています。 |
| ・Huffman符号 |
基本中の基本。現在一番広く使われている。 |
| ・算術符号 |
実力はNO.1。しかし、速度と特許の問題がある。 |
| ・RangeCoder |
現在の注目株。実用度NO.1
はおそらくこれ。 |
|
この順番に説明します。
前段階の「モデル化」を行わなくても、この符号化だけでも多少のデータの圧縮は可能です。ですが、効率的な圧縮を行うにはどうしても「モデル化」と「符号化」をペアで組み合わせる必要があります。
ただ、「符号化」どうしを複数組み合わせても意味がありません。むしろデータ量は増えるでしょう。エントロピー符号とは、情報の隙間を削り、ほぼランダムに等しいデータに変換する作業なのですから、2度目の符号化にはデータの隙間はありません。エントロピー符号は最後に1度だけ通すものなのです。
ここでは、なにも考えず(深いアルゴリズムは無し)に符号化する例を示します。一番、簡単で単純な方法です。
まず、"AAABCAAABD" という10文字の文字列をなにも考えずにそのまま bit 列に変換することを考えてみましょう。1bit
で2種類。2bitで4種類と、bit数をnとすると2のn乗の状態をあらわすことができます。この場合、使用している文字は4種類(A,B,C,D)です。ちょうど2bit
であらわせます。
|
| 対応表 |
| 文字 |
bit |
| 'A' |
00 |
| 'B' |
01 |
| 'C' |
10 |
| 'D' |
11 |
|
この文字列を符号化すると、'00 00 00 01 10 00 00 00 01 11' と20bit であらわすことができます。また、この対応表さえあれば、20bit
を元の10文字に戻すこともできます。この文字列は、特に考慮しない単純な符号化では
20bit になるデータであるとも言えます。
| ●Huffman符号 (Huffman-Coder) |
|
エントロピー符号の基本中の基本である、Huffman符号から説明します。
Huffman 符号は「文字の出現頻度を考慮して bit
を割り当てよう」という考え方です。上の例の文字列 "AAABCAAABD" の各文字の出現頻度は、
|
| 出現頻度表 |
| 文字 |
回数 |
| 'A' |
6 |
| 'B' |
2 |
| 'C' |
1 |
| 'D' |
1 |
|
です。Huffman 符号では、出現頻度の低い文字から bit を割り当てます。出現頻度の低い2つを探し出して 0 と 1 の bit
をつけて行くわけです。まず最初は C と D に、0 と 1 を割り当てます。
|
| 出現頻度表 |
| 文字 |
回数→bit |
| 'A' |
6 |
| 'B' |
2 |
| 'C' |
1 → 0 |
| 'D' |
1 →
1 |
|
次に、C と D の出現頻度は合わせて2です。次から、C と D は 出現回数2の
CD というかたまりとして扱うわけです。
で、次に出現回数が低いのは、B と CD です。B に 0、CD に 1 を割り当てます。
|
| 出現頻度表 |
| 文字 |
回数→bit |
| 'A' |
6 |
| 'B' |
2 → 0 |
| 'C' |
1 → 10 |
| 'D' |
1 →
11 |
|
ここで、BCD を1つのかたまり出現度4と考え直して、最後に、A に 0、BCD に 1 を割り当てます。
|
| 出現頻度表 |
| 文字 |
回数→bit |
| 'A' |
6 → 0 |
| 'B' |
2 → 10 |
| 'C' |
1 → 110 |
| 'D' |
1 →
111 |
|
これが、Huffman 符号の bit の割り当て方式(の一例)です。
また、モビールのような「木」を作る方式でも同じ結果が得られます。
|
| 出現頻度の木 |
|
10 |
|
|
|
| ┏ |
┻ |
┓ |
|
|
| 6 |
|
4 |
|
|
| : |
┏ |
┻ |
┓ |
|
| : |
2 |
|
2 |
|
| : |
: |
┏ |
┻ |
┓ |
| : |
: |
1 |
|
1 |
| 'A' |
'B' |
'C' |
|
'D' |
|
木の左の枝に '0' 右の枝に '1' を割り当てて符号 bit を決定します。結果は上の割り当て方と同じです。
|
| 枝にbitを割り当て |
| ┏ |
┻ |
┓ |
|
|
| 0 |
|
1 |
|
|
| : |
┏ |
┻ |
┓ |
|
| : |
0 |
|
1 |
|
| : |
: |
┏ |
┻ |
┓ |
| : |
: |
0 |
|
1 |
| 'A' |
'B' |
'C' |
|
'D' |
| | |
| |
| |
|
| |
| 0 |
1 |
1 |
|
1 |
|
0 |
1 |
|
1 |
|
|
0 |
|
1 |
|
この bit 対応表で "AAABCAAABD" の文字列を bit 列に変換すると、'0 0 0 10 110 0 0 0 10
111' 16bit となります。何も考えない場合(20bit)よりも4bit も短くなりました。また、この bit 対応表があれば、この16bit
のデータを10文字の文字列に戻すことができます。
Huffman 符号では、必ず1文字に1bit 以上の bit を割り当てるので、どうしても
1/2, 1/4, 1/8 という bit 表現可能な出現頻度の組み合わせに近似してしまいます。具体的には
1/3 とか 2/3 という、微妙な出現頻度を符号化できないのです。
先ほどのHuffman符号の例であっても、
|
| 文字 |
出現頻度とbit |
| 'A' |
0.6 → 0 = 0.5 |
| 'B' |
0.2 → 10 = 0.25 |
| 'C' |
0.1 → 110 = 0.125 |
| 'D' |
0.1 → 111 =
0.125 |
|
と、微妙なズレがあります。
例えば "ABA" という文字列を bit 列に変換すると、Huffman 符号でも3bit
必要になります。
|
| 頻度表とHuffman符号化 |
| 文字 |
回数→bit (bit数) |
| 'A' |
2 → 0 (1bit) |
| 'B' |
1 → 1
(1bit) |
|
これは普通に bit を割り当てたのと同じになってしまい、出現頻度を考慮する効果が発揮されません。この微妙な出現頻度を符号化できる方式が算術符号であるともいえます。
算術符号では、実数(以下の解説では分数)を使って計算します。また、Huffman符号のように1文字ずつ bit を割り当てるのではなく、文字列全体を考慮して bit の割り当てを行います。
まず、"ABA"
の出現頻度表を分数で作ります。
|
| 頻度表 |
| 文字 |
頻度 |
| 'A' |
2/3 |
| 'B' |
1/3 |
|
その出現頻度をグラフにすると、以下のイメージになります。
|
1 ---+-- 3/3
A|
A|
A|
A|
A|
A|
A|
A|
A|
A|
A|
--A+-- 1/3
B|
B|
B|
B|
0 --B+-- 0/3
|
次に、最初の文字が A だった場合の2文字目の概念もグラフ内に書き込むとこうなります。
|
1 ---+-- 3/3 ---+-- 9/9
A| A|
A| A|
A| A|
A| A|
A| A|
A| --A+-- 5/9
A| B|
A| B|
--A+-- 1/3 --B+-- 3/9
B|
B|
B|
B|
0 --B+-- 0/3
|
そして、2文字目が B だった場合を追記するとこうなります。
|
1 ---+-- 3/3 ---+-- 9/9
A| A|
A| A|
A| A|
A| A|
A| A|
A| --A+-- 5/9 ---+-- 15/27
A| B| A|
A| B| --A+-- 11/27
--A+-- 1/3 --B+-- 3/9 --B+-- 9/27
B|
B|
B|
B|
0 --B+-- 0/3
|
ここが重要なのですが、このグラフから読み取れることは、11/27 〜 15/27
という範囲はすべて "ABA" という文字列の情報を示しているということです。これは、11/27
〜 15/27 の範囲内の数値は "ABA" という文字列と相互変換できるということなのです。
11/27 〜 15/27 の範囲には、1/2 が含まれます。1/2 は 0.5 なので、なんと1bit であらわすことができます。
|
: :
A| A|
A| --A+-- 5/9 ---+-- 15/27 = 0.555..
A| B| A|
[A] [B] [A] ← 1/2 = 0.5 (1bit)
A| B| A|
A| B| --A+-- 11/27 = 0.407407..
A| B| B|
--A+-- 1/3 --B+-- 3/9 --B+-- 9/27
B|
:
|
0.5 (1/2) という情報を受け取った場合、それを "ABA" という文字列に変換できるのです。
出現頻度が高い文字は、それだけ数値の範囲が広がります。範囲が広いと言うことは、それだけ短い
bit 数で表現できる確率が上がると解釈することもできます。
同じように、"AAB" や "BAA" の bit をあらわすと、
|
0 ---+-- 3/3 ---+-- 9/9 ---+-- 27/27 = 1.0
A| A| A|
A| A| A|
A| A| A|
A| A| A|
A| A| A|
A| A| A|
A| A| A|
A| A| --A+-- 19/27 = 0.703703..
A| A| B|
[A] [A] [B] ← 5/8 = 0.625 (3bit)
A| A| B|
A| --A+-- 5/9 --B+-- 15/27 = 0.555..
A| B|
A| B|
A| B|
A| B|
A| B|
--A+-- 1/3 --B+-- 3/9 ---+-- 9/27 = 0.333..
B| A| A|
[B] [A] [A] ← 1/4 = 0.25 (2bit)
B| A| A|
B| A| --A+-- 5/27 = 0.185185..
B| A| B|
B| -A+ - 1/9 --B+--
B| B|
B| B|
0 --B+-- 0/3 --B+-- 0/9
|
"AAB"
は3bit、"BAA" は2bit であらわせます。
どちらも、Huffman 符号の場合の3bit 以下の bit 数であらわせていることが確認できます。
これが算術符号のアルゴリズム(考え方)です。
ただ、算術符号には大きな問題が2つあります。
1つめの問題は、演算速度です。
プログラムでは一般に上記の分数を実数で計算し、掛け算や割り算を多用します。これが Huffman 符号などの他の符号化に比べて処理速度の点で不利になります。
もう1つの問題は、特許です。
この速度の遅さを改善する方式に特許がとられているということなのです。この問題は根が深いのですが「特許を回避して算術符号を使うことは不可能」とも言われるほど多くの特許が取られているようです。具体的にどのような特許が取られているのか、詳細な情報はありませんが、「算術符号は使えない」という言葉が常識ともいえる状態になってしまっているのは一人のプログラマとしては悲しい状態であると思います。アルゴリズムで特許を取得する場合は、もう少し考えてほしいものです。
最近、もっとも注目されているエントロピー符号化方式が、この RangeCoder です。
考えかたは算術符号とほぼ同じなので、圧縮率も高く、アルゴリズムも簡単なので
Huffman 符号と同等かそれ以上の処理速度が発揮できます。
さらに、RangeCoder の発案者は「特許を取らない」と言っている(らしい)ので、自由に使うことができます(ここが重要)。ただ、本当に特許が取られていないことが証明されているわけではありません(みんながそう言っていて、みんながそう信じているということ)。しかし、これだけのメリットがあれば、RangeCoder
が注目されるのも無理も無い話でしょう。
符号化の考え方は、算術符号も、RangeCoder もほとんど同じだと思います。両者の違いはとても微妙で、算術符号のほうは 0.0〜1.0 の幅から、実数で分割して行くのに対して、RangeCoder では 0〜100000000000 (大きな整数)の幅から、「整数で」分割して行きます。整数を使うと、いくら大きな数であっても、処理を繰り返すうちにいつか分割できない数値になってしまいます。そこで、RangeCoder では、一定の数値を下回らないように、数値を補正していきます。単純には10倍するとか、2倍するという方法です。また、算術符号であらわしている「範囲」という概念も、「上の値と下の値」であらわすのではなく、「下の値と幅(Range)」であらわしています。その他には、一般的に出力単位と精度の違いがあります。同じ bit 数を使っていても、算術符号のほうが RangeCoder より符号化の精度が高くなります。しかし、処理速度の面では、8bit単位に出力する RangeCoder が有利になっています。
|
|
特許 |
内部表現 |
範囲の概念 |
出力単位 |
精度 |
速度 |
| 算術符号 |
多数あり |
実数 |
上の値と下の値 |
1bit |
高い |
不利 |
| RangeCoder |
なし |
整数 |
下の値と幅(Range) |
8bit |
低い |
有利 |
|
|
それでは、ここから具体的な処理を説明します。"ABA"
の文字列を算術符号と同じ要領で符号化します。
まず、頻度表を作ります。
|
| 出現頻度表 |
| 文字 |
回数 |
| 'A' |
2 |
| 'B' |
1 |
| 母数 |
3 |
|
これを、Range であらわします。R = Range(幅)、L = Low(下の値)の意味です。
|
| 対応表 |
| 文字 |
Range |
備考 |
| 'A' |
[R:2, L:1] |
1 <= A < 3 |
| 'B' |
[R:1, L:0] |
0 <= B <
1 |
|
となります。ここから符号化開始です。
ここでは説明を簡単にするために精度を4bit とし最低確保する精度は3bit
とします。(ビット数多く確保するとそれだけ符号化時の圧縮性能が上がります。一般にはプロセッサ(CPU)が高速にあつかえる最大サイズ(31bitなど)がよいでしょう。)
算術符号のグラフと同じイメージで、幅の初期値は4bit の最大値(+1)です。
|
| R:16 |
← 4bit の最高値15に1を足す。 |
| L:0 |
←
最低値は0 |
|
最初に、幅 R:16 を頻度の母数3で割ります。整数で割るので小数点以下は切り捨てます。答えは5です。
これに、最初の文字 A の頻度を加えます。R は置き換え 5*2 → 10 、L は加えて
0+5*1 → 5 になります。(算術符号のグラフのイメージと照らし合わせて考えてください。)
|
| R:16 / 3 = 5 |
A[R:2] |
→ R:10 |
| L:0 |
A[L:1] |
→
L:5 |
|
この後、幅 R:10 が3bit(7) 以下にならないように注意しますが、ここでは10なのでOKです。
これを繰り返します。R:10 を3で割ると今度は3です。
次の文字は B です。R は置き換え 3*1 → 3、L は加えて 5+3*0 → 5 になります。
|
| R:10 / 3 = 3 |
B[R:1] |
→ R:3 |
| L:5 |
B[L:0] |
→
L:5 |
|
ここで幅 R が3bit(7) 以下なので、R, L ともにビットの調整(左シフト=2倍)をします。
|
| R:3 |
→ R:6 |
→ R:12 |
| L:5 |
→ L:10 |
→
L:20 |
|
ここで2bit シフトしたことを覚えておきます。
また繰り返します。R:12 を3で割ると4です。
次の文字は A です。R は置き換え 4*2 → 8、L は加えて 20+4*1 → 24 になります。
|
| R:12 / 3 = 4 |
A[R:2] |
→ R:8 |
| L:20 |
A[L:1] |
→
L:24 |
|
幅 R:8 が3bit(7) 以下にならないように注意しますが、ここでは8なのでOKです。
これで繰り返しは終わりです。
最後に、バッファの内容を出力します。ここでいうバッファとは、「4bit の精度で、最低確保する精度は3bit
」としたので、その間の1bit 分がバッファ(ワーク)になっているのです。
ここでまた1bit シフトしたことを覚えておきます。全部でシフトしたのは3bit です。最初の精度が4bit で3bit シフトしたので L は7bit 分の精度になっています。
L:48 を7bit であらわすと '0110000'です。この時、シフトした左3bit
が符号化された bit になります。ここでは '011' です。
これで符号化は終わりです。
次は、この '011'3bit を復号化してみます。
R の初期値は符号化と同じ R:16 です。(幅 R の処理は符号化とまったく同じです。)
L は4bit です。符号化された3bit '011' を左に詰めて、後ろの空きに '0'
を埋めます。('0'でなくても、OKですが。'0'のほうが楽です。^^;)L の4bit '0110' は6です。L:6
から始めます。
最初に、幅 R:16 を頻度の母数3で割ります。答えは5です。
ここで、L:6 を5で割ります。答えは1です。
この1が頻度表の中の位置を示します。頻度表で下の値が1なのは A です。
A が分かったところで、符号化と同じように文字 A の頻度を加えます。R は置き換え 5*2 → 10 ですが、L は加えるのではなく引きます。 6-5*1 → 1 になります。
|
| R:16 / 3 = 5 |
A[R:2] |
→ R:10 |
| L:6 |
A[L:1] |
→
L:1 |
|
符号化と同じように、R:10 が3bit(7) 以下にならないように注意します。ここでは10なので、OKです。
これを繰り返します。R:10 を3で割ると今度は3です。
L:1 を3で割ると答えは0です。頻度表で下の値が0なのは B です。
B の頻度を加えます。R は置き換え 3*1 → 3、L は引いて 1-3*0 → 1 になります。
|
| R:10 / 3 = 3 |
B[R:1] |
→ R:3 |
| L:1 |
B[L:0] |
→
L:1 |
|
ここで幅 R:3 が3bit(7) 以下なので、R,L ともにビットの調整(左シフト=2倍)をします。
|
| R:3 |
→ R:6 |
→ R:12 |
| L:1 |
→ L:2 |
→
L:4 |
|
L のシフトは本来は符号化で出力された bit が順に入ります。ここではもう入れる bit データがなくなっている(初期値で全部入れてしまった)ので、'0' を入れていると思ってください。
さらに繰り返します。R:12
を3で割ると今度は4です。
L:4 を4で割ると答えは1です。頻度表で下の値が1なのは A です。
ここまでで、"ABA"
が復元されました。復号はこれで終わりです。
|
符号/復号ともにやっている計算は非常に単純です。高速に処理できる理由も理解できるかと思います。しかし、どうしてこれが算術符号と同じような理屈になるのか、理解しがたいことであるとも思います。
何度かグラフに書くなどしてこの動きを追いかけてみると理解が深まると思います。算術符号と照らし合わせて考えることも有効でしょう。とても有用な符号化アルゴリズムなので、ぜひ、皆さんにもマスターしていただきたいと思っています。
他の解説は DO++ さんのページ
http://member.nifty.ne.jp/DO/algorithm/rangecoder.htm
にあります。
また、yz2α
でも RangeCoder を採用しています。
yz2α のコードは公開されていますので、そちらも参考にしてください。
http://www.01-tec.com/yz2/index.html
RangeCoder
の詳細ページ(英語)
http://www.compressconsult.com/rangecoder/
Huffman符号でも、算術符号でも、RangeCoder でも、どのエントロピー符号であっても同じような出現頻度表を作成します。この頻度表は圧縮時(符号化時)に作成されて、展開時(復号化時)に必要になります。
よって、頻度表の情報もなんらかの形で渡す必要があるわけです。頻度表の渡し方にもいくつかの方法があります。
|
一番簡単なのは、完全に固定でプログラムに組み込んでしまう方法です。
これは、いろいろなデータを用いてあらかじめ頻度情報を収集します。ある程度、頻度情報がたまったところで、その情報を固定のテーブルとしてプログラム内に組み込んでしまうわけです。圧縮時も、展開時も同じ頻度テーブルを引くわけですから、大きな問題はありません。
ただ、この作成した頻度表とは無関係の頻度を持ったデータを圧縮する場合に問題が出てきます。データを圧縮するどころか膨らんでしまう可能性だってあります。
よって、この方法は、あまりスマートであるとは言えません。
* FAX(ファクシミリ)のMH符号がこの方式ってことは意外に知られていないようです。^^;
次の方法は、データごとに出来上がった頻度表データを、圧縮データの先頭に付けて、まとめて一緒に送るという方法です。
圧縮データが少し膨らんでしまう欠点がありますが、元のデータ量が充分多ければ、頻度表のデータ量よりも圧縮されるデータ量が多くなるので、全体として
bit 数が減るわけです。どんなデータに対しても適切に対応できるところが優れています。
多くのプログラムではこの方式を採用しているようです。
* yz1 の Huffman符号 はこの方式でした。
また別の方法では、頻度表は送らずに、頻度表を実際のデータから読み取り、常に更新しながら
bit を決定していく方法があります。
データ圧縮開始時点では、頻度は均等であるとして bit を割り当てます。その後、一定(可変でも可)のデータ数ごとに、そのデータの頻度を頻度表に加えて
bit の割り当てをその都度変更していきます。
処理が重くなるし、頻度が実際の頻度に近くなるまでには少なからずデータが必要であり、そのデータに対しては圧縮がかからないという問題もあります。
しかし、データ量が多くなってくると、そのデータ内で変動する偏りにまで反映することができます。例えば、20Kバイトのデータで、前10Kバイトと後ろ10Kバイトのデータの出現頻度が違うようなデータはよくあることなのです。この場合、すべてを一括して1つの出現頻度表を作ってしまうよりも、この方式のように徐々に頻度表を変化させていったほうが、データの変化に追従できる頻度表を作ることができるのです。
* yz2 の RangeCoder はこの方式にしました。
(3)の方法では、頻度表が安定するまである程度のデータが必要です。そこで、(2)の方法を組み合わせる方法もあります。
頻度表の初期値も送り、さらにデータによって頻度を変化させてゆく。この2段構成がもっとも優れた頻度表の送り方なのではないかと思われます。
|
一般的に圧縮の前段階である、モデル化について説明します。
|
| モデル化 |
| ・ランレングス |
超簡単。誰でも知ってる圧縮方法。実用的とは言いがたい。 |
| ・前方移動 |
これも簡単。でも、あまり使われていない。 |
| ・LZ(LZ77,LZ78) |
実用度NO.1。現在一番広く使われている。 |
| ・BlockSorting |
原理は簡単、圧縮率もよい。しかし、速度が・・・。 |
| ・PPM |
現在の注目株。圧縮率 NO.1
はおそらくこれ?。 |
|
の順番に説明します。
一般に「モデル化」だけを行ってもデータが大きく圧縮されることはありません。「モデル化」の後「符号化」を行うことで、最終的にデータが圧縮されます。「モデル化」では「符号化」しやすいデータに変換しているともいえます。
また、複数の「モデル化」を組み合わせることもできます。どの組み合わせがよいか?はまだまだ未知の領域といえます。
一番簡単な変換(モデル化)方法です。略して'RLE'と呼ばれることもあります。
例えば、"AABBBCCCCDDDDD"
という4種類14文字の文字列があったとします。これに「繰り返し数」の「かける(×)」という概念を取り入れて、
|
A, A, B, ×3, C, ×4, D, ×5 |
と8個のデータに変換します。
このときの情報量の変化は、4種類の情報(A,B,C,D)が14個で(2bit×14)約28bit
から、7種類の情報(A,B,C,D,×3,×4,×5)が8個で(3bit×8)約24bit へ、4bit
減っています。
|
| "AABBBCCCCDDDDD" |
4種類の情報(A,B,C,D)×14個 |
→(2bit×14)約28bit |
| A, A, B, ×3, C, ×4, D, ×5 |
7種類の情報(A,B,C,D,×3,×4,×5)×8個 |
→(3bit×8)約24bit |
|
(「約24bit」と書いたのは、このモデル化の後のエントロピー符号のアルゴリズムは単純な符号化でも
Huffman符号でも 算術符号でも RangeCoder でもなんでもよいわけです。「約24bit」とは「最悪でも24bit以下には入るでしょう」という意味です。)
このように「繰り返し数」の概念を取り入れるのがランレングスです。同じ文字の繰り返しの多いデータには有効ですが、複雑なデータには向いていません。本格的なデータ圧縮では、あまり使われることはありません。
これも簡単な変換(モデル化)方法です。略して'MTF'と呼ばれることもあります。
出現文字テーブル(MTFテーブル)を作成し、出現した文字コードを常に先頭に移動(登録)する方法です。
例をあげると "PAPAMAMA" という文字列があった場合、最初の文字は P です。
次の文字は A です。MTFテーブルの先頭は 'A' になります。
|
| 出力コード |
P, A |
| MTFテーブル |
'A', 'P' |
|
次の文字は P ですが、MTFテーブルにすでに登録されているので、MTFテーブル内の位置(先頭からの位置)である2を出力します。そして、MTFテーブルは
P が先頭になります。
|
| 出力コード |
P, A, 2 |
| MTFテーブル |
'P', 'A' |
|
次は A です。P と同様に2になります。MTFテーブルの先頭は 'A' です。
|
| 出力コード |
P, A, 2, 2 |
| MTFテーブル |
'A', 'P' |
|
これを最後まで繰り返すと・・・
|
| 出力コード |
P, A, 2, 2, M, 2, 2, 2 |
| MTFテーブル |
'A', 'M',
'P' |
|
となります。
このように 2 という小さな数値(MTFテーブルの先頭付近を示す数値)が多く出てくる形に変換されます。出力コードの頻度が偏るデータに変換できると、後段の符号化で高い圧縮率が出せるという理屈です。
LZ とはデータ圧縮で最も有名な方式です。一般に「辞書」と言われる文字列のテーブルを作成し、データを圧縮します。
辞書とは、文字列を登録したテーブルのことです。例えば、"ABURAKATABURA"
という文字列では、"ABURA"という文字列が2回出てきています。このように、頻繁に使う文字列を辞書に登録しておくことで、文字列を辞書コードに変換できるので、情報量を減らすことができます。
LZ には大きく LZ77 系と、LZ78 系の2種類があります。Lempel さんと Ziv
さんが 1977年に発表した方式と、1978年に発表した方式という意味です。
|
LZ77 とは「スライド辞書」という概念が使われます。(辞書には圧縮元(展開先)データがそのまま利用されます。)
「スライド辞書」と呼ばれるのは辞書の表現形式が「何文字前から何文字分」という情報であらわされるからです。
例えば、"ABURAKATABURA"
という文字列では、
|
A, B, U, R, A, K, A, T, 8, 5 |
に変換されます。
8,5 とは「8文字前のから5文字分の文字列がここに入る」という意味です。n文字前という形の辞書なので、処理を進めるうちに辞書の範囲が後ろにスライドしていきます。これが「スライド辞書」と言われる理由です。
変換の結果、6種類の情報(A,B,U,R,K,T)が13個から、8種類の情報(A,B,U,R,K,T,8,5)が10個になっています。単純な符号化では、3bit×13=39bit
から3bit×10=30bit になり、9bit 減っていることになります。
|
| "ABURAKATABURA" |
6種類の情報(A,B,U,R,K,T)×13個 |
→(3bit×13)約39bit |
| A,B,U,R,A,K,A,T,8,5 |
8種類の情報(A,B,U,R,K,T,8,5)×10個 |
→(3bit×10)約30bit |
|
* 多くの圧縮プログラムは、このモデル化(LZ77)を採用しています。
LZ78系では、スライド辞書ではなく、ちゃんと辞書を作ります。
スライドはしないので、一度登録した文字列は破棄しないかぎり最後まで使用できるのが特徴です。しかし、すべての単語を登録するのは非効率ですし、どの文字列をどのタイミングで登録&破棄するのか?は実装時の大きな問題であったりします。
以下、"ABURAKATABURA"
という文字列を使って動作を説明します。
例えば、2文字の辞書を作ると決めたとします。(これは例ですので、正確な
LZ78 とは異なります。^^;)文字が現れた順番に2文字ずつどんどん辞書に登録していきます。
|
| LZ78辞書 |
| 番号 |
文字列 |
| 1 |
'AB' |
| 2 |
'BU' |
| 3 |
'UR' |
| 4 |
'RA' |
| 5 |
'AK' |
| 6 |
'KA' |
| 7 |
'AT' |
| 8 |
'TA' |
| (9) |
('AB') |
| (10) |
('BU') |
| (11) |
('UR') |
| (12) |
('RA') |
|
ですが、番号 9〜12 の文字列は、番号 1〜4 の文字列と同じです。同じということは、1〜4 で置き換えればよいと言うことです。番号 9〜12 の文字列は、あえて辞書に登録する必要はありません。
で、元の文字列 "ABURAKATABURA"
を辞書コードありに変換すると、
|
A, B, U, R, A, K, A, T, 1, 3,
A |
となります。1 と 3
は辞書の番号です。
変換の結果、6種類の情報(A,B,U,R,K,T)が13個から、8種類の情報(A,B,U,R,K,T,1,3)が11個になっています。単純な符号化では、3bit×13=39bit
から3bit×11=33bit になり、6bit 減っていることになります。
|
| "ABURAKATABURA" |
6種類の情報(A,B,U,R,K,T)×13個 |
→(3bit×13)約39bit |
| A,B,U,R,A,K,A,T,1,3 |
8種類の情報(A,B,U,R,K,T,1,3)×11個 |
→(3bit×11)約33bit |
|
| ▼LZWって・・・(Lempel + Ziv + Welch) |
LZ78系の後継に、LZW というモデル化の方式があります。考え方は LZ78 とほぼ同じです。(ある程度の文字列をあらかじめ辞書の初期値として埋めているらしい。)
LZW というと・・・そうです。
あの悪名高き画像形式 GIF で使われているアルゴリズムです。この LZW は
UNISYS という会社が特許を取っていまして、インターネットで普及した GIF 形式に対して、急にライセンス料金を徴収し始めました。こういうやり方にはとても不満が残ります。
ご参考「LZW非抵GIF」
http://hp.vector.co.jp/authors/VA012541/gif_main.html
|
| ●BlockSorting (Burrows-Wheeler
Transform) |
|
比較的新しいモデル化の概念で、LZよりも高い圧縮率(効率のよい変換)をします。(発案者の名前をとって、 BWT = Burrows-Wheeler Transform とも呼ばれています。)
ただ、ソートの処理時間がデータ量に指数比例してしまうため、どの程度のデータを処理させるのかあらかじめ上限を決める必要があります。よって、データ量が多いと単純に圧縮率が上がるというわけにはなりません。
|
では、"ACAB"
という文字列を変換してみます。
まず、ブロックを作ります。ぐるっと文字を1つずつシフトするのです。
|
|
A |
C |
A |
B |
|
C |
A |
B |
A |
|
A |
B |
A |
C |
|
B |
A |
C |
A |
|
これをソートします。ソートするときに最初のデータがわかるようにしるし(→)を付けます。
|
| → |
A |
C |
A |
B |
|
C |
A |
B |
A |
|
A |
B |
A |
C |
|
B |
A |
C |
A |
|
|
|
A |
B |
A |
C |
| → |
A |
C |
A |
B |
|
B |
A |
C |
A |
|
C |
A |
B |
A |
|
このときの最初のデータの位置(上から2番目という)情報と、ソート後の一番最後の桁、C,B,A,A
が変換後のデータになります。
処 理手順としてはこれだけなのですが、ソートの処理量は、データの量(ブロックの量)に比例して重くなります。処理速度を考慮して、データの量を制限するなどの対策が必要になります。
変換後の情報量は減っているようには見えませんが、この並びが圧縮に効くのです。例えば、'AB'
という単語がペアでたくさん出てくるような文字列をソートすると。
と、最後の文字に A がたくさん並びます。この同じ文字がたくさん並ぶこと(偏ること)は符号化しやすい(データ量が減っている)といえるわけです。同じ単語がたくさん出てくれば出てくるほどデータ量を減らせるという理屈なのです。
今度は、この情報を復元してみます。受け取れる情報は以下のようなモノです。
図にすると。
|
|
x |
x |
x |
C |
| → |
x |
x |
x |
B |
|
x |
x |
x |
A |
|
x |
x |
x |
A |
|
そして、先頭の文字列はわかります。C,B,A,A を 文字としてソートすれば先頭にそのまま入ります。この場合、上から
A,A,B,C です。
|
|
A |
x |
x |
C |
| → |
A |
x |
x |
B |
|
B |
x |
x |
A |
|
C |
x |
x |
A |
|
さらにここでわかる情報は、後ろの文字と先頭の文字はループしている(繋がっている)ということです。
|
|
A |
x |
x |
C |
→ |
A |
| → |
A |
x |
x |
B |
→ |
A |
|
B |
x |
x |
A |
→ |
B |
|
C |
x |
x |
A |
→ |
C |
|
ここまで来るとあと一息です。説明しやすいように図に座標を書きます。
|
|
|
a |
b |
c |
d |
|
e |
|
1 |
A |
x |
x |
C |
→ |
A |
| → |
2 |
A |
x |
x |
B |
→ |
A |
|
3 |
B |
x |
x |
A |
→ |
B |
|
4 |
C |
x |
x |
A |
→ |
C |
|
まずスタートは、(e,2) A です。(a,2) A と意味は同じです。
この A は e の列の上から2番目の A なので、d 列でも上から2番目の(d,4) A と同じなのです。よって(d,4) A の次は(e,4) C です。
この C は e の列の上から1番目の C なので、d 列でも上から1番目の(d,1)
C と同じです。よって(d,1) C の次は(e,1) A です。
この A は e の列の上から1番目の A なので、d 列でも上から1番目の(d,3)
A と同じです。よって(d,3) A の次は(e,3) B です。
これで、復元作業は終わりです。
理屈がわかれば、やっている事は簡単なのです。復元処理の速さも BlockSorting
の特徴と言えます。
|
他の解説として、DO++ さんのページも参照ください
http://member.nifty.ne.jp/DO/algorithm/blocksorting.htm
BlockSorting
を採用したアーカイバソフトには、GCA や BZIP2 があります。
どちらも圧縮速度は高速で、他のアーカイバソフトに負けていません。
GCA
http://www1.odn.ne.jp/synsyr/gca.html
BZIP2
http://www.linux.or.jp/JM/html/bzip2/man1/bzip2.1.html
| ●PPM (Prediction by Partial
Matching) |
|
直訳すると「部分一致予測」です。
PPM は「モデル化」+「符号化」の2段構成方式とは言い難い面を持っていますが、どちらかと言えば「モデル化」の概念です。
簡単に説明すると、複数の出現頻度表を持って、場合によって使い分けることを主な特徴としています。(符号化ではなく、PPM
で頻度表を作成するところが単純に2段構成とは言い難いところ)
例えば、英字と数字の混在する文字列を圧縮する場合、頻度表を2つ作り、
|
| (A) |
・英字が来た場合の次の文字の出現頻度表 |
| (B) |
・数字が来た場合の次の文字の出現頻度表 |
|
と用途を決めます。
一般的な文字列では、英字の次に来る文字は英字である確率が高いので、英字が来た場合の出現頻度表(A)の値は、英字の出現頻度が高くなります。またもう一方の、数字が来た場合の出現頻度表(B)の値は、数字の出現頻度が高くなります。
このように、出現確率は均等ではなく、ある部分(ある状態)によって次の文字の出現確率が変化していることに着目した考え方なのです。
この方式によって、出現頻度表が1つの符号化アルゴリズムよりも、より的確な符号化ができるようになります。
文字の前後関係によって、出現頻度(出現確率)を変えるので、「次の文字を予測する方式」といわれます。
この考え方を進めて行くと、'A〜Z'など、各文字単位に頻度表を作成する方法が考えられます。256種類の文字があるのなら、256個の頻度表を作るという考え方です。(この1文字分のすべての頻度表を作る考え方を
Order-1 と呼んでいるようです。また2文字分の頻度表を作ると Order-2 と呼ぶそうです。Order-2
では1文字256種類とすると、すべての頻度表を確保するには65536個分のメモリ空間が必要になりますが。)
256個もの頻度表を実際に実装するには膨大なメモリ空間が必要になりますので、使用頻度の高い文字コードの頻度表から動的に作成したり、使わなそうな頻度表を破棄したりする工夫も必要になってきます。
では、簡単な例をみてみましょう。
例えば
"ABAB1212" を変換する場合、これを1つの頻度表で符号化(Huffman符号化)したとすると、
|
| Huffman符号化 |
| 文字 |
出現回数 |
bit |
| 'A' |
2 |
00 |
| 'B' |
2 |
01 |
| '1' |
2 |
10 |
| '2' |
2 |
11 |
|
となり、bit に変換すると
|
'00 01 00 01 10 11 10 11' |
16bit となります。では、英字と数字の2つの頻度表を作った場合を考えてみます。
|
英字が来た場合の次の文字の
出現頻度表(A) |
|
| 文字 |
出現回数 |
|
| 'A' |
2 |
先頭の A はその前に英字が来ていたと仮定してカウント |
| 'B' |
2 |
|
| '1' |
1 |
|
数字が来た場合の次の文字の
出現頻度表(B) |
|
| 文字 |
出現回数 |
|
| '2' |
2 |
|
| '1' |
1 |
|
|
この2つの頻度表を Huffman符号で符号化してみます。
|
英字が来た場合の次の文字の
出現頻度表(A) |
| 文字 |
出現回数 |
bit |
| 'A' |
2 |
0 |
| 'B' |
2 |
10 |
| '1' |
1 |
11 |
数字が来た場合の次の文字の
出現頻度表(B) |
| 文字 |
出現回数 |
bit |
| '2' |
2 |
0 |
| '1' |
1 |
1 |
|
この2つの頻度表から "ABAB1212" を符号化すると
11bit になりました。5bit 圧縮されたことになります。
これはかなり極端な例ですが、考え方はこのような考え方であると思っていただいてよいかと思います。
理論だけがあっても実装できなければ意味がありません。
いままで紹介した「モデル化」と「符号化」の理論を実際に実装したアーカイバソフトを紹介します。
|
▼LZH |
|
LZ77 + Huffman の2段構成です。日本で最も有名なソフトです。
|
|
▼ZIP |
|
LZ77 + Huffman の2段構成です。世界で最も有名なソフトです。
|
|
▼yz1 |
|
私が作った最初のアーカイバソフトです。基本は、LZ77 + Huffman の2段構成です。
ですが、他のLZ77系のアルゴリズムと決定的に違うところはLZ77 の辞書の作り方です。
実は yz1 では、LZ77
の辞書が256個あります。PPM の Order-1
のように、1つ前の文字コードによって、使う辞書を変えているのです。これによって、高速でかつ圧縮率の高いアルゴリズムを実現しています。
|
|
▼yz2 |
|
基本は、yz1 と同じです。ただ、後段の符号化を Huffman から RangeCoder
に変更しています。yz1 より、ちょっぴり高速で、ちょっぴり高圧縮なアルゴリズムになっています。一応完成はしたのですが、作者(私)が納得できないため、まだα版のままだったりします。
|
より深く勉強したい人のために参考文献をあげておきます。
last update
2001/12/19
since 2001/07/17
|
|