携帯電話で通話しているとき、電波が弱くなっても音声が途切れにくいのはなぜでしょうか? 衛星からの微弱な信号が何億キロも離れた地球で正確に受信できるのはなぜでしょうか? その立役者の一つが 畳み込み符号(Convolutional Code) と ビタビ復号(Viterbi Decoding) です。
ブロック符号(ハミング符号やリード・ソロモン符号など)がデータを固定長のブロックに区切って冗長ビットを付加するのに対し、畳み込み符号はデータを 連続的にストリーム処理 し、過去の入力ビットの「記憶」を使って冗長性を生成します。この「記憶のある符号化」が、畳み込み符号の強力な誤り訂正能力の源泉です。
そして、畳み込み符号の性能を最大限に引き出すのがビタビアルゴリズムです。ビタビアルゴリズムは 最尤復号(Maximum Likelihood Decoding) を効率的に実現する動的計画法であり、1967年にアンドリュー・ビタビ(Qualcommの共同創業者)によって発明されました。
畳み込み符号とビタビ復号を理解すると、以下のような幅広い応用に直結します。
- 深宇宙通信 — NASA の Voyager, Mars Exploration Rover 等が畳み込み符号を使用
- 携帯電話(GSM, 3G) — 音声・データチャネルの誤り訂正
- Wi-Fi(IEEE 802.11) — OFDMの各サブキャリアで畳み込み符号を使用
- デジタル放送(DVB, ISDB) — 映像・音声ストリームの保護
- ターボ符号・LDPC符号の基盤 — 現代の高性能符号は畳み込み符号のアイデアを発展させたもの
本記事の内容
- 畳み込み符号の構造 — シフトレジスタと生成多項式
- 拘束長、符号化率 $(k, n, m)$ のパラメータの意味
- 状態遷移図とトレリス線図
- ビタビアルゴリズムの原理 — 加算-比較-選択(ACS)
- 自由距離と BER 性能の理論的関係
- Python による畳み込み符号化 + ビタビ復号シミュレーション
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
畳み込み符号とは
直感的理解
畳み込み符号を理解するために、次のアナロジーを考えましょう。
あなたが友人にメッセージを伝えるとき、各単語を1回だけ言う代わりに、直前の数単語を覚えておいて、それらを組み合わせた「合言葉」も一緒に伝えるとします。友人は合言葉を手がかりにして、聞き間違えた単語を推理できます。
畳み込み符号もこれと同じ原理です。現在の入力ビットだけでなく、過去の入力ビットも使って出力(冗長ビット)を生成します。この「過去何ビット分を覚えているか」が メモリ長(memory length) $m$ であり、符号器は $m$ ビットの記憶(シフトレジスタ)を持つ有限状態機械として実現されます。
ブロック符号との違いを表にまとめると、畳み込み符号の特性が明確になります。
| 特性 | ブロック符号 | 畳み込み符号 |
|---|---|---|
| 処理単位 | 固定長ブロック | ビットストリーム(連続) |
| 記憶 | ブロック内のみ | 過去 $m$ ビットに依存 |
| 構造 | 代数的(多項式・行列) | 状態機械(シフトレジスタ) |
| 復号 | シンドローム復号、代数復号 | ビタビ復号(最尤復号) |
| 遅延 | ブロック長に比例 | 最小限(リアルタイム処理可能) |
畳み込み符号はストリーム処理が可能なため、音声通信のようなリアルタイム性が求められる用途に特に適しています。
パラメータの定義
畳み込み符号は3つの基本パラメータ $(k, n, m)$ で特徴づけられます。
- $k$: 入力ビット数(1タイムステップあたりの入力)
- $n$: 出力ビット数(1タイムステップあたりの出力)
- $m$: メモリ長(シフトレジスタの段数)
符号化率 は $R = k/n$ で、$R < 1$ です。例えば $k = 1, n = 2$ なら $R = 1/2$ で、1ビットの入力に対して2ビットの出力を生成します。冗長度は $(n-k)/k$ で、$R = 1/2$ なら冗長度は100%(出力の半分が冗長ビット)です。
拘束長(Constraint Length) $K$ は
$$ K = m + 1 $$
と定義されます。これは「出力の生成に影響する入力ビットの範囲」を表します。$K$ が大きいほど過去の情報をより多く利用するため、符号の誤り訂正能力が向上しますが、復号の計算量は指数的に増大します。
最もよく使われる構成は $k = 1$(1ビット入力)の場合で、状態数は $2^m$ です。$m = 2$($K = 3$)の符号化率 $R = 1/2$ の畳み込み符号が最も基本的な例であり、以下ではこの構成を中心に解説します。
ここまでで畳み込み符号の概念的な位置づけを把握しました。次に、符号器の具体的な内部構造を見ていきましょう。
符号器の構造 — シフトレジスタと生成多項式
符号器の回路構成
$k = 1, n = 2, m = 2$(拘束長 $K = 3$, 符号化率 $R = 1/2$)の畳み込み符号器を考えます。この符号器は1つの入力と2つの出力を持ち、内部に2つのシフトレジスタ $s_1, s_2$ を備えます。
動作を順を追って説明します。時刻 $t$ において入力ビット $u_t$ がシフトレジスタに入ると、2つの出力ビット $c_t^{(1)}, c_t^{(2)}$ が次のように生成されます。
$$ c_t^{(1)} = u_t \oplus s_1 \oplus s_2 $$
$$ c_t^{(2)} = u_t \oplus s_2 $$
ここで $\oplus$ はモジュロ2加算(XOR)です。各出力は、現在の入力ビットとシフトレジスタ内のビットの特定の組み合わせの XOR で計算されます。
出力が生成された後、シフトレジスタの内容が更新されます。$s_2 \leftarrow s_1$(古い値が1段右にシフト)、$s_1 \leftarrow u_t$(新しい入力がレジスタの先頭に入る)。この操作は「畳み込み」の名前の由来です — 入力列と生成多項式の畳み込みが出力列を生成します。
生成多項式
上記の符号器を多項式で表現しましょう。遅延素子を $D$(1タイムステップの遅延)とすると、各出力に対応する 生成多項式(generator polynomial) は
$$ g^{(1)}(D) = 1 + D + D^2 $$
$$ g^{(2)}(D) = 1 + D^2 $$
です。$g^{(1)}$ の係数 $(1, 1, 1)$ は「入力ビット、1段前のレジスタ、2段前のレジスタ全てを XOR する」ことを意味します。$g^{(2)}$ の係数 $(1, 0, 1)$ は「入力ビットと2段前のレジスタだけを XOR し、1段前のレジスタは使わない」ことを意味します。
生成多項式は 8進表記 でも表現されます。$(1, 1, 1)_2 = 7_8$, $(1, 0, 1)_2 = 5_8$ なので、この符号は $(7, 5)_8$ の畳み込み符号と呼ばれます。
より一般的な表記では、$k = 1$ の場合、符号器の入力 $u(D) = \sum_t u_t D^t$ に対して、第 $j$ 出力は
$$ c^{(j)}(D) = u(D) \cdot g^{(j)}(D) \mod 2 $$
と書けます。これがまさに多項式の「畳み込み」であり、符号名の由来です。
符号化の具体例
入力列 $u = (1, 0, 1, 1, 0)$ を $(7, 5)_8$ 符号で符号化してみましょう。シフトレジスタの初期状態は $(s_1, s_2) = (0, 0)$ とします。
| 時刻 $t$ | 入力 $u_t$ | 状態 $(s_1, s_2)$ | $c_t^{(1)} = u_t \oplus s_1 \oplus s_2$ | $c_t^{(2)} = u_t \oplus s_2$ | 出力 |
|---|---|---|---|---|---|
| 0 | 1 | (0, 0) | $1 \oplus 0 \oplus 0 = 1$ | $1 \oplus 0 = 1$ | 11 |
| 1 | 0 | (1, 0) | $0 \oplus 1 \oplus 0 = 1$ | $0 \oplus 0 = 0$ | 10 |
| 2 | 1 | (0, 1) | $1 \oplus 0 \oplus 1 = 0$ | $1 \oplus 1 = 0$ | 00 |
| 3 | 1 | (1, 0) | $1 \oplus 1 \oplus 0 = 0$ | $1 \oplus 0 = 1$ | 01 |
| 4 | 0 | (1, 1) | $0 \oplus 1 \oplus 1 = 0$ | $0 \oplus 1 = 1$ | 01 |
出力列は $c = (11, 10, 00, 01, 01)$ です。5ビットの入力から10ビットの出力が得られ、符号化率 $R = 1/2$ を確認できます。
状態 $(s_1, s_2)$ が時刻とともに変化していることに注目してください。この状態の遷移パターンが、畳み込み符号の構造を理解する鍵です。次に、状態遷移を体系的に表現する方法を見ていきましょう。
状態遷移図とトレリス線図
状態遷移図
$m = 2$ の符号器は $2^m = 4$ 個の状態を持ちます。
$$ S = \{(s_1, s_2)\} = \{00, 01, 10, 11\} $$
各状態から、入力ビット $u = 0$ または $u = 1$ に応じて次の状態に遷移し、同時に出力ビットが生成されます。$(7, 5)_8$ 符号の全ての状態遷移は次の表にまとめられます。
| 現在の状態 $(s_1, s_2)$ | 入力 $u$ | 次の状態 $(u, s_1)$ | 出力 $(c^{(1)}, c^{(2)})$ |
|---|---|---|---|
| 00 | 0 | 00 | 00 |
| 00 | 1 | 10 | 11 |
| 01 | 0 | 00 | 11 |
| 01 | 1 | 10 | 00 |
| 10 | 0 | 01 | 10 |
| 10 | 1 | 11 | 01 |
| 11 | 0 | 01 | 01 |
| 11 | 1 | 11 | 10 |
この表を有向グラフとして描いたものが 状態遷移図(State Diagram) です。ノードが状態、エッジが遷移を表し、各エッジに入力/出力のラベルが付きます。
状態遷移図は符号の構造を把握するのに便利ですが、時間の経過を表現できません。符号語列の時間展開を見るために、状態遷移図を時間軸方向に「展開」したものが トレリス線図 です。
トレリス線図(Trellis Diagram)
トレリス線図は、横軸に時刻 $t$、縦軸に状態 $S$ をとり、各時刻の状態から次の時刻の状態への遷移を線で結んだグラフです。
トレリス線図の構造を整理します。
- ノード(node): 時刻 $t$ における状態 $(s_1, s_2)$。全部で $T \times 2^m$ 個のノードがある
- ブランチ(branch): 2つの隣接する時刻のノードを結ぶ辺。各ノードから入力ビット0と1に対応する2本のブランチが出る
- パス(path): 初期状態から最終状態までのブランチの連なり。1つのパスが1つの入力系列に対応
- ブランチラベル: 入力ビット/出力ビット
トレリス線図が重要な理由は、全ての可能な符号語がトレリス上の異なるパスとして表現されることです。符号化は初期状態 $00$ から出発してパスを辿る操作であり、復号は受信信号に最も「近い」パスを見つける操作です。
先ほどの具体例 $u = (1, 0, 1, 1, 0)$ は、トレリス上では $00 \to 10 \to 01 \to 10 \to 11 \to 01$ というパスに対応します。各ブランチの出力ラベルを連結すると $c = (11, 10, 00, 01, 01)$ が得られます。
なぜトレリスが復号に役立つのか
受信側がビット誤りを含む受信系列 $r$ を受け取ったとき、「$r$ に最も近い有効な符号語パス」を見つければ、元の情報系列を復元できます。これが 最尤復号 の考え方です。
素朴に全パスを探索すると、長さ $T$ の入力系列に対して $2^T$ 個のパスを調べなければなりません。しかし、トレリスの構造を利用すると、この指数的な計算量を大幅に削減できます。なぜなら、あるノード(時刻 $t$ の状態 $S$)に到達する最良のパスは、それ以前のパス選択に関わらず、そのノードまでの累積メトリックだけで決まるからです。これは 最適性の原理(Principle of Optimality) であり、動的計画法の核心です。
この動的計画法こそが、次に解説するビタビアルゴリズムです。
ビタビアルゴリズムの原理
最尤復号問題の定式化
送信側が畳み込み符号で符号化した系列 $\bm{c} = (c_0, c_1, \dots, c_{T-1})$ を AWGN 通信路で送信し、受信側が受信系列 $\bm{r} = (r_0, r_1, \dots, r_{T-1})$ を得たとします。ここで $c_t$ は時刻 $t$ の出力ビット組($n$ ビット)、$r_t$ は対応する受信値です。
最尤復号器は、受信系列 $\bm{r}$ が与えられたとき、尤度 $P(\bm{r} | \bm{c})$ を最大化する符号語 $\bm{c}^*$ を見つけます。
$$ \bm{c}^* = \arg\max_{\bm{c}} P(\bm{r} | \bm{c}) $$
AWGN 通信路では、各受信サンプルが独立に加法的ガウス雑音の影響を受けるため、尤度は各時刻の尤度の積になります。
$$ P(\bm{r} | \bm{c}) = \prod_{t=0}^{T-1} P(r_t | c_t) $$
対数をとると積が和に変わります。
$$ \log P(\bm{r} | \bm{c}) = \sum_{t=0}^{T-1} \log P(r_t | c_t) $$
ガウス分布の対数尤度は、受信値と送信値のユークリッド距離の二乗に比例するため、対数尤度の最大化は 距離の最小化 と等価です。
$$ \bm{c}^* = \arg\min_{\bm{c}} \sum_{t=0}^{T-1} d(r_t, c_t)^2 $$
硬判定(Hard Decision) の場合、受信値を0/1に量子化してから復号するため、距離メトリックは ハミング距離 になります。
$$ d_H(r_t, c_t) = \text{(対応するビットが異なる数)} $$
軟判定(Soft Decision) の場合、受信値をそのまま(実数値のまま)使い、距離メトリックは ユークリッド距離 になります。軟判定は硬判定に比べて約2 dBの利得があります。
ビタビアルゴリズムの手順
ビタビアルゴリズムは、トレリス上で各ノードまでの「最良のパス」を逐次的に計算する動的計画法です。手順を4つのステップに分けて説明します。
ステップ1 — 初期化: 時刻 $t = 0$ において、初期状態 $S = 00$ のパスメトリック(累積距離)を0に、他の状態のパスメトリックを $\infty$(到達不可能)に設定します。
$$ \Gamma_0(S) = \begin{cases} 0 & (S = 00) \\ \infty & (\text{otherwise}) \end{cases} $$
ステップ2 — ブランチメトリック計算: 時刻 $t$ の各ブランチについて、そのブランチの出力 $c_t^{(\text{branch})}$ と受信値 $r_t$ の距離を計算します。
$$ \lambda_t(S \to S’) = d(r_t, c_t^{(\text{branch})}) $$
硬判定ではハミング距離、軟判定ではユークリッド距離です。
ステップ3 — ACS(加算-比較-選択): これがアルゴリズムの核心です。各状態 $S’$ に到達する複数のパスの中から、累積メトリックが最小のものを選択します。
状態 $S’$ に遷移できる2つの状態を $S_a$ と $S_b$ とすると
$$ \Gamma_{t+1}(S’) = \min\left(\Gamma_t(S_a) + \lambda_t(S_a \to S’), \; \Gamma_t(S_b) + \lambda_t(S_b \to S’)\right) $$
- 加算(Add): 前の状態のパスメトリックにブランチメトリックを加算
- 比較(Compare): 2つの候補パスのメトリックを比較
- 選択(Select): メトリックが小さい方のパスを 生き残りパス(survivor path) として残す
各ノードで2つの候補パスのうち1つだけが生き残ります。もう1つは捨てられます。この刈り込みにより、パス数が指数的に増大することを防ぎます。
ステップ4 — トレースバック: 最終時刻 $T$ に到達した後、最小メトリックの状態から出発して、各時刻で選択された遷移元を逆にたどります。このトレースバックにより、最尤パス(最も可能性の高い入力系列)が復元されます。
計算量の解析
ビタビアルゴリズムの計算量を評価しましょう。
- 状態数: $2^m$ 個
- 各時刻のACS演算: $2^m$ 個の状態 $\times$ 2本の入力ブランチ = $2^{m+1}$ 回の加算と $2^m$ 回の比較・選択
- 全体の計算量: $O(T \cdot 2^m)$
全パス探索の $O(2^T)$ に対して、ビタビアルゴリズムは入力長 $T$ に対して 線形 であり、メモリ長 $m$ に対して指数的です。$m$ が小さい(典型的には $m \leq 8$)場合、実用的な計算量で最尤復号が実現できます。
メモリ使用量は、各状態について生き残りパスを保存する必要があるため $O(2^m \cdot T)$ です。実用的にはトランケーション(パス長の打ち切り)により $O(2^m \cdot D)$($D$ はトランケーション深さ)に削減できます。典型的には $D \approx 5K$ が推奨されます。
具体例によるビタビ復号の動作
$(7, 5)_8$ 符号で、送信系列 $c = (11, 10, 00, 01, 01)$ に対して受信系列 $r = (11, 10, 01, 01, 01)$ を受信した場合(時刻 $t = 2$ の第1ビットにエラー)を考えます。硬判定ビタビ復号の動作を追跡します。
$t = 0$: 受信 $r_0 = 11$
| 遷移 | ブランチ出力 | ハミング距離 | 累積メトリック |
|---|---|---|---|
| $00 \to 00$ | 00 | $d_H(11, 00) = 2$ | 2 |
| $00 \to 10$ | 11 | $d_H(11, 11) = 0$ | 0 |
$\Gamma_1(00) = 2$, $\Gamma_1(10) = 0$. 他の状態は到達不可能。
$t = 1$: 受信 $r_1 = 10$
| 遷移 | ブランチ出力 | ハミング距離 | 累積メトリック |
|---|---|---|---|
| $00 \to 00$ | 00 | $d_H(10, 00) = 1$ | $2 + 1 = 3$ |
| $00 \to 10$ | 11 | $d_H(10, 11) = 1$ | $2 + 1 = 3$ |
| $10 \to 01$ | 10 | $d_H(10, 10) = 0$ | $0 + 0 = 0$ |
| $10 \to 11$ | 01 | $d_H(10, 01) = 2$ | $0 + 2 = 2$ |
$\Gamma_2(00) = 3$, $\Gamma_2(10) = 3$, $\Gamma_2(01) = 0$, $\Gamma_2(11) = 2$.
$t = 2$: 受信 $r_2 = 01$(ここに1ビットエラーが含まれる。正しくは $00$)
$\Gamma_2(01) = 0$ の状態が最も有利です。状態 $01$ からの遷移を計算すると
| 遷移 | ブランチ出力 | ハミング距離 | 累積メトリック |
|---|---|---|---|
| $01 \to 00$ | 11 | $d_H(01, 11) = 1$ | $0 + 1 = 1$ |
| $01 \to 10$ | 00 | $d_H(01, 00) = 1$ | $0 + 1 = 1$ |
他の状態からの遷移も同様に計算し、各状態で ACS(比較・選択)を行います。最終的に、全時刻を処理してトレースバックすると、パスメトリックが最小のパスとして $00 \to 10 \to 01 \to 10 \to 11 \to 01$ が得られ、入力系列 $u = (1, 0, 1, 1, 0)$ が正しく復号されます。受信系列に含まれていた1ビットのエラーが訂正されたことになります。
ビタビアルゴリズムの動作がわかりました。次に、畳み込み符号の誤り訂正能力を理論的に特徴づける「自由距離」の概念に進みましょう。
自由距離と BER 性能
自由距離の定義
畳み込み符号の誤り訂正能力を決定する最も重要なパラメータが 自由距離(free distance) $d_{\text{free}}$ です。
自由距離とは、トレリス上で 全ゼロパス(All-Zero Path) から分岐して再び合流する 最短の非ゼロパス のハミング重み(1の数)です。より正式には、全ての有効な符号語対のハミング距離の最小値です。
$$ d_{\text{free}} = \min_{\bm{c} \neq \bm{c}’} d_H(\bm{c}, \bm{c}’) $$
線形符号の性質を使うと、$\bm{c}$ と $\bm{c}’$ の差も有効な符号語になるため
$$ d_{\text{free}} = \min_{\bm{c} \neq \bm{0}} w_H(\bm{c}) $$
と簡略化できます。ここで $w_H(\bm{c})$ はハミング重み(符号語中の1の数)です。
自由距離の求め方
$(7, 5)_8$ 符号の自由距離を具体的に求めましょう。全ゼロ状態 $00$ から出発し、入力1で分岐して、再び状態 $00$ に戻る最短パスを探します。
- パス $00 \xrightarrow{u=1} 10 \xrightarrow{u=0} 01 \xrightarrow{u=0} 00$: 出力は $(11, 10, 11)$、重み $= 2 + 1 + 2 = 5$
- パス $00 \xrightarrow{u=1} 10 \xrightarrow{u=1} 11 \xrightarrow{u=0} 01 \xrightarrow{u=0} 00$: 出力は $(11, 01, 01, 11)$、重み $= 2 + 1 + 1 + 2 = 6$
最短パスの重みは5なので、$d_{\text{free}} = 5$ です。
自由距離の直感的な意味は次の通りです。「全ゼロ系列を送信したときに、5ビット以上のエラーがないと別の符号語パスに誤復号されない」ということです。したがって、$(7, 5)_8$ 符号は
$$ t = \left\lfloor \frac{d_{\text{free}} – 1}{2} \right\rfloor = \left\lfloor \frac{4}{2} \right\rfloor = 2 $$
ビットまでのエラーを訂正できます。
代表的な畳み込み符号の自由距離
拘束長 $K$ と符号化率 $R$ に対する最適な生成多項式とその自由距離を表にまとめます。
| $K$ | $R$ | 生成多項式(8進) | $d_{\text{free}}$ |
|---|---|---|---|
| 3 | 1/2 | (7, 5) | 5 |
| 4 | 1/2 | (17, 15) | 6 |
| 5 | 1/2 | (35, 23) | 7 |
| 6 | 1/2 | (75, 53) | 8 |
| 7 | 1/2 | (171, 133) | 10 |
| 3 | 1/3 | (7, 7, 5) | 8 |
| 4 | 1/3 | (17, 15, 13) | 10 |
この表から、拘束長 $K$ を1増やすごとに $d_{\text{free}}$ がおよそ1ずつ増加する傾向が読み取れます。また、符号化率 $R$ を $1/2$ から $1/3$ に下げると(冗長度を増やすと)、$d_{\text{free}}$ が大幅に向上します。
$K = 7, R = 1/2$ の $(171, 133)_8$ 符号は NASA標準符号 として知られ、宇宙通信で広く採用されてきました。
BER の理論的解析
AWGN通信路でのビタビ復号のBERは、自由距離 $d_{\text{free}}$ を用いた上界で近似できます。
硬判定ビタビ復号の場合、BERの上界は
$$ P_b \lesssim \frac{1}{k} \sum_{d=d_{\text{free}}}^{\infty} c_d \, P_d $$
で与えられます。ここで $c_d$ は距離 $d$ の符号語に対応する情報ビット誤りの重み合計、$P_d$ は距離 $d$ のパスを選択する確率です。BSC(二元対称通信路)のビット反転確率を $p$ とすると
$$ P_d \leq \binom{d}{(d+1)/2} p^{(d+1)/2} (1-p)^{(d-1)/2} \quad (\text{$d$ が奇数の場合}) $$
ですが、主要項($d = d_{\text{free}}$)の寄与が支配的なので、BER の近似式は
$$ P_b \approx \frac{c_{d_{\text{free}}}}{k} \cdot \text{erfc}\left(\sqrt{d_{\text{free}} R \frac{E_b}{N_0}}\right) $$
となります。この式から重要な結論が得られます。
- $d_{\text{free}}$ が大きいほど BER が指数的に減少: erfc 関数の引数内に $d_{\text{free}}$ が入っているため、自由距離の増加は BER に劇的な影響を与えます
- 符号化利得(Coding Gain): 符号化なしの BPSK の BER が $P_b = Q(\sqrt{2E_b/N_0})$ であるのに対し、畳み込み符号+ビタビ復号では実効的な $E_b/N_0$ が $d_{\text{free}} R$ 倍されます。$(7, 5)_8$ 符号($d_{\text{free}} = 5, R = 1/2$)の符号化利得は $10\log_{10}(5 \times 0.5) = 4.0$ dB です
軟判定の場合は硬判定に対してさらに約2 dBの利得があるため、合計で 約6 dB の符号化利得 が得られます。これは、同じ BER を達成するために必要な送信電力を約4分の1に削減できることを意味します。
理論的な性能を確認したところで、Python シミュレーションで畳み込み符号化とビタビ復号を実装し、理論を検証しましょう。
Python による畳み込み符号化の実装
符号化器の実装
$(7, 5)_8$ 畳み込み符号器を Python でスクラッチ実装します。
import numpy as np
def convolutional_encode(bits, generators):
"""
畳み込み符号化器
bits: 入力ビット列 (0/1の配列)
generators: 生成多項式のリスト(各多項式は係数のタプル)
例: [(1,1,1), (1,0,1)] は (7,5)_8
戻り値: 符号化されたビット列
"""
n_outputs = len(generators)
K = len(generators[0]) # 拘束長
m = K - 1 # メモリ長
# シフトレジスタ(初期状態は全ゼロ)
register = [0] * m
encoded = []
for bit in bits:
# 現在のレジスタ内容と入力ビットを結合
state = [bit] + register
# 各生成多項式に対して出力ビットを計算
for g in generators:
output_bit = 0
for j in range(K):
output_bit ^= (state[j] * g[j])
encoded.append(output_bit)
# シフトレジスタを更新(右シフト、新入力を先頭に)
register = [bit] + register[:-1]
return np.array(encoded)
# 動作確認: 具体例と一致するか検証
generators_75 = [(1, 1, 1), (1, 0, 1)] # (7,5)_8
test_bits = np.array([1, 0, 1, 1, 0])
encoded = convolutional_encode(test_bits, generators_75)
print("入力:", test_bits)
print("符号化出力:", encoded)
print("出力ペア:", [(encoded[2*i], encoded[2*i+1]) for i in range(len(test_bits))])
このコードの出力は、先ほどの手計算の具体例と一致します。入力 $(1, 0, 1, 1, 0)$ に対して出力 $(1,1, 1,0, 0,0, 0,1, 0,1)$ が得られ、ペアごとに区切ると $(11, 10, 00, 01, 01)$ です。シフトレジスタが過去の入力を保持し、各時刻で生成多項式に基づいて XOR を計算する動作が正しく実装されていることが確認できます。
トレリスの構築と可視化
次に、トレリス線図をプログラム的に構築し、可視化します。
import numpy as np
import matplotlib.pyplot as plt
def build_trellis(generators):
"""トレリスの遷移テーブルを構築"""
n_outputs = len(generators)
K = len(generators[0])
m = K - 1
n_states = 2 ** m
# 遷移テーブル: next_state[state][input] = (next_state, output_bits)
trellis = {}
for state in range(n_states):
trellis[state] = {}
# 状態をビット列に変換
state_bits = [(state >> (m - 1 - i)) & 1 for i in range(m)]
for input_bit in [0, 1]:
# レジスタの内容
full_state = [input_bit] + state_bits
# 出力計算
outputs = []
for g in generators:
out = 0
for j in range(K):
out ^= (full_state[j] * g[j])
outputs.append(out)
# 次の状態
next_state_bits = [input_bit] + state_bits[:-1]
next_state = 0
for i, b in enumerate(next_state_bits):
next_state |= (b << (m - 1 - i))
trellis[state][input_bit] = (next_state, tuple(outputs))
return trellis, n_states, m
# (7,5)_8 のトレリス構築
generators_75 = [(1, 1, 1), (1, 0, 1)]
trellis, n_states, m = build_trellis(generators_75)
# トレリス線図の可視化
fig, ax = plt.subplots(figsize=(14, 6))
T = 6 # 表示する時刻数
state_labels = ['00', '01', '10', '11']
y_positions = {0: 3, 1: 2, 2: 1, 3: 0} # 状態の縦位置
# ノード(状態)の描画
for t in range(T + 1):
for s in range(n_states):
if t == 0 and s != 0:
# 初期状態は00のみ
ax.plot(t, y_positions[s], 'o', color='lightgray', markersize=15, zorder=3)
else:
ax.plot(t, y_positions[s], 'o', color='white', markersize=15, zorder=3,
markeredgecolor='black', markeredgewidth=1.5)
if t == 0:
ax.text(-0.5, y_positions[s], state_labels[s], ha='right', va='center',
fontsize=10, fontweight='bold')
# ブランチ(遷移)の描画
for t in range(T):
for s in range(n_states):
if t == 0 and s != 0:
continue # 初期は00のみ
for inp in [0, 1]:
ns, outputs = trellis[s][inp]
output_str = ''.join(map(str, outputs))
color = 'tab:blue' if inp == 0 else 'tab:red'
linestyle = '-' if inp == 0 else '--'
ax.plot([t, t+1], [y_positions[s], y_positions[ns]],
color=color, linestyle=linestyle, linewidth=1.5, alpha=0.6)
# 出力ラベル
mid_x = t + 0.5
mid_y = (y_positions[s] + y_positions[ns]) / 2
ax.text(mid_x, mid_y + 0.15, output_str, ha='center', va='bottom',
fontsize=7, color=color, alpha=0.8)
ax.set_xlabel('Time step t')
ax.set_ylabel('State')
ax.set_title('Trellis Diagram for (7,5)₈ Convolutional Code (K=3, R=1/2)')
ax.set_yticks([y_positions[s] for s in range(n_states)])
ax.set_yticklabels(state_labels)
ax.set_xlim(-1, T + 0.5)
ax.grid(True, alpha=0.2)
# 凡例
from matplotlib.lines import Line2D
legend_elements = [Line2D([0], [0], color='tab:blue', linestyle='-', label='Input = 0'),
Line2D([0], [0], color='tab:red', linestyle='--', label='Input = 1')]
ax.legend(handles=legend_elements, loc='upper right')
plt.tight_layout()
plt.savefig('trellis_diagram.png', dpi=150, bbox_inches='tight')
plt.show()
このトレリス線図から、畳み込み符号の構造が視覚的に理解できます。青い実線が入力0に対応する遷移、赤い破線が入力1に対応する遷移です。各状態から正確に2本のブランチが出ていることが確認できます。初期状態 $00$ から始まり、時間の経過とともに到達可能な状態が増えていく様子が観察できます。各ブランチに付記された2桁の数字が出力ビットであり、これが受信側でのビタビ復号時のブランチメトリック計算に使われます。
Python によるビタビ復号の実装
ビタビ復号器(硬判定)
ビタビアルゴリズムの硬判定復号器を実装します。
import numpy as np
def viterbi_decode(received, generators, traceback_depth=None):
"""
ビタビ復号器(硬判定)
received: 受信ビット列 (0/1の配列)
generators: 生成多項式のリスト
traceback_depth: トレースバック深さ(Noneなら全長)
戻り値: 復号されたビット列
"""
n_outputs = len(generators)
K = len(generators[0])
m = K - 1
n_states = 2 ** m
# トレリス構築
trellis = {}
for state in range(n_states):
trellis[state] = {}
state_bits = [(state >> (m - 1 - i)) & 1 for i in range(m)]
for input_bit in [0, 1]:
full_state = [input_bit] + state_bits
outputs = []
for g in generators:
out = 0
for j in range(K):
out ^= (full_state[j] * g[j])
outputs.append(out)
next_state_bits = [input_bit] + state_bits[:-1]
next_state = 0
for i, b in enumerate(next_state_bits):
next_state |= (b << (m - 1 - i))
trellis[state][input_bit] = (next_state, tuple(outputs))
# 受信信号をn_outputsビットずつに分割
n_steps = len(received) // n_outputs
received_groups = received.reshape(n_steps, n_outputs)
# パスメトリック(累積ハミング距離)
INF = float('inf')
path_metrics = np.full(n_states, INF)
path_metrics[0] = 0 # 初期状態は00
# 生き残りパスの履歴
survivor_paths = [np.zeros((n_states, 0), dtype=int)]
for t in range(n_steps):
new_metrics = np.full(n_states, INF)
new_predecessors = np.full(n_states, -1, dtype=int)
new_inputs = np.full(n_states, -1, dtype=int)
for state in range(n_states):
if path_metrics[state] == INF:
continue
for input_bit in [0, 1]:
next_state, outputs = trellis[state][input_bit]
# ブランチメトリック(ハミング距離)
branch_metric = np.sum(np.array(outputs) != received_groups[t])
# 累積メトリック
total_metric = path_metrics[state] + branch_metric
# ACS: より良いパスを選択
if total_metric < new_metrics[next_state]:
new_metrics[next_state] = total_metric
new_predecessors[next_state] = state
new_inputs[next_state] = input_bit
path_metrics = new_metrics
# 生き残りパスの更新
old_paths = survivor_paths[-1]
new_paths = np.zeros((n_states, old_paths.shape[1] + 1), dtype=int)
for state in range(n_states):
if new_predecessors[state] >= 0:
pred = new_predecessors[state]
if old_paths.shape[1] > 0:
new_paths[state, :-1] = old_paths[pred]
new_paths[state, -1] = new_inputs[state]
survivor_paths.append(new_paths)
# トレースバック: 最小メトリックの状態から
best_state = np.argmin(path_metrics)
decoded = survivor_paths[-1][best_state]
return decoded
# 動作確認: エラー訂正のテスト
generators_75 = [(1, 1, 1), (1, 0, 1)]
# 元の入力
original_bits = np.array([1, 0, 1, 1, 0])
# 符号化
encoded = convolutional_encode(original_bits, generators_75)
print("符号化出力:", encoded)
# 1ビットエラーを挿入
received = encoded.copy()
received[4] = 1 - received[4] # 位置4のビットを反転
print("受信(エラーあり):", received)
# ビタビ復号
decoded = viterbi_decode(received, generators_75)
print("復号結果:", decoded)
print("元の入力:", original_bits)
print("一致:", np.array_equal(decoded, original_bits))
このテストにより、ビタビ復号器が1ビットの伝送エラーを正しく訂正できることが確認できます。符号化出力の位置4のビットを反転して人工的なエラーを挿入しましたが、ビタビ復号により元の入力 $(1, 0, 1, 1, 0)$ が正確に復元されています。これは $(7, 5)_8$ 符号の $d_{\text{free}} = 5$ が2ビットまでのエラー訂正を保証することと整合しています。
BER シミュレーション
AWGN通信路での BER 曲線
畳み込み符号+ビタビ復号の BER を Monte Carlo シミュレーションで測定し、符号化なし(BPSK)と比較します。
import numpy as np
import matplotlib.pyplot as plt
from scipy.special import erfc
def simulate_conv_code_ber(generators, snr_db_range, n_info_bits=10000):
"""畳み込み符号+ビタビ復号のBERシミュレーション"""
n_outputs = len(generators)
R = 1.0 / n_outputs # 符号化率
ber_list = []
for snr_db in snr_db_range:
# Eb/N0からチャネルのSNRに変換
snr_lin = 10 ** (snr_db / 10)
# BPSK変調: ビットエネルギー Eb, コード化率Rを考慮
# 符号化ビットあたりのエネルギー Ec = R * Eb
# sigma^2 = N0/2 = Ec/(2*snr_lin*R)
sigma = 1.0 / np.sqrt(2 * snr_lin * R)
total_errors = 0
total_bits = 0
while total_bits < n_info_bits:
# 情報ビット生成
batch_size = min(100, n_info_bits - total_bits)
info_bits = np.random.randint(0, 2, batch_size)
# テイルビット追加(シフトレジスタをゼロに戻す)
K = len(generators[0])
tail_bits = np.zeros(K - 1, dtype=int)
bits_with_tail = np.concatenate([info_bits, tail_bits])
# 符号化
encoded = convolutional_encode(bits_with_tail, generators)
# BPSK変調: 0 -> -1, 1 -> +1
modulated = 2.0 * encoded - 1.0
# AWGN通信路
noise = sigma * np.random.randn(len(modulated))
received_soft = modulated + noise
# 硬判定
received_hard = (received_soft > 0).astype(int)
# ビタビ復号
decoded = viterbi_decode(received_hard, generators)
# 情報ビット部分のみ比較
decoded_info = decoded[:batch_size]
bit_errors = np.sum(decoded_info != info_bits)
total_errors += bit_errors
total_bits += batch_size
ber = total_errors / total_bits
ber_list.append(max(ber, 1e-7))
return np.array(ber_list)
# シミュレーション実行
snr_db = np.arange(0, 11, 1)
# (7,5) K=3
ber_75 = simulate_conv_code_ber(
[(1, 1, 1), (1, 0, 1)], snr_db, n_info_bits=50000)
# (17,15) K=4
ber_1715 = simulate_conv_code_ber(
[(1, 1, 1, 1), (1, 1, 0, 1)], snr_db, n_info_bits=50000)
# (35,23) K=5
ber_3523 = simulate_conv_code_ber(
[(1, 1, 1, 0, 1), (1, 0, 0, 1, 1)], snr_db, n_info_bits=50000)
# 理論曲線
snr_fine = np.linspace(0, 10, 200)
snr_lin_fine = 10 ** (snr_fine / 10)
# 符号化なしBPSK
ber_bpsk = 0.5 * erfc(np.sqrt(snr_lin_fine))
fig, ax = plt.subplots(figsize=(10, 7))
ax.semilogy(snr_db, ber_75, 'bo-', markersize=6, linewidth=1.5,
label='(7,5)₈ K=3, dfree=5 (sim)')
ax.semilogy(snr_db, ber_1715, 'rs-', markersize=6, linewidth=1.5,
label='(17,15)₈ K=4, dfree=6 (sim)')
ax.semilogy(snr_db, ber_3523, 'g^-', markersize=6, linewidth=1.5,
label='(35,23)₈ K=5, dfree=7 (sim)')
ax.semilogy(snr_fine, ber_bpsk, 'k--', linewidth=2, label='Uncoded BPSK (theory)')
ax.set_xlabel('Eb/N0 [dB]')
ax.set_ylabel('BER')
ax.set_title('BER of Convolutional Codes with Viterbi Decoding (Hard Decision)')
ax.legend()
ax.grid(True, which='both', alpha=0.3)
ax.set_ylim(1e-6, 1)
ax.set_xlim(0, 10)
plt.tight_layout()
plt.savefig('conv_code_ber.png', dpi=150, bbox_inches='tight')
plt.show()
この BER 曲線から、畳み込み符号の誤り訂正効果が明確に確認できます。
- 符号化利得の確認: BER $= 10^{-5}$ を達成するのに必要な $E_b/N_0$ は、符号化なし BPSK では約 9.6 dB ですが、$(7, 5)_8$ 符号($d_{\text{free}} = 5$)では約 6-7 dB 程度まで改善されています。この差(約 3 dB)が硬判定ビタビ復号の符号化利得です。
- 拘束長の効果: 拘束長 $K$ が増加するにつれて BER 曲線が左にシフトしています。$K = 3$ から $K = 5$ への改善は明確であり、$d_{\text{free}}$ の増加(5 → 6 → 7)が性能改善に直結していることが数値的に確認できます。
- ウォーターフォール領域: 畳み込み符号の BER 曲線は、ある $E_b/N_0$ を超えると急激に減少する「ウォーターフォール」特性を示しています。この閾値効果は、符号の誤り訂正が有効に機能し始める領域に対応します。
符号化利得の可視化
符号化利得をより明確に示すため、異なる符号のパラメータと理論的な符号化利得を比較します。
import numpy as np
import matplotlib.pyplot as plt
# 符号パラメータ
codes = [
{'name': '(7,5)₈ K=3', 'dfree': 5, 'R': 0.5, 'color': 'tab:blue'},
{'name': '(17,15)₈ K=4', 'dfree': 6, 'R': 0.5, 'color': 'tab:red'},
{'name': '(35,23)₈ K=5', 'dfree': 7, 'R': 0.5, 'color': 'tab:green'},
{'name': '(75,53)₈ K=6', 'dfree': 8, 'R': 0.5, 'color': 'tab:orange'},
{'name': '(171,133)₈ K=7', 'dfree': 10, 'R': 0.5, 'color': 'tab:purple'},
]
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# 左: 符号化利得 vs 拘束長
ax = axes[0]
K_values = [3, 4, 5, 6, 7]
dfree_values = [5, 6, 7, 8, 10]
# 硬判定の符号化利得(近似): 10*log10(R*dfree)
coding_gain_hard = [10 * np.log10(0.5 * d) for d in dfree_values]
# 軟判定の符号化利得: 硬判定 + 約2dB
coding_gain_soft = [g + 2.0 for g in coding_gain_hard]
ax.plot(K_values, coding_gain_hard, 'bo-', markersize=8, linewidth=2,
label='Hard decision')
ax.plot(K_values, coding_gain_soft, 'rs-', markersize=8, linewidth=2,
label='Soft decision')
ax.set_xlabel('Constraint Length K')
ax.set_ylabel('Asymptotic Coding Gain [dB]')
ax.set_title('Coding Gain vs Constraint Length (R=1/2)')
ax.legend()
ax.grid(True, alpha=0.3)
ax.set_xticks(K_values)
# 右: 復号の計算量 vs 拘束長
ax = axes[1]
m_values = [K - 1 for K in K_values]
states = [2 ** m for m in m_values]
# 1ビットあたりのACS演算回数
acs_per_bit = [2 * s for s in states] # 各状態で2つの入力
ax.semilogy(K_values, acs_per_bit, 'ko-', markersize=8, linewidth=2)
ax.set_xlabel('Constraint Length K')
ax.set_ylabel('ACS Operations per Information Bit')
ax.set_title('Decoding Complexity vs Constraint Length')
ax.grid(True, which='both', alpha=0.3)
ax.set_xticks(K_values)
# 各点にラベル
for K, acs in zip(K_values, acs_per_bit):
ax.annotate(f'{acs}', (K, acs), textcoords='offset points',
xytext=(10, 5), fontsize=10)
plt.tight_layout()
plt.savefig('coding_gain_complexity.png', dpi=150, bbox_inches='tight')
plt.show()
この図から、畳み込み符号の設計における基本的なトレードオフが見て取れます。
- 左図(符号化利得): 拘束長 $K$ が増加すると符号化利得が単調に向上します。$K = 3$ の硬判定で約4 dB、$K = 7$ で約7 dB の利得が得られます。軟判定ではさらに約2 dB の改善があり、$K = 7$ + 軟判定で約9 dB の利得が実現されます。これは NASA が $(171, 133)_8$ 符号を宇宙通信に採用した理由を端的に示しています。
- 右図(計算量): 復号の計算量は $K$ に対して指数的に増大します。$K = 3$ では8回のACS演算で済みますが、$K = 7$ では256回となります。$K = 10$ を超えると実用的でなくなり、これが古典的な畳み込み符号の拘束長の上限を決めています。この限界を超える性能を達成するために、ターボ符号(並列連接畳み込み符号の反復復号)や LDPC 符号が開発されました。
軟判定 vs 硬判定の比較
最後に、軟判定ビタビ復号を実装して硬判定との性能差を確認します。
import numpy as np
import matplotlib.pyplot as plt
from scipy.special import erfc
def viterbi_decode_soft(received_soft, generators):
"""
ビタビ復号器(軟判定)
received_soft: 軟判定受信値(実数値の配列, BPSK: -1/+1 + noise)
"""
n_outputs = len(generators)
K = len(generators[0])
m = K - 1
n_states = 2 ** m
# トレリス構築
trellis = {}
for state in range(n_states):
trellis[state] = {}
state_bits = [(state >> (m - 1 - i)) & 1 for i in range(m)]
for input_bit in [0, 1]:
full_state = [input_bit] + state_bits
outputs = []
for g in generators:
out = 0
for j in range(K):
out ^= (full_state[j] * g[j])
outputs.append(out)
next_state_bits = [input_bit] + state_bits[:-1]
next_state = 0
for i, b in enumerate(next_state_bits):
next_state |= (b << (m - 1 - i))
trellis[state][input_bit] = (next_state, tuple(outputs))
n_steps = len(received_soft) // n_outputs
received_groups = received_soft.reshape(n_steps, n_outputs)
INF = float('inf')
path_metrics = np.full(n_states, INF)
path_metrics[0] = 0.0
# 生き残りパスの履歴
predecessors = []
for t in range(n_steps):
new_metrics = np.full(n_states, INF)
new_pred = np.full(n_states, -1, dtype=int)
new_inp = np.full(n_states, -1, dtype=int)
for state in range(n_states):
if path_metrics[state] == INF:
continue
for input_bit in [0, 1]:
next_state, outputs = trellis[state][input_bit]
# 軟判定ブランチメトリック(ユークリッド距離の二乗)
expected = np.array([2.0 * o - 1.0 for o in outputs]) # BPSK: 0->-1, 1->+1
branch_metric = np.sum((received_groups[t] - expected) ** 2)
total = path_metrics[state] + branch_metric
if total < new_metrics[next_state]:
new_metrics[next_state] = total
new_pred[next_state] = state
new_inp[next_state] = input_bit
path_metrics = new_metrics
predecessors.append((new_pred.copy(), new_inp.copy()))
# トレースバック
best_state = np.argmin(path_metrics)
decoded = np.zeros(n_steps, dtype=int)
state = best_state
for t in range(n_steps - 1, -1, -1):
pred, inp = predecessors[t]
decoded[t] = inp[state]
state = pred[state]
return decoded
def simulate_conv_code_ber_both(generators, snr_db_range, n_info_bits=10000):
"""硬判定と軟判定の両方のBERをシミュレーション"""
n_outputs = len(generators)
R = 1.0 / n_outputs
K = len(generators[0])
ber_hard_list = []
ber_soft_list = []
for snr_db in snr_db_range:
snr_lin = 10 ** (snr_db / 10)
sigma = 1.0 / np.sqrt(2 * snr_lin * R)
errors_hard = 0
errors_soft = 0
total_bits = 0
while total_bits < n_info_bits:
batch_size = min(100, n_info_bits - total_bits)
info_bits = np.random.randint(0, 2, batch_size)
tail_bits = np.zeros(K - 1, dtype=int)
bits_with_tail = np.concatenate([info_bits, tail_bits])
encoded = convolutional_encode(bits_with_tail, generators)
modulated = 2.0 * encoded - 1.0
noise = sigma * np.random.randn(len(modulated))
received_soft = modulated + noise
# 硬判定復号
received_hard = (received_soft > 0).astype(int)
decoded_hard = viterbi_decode(received_hard, generators)
errors_hard += np.sum(decoded_hard[:batch_size] != info_bits)
# 軟判定復号
decoded_soft = viterbi_decode_soft(received_soft, generators)
errors_soft += np.sum(decoded_soft[:batch_size] != info_bits)
total_bits += batch_size
ber_hard_list.append(max(errors_hard / total_bits, 1e-7))
ber_soft_list.append(max(errors_soft / total_bits, 1e-7))
return np.array(ber_hard_list), np.array(ber_soft_list)
# シミュレーション実行
snr_db = np.arange(0, 10, 1)
generators_75 = [(1, 1, 1), (1, 0, 1)]
ber_hard, ber_soft = simulate_conv_code_ber_both(generators_75, snr_db, n_info_bits=50000)
# 理論曲線
snr_fine = np.linspace(0, 10, 200)
snr_lin_fine = 10 ** (snr_fine / 10)
ber_bpsk = 0.5 * erfc(np.sqrt(snr_lin_fine))
fig, ax = plt.subplots(figsize=(10, 7))
ax.semilogy(snr_db, ber_hard, 'bo-', markersize=6, linewidth=1.5,
label='(7,5)₈ Hard Decision')
ax.semilogy(snr_db, ber_soft, 'rs-', markersize=6, linewidth=1.5,
label='(7,5)₈ Soft Decision')
ax.semilogy(snr_fine, ber_bpsk, 'k--', linewidth=2, label='Uncoded BPSK')
ax.set_xlabel('Eb/N0 [dB]')
ax.set_ylabel('BER')
ax.set_title('Hard vs Soft Decision Viterbi Decoding: (7,5)₈ Code')
ax.legend()
ax.grid(True, which='both', alpha=0.3)
ax.set_ylim(1e-6, 1)
ax.set_xlim(0, 10)
# 符号化利得を矢印で表示
target_ber = 1e-4
ax.axhline(y=target_ber, color='gray', linestyle=':', alpha=0.5)
ax.text(0.5, target_ber * 1.5, f'BER = {target_ber}', fontsize=9, color='gray')
plt.tight_layout()
plt.savefig('hard_vs_soft_viterbi.png', dpi=150, bbox_inches='tight')
plt.show()
このグラフから、硬判定と軟判定のビタビ復号の性能差が明確に確認できます。
- 軟判定の優位性: 軟判定(赤い四角)は硬判定(青い丸)に対して約2 dB の改善があります。例えば BER $= 10^{-4}$ 付近では、硬判定が約 6-7 dB で達成するのに対し、軟判定は約 4-5 dB で達成しています。この2 dBの差は、硬判定の量子化による情報損失に対応しています。
- 総合的な符号化利得: 符号化なし BPSK(黒の破線)に対して、$(7, 5)_8$ 符号 + 軟判定ビタビ復号は約5-6 dBの符号化利得を達成しています。これは送信電力を約3-4分の1に削減できることを意味し、電力が限られた宇宙通信や衛星通信で極めて大きな価値を持ちます。
- 実装上の考慮: 軟判定は硬判定より性能が良い一方、受信値の量子化精度(ビット数)が必要です。実用的には3-4ビットの量子化(8-16レベル)で十分な利得が得られることが知られています。
畳み込み符号の発展と応用
パンクチャリングによる符号化率の調整
基本的な $R = 1/2$ の畳み込み符号から、より高い符号化率($R = 2/3, 3/4, 5/6$ 等)を得るために パンクチャリング(puncturing) が使われます。パンクチャリングとは、符号化出力の一部のビットを周期的に間引く操作です。
例えば $R = 1/2$ の符号から $R = 2/3$ を得るには、2入力ビットに対する4出力ビットのうち1ビットを間引いて3ビットにします。パンクチャリングパターンは パンクチャリング行列 で指定します。
$$ \bm{P} = \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix} $$
この行列の1の位置のビットは送信し、0の位置のビットは削除します。受信側では、間引かれた位置に「消失(erasure)」マーカーを挿入してからビタビ復号を行います。
パンクチャリングの利点は、1つの母符号(parent code)と1つのビタビ復号器で複数の符号化率に対応できることです。Wi-Fi(IEEE 802.11a/g/n)では、$R = 1/2$ の畳み込み符号をベースとして、$R = 2/3, 3/4$ のパンクチャード符号を適応的に切り替えて使用しています。
ターボ符号・LDPC符号への発展
畳み込み符号は、現代の高性能符号の出発点にもなっています。
ターボ符号(Turbo Code, 1993年): 2つの畳み込み符号器をインターリーバで接続した 並列連接畳み込み符号(PCCC) です。反復復号(iterative decoding)により、シャノン限界に0.5 dB以内まで迫る性能を達成しました。3G/4Gの通信規格で採用されています。
LDPC符号(Low-Density Parity-Check Code): 畳み込み符号とは異なるブロック符号ですが、反復復号の考え方を共有しています。5GのNR(New Radio)やWi-Fi 6(802.11ax)で採用されています。
畳み込みLDPC符号(Spatially Coupled LDPC): 畳み込み符号の「記憶を持つ」構造とLDPC符号の「疎なパリティ検査行列」を融合した最新の符号です。
標準規格における畳み込み符号
| 標準規格 | 符号パラメータ | 用途 |
|---|---|---|
| Voyager (NASA) | $K=7, R=1/2$ (171,133)₈ | 深宇宙通信 |
| GSM | $K=5, R=1/2$ | 音声チャネル |
| IS-95 (cdmaOne) | $K=9, R=1/2$ | 順方向トラフィック |
| 802.11a/g/n (Wi-Fi) | $K=7, R=1/2$ + パンクチャリング | データ通信 |
| DVB-S | $K=7, R=1/2, 2/3, 3/4, 5/6, 7/8$ | 衛星デジタルTV |
| CCSDS | $K=7, R=1/2$ + RS外符号 | 宇宙通信 |
この表から、$K = 7$ の畳み込み符号が最も広く実用化されていることがわかります。$K = 7$ は性能と計算量の良いバランス点であり、64状態のビタビ復号器は専用ハードウェアで効率的に実装できます。
まとめ
本記事では、畳み込み符号の構造とビタビ復号アルゴリズムの原理を体系的に解説しました。
- 畳み込み符号の構造: シフトレジスタと生成多項式による連続的な符号化。パラメータ $(k, n, m)$ で特徴づけられ、$2^m$ 個の状態を持つ有限状態機械として動作する
- トレリス線図: 状態遷移の時間展開。全ての有効な符号語がトレリス上のパスとして表現され、復号問題は「最良のパスの探索」に帰着する
- ビタビアルゴリズム: 動的計画法(ACS:加算-比較-選択)による最尤復号。計算量は入力長 $T$ に対して線形 $O(T \cdot 2^m)$ であり、全探索の $O(2^T)$ に対して劇的に効率的
- 自由距離 $d_{\text{free}}$: 符号の誤り訂正能力を決定する最重要パラメータ。$(7, 5)_8$ 符号で $d_{\text{free}} = 5$、$(171, 133)_8$ 符号で $d_{\text{free}} = 10$
- 符号化利得: $(7, 5)_8$ 符号 + 硬判定で約3-4 dB、軟判定で約5-6 dB。拘束長を増やすことで利得は向上するが、計算量は指数的に増大
- 軟判定の優位性: 硬判定に対して約2 dBの利得。受信信号の実数値情報を活用することで情報損失を低減
畳み込み符号とビタビ復号は、半世紀以上にわたってデジタル通信の基盤技術であり続けています。ターボ符号やLDPC符号の登場により「最強の符号」の座は譲りましたが、その概念 — 状態機械による符号化、トレリス上の動的計画法による復号 — は現代の通信理論を理解する上で不可欠な基盤です。
次のステップとして、以下の記事も参考にしてください。