M.Hiroi's Home Page

Common Lisp Programming

お気楽 Common Lisp プログラミング入門

[ PrevPage | Common Lisp | NextPage ]

有限文脈モデル (2)

有限文脈モデルの続きです。今回は、多値レンジコーダを用いた有限文脈モデルにおいて、圧縮率を向上させる有効な方法を紹介しましょう。

●PPM (Prediction by Partial Matching) とエスケープ記号

高い圧縮率を実現しているデータ圧縮アルゴリズムに "PPM (Prediction by Partial Matching)" があります。PPM は 1984 年に J. G. Cleary 氏と I. H. Witten 氏によって提案されたアルゴリズムで、文脈から作られる統計的モデルによって次の文字の出現確率を予測する方法です。

簡単にいえば「高次の有限文脈モデル」になりますが、PPM はまだ現れていない記号(未出現記号)をエスケープ (escape) 記号とし、このエスケープ記号を使って符号化するところが特徴です。

もう少し具体的に説明しましょう。PPM の基本的な考え方はそれほど難しくありません。たとえば、有限文脈モデルの次数を 5 次 (order-5) としましょう。記号 a を符号化する場合、まず order-5 の出現頻度表を調べます。ここで、order-5 に記号 a が存在すれば、それを算術符号 (またはレンジコーダ) で符号化します。もし、記号 a が存在しない場合は、エスケープ記号を符号化して order-4 の出現頻度表を調べます。つまり、次数を下げながら記号 a が出現している文脈モデルを探すわけです。

PPM は高次の有限文脈モデルを操作するため、実際のプログラムはかなり複雑になります。いきなり PPM をプログラムするのは大変ですが、エスケープ記号というアイデアは PPM 以外の圧縮アルゴリズムにも用いることができそうです。そこで、有限文脈モデル (order-2) のプログラムにエスケープ記号を適用して、どのくらい効果があるか試してみましょう。

●エスケープ確率の計算方法

プログラムは簡単です。order-2 の出現頻度表で記号が見つからない場合は、その出現頻度表でエスケープ記号を符号化します。それから、order-0 の出現頻度表で記号を符号化します。つまり、有限文脈モデルは order-2 -> order-0 という 2 段階になります。今回は簡単なテストということで、order-2 -> order-1 -> order-0 という 3 段階のモデルにはしません。ご了承くださいませ。

PPM の場合、エスケープ記号に与える出現確率をエスケープ確率と呼びます。そして、エスケープ確率の計算方法を Method と呼びます。Method にはいくつかの方式が提案されていますが、PPM では Method C や Method D が経験的に良い性能を持つと言われています。

Method C : u / (n + u) 
Method D : (u / 2) / n
n : そのモデルで出現した記号の総数
u : そのモデルで出現したエスケープ記号の総数

適応型レンジコーダで符号化する場合、Method C はとても簡単に実現できます。まず、記号の出現頻度表は、エスケープ記号を 1 とし他の記号は 0 に初期化します。たとえば、記号 a を符号化する場合、表に記号 a がない(個数 0) 場合はエスケープ記号を符号化し、出現頻度表を更新します。このとき、記号 a とエスケープ記号の個数を増やせば、通常のレンジコーダで Method C を実現することができます。

つまり、記号 a を符号化したときは出現頻度表の a の個数を +1 して、エスケープ記号を符号化したときは記号 a とエスケープ記号の個数を +1 するのです。これでエスケープ記号の出現確率は u / (n + u) になるので、あとは通常のレンジコーダで符号化すればいいわけです。とても簡単ですね。

Method D は Method C の改良版です。Method C でエスケープ記号を符号化したとき、記号 a とエスケープ記号の個数を +1 ずつしましたが、これを 0.5 ずつにして合わせて +1 とするのが Method D です。Method C では、エスケープ記号の個数を +1 しているため、記号の出現確率よりもエスケープ確率の割合が増大する場合がありますが、Method D ではこれを補正することができます。

Method D を簡単に実現するには、記号 a を符号化するときに記号の個数を +2 すればいいでしょう。これでエスケープ記号の割合が増大するのを補正することができます。あとは通常の適応型レンジコーダで符号化すれば、Method D に相当する出現確率で符号化することができます。

●出現頻度表の生成と更新

それではプログラムを作りましょう。エスケープ記号を含む出現頻度表は bitree を使って簡単に作成することができます。

リスト : 出現頻度表の初期化

;;; order-0 用出現頻度表
(defun initialize-bitree ()
  (let ((bt (make-bitree code-size)))
    (dotimes (x code-size bt)
      (bitree-update bt x 1))))

;;; order-2 用出現頻度表
(defun initialize-bitree-with-esc ()
  (let ((bt (make-bitree (1+ code-size))))
    (bitree-update bt esc 1)
    bt))

order-2 用の出現頻度表は関数 initialize-bitree-with-esc で初期化します。エスケープ記号の値 (256) は変数 esc にセットします。esc を付け加えるので記号の種類は code-size + 1 になります。make-bitree で出現頻度表 bt を生成するとき、各記号の出現頻度は 0 に初期化されているので、bitree-update で esc の値を +1 します。最後に bt を返します。

次は出現頻度表を更新する関数 update を修正します。

リスト : 出現頻度表の更新

(defun update (bt c inc &optional c1)
  (bitree-update bt c inc)
  (when c1
    (bitree-update bt c1 inc))
  (when (<= *max-sum* (bitree-sum bt))
    (dotimes (x (bitree::bitree-size bt))
      (let ((n (ash (bitree-frequency bt x) -1)))
        (when (plusp n)
          (bitree-update bt x (- n)))))))

エスケープ記号を符号化したあと、記号とエスケープ記号の出現頻度を更新します。エスケープ記号だけ更新すると、いつまでたっても他の記号を符号化することはできません。記号を符号化した場合は、引数 c と c1 には同じ記号を渡します。これで Method D に相当する出現確率になります。出現頻度の合計値が *max-sum* 以上になったら、出現している記号の個数を半分にします。このとき、エスケープ記号の値も半分にする必要があるので、出現頻度表の大きさを関数 bitree-size で取得します。

なお、記号の増分値 inc に大きな値を設定すると、圧縮率が向上する場合があります。これはあとで試してみましょう。

●update exclusion

次は記号を符号化する関数 encode-with-esc を作ります。

リスト : 記号の符号化

(defun encode-with-esc (rc c bt2 bt0)
  (cond ((zerop (bitree-frequency bt2 c))
         (encode rc bt2 esc)
         (encode rc bt0 c)
         (update bt2 c *inc2* esc)
         (update bt0 c inc0))
        (t
         (encode rc bt2 c)
         (update bt2 c *inc2* c))))

引数 rc はレンジコーダのオブジェクト、bt2 は order-2 の出現頻度表、bt0 は order-0 の出現頻度表、c は符号化する記号です。bitree-frequency で記号 c の出現頻度を求めます。それが 0 の場合、記号 c は出現していないので関数 encode でエスケープ記号を bt2 で符号化し、それから bt0 で c を符号化します。そうでなければ bt2 で c を符号化します。

次に、update で出現頻度表 bt0 と bt2 を更新します。inc0 と *inc2* は記号の増分値です。inc0 は order-0 用で値は 4 に、*inc2* は order-2 用で 4 以上の値に設定します。ここで、出現頻度表 bt2 と bt0 の更新に注目してください。order-2 で記号 c を符号化したあと、order-0 の出現頻度表 bt0 の更新は行っていませんね。このように、低次の出現頻度表を更新しない方法を update exclusion といいます。

exclusion には「除外」という意味があり、低次のモデルを更新から除外することで、圧縮率を向上させることができます。実際に試してみると高次のモデルだけ更新した方が圧縮率は高くなります。また、低次のモデルを更新する必要がないので、実行速度の点でも有利になります。

次は記号を復号する関数 decode-with-esc を作ります。

リスト : 記号の復号

(defun decode-with-esc (rc bt2 bt0)
  (let ((c (decode rc bt2)))
    (cond ((= c esc)
           (setq c (decode rc bt0))
           (update bt0 c inc0)
           (update bt2 c *inc2* esc))
          (t (update bt2 c *inc2* c)))
    c))

order-2 の出現頻度表 bt2 を使って記号を復号します。復号した記号 c がエスケープ記号であれば、order-0 の出現頻度表 bt0 で記号を復号して変数 c にセットします。それから、update で bt2 と bt0 を更新します。bt2 で復号した記号がエスケープ記号でなければ bt2 を更新するだけです。最後に記号 c を返します。

●符号化と復号処理

最後に符号化と復号を行う関数 encode と decode を作ります。

リスト : エスケープ記号付き有限文脈モデル

(defun encode-order-2 (rc in size)
  (let ((bt0 (initialize-bitree))
        (bt2-table (make-bitree-order-2))
        (c0 0)
        (c1 0))
    (dotimes (x size)
      (let ((c2 (read-byte in)))
        (encode-with-esc rc
                         c2
                         (get-bitree-order-2 bt2-table c0 c1)
                         bt0)
        (setq c0 c1
              c1 c2)))))

(defun decode-order-2 (rc out size)
  (let ((bt0 (initialize-bitree))
        (bt2-table (make-bitree-order-2))
        (c0 0)
        (c1 0))
    (dotimes (x size)
      (let ((c2 (decode-with-esc rc
                                 (get-bitree-order-2 bt2-table c0 c1)
                                 bt0)))
        (write-byte c2 out)
        (setq c0 c1
              c1 c2)))))

有限文脈モデル (order-2) のプログラムとの違いは、order-0 と order-2 の出現頻度表を用意することです。encode-with-esc と decode-with-esc を呼び出すとき、order-2 用の出現頻度表を get-bitree-order-2 で求めます。order-0 用の出現頻度表 bt0 はそのまま渡します。なお、累積度数の上限値 *max-sum* は #x4000 としました。

●実行結果

それでは、実際に Canterbury Corpus で配布されているテストデータ The Canterbury Corpus を圧縮してみましょう。*inc2* の値を +16, +32, +64 に変更して試してみたところ、結果は次にようになりました。

        表 : 有限文脈モデル + ESC 記号の結果

  ファイル名      サイズ    LHA(lh5)    +16       +32       +64
  ----------------------------------------------------------------
  alice29.txt    152,089     59,117    52,024    51,989    52,128
  asyoulik.txt   125,179     52,341    44,347    44,327    44,453
  cp.html         24,603      8,384     8,597     8,585     8,574
  fields.c        11,150      3,170     3,445     3,441     3,428
  grammar.lsp      3,721      1,271     1,282     1,281     1,279
  kennedy.xls  1,029,744    198,342   177,737   166,218   155,516
  lcet10.txt     426,754    159,558   147,007   146,859   147,301
  plrabn12.txt   481,861    210,045   170,076   170,524   171,968
  ptt5           513,216     52,305    53,374    53,746    54,612
  sum             38,240     13,993    15,163    15,125    15,056
  xargs.1          4,227      1,778     1,763     1,763     1,763
  ----------------------------------------------------------------
  合計         2,810,784    760,304   674,815   663,858   656,078

エスケープ記号の効果はとても大きいですね。*inc2* は +32 程度でよさそうです。order-2 にエスケープ記号を適用しただけで、LHA を上回る圧縮率になるとは大変驚きました。PPM の圧縮率がなぜ高いのか、その理由が少しだけわかったような気がしました。

●exclusion

PPM にはもうひとつ重要な方法があります。高次のモデルでエスケープ記号を符号化したあと、低次のモデルで記号を符号化するとき、高次のモデルで出現している記号を低次のモデルの出現頻度表から除外することができます。これを exclusion といいます。

実は、低次のモデルで符号化するとき、高次のモデルで出現した記号は必要ありません。復号処理から見た場合、エスケープ記号が復号されるということは、復号される記号はそのモデルに出現していない記号であることがわかります。これらの記号は復号処理に必要ありませんね。

したがって、低次のモデルで高次モデルの出現記号を取り除いて符号化しても、復号処理で同じように高次モデルの出現記号を除外すれば、記号をきちんと復号することができるのです。そして exclusion を行うことにより、符号化する記号に割り当てられる出現確率を増やすことができます。これが exclusion の真の目的です。PPM ではこの効果が絶大で、圧縮率を大幅に向上させることができます。

簡単な例を示しましょう。記号 c を符号化することを考えてみます。

記号 c を符号化                            exclusion  無        有
5 次 => {a (1), escape (1)}                escape   : 1/2       1/2
4 次 => {a (1), b (1), escape (2)}         escape   : 2/4       2/3 (a を除外)
3 次 => {a (1), b (1), c (1), escape (3)}  c        : 1/6       1/4 (a, b を除外)
                                           確率     : 1/24      1/12
                                           情報量   : 4.58 bit  3.58 bit

5 次と 4 次のモデルに記号 c はないので、エスケープ記号が符号化されます。3 次のモデルには記号 c があるので、このモデルで c が符号化されます。exclusion がない場合、確率は 1/2 * 2/4 * 1/6 = 1/24 になります。

exclusion がある場合、5 次のモデルではエスケープ記号の確率は同じですが、4 次のモデルでは記号 a を除外できるので、エスケープ記号の確率は 2/3 となります。同様に 3 次のモデルで記号 c を符号化するとき、5 次と 4 次で出現している記号 a と b を除外できるので、確率は 1/4 になります。したがって、記号 c の確率は全体で 1/2 * 2/3 * 1/4 = 1/12 になります。

情報量 (-log2確率) を計算すると、exclusion がない場合が 4.58 bit になり、exclusion がある場合が 3.58 bit になります。exclusion を行った方が 1 bit 少なくなりますね。それだけ短い符号語で記号を符号化することができるわけです。これが exclusion の効果です。

この exclusion と update exclusion は効果がとても高く、PPM では常套手段といえる方法です。

●exclusion の実装

それでは exclusion のプログラムを作りましょう。最初に exclusion 用の操作関数を定義します。次のリストを見てください。

リスト : exclusion 用操作関数

;;; ESC 以外の出現している記号をすべて求める
(defun get-symbol (bt)
  (do ((a nil)
       (x 0 (1+ x)))
      ((= x code-size) a)
    (if (plusp (bitree-frequency bt x))
        (push x a))))

;;; 記号を除外する
(defun exclusion (bt ls)
  (mapcar (lambda (x)
            (let ((n (bitree-frequency bt x)))
              (bitree-update bt x (- n))
              (cons x n)))
          ls))

;;; 元に戻す
(defun restore-exclusion (bt ls)
  (dolist (x ls)
    (bitree-update bt (car x) (cdr x))))

関数 get-symbol は出現している記号をリストに格納して返します。0 から 255 までの記号の出現頻度を関数 bitree-frequency で求め、その値が 0 でなければ記号 x を変数 a のリストに追加します。

関数 exclusion は出現頻度表 bt からリスト ls に格納されている記号を除外します。まず、bitree-frequency で記号 x の出現頻度を求めて変数 n にセットし、次に bitee-update で (- n) を加算します。これで記号の出現頻度を 0 にすることができます。なお、元の値に復元するため、記号と出現頻度をリストに格納して返します。関数 restore-exclusion は exclusion で除外した記号の出現頻度を元に戻します。

次は、符号化と復号のプログラムを修正します。

リスト : 符号化と復号 (exclusion)

(defun encode-with-esc (rc c bt2 bt0)
  (cond ((zerop (bitree-frequency bt2 c))
         (encode rc bt2 esc)
         (let ((ls (exclusion bt0 (get-symbol bt2))))
           (encode rc bt0 c)
           (restore-exclusion bt0 ls))
         (update bt2 c *inc2* esc)
         (update bt0 c inc0))
        (t
         (encode rc bt2 c)
         (update bt2 c *inc2* c))))

(defun decode-with-esc (rc bt2 bt0)
  (let ((c (decode rc bt2)))
    (cond ((= c esc)
           (let ((ls (exclusion bt0 (get-symbol bt2))))
             (setq c (decode rc bt0))
             (restore-exclusion bt0 ls))
           (update bt0 c inc0)
           (update bt2 c *inc2* esc))
          (t (update bt2 c *inc2* c)))
    c))

符号化の場合、bt0 で記号を符号化するときに exclusion を適用します。get-symbol で bt2 の記号を求め、exclusion で bt0 から除外します。そのあと、encode で記号を符号化して restore-exclusion で元に戻します。

復号も同様です。bt0 で記号を復号するとき、get-symbol で bt2 の記号を求め、exclusion で bt0 から除外します。そのあと、decode で記号を復号して restore-exclusion で元に戻します。

●実行結果 (2)

それでは、実際に Canterbury Corpus で配布されているテストデータ The Canterbury Corpus を圧縮してみましょう。*inc2* の値を +16, +32, +64 に変更して試してみたところ、結果は次にようになりました。

        表 : 有限文脈モデル + ESC 記号 + exclusion の結果

                                                exclusion          exclusion
                                                   有                  無
  ファイル名      サイズ    LHA(lh5)    +16       +32       +64       +64
  ---------------------------------------------------------------------------
  alice29.txt    152,089     59,117    51,729    51,694    51,833    52,128
  asyoulik.txt   125,179     52,341    44,057    44,037    44,162    44,453
  cp.html         24,603      8,384     8,481     8,469     8,458     8,574
  fields.c        11,150      3,170     3,417     3,412     3,400     3,428
  grammar.lsp      3,721      1,271     1,271     1,271     1,269     1,279
  kennedy.xls  1,029,744    198,342   163,025   151,506   140,804   155,516
  lcet10.txt     426,754    159,558   146,523   146,375   146,817   147,301
  plrabn12.txt   481,861    210,045   169,518   169,967   171,410   171,968
  ptt5           513,216     52,305    52,590    52,961    53,827    54,612
  sum             38,240     13,993    14,907    14,869    14,800    15,056
  xargs.1          4,227      1,778     1,744     1,744     1,744     1,763
  ----------------------------------------------------------------------------
  合計         2,810,784    760,304   657,262   646,305   638,524   656,078

*inc2* が +64 の場合、exclusion によりすべてのファイルで圧縮率が向上しています。exclusion の効果はとても高いですね。ただし、exclusion を行うことで符号化・復号ともに時間がかかるようになります。

ところで、最高次数を増やすと圧縮率はさらに向上します。ご参考までに、order-3 の結果を示します。なお、このプログラムは order-3 から order-2, order-1, order-0 と次数を減らして記号が出現しているか調べています。基本的には最高次数が 3 の PPM とほぼ同じ処理になります。

  表 : 有限文脈モデル (order-3) + ESC 記号 + exclusion の結果

  ファイル名      サイズ  order-3    LHA     bzip2
  --------------------------------------------------
  alice29.txt    152,089   43,168   59,117   43,202
  asyoulik.txt   125,179   38,922   52,341   39,569
  cp.html         24,603    7,146    8,384    7,624
  fields.c        11,150    2,932    3,170    3,039
  grammar.lsp      3,721    1,124    1,271    1,283
  kennedy.xls  1,029,744  130,976  198,342  130,280
  lcet10.txt     426,754  115,182  159,558  107,706
  plrabn12.txt   481,861  145,647  210,045  145,577
  ptt5           513,216   52,702   52,305   49,759
  sum             38,240   12,961   13,993   12,909
  xargs.1          4,227    1,563    1,778    1,762
  --------------------------------------------------
  合計         2,810,784  552,323  760,304  542,710

order-3 で bzip2 に迫る圧縮率になりました。PPM は大変優れた圧縮アルゴリズムであることがわかります。ただし、最高次数を増やすとメモリの使用量は大幅に増加し、符号化と復号ともに時間がとてもかかるようになります。今回の order-3 のプログラムでも、大きなファイルを圧縮するとメモリが足りなくなるかもしれません。実用的に使うには処理の高速化と、限られたメモリを有効に使う工夫が必要なります。

PPM に興味のある方は拙作のページ Algorithms with Python Prediction by Partial Matching (PPM) をお読みくださいませ。


●プログラムリスト1

;;;
;;; rce2.lsp : 適応型レンジコーダ (order-2 with ESC)
;;;
;;;            Copyright (C) 2010-2023 Makoto Hiroi
;;;
(require :rangecoder "rangecoder.lsp")
(use-package :rangecoder)
(require :bitree "bitree.lsp")
(use-package :bitree)

;;; 定数
(defconstant esc code-size)
(defconstant inc0 4)

;;; グローバル変数
(defvar *max-sum* #x4000)
(defvar *inc2* 64)

;;; ファイルサイズの書き込み
(defun write-file-size (out size)
  (write-byte (logand (ash size -24) #xff) out)
  (write-byte (logand (ash size -16) #xff) out)
  (write-byte (logand (ash size -8) #xff) out)
  (write-byte (logand size #xff) out))

;;; ファイルサイズの読み込み
(defun read-file-size (in)
  (+ (ash (read-byte in) 24)
     (ash (read-byte in) 16)
     (ash (read-byte in) 8)
     (read-byte in)))

;;; 更新
(defun update (bt c inc &optional c1)
  (bitree-update bt c inc)
  (when c1
    (bitree-update bt c1 inc))
  (when (<= *max-sum* (bitree-sum bt))
    (dotimes (x (bitree::bitree-size bt))
      (let ((n (ash (bitree-frequency bt x) -1)))
        (when (plusp n)
          (bitree-update bt x (- n)))))))

;;; 初期化
(defun initialize-bitree ()
  (let ((bt (make-bitree code-size)))
    (dotimes (x code-size bt)
      (bitree-update bt x 1))))

(defun initialize-bitree-with-esc ()
  (let ((bt (make-bitree (1+ code-size))))
    (bitree-update bt esc 1)
    bt))

;;;
;;; 記号の符号化と復号
;;;

;;; 符号化
(defun encode (rc bt c)
  (let ((temp (floor (range-coder-range rc)
                     (bitree-sum bt))))
    (incf (range-coder-low rc)
          (* (bitree-cumul bt c) temp))
    (setf (range-coder-range rc)
          (* (bitree-frequency bt c) temp))
    (encode-normalize rc)))

;;; 復号
(defun decode (rc bt)
  (let ((temp (floor (range-coder-range rc)
                     (bitree-sum bt))))
    (multiple-value-bind
        (c cumul)
        (bitree-find bt (floor (range-coder-low rc) temp))
      (decf (range-coder-low rc)
            (* cumul temp))
      (setf (range-coder-range rc)
            (* (bitree-frequency bt c) temp))
      (decode-normalize rc)
      c)))

;;;
;;; 出現頻度表の生成
;;;

;;; order-2 用出現頻度表の作成
(defun make-bitree-order-2 ()
  (make-array (list code-size code-size) :initial-element nil))

;;; order-2 の出現頻度表を取得
(defun get-bitree-order-2 (table c0 c1)
  (let ((bt (aref table c0 c1)))
    (if bt
        bt
      (setf (aref table c0 c1) (initialize-bitree-with-esc)))))

;;;
;;; exclusion 用操作関数
;;;

;;; ESC 以外の出現している記号をすべて求める
(defun get-symbol (bt)
  (do ((a nil)
       (x 0 (1+ x)))
      ((= x code-size) a)
    (if (plusp (bitree-frequency bt x))
        (push x a))))

;;; 記号を除外する
(defun exclusion (bt ls)
  (mapcar (lambda (x)
            (let ((n (bitree-frequency bt x)))
              (bitree-update bt x (- n))
              (cons x n)))
          ls))

;;; 元に戻す
(defun restore-exclusion (bt ls)
  (dolist (x ls)
    (bitree-update bt (car x) (cdr x))))

;;;
;;; エスケープ記号付き有限文脈モデル
;;;

;;; 符号化
(defun encode-with-esc (rc c bt2 bt0)
  (cond ((zerop (bitree-frequency bt2 c))
         (encode rc bt2 esc)
         (let ((ls (exclusion bt0 (get-symbol bt2))))
           (encode rc bt0 c)
           (restore-exclusion bt0 ls))
         (update bt2 c *inc2* esc)
         (update bt0 c inc0))
        (t
         (encode rc bt2 c)
         (update bt2 c *inc2* c))))

(defun encode-order-2 (rc in size)
  (let ((bt0 (initialize-bitree))
        (bt2-table (make-bitree-order-2))
        (c0 0)
        (c1 0))
    (dotimes (x size)
      (let ((c2 (read-byte in)))
        (encode-with-esc rc
                         c2
                         (get-bitree-order-2 bt2-table c0 c1)
                         bt0)
        (setq c0 c1
              c1 c2)))))

;;; 復号
(defun decode-with-esc (rc bt2 bt0)
  (let ((c (decode rc bt2)))
    (cond ((= c esc)
           (let ((ls (exclusion bt0 (get-symbol bt2))))
           (setq c (decode rc bt0))
             (restore-exclusion bt0 ls))
           (update bt0 c inc0)
           (update bt2 c *inc2* esc))
          (t (update bt2 c *inc2* c)))
    c))

(defun decode-order-2 (rc out size)
  (let ((bt0 (initialize-bitree))
        (bt2-table (make-bitree-order-2))
        (c0 0)
        (c1 0))
    (dotimes (x size)
      (let ((c2 (decode-with-esc rc
                                 (get-bitree-order-2 bt2-table c0 c1)
                                 bt0)))
        (write-byte c2 out)
        (setq c0 c1
              c1 c2)))))

;;; ファイルの符号化
(defun encode-file (in-file out-file)
  (call-with-byte-output-file
   out-file
   (lambda (out)
     (call-with-byte-input-file
      in-file
      (lambda (in)
        (let ((size (file-length in)))
          (write-file-size out size)
          (when (plusp size)
            (call-with-range-encoder
             out
             (lambda (rc)
               (encode-order-2 rc in size))))))))))

;;; ファイルの復号
(defun decode-file (in-file out-file)
  (call-with-byte-input-file
   in-file
   (lambda (in)
     (let ((size (read-file-size in)))
       (call-with-byte-output-file
        out-file
        (lambda (out)
          (when (plusp size)
            (call-with-range-decoder
             in
             (lambda (rc)
               (decode-order-2 rc out size))))))))))

;;; 簡単なテスト
(defun test ()
  (let ((files '("alice29.txt" "asyoulik.txt" "cp.html" "fields.c" "grammar.lsp"
                 "kennedy.xls" "lcet10.txt" "plrabn12.txt" "ptt5" "sum" "xargs.1")))
    (format t "----- encode -----~%")
    (time
     (dolist (file files)
       (encode-file (format nil "./canterbury/~a" file)
                    (format nil "~a.en" file))))
    ;;
    (format t "----- decode -----~%")
    (time
     (dolist (file files)
       (decode-file (format nil "~a.en" file)
                    (format nil "~a.de" file))))))

●プログラムリスト2

;;;
;;; rce3.l : 適応型レンジコーダ (order-3 with ESC)
;;;
;;;          Copyright (C) 2010-2023 Makoto Hiroi
;;;
(require :rangecoder "rangecoder.lsp")
(use-package :rangecoder)
(require :bitree "bitree.lsp")
(use-package :bitree)

;;; 定数
(defconstant esc code-size)

(defvar *max-sum* #x4000)
(defvar *inc2* 64)

;;; ファイルサイズの書き込み
(defun write-file-size (out size)
  (write-byte (logand (ash size -24) #xff) out)
  (write-byte (logand (ash size -16) #xff) out)
  (write-byte (logand (ash size -8) #xff) out)
  (write-byte (logand size #xff) out))

;;; ファイルサイズの読み込み
(defun read-file-size (in)
  (+ (ash (read-byte in) 24)
     (ash (read-byte in) 16)
     (ash (read-byte in) 8)
     (read-byte in)))

;;; 更新
(defun update (bt c inc &optional c1)
  (bitree-update bt c inc)
  (when c1
    (bitree-update bt c1 inc))
  (when (<= *max-sum* (bitree-sum bt))
    (dotimes (x (bitree::bitree-size bt))
      (let ((n (ash (bitree-frequency bt x) -1)))
        (when (plusp n)
          (bitree-update bt x (- n)))))))

;;; 初期化
(defun initialize-bitree ()
  (let ((bt (make-bitree code-size)))
    (dotimes (x code-size bt)
      (bitree-update bt x 1))))

(defun initialize-bitree-with-esc ()
  (let ((bt (make-bitree (1+ code-size))))
    (bitree-update bt esc 1)
    bt))

;;; 記号の符号化
(defun encode (rc bt c)
  (let ((temp (floor (range-coder-range rc)
                     (bitree-sum bt))))
    (incf (range-coder-low rc)
          (* (bitree-cumul bt c) temp))
    (setf (range-coder-range rc)
          (* (bitree-frequency bt c) temp))
    (encode-normalize rc)))

;;; 記号の復号
(defun decode (rc bt)
  (let ((temp (floor (range-coder-range rc)
                     (bitree-sum bt))))
    (multiple-value-bind
        (c cumul)
        (bitree-find bt (floor (range-coder-low rc) temp))
      (decf (range-coder-low rc)
            (* cumul temp))
      (setf (range-coder-range rc)
            (* (bitree-frequency bt c) temp))
      (decode-normalize rc)
      c)))

;;;
;;; 出現頻度表を格納するハッシュ表
;;;

(defun make-bitree-table ()
  (make-hash-table :test 'equal))

(defun get-bitree (table &rest keys)
  (let ((bt (gethash keys table nil)))
    (if bt
        bt
      (setf (gethash keys table) (initialize-bitree-with-esc)))))

;;;
;;; exclusion 用操作関数
;;;

;;; ESC 以外の出現している記号をすべて求める
(defun get-symbol (bt)
  (do ((a nil)
       (x 0 (1+ x)))
      ((= x code-size) a)
    (if (plusp (bitree-frequency bt x))
        (push x a))))

;;; 記号を除外する
(defun exclusion (bt ls)
  (mapcar (lambda (x)
            (let ((n (bitree-frequency bt x)))
              (bitree-update bt x (- n))
              (cons x n)))
          ls))

;;; 元に戻す
(defun restore-exclusion (bt ls)
  (dolist (x ls)
    (bitree-update bt (car x) (cdr x))))

;;;
;;; エスケープ記号付き有限文脈モデル
;;;

;;; 符号化
(defun encode-with-exclusion (rc bt c xs)
  (let ((ys (exclusion bt xs)))
    (encode rc bt c)
    (restore-exclusion bt ys)))

(defun encode-with-esc (rc c &rest ls)
  (labels ((encode-sub (ls inc xs)
             (let ((bt (car ls)))
               (cond ((plusp (bitree-frequency bt c))
                      (encode-with-exclusion rc bt c xs)
                      (update bt c inc c))
                     (t
                      (encode-with-exclusion rc bt esc xs)
                      (let ((zs (union xs (get-symbol bt))))
                        (update bt c inc esc)
                        (encode-sub (cdr ls) (/ inc 2) zs)))))))
    (encode-sub ls *inc2* nil)))

(defun encode-order-3 (rc in size)
  (let ((bt0 (initialize-bitree))
        (bt-table (make-bitree-table))
        (c0 0)
        (c1 0)
        (c2 0))
    (dotimes (x size)
      (let ((c (read-byte in)))
        (encode-with-esc rc
                         c
                         (get-bitree bt-table c0 c1 c2)
                         (get-bitree bt-table c1 c2)
                         (get-bitree bt-table c2)
                         bt0)
        (setq c0 c1
              c1 c2
              c2 c)))))

;;; 復号
(defun decode-with-exclusion (rc bt xs)
  (let ((ys (exclusion bt xs)))
    (prog1
        (decode rc bt)
      (restore-exclusion bt ys))))

(defun decode-with-esc (rc &rest ls)
  (labels ((decode-sub (ls inc xs)
             (let ((c (decode-with-exclusion rc (car ls) xs)))
               (cond ((/= c esc)
                      (update (car ls) c inc c))
                     (t
                      (setq c (decode-sub (cdr ls)
                                          (/ inc 2)
                                          (union xs (get-symbol (car ls)))))
                      (update (car ls) c inc esc)))
               c)))
    (decode-sub ls *inc2* nil)))

(defun decode-order-3 (rc out size)
  (let ((bt0 (initialize-bitree))
        (bt-table (make-bitree-table))
        (c0 0)
        (c1 0)
        (c2 0))
    (dotimes (x size)
      (let ((c (decode-with-esc rc
                                (get-bitree bt-table c0 c1 c2)
                                (get-bitree bt-table c1 c2)
                                (get-bitree bt-table c2)
                                bt0)))
        (write-byte c out)
        (setq c0 c1
              c1 c2
              c2 c)))))

;;; ファイルの符号化
(defun encode-file (in-file out-file)
  (call-with-byte-output-file
   out-file
   (lambda (out)
     (call-with-byte-input-file
      in-file
      (lambda (in)
        (let ((size (file-length in)))
          (write-file-size out size)
          (when (plusp size)
            (call-with-range-encoder
             out
             (lambda (rc)
               (encode-order-3 rc in size))))))))))

;;; ファイルの復号
(defun decode-file (in-file out-file)
  (call-with-byte-input-file
   in-file
   (lambda (in)
     (let ((size (read-file-size in)))
       (call-with-byte-output-file
        out-file
        (lambda (out)
          (when (plusp size)
            (call-with-range-decoder
             in
             (lambda (rc)
               (decode-order-3 rc out size))))))))))

;;; 簡単なテスト
(defun test ()
  (let ((files '("alice29.txt" "asyoulik.txt" "cp.html" "fields.c" "grammar.lsp"
                 "kennedy.xls" "lcet10.txt" "plrabn12.txt" "ptt5" "sum" "xargs.1")))
    (format t "----- encode -----~%")
    (time
     (dolist (file files)
       (encode-file (format nil "./canterbury/~a" file)
                    (format nil "~a.en" file))))
    ;;
    (format t "----- decode -----~%")
    (time
     (dolist (file files)
       (decode-file (format nil "~a.en" file)
                    (format nil "~a.de" file))))))

初版 2010 年 11 月 14 日
改訂 2023 年 7 月 15 日

Copyright (C) 2010-2023 Makoto Hiroi
All rights reserved.

[ PrevPage | Common Lisp | NextPage ]