· 7分で読了

ハフマン符号 — 最小限の bit で最大限のデータ

# コンピュータ工学
この記事は中国語から自動翻訳されたものです。翻訳によりニュアンスが失われている場合があります。

僕たちは毎日、ネット上で画像、音声、テキストを送っているが、これらのファイルは送信前にほとんど圧縮されている。圧縮は一体どうやって実現しているのだろうか?なぜ圧縮後のファイルを元の形に復元できるのだろうか?

前回の JPEG 圧縮についての記事では、ハフマン符号が JPEG 圧縮フローの最後のステップだと触れた。この記事では、ハフマン符号そのものについて、より深く掘り下げていきたい。

頻度と符号化

A、B、C、D の4文字だけからなる文章を送るとしよう。最も直感的な方法は、固定長の二進符号を使うことだ。

文字符号
A00
B01
C10
D11

各文字 2 bits で、明快だ。だが、A が 1000 回現れて、D は 1 回しか現れないとしたらどうだろう。頻度に大きな差がある文字を同じ長さの符号で表すのは、どうにも無駄に思える。

これこそがハフマン符号が解決したい問題だ。

頻度の高い文字には短い符号を、頻度の低い文字には長い符号を割り当てる。

この考え方はモールス信号にかなり似ている。英語で最もよく使われる文字 E は . ひとつで表され、あまり使われない Q は --.- という4つの記号が必要だ。

情報量とエントロピー

圧縮の話をする前に、ひとつ考えたいことがある。ある事象の「情報量」は、どうやって測るべきなのだろうか?

誰かが「明日、太陽は東から昇る」と教えてくれたとしても、僕たちは何も新しいことを学んだ気にはならない。ほぼ確実だからだ。だが「明日はひょうが降る」と言われたら、かなり驚くだろう。起こる確率が非常に低いからだ。

Claude Shannon は 1948 年に情報量の数学的定義を提案した。発生確率が pp の事象の情報量(information content)は次のように表される。

I=log2(p)I = -\log_2(p)

確率が低いほど情報量は大きい。これは直感的だ。より意外な出来事ほど、より多くの情報をもたらす。あるメッセージ源のエントロピー(entropy)は、すべての記号の平均情報量である。

H=ipilog2(pi)H = -\sum_{i} p_i \log_2(p_i)

エントロピーが示すのは、このメッセージ源に対して、理論上、各記号を符号化するのに平均で最低何 bits 必要かということだ。ハフマン符号のすごさは、この理論的下限に非常に近づける点にある。

ハフマン符号のアルゴリズム

David A. Huffman がこのアルゴリズムを提案したのは 1952 年だ。当時彼は MIT の大学院生で、これは授業課題だった。教授の Robert Fano が「最も効率のよい二進符号化方法を見つけよ」という問題を出したのだ。Huffman は Fano と Shannon のトップダウン方式をそのまま使わず、逆にボトムアップで構築した。

このアルゴリズムは非常に理解しやすく、実装も簡単だが、効果は驚くほど大きい。

手順

  1. 頻度を数える:各文字の出現回数を計算する
  2. Priority Queue を作る:すべての文字を頻度の小さい順に並べる
  3. 繰り返し結合する:頻度の最小な2つのノードを取り出して、新しいノード(頻度は両者の和)にまとめ、キューへ戻す
  4. 1つだけ残るまで繰り返す:そのノードがハフマン木の根になる
  5. 符号表を生成する:根からたどり、左に進んだら 0、右に進んだら 1 を記録する

実際にたどってみる

文字列 "aabbbcccc" を例にする。

Step 1 — 頻度を数える:

a: 2, b: 3, c: 4

Step 2 — 最小の2つを取り出して結合する:

a(2) + b(3) → ab(5)

Step 3 — さらに最小の2つを取り出す:

c(4) + ab(5) → root(9)

最終的なハフマン木:

        (9)
       /   \
     0       1
    c(4)   (5)
           /   \
         0       1
        a(2)   b(3)

符号表:

文字符号長さ
c01
a102
b112

元の ASCII 符号では、9×8=729 \times 8 = 72 bits。ハフマン符号化後は、4×1+2×2+3×2=144 \times 1 + 2 \times 2 + 3 \times 2 = 14 bits だ。

なぜハフマン符号はそんなに便利なのか

ハフマン符号は前綴り符号(prefix code)である。前綴り符号の定義は、どの符号も他の符号の前綴りにならないことだ。

例えば、A = 0、B = 01 だとすると、復号器が 01 を読んだとき、それが「A の後ろに 1 が続いている」のか、それとも「B」なのか判断できない。

ハフマン符号ではそんなことは起こらない。すべての文字が葉ノード上にあるからだ。根から任意の葉への経路は、別の経路の前綴りにはならない。この性質により、解析そのものも非常に簡単になる。すべての前綴り符号の中で、ハフマン符号は最短の平均符号長を生み出す

コード実装

以下は JavaScript の実装だ。

function buildTree(freqMap) {
  const nodes = [...freqMap.entries()]
    .map(([char, freq]) => ({ char, freq, left: null, right: null }))

  while (nodes.length > 1) {
    nodes.sort((a, b) => a.freq - b.freq)
    const left = nodes.shift()
    const right = nodes.shift()
    nodes.push({
      char: null,
      freq: left.freq + right.freq,
      left,
      right,
    })
  }

  return nodes[0]
}

function buildCodeTable(node, prefix = '', table = {}) {
  if (node.char !== null) {
    table[node.char] = prefix || '0'
    return table
  }
  buildCodeTable(node.left, prefix + '0', table)
  buildCodeTable(node.right, prefix + '1', table)
  return table
}

buildTree は毎回最小の2つを取り出して結合し、1つの根ノードだけが残るまで続ける。buildCodeTable は根から再帰的にたどり、左へ進むたびに 0 を、右へ進むたびに 1 を追加する。

復号の過程も直感的だ。根ノードから始め、0 を読んだら左へ、1 を読んだら右へ進み、葉ノードに到達したら対応する文字を出力し、また根へ戻って続ける。

function decode(encoded, root) {
  let result = ''
  let current = root

  for (const bit of encoded) {
    current = bit === '0' ? current.left : current.right
    if (current.char !== null) {
      result += current.char
      current = root
    }
  }

  return result
}

インタラクティブ:ハフマン符号エンコーダ

下に任意のテキストを入力して、ハフマン符号化の結果をリアルタイムで観察してみよう。符号表のある文字にマウスを乗せると、符号結果中の対応する bits がハイライトされる。

ハフマン符号エンコーダを読み込み中…

さまざまなテキストを入力して、いくつかの興味深い現象を観察してみよう。

  • 1 種類の文字しかないテキスト(例: aaaa)を入力すると、各文字に必要なのは 1 bit だけだ
  • すべての文字の頻度が同じテキストを入力すると、符号長は固定長に近づく
  • 頻度差が大きいテキストほど、圧縮率は高くなる

ハフマン符号の限界

ハフマン符号は万能ではなく、実務上いくつかの制約がある。

各記号は少なくとも 1 bit 必要だ。 たとえある文字の理論上の最適符号長が 0.3 bits でも、ハフマン符号は最低でも 1 bit しか割り当てられない。これは極端な分布では効率の損失につながる。

事前に頻度を知る必要がある。 符号化の前に一度データを走査して頻度を集計するか、あらかじめ定義された頻度表を使う必要がある。JPEG では標準でデフォルトのハフマン表が定義されており、カスタム表も許可されている。

符号表も一緒に送る必要がある。 復号側はデータを復元するために符号表を知っていなければならないので、符号表自体にも空間が必要だ。小さなデータでは、符号表のオーバーヘッドが圧縮の利点を打ち消すこともある。

ハフマン符号はどこで使われているのか

  • JPEG — 画像圧縮の最後のステップがハフマン符号化だ
  • DEFLATE(ZIP、gzip)— LZ77 とハフマン符号化を組み合わせている
  • MP3 — 音声圧縮でもハフマン符号が使われている
  • HTTP/2 — HPACK のヘッダ圧縮では静的ハフマン表が使われる

後記

ハフマン符号は、「理解した瞬間に当然だと思える」タイプのアルゴリズムだ。その核心は極めて単純で、よく出るものは短く、まれなものは長く表す。ただし、この発想を汎用アルゴリズムに落とし込むには、多くの設計上の工夫が必要になる。

70年以上が過ぎた今でも、このアルゴリズムは僕たちが毎日使うさまざまなフォーマットの中で生き続けている。

この人たちが AI の補助を得たら、いったいどうなっていたのだろうか。