VKVlad KulikovPublic / lectures
Information Theory for ML9 / 16

Module 9. Rate–distortion and the Information Bottleneck: managing information loss

Try each exercise first. Expand worked answers or project guidance when needed; their open state is shared between RU/EN.

Download course files (.zip) 33.9 KB

The archive preserves the folder structure of nine Python solution files shared by RU/EN.

Exercise 1. Bernoulli rate–distortion

Let X∼Bernoulli⁡(p)X\sim\operatorname{Bernoulli}(p), p≤1/2p\le1/2, with

d(x,x^)=1{x≠x^}.d(x,\widehat x)=\mathbf1\{x\ne\widehat x\}.
  1. Prove the lower bound

    I(X;X^)≥h2(p)−h2(D)I(X;\widehat X)\ge h_2(p)-h_2(D)

    for 0≤D≤p0\le D\le p.

  2. Construct a test channel that attains equality.

  3. Explain why R(D)=0R(D)=0 for D≥pD\ge p.

Show answer and solution

Let

Pe=P(X≠X^)≤D.P_e=P(X\ne\widehat X)\le D.

Because XX is binary, X^\widehat X together with the error indicator E=1{X≠X^}E=\mathbf1\{X\ne\widehat X\} determines XX. Hence

H(X∣X^)≤H(E∣X^)≤H(E)=h2(Pe).H(X\mid\widehat X) \le H(E\mid\widehat X) \le H(E) =h_2(P_e).

For 0≤D≤p≤1/20\le D\le p\le1/2, h2h_2 is increasing, so

H(X∣X^)≤h2(Pe)≤h2(D).H(X\mid\widehat X) \le h_2(P_e) \le h_2(D).

Therefore

I(X;X^)=H(X)−H(X∣X^)≥h2(p)−h2(D).\boxed{ I(X;\widehat X) =H(X)-H(X\mid\widehat X) \ge h_2(p)-h_2(D) }.

To attain equality, it is convenient to specify the backward test channel. Let

X^∼Bernoulli⁡(q),Z∼Bernoulli⁡(D)\widehat X\sim\operatorname{Bernoulli}(q), \qquad Z\sim\operatorname{Bernoulli}(D)

be independent, and define

X=X^⊕Z.X=\widehat X\oplus Z.

Then P(X≠X^)=DP(X\ne\widehat X)=D, while

P(X=1)=D+q(1−2D).P(X=1)=D+q(1-2D).

Choose

q=p−D1−2D,q=\frac{p-D}{1-2D},

which is valid for 0≤D≤p≤1/20\le D\le p\le1/2 (with the boundary case p=D=1/2p=D=1/2 handled separately). The marginal of XX is then Bernoulli⁡(p)\operatorname{Bernoulli}(p), and

H(X∣X^)=H(Z)=h2(D).H(X\mid\widehat X)=H(Z)=h_2(D).

Thus

I(X;X^)=h2(p)−h2(D).I(X;\widehat X)=h_2(p)-h_2(D).

The forward channel P(x^∣x)P(\widehat x\mid x) follows from Bayes' rule applied to this joint distribution.

For D≥pD\ge p, the decoder can always emit X^=0\widehat X=0. The rate is then zero and the average distortion is P(X=1)=p≤DP(X=1)=p\le D. Therefore

R(D)=0,D≥p.R(D)=0, \qquad D\ge p.

Exercise 2. Predictive IB distortion

For the Markov chain Y→X→TY\to X\to T, prove

I(X;Y)−I(T;Y)=EX,TDKL(PY∣X∥PY∣T).I(X;Y)-I(T;Y) = \mathbb E_{X,T} D_{\mathrm{KL}} \bigl(P_{Y\mid X}\|P_{Y\mid T}\bigr).

Show that minimizing I(X;T)−βI(T;Y)I(X;T)-\beta I(T;Y) is equivalent to a rate–distortion Lagrangian with this distortion.

Show answer and solution

The chain rule gives

I(X,T;Y)=I(T;Y)+I(X;Y∣T)I(X,T;Y)=I(T;Y)+I(X;Y\mid T)

and also

I(X,T;Y)=I(X;Y)+I(T;Y∣X).I(X,T;Y)=I(X;Y)+I(T;Y\mid X).

For the Markov chain Y→X→TY\to X\to T,

I(T;Y∣X)=0.I(T;Y\mid X)=0.

Therefore

I(X;Y)−I(T;Y)=I(X;Y∣T).I(X;Y)-I(T;Y)=I(X;Y\mid T).

Conditional mutual information is an average KL divergence:

I(X;Y∣T)=EX,TDKL(PY∣X,T∥PY∣T).I(X;Y\mid T) = \mathbb E_{X,T} D_{\mathrm{KL}} \bigl(P_{Y\mid X,T}\|P_{Y\mid T}\bigr).

Markovity gives PY∣X,T=PY∣XP_{Y\mid X,T}=P_{Y\mid X}, hence

I(X;Y)−I(T;Y)=EX,TDKL(PY∣X∥PY∣T).\boxed{ I(X;Y)-I(T;Y) = \mathbb E_{X,T} D_{\mathrm{KL}} \bigl(P_{Y\mid X}\|P_{Y\mid T}\bigr) }.

Calling the KL term dIB(X,T)d_{\mathrm{IB}}(X,T),

I(X;T)−βI(T;Y)=I(X;T)+βE[dIB(X,T)]−βI(X;Y).I(X;T)-\beta I(T;Y) = I(X;T)+\beta\mathbb E[d_{\mathrm{IB}}(X,T)]-\beta I(X;Y).

The last term does not depend on PT∣XP_{T\mid X}. The minimizers therefore coincide with those of the rate–distortion Lagrangian

I(X;T)+βE[dIB(X,T)].I(X;T)+\beta\mathbb E[d_{\mathrm{IB}}(X,T)].

Exercise 3. Variational VIB bounds

  1. Prove

    I(T;Y)≥H(Y)+E[log⁡rψ(Y∣T)].I(T;Y) \ge H(Y)+\mathbb E[\log r_\psi(Y\mid T)].
  2. Prove

    EXDKL(q(T∣X)∥r(T))=I(X;T)+DKL(q(T)∥r(T)).\mathbb E_XD_{\mathrm{KL}}(q(T\mid X)\|r(T)) = I(X;T)+D_{\mathrm{KL}}(q(T)\|r(T)).
  3. Explain when both bounds are tight simultaneously.

Show answer and solution

1. Lower bound on relevance

For any decoder rψ(y∣t)r_\psi(y\mid t),

E[−log⁡rψ(Y∣T)]=H(Y∣T)+ETDKL(PY∣T∥rψ(⋅∣T))≥H(Y∣T).\begin{aligned} \mathbb E[-\log r_\psi(Y\mid T)] &=H(Y\mid T) + \mathbb E_TD_{\mathrm{KL}} \bigl(P_{Y\mid T}\|r_\psi(\cdot\mid T)\bigr)\\ &\ge H(Y\mid T). \end{aligned}

Therefore

I(T;Y)=H(Y)−H(Y∣T)≥H(Y)+E[log⁡rψ(Y∣T)].\begin{aligned} I(T;Y) &=H(Y)-H(Y\mid T)\\ &\ge H(Y)+\mathbb E[\log r_\psi(Y\mid T)]. \end{aligned}

The bound is tight exactly when

rψ(y∣t)=P(y∣t)r_\psi(y\mid t)=P(y\mid t)

almost surely under PTP_T.

2. Upper bound on rate

Let

q(t)=∫q(t∣x)p(x) dx.q(t)=\int q(t\mid x)p(x)\,dx.

Then

EXDKL(q(T∣X)∥r(T))=EX,Tlog⁡q(t∣x)r(t)=EX,Tlog⁡q(t∣x)q(t)+ETlog⁡q(t)r(t)=Iq(X;T)+DKL(q(T)∥r(T)).\begin{aligned} \mathbb E_XD_{\mathrm{KL}}(q(T\mid X)\|r(T)) &=\mathbb E_{X,T}\log\frac{q(t\mid x)}{r(t)}\\ &=\mathbb E_{X,T}\log\frac{q(t\mid x)}{q(t)} + \mathbb E_T\log\frac{q(t)}{r(t)}\\ &=I_q(X;T)+D_{\mathrm{KL}}(q(T)\|r(T)). \end{aligned}

Thus KL to the chosen prior upper-bounds I(X;T)I(X;T).

3. Simultaneous tightness

Both bounds are tight when

rψ(y∣t)=P(y∣t)r_\psi(y\mid t)=P(y\mid t)

and

r(t)=q(t).r(t)=q(t).

The first requires an exact variational decoder; the second requires the prior to equal the aggregate posterior. A restricted parametric model may not satisfy either condition.


Exercise 4. VAE: the likelihood defines distortion

Consider

pθ(x∣z)=N(μθ(z),σ2I).p_\theta(x\mid z) = \mathcal N(\mu_\theta(z),\sigma^2I).
  1. Derive the negative log-likelihood and its connection to MSE.

  2. Explain why changing σ2\sigma^2 changes the relative weighting of distortion and rate even when β=1\beta=1.

  3. Derive

    R=I(X;Z)+DKL(q(Z)∥p(Z)).\mathcal R = I(X;Z)+D_{\mathrm{KL}}(q(Z)\|p(Z)).
Show answer and solution

For a dd-dimensional observation,

pθ(x∣z)=(2πσ2)−d/2exp⁡[−∥x−μθ(z)∥22σ2].p_\theta(x\mid z) =(2\pi\sigma^2)^{-d/2} \exp\left[-\frac{\|x-\mu_\theta(z)\|^2}{2\sigma^2}\right].

Hence

−log⁡pθ(x∣z)=d2log⁡(2πσ2)+∥x−μθ(z)∥22σ2.\boxed{ -\log p_\theta(x\mid z) = \frac d2\log(2\pi\sigma^2) + \frac{\|x-\mu_\theta(z)\|^2}{2\sigma^2} }.

At fixed σ2\sigma^2, minimizing NLL is equivalent to minimizing MSE. The coefficient of MSE is 1/(2σ2)1/(2\sigma^2), however, so changing σ2\sigma^2 changes the relative strength of reconstruction and KL-rate even at formal β=1\beta=1.

For the rate decomposition, add and subtract log⁡q(z)\log q(z):

R=EX,Zlog⁡q(z∣x)p(z)=EX,Zlog⁡q(z∣x)q(z)+EZlog⁡q(z)p(z)=Iq(X;Z)+DKL(q(Z)∥p(Z)).\begin{aligned} \mathcal R &=\mathbb E_{X,Z}\log\frac{q(z\mid x)}{p(z)}\\ &=\mathbb E_{X,Z}\log\frac{q(z\mid x)}{q(z)} + \mathbb E_Z\log\frac{q(z)}{p(z)}\\ &=I_q(X;Z)+D_{\mathrm{KL}}(q(Z)\|p(Z)). \end{aligned}

Exercise 5. Quantization: which rate–distortion problem is being posed?

For weight-only quantization, list:

  1. the source alphabet;

  2. everything included in the actual rate;

  3. three possible distortion measures;

  4. why the Gaussian MSE bound cannot be converted directly into a bound on downstream accuracy.

Show answer and solution

A careful answer may use the following choices.

Source alphabet

One must decide whether the source consists of individual weights, weight blocks, matrix rows or columns, or channel groups together with activation statistics. Classical theory also requires a probabilistic ensemble, not merely one fixed matrix without a source model.

Actual rate

It may include quantized values or indices, scales and zero-points, codebooks, group boundaries, sparsity metadata, padding and alignment, and container headers. Nominal bit-width is therefore not always the actual number of bits per parameter.

Possible distortions

Examples are:

  1. ∥W−W^∥22\|W-\widehat W\|_2^2;

  2. E∥WX−W^X∥22\mathbb E\|WX-\widehat W X\|_2^2 on calibration activations;

  3. an increase in NLL or KL between the original and quantized predictive distributions;

  4. a change in a downstream metric.

Why the Gaussian bound does not bound accuracy

D≥σ22−2RD\ge\sigma^2 2^{-2R} assumes an i.i.d. Gaussian source, squared error, and an optimal long-block code. Downstream accuracy is a nonlinear function of the entire network, data distribution, and decision boundaries. Equal weight MSE can have very different functional effects in different parameter directions.


Exercise 6. Python: discrete IB and the VAE-rate decomposition

Run M9_ib_rate_distortion_experiments.py. The script:

  1. plots theoretical rate–distortion curves;

  2. solves a toy Information Bottleneck problem with self-consistent iterations;

  3. verifies the exact decomposition of VAE rate into mutual information and prior mismatch.

Answer:

  • why the IB representation is trivial at small β\beta;
  • why I(T;Y)I(T;Y) saturates before I(X;T)I(X;T);
  • how a poor prior increases KL-rate without adding information about the input.
Show answer and solution

The script performs three independent checks.

Rate–distortion curves

The Bernoulli curve decreases from h2(p)h_2(p) at D=0D=0 to zero at D=pD=p. The Gaussian curve diverges as D↓0D\downarrow0 and becomes zero for D≥σ2D\ge\sigma^2.

Toy Information Bottleneck

At small β\beta, the penalty on I(X;T)I(X;T) dominates. The optimal representation is nearly unused:

I(X;T)≈0,I(T;Y)≈0.I(X;T)\approx0, \qquad I(T;Y)\approx0.

As β\beta grows, the representation separates inputs with different P(Y∣x)P(Y\mid x). Relevance saturates near I(X;Y)I(X;Y) because XX contains no more information about YY. Meanwhile I(X;T)I(X;T) can continue to grow if the code preserves input details that add no predictive relevance.

VAE-rate decomposition

Numerically one should find

EXDKL(q(Z∣X)∥p(Z))=I(X;Z)+DKL(q(Z)∥p(Z))\mathbb E_XD_{\mathrm{KL}}(q(Z\mid X)\|p(Z)) = I(X;Z)+D_{\mathrm{KL}}(q(Z)\|p(Z))

to floating-point accuracy.

Replacing a good prior with a poor one leaves I(X;Z)I(X;Z) unchanged because the encoder and joint distribution are unchanged. It increases DKL(q(Z)∥p(Z))D_{\mathrm{KL}}(q(Z)\|p(Z)) and hence the KL-rate. This is an extra mismatch cost, not extra information about the data.


Code solution · the same source file for RU/EN

PythonM9_ib_rate_distortion_experiments.pyDownload .py
from __future__ import annotations

"""Reproducible numerical experiments for Module 9.

The script:
1. plots Bernoulli and Gaussian rate-distortion curves;
2. solves a finite Information Bottleneck problem by self-consistent updates;
3. verifies the exact VAE-rate decomposition on a discrete toy encoder.

All logarithms used for information quantities are base 2 unless stated otherwise.
"""

from pathlib import Path
import math

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np

ROOT = Path(__file__).resolve().parent
OUT = ROOT / "generated_experiment"
OUT.mkdir(exist_ok=True)
EPS = 1e-15


def normalize(a: np.ndarray, axis: int = -1) -> np.ndarray:
    total = a.sum(axis=axis, keepdims=True)
    if np.any(total <= 0):
        raise ValueError("Cannot normalize non-positive mass")
    return a / total


def xlog2x(x: np.ndarray) -> np.ndarray:
    x = np.asarray(x, dtype=float)
    return np.where(x > 0, x * np.log2(x), 0.0)


def binary_entropy(p: np.ndarray | float) -> np.ndarray:
    p = np.asarray(p, dtype=float)
    p = np.clip(p, EPS, 1 - EPS)
    return -(p * np.log2(p) + (1 - p) * np.log2(1 - p))


def mutual_information(joint: np.ndarray) -> float:
    joint = np.asarray(joint, dtype=float)
    joint = joint / joint.sum()
    px = joint.sum(axis=1, keepdims=True)
    py = joint.sum(axis=0, keepdims=True)
    ratio = np.divide(joint, px @ py, out=np.ones_like(joint), where=joint > 0)
    return float(np.sum(np.where(joint > 0, joint * np.log2(ratio), 0.0)))


def kl_rows(p: np.ndarray, q: np.ndarray) -> np.ndarray:
    """Return all pairwise KL divergences D(p_i || q_j) in bits."""
    p = np.asarray(p, dtype=float)
    q = np.asarray(q, dtype=float)
    p_safe = np.clip(p, EPS, 1)
    q_safe = np.clip(q, EPS, 1)
    return np.sum(p[:, None, :] * (np.log2(p_safe[:, None, :]) - np.log2(q_safe[None, :, :])), axis=-1)


def plot_rate_distortion() -> None:
    p = 0.25
    d_b = np.linspace(0, p, 250)
    r_b = binary_entropy(p) - binary_entropy(d_b)
    r_b[-1] = 0.0

    sigma2 = 1.0
    d_g = np.geomspace(0.01, sigma2, 250)
    r_g = 0.5 * np.log2(sigma2 / d_g)

    fig, ax = plt.subplots(figsize=(8.6, 5.0))
    ax.plot(d_b, r_b, label=r"Bernoulli($p=0.25$), Hamming")
    ax.plot(d_g, r_g, label=r"Gaussian($\sigma^2=1$), MSE")
    ax.set_xlabel("Allowed distortion D")
    ax.set_ylabel("Rate R(D), bits/source symbol")
    ax.set_title("Two rate-distortion benchmarks")
    ax.legend()
    ax.grid(alpha=0.25)
    fig.tight_layout()
    fig.savefig(OUT / "M9_experiment_rate_distortion.png", dpi=180)
    plt.close(fig)


def ib_metrics(px: np.ndarray, py_given_x: np.ndarray, qt_given_x: np.ndarray) -> tuple[float, float]:
    pxt = px[:, None] * qt_given_x
    ixt = mutual_information(pxt)
    pty = np.einsum("x,xt,xy->ty", px, qt_given_x, py_given_x)
    ity = mutual_information(pty)
    return ixt, ity


def solve_ib(
    px: np.ndarray,
    py_given_x: np.ndarray,
    n_t: int,
    beta: float,
    rng: np.random.Generator,
    restarts: int = 24,
    max_iter: int = 5000,
    tol: float = 1e-12,
) -> tuple[np.ndarray, float, float, float]:
    """Solve a finite IB problem by self-consistent updates.

    Returns q(t|x), I(X;T), I(T;Y), and the IB objective I(X;T)-beta I(T;Y).
    Multiple random restarts are used because the fixed-point equations may have
    several stationary solutions.
    """
    best: tuple[np.ndarray, float, float, float] | None = None
    n_x = px.size

    for restart in range(restarts):
        if restart == 0:
            qtx = np.full((n_x, n_t), 1.0 / n_t)
            qtx += 1e-3 * rng.random((n_x, n_t))
        else:
            qtx = rng.dirichlet(np.ones(n_t), size=n_x)
        qtx = normalize(qtx, axis=1)

        for _ in range(max_iter):
            pt = px @ qtx
            pt = np.clip(pt, EPS, None)
            pyt = np.einsum("x,xt,xy->ty", px, qtx, py_given_x)
            pyt = normalize(pyt, axis=1)
            distortion = kl_rows(py_given_x, pyt)

            # Exponents use natural exp. Convert KL bits to nats by ln(2).
            log_new = np.log(pt)[None, :] - beta * math.log(2.0) * distortion
            log_new -= log_new.max(axis=1, keepdims=True)
            new = normalize(np.exp(log_new), axis=1)
            if np.max(np.abs(new - qtx)) < tol:
                qtx = new
                break
            qtx = new

        ixt, ity = ib_metrics(px, py_given_x, qtx)
        objective = ixt - beta * ity
        candidate = (qtx.copy(), ixt, ity, objective)
        if best is None or objective < best[3] - 1e-11:
            best = candidate

    assert best is not None
    return best


def run_ib_experiment() -> list[tuple[float, float, float]]:
    px = np.full(6, 1 / 6)
    # Three progressively more positive regions. Neighboring states have similar
    # predictive distributions, making the bottleneck geometry visible.
    p1 = np.array([0.03, 0.08, 0.30, 0.70, 0.92, 0.97])
    py_given_x = np.column_stack([1 - p1, p1])
    rng = np.random.default_rng(20260713)
    betas = np.array([0.05, 0.10, 0.20, 0.40, 0.75, 1.25, 2.0, 3.5, 6.0, 10.0, 18.0])
    rows: list[tuple[float, float, float]] = []

    for beta in betas:
        _, ixt, ity, _ = solve_ib(px, py_given_x, n_t=6, beta=float(beta), rng=rng)
        rows.append((float(beta), ixt, ity))

    ixy = mutual_information(px[:, None] * py_given_x)

    fig, ax = plt.subplots(figsize=(7.2, 5.4))
    ixt = np.array([r[1] for r in rows])
    ity = np.array([r[2] for r in rows])
    ax.plot(ixt, ity, marker="o", label="IB fixed points")
    for beta, x, y in rows[::2]:
        ax.annotate(f"β={beta:g}", (x, y), xytext=(5, 5), textcoords="offset points", fontsize=8)
    ax.axhline(ixy, linestyle="--", linewidth=1.2, label=f"I(X;Y)={ixy:.3f} bits")
    ax.set_xlabel("I(X;T), bits")
    ax.set_ylabel("I(T;Y), bits")
    ax.set_title("Toy Information Bottleneck frontier")
    ax.legend()
    ax.grid(alpha=0.25)
    fig.tight_layout()
    fig.savefig(OUT / "M9_experiment_ib_frontier.png", dpi=180)
    plt.close(fig)
    return rows


def expected_conditional_kl(px: np.ndarray, qz_given_x: np.ndarray, prior: np.ndarray) -> float:
    """Average KL in bits for normalized probability arrays, without smoothing."""
    joint = px[:, None] * qz_given_x
    positive = joint > 0
    prior_grid = np.broadcast_to(prior, qz_given_x.shape)
    # A prior must cover every latent with positive joint mass. Rows with
    # px[x] = 0 and terms with q(z|x) = 0 contribute nothing, including 0 log 0.
    if np.any(prior_grid[positive] == 0):
        return math.inf
    return float(np.sum(joint[positive] * (
        np.log2(qz_given_x[positive]) - np.log2(prior_grid[positive])
    )))


def run_rate_decomposition(bad_prior: np.ndarray | None = None) -> dict[str, float]:
    px = np.array([0.40, 0.35, 0.25])
    qz_given_x = np.array([
        [0.80, 0.15, 0.05],
        [0.20, 0.65, 0.15],
        [0.05, 0.20, 0.75],
    ])
    qz = px @ qz_given_x
    good_prior = qz.copy()
    if bad_prior is None:
        bad_prior = np.array([0.78, 0.12, 0.10])

    joint = px[:, None] * qz_given_x
    ixz = mutual_information(joint)
    # Ordinary KL is the same calculation with a single source state.
    mismatch_good = expected_conditional_kl(np.ones(1), qz[None, :], good_prior)
    mismatch_bad = expected_conditional_kl(np.ones(1), qz[None, :], bad_prior)
    rate_good = expected_conditional_kl(px, qz_given_x, good_prior)
    rate_bad = expected_conditional_kl(px, qz_given_x, bad_prior)

    if not np.isclose(rate_good, ixz + mismatch_good, atol=1e-12):
        raise AssertionError("Good-prior decomposition failed")
    if not np.isclose(rate_bad, ixz + mismatch_bad, atol=1e-12):
        raise AssertionError("Bad-prior decomposition failed")

    labels = ["I(X;Z)", "prior mismatch", "total KL-rate"]
    good = [ixz, mismatch_good, rate_good]
    bad = [ixz, mismatch_bad, rate_bad]
    x = np.arange(len(labels))
    width = 0.35
    fig, ax = plt.subplots(figsize=(7.7, 5.0))
    ax.bar(x - width / 2, good, width, label="prior = aggregate posterior")
    finite = np.isfinite(bad)
    ax.bar(x[finite] + width / 2, np.asarray(bad)[finite], width,
           label="mismatched prior")
    for position in x[~finite] + width / 2:
        ax.text(position, 0.78, "∞", transform=ax.get_xaxis_transform(),
                ha="center", color="C1", fontsize=22)
    ax.set_xticks(x, labels)
    ax.set_ylabel("Bits")
    ax.set_title("VAE-rate decomposition: information plus prior mismatch")
    ax.legend()
    ax.grid(axis="y", alpha=0.25)
    if not np.all(finite):
        # Text annotations do not participate in Matplotlib's autoscaling.
        ax.set_xlim(-0.6, len(labels) - 0.4)
        fig.text(0.5, 0.02, "∞: prior assigns zero mass to a possible latent",
                 ha="center", fontsize=10)
    fig.tight_layout(rect=(0, 0.06 if not np.all(finite) else 0, 1, 1))
    fig.savefig(OUT / "M9_experiment_rate_decomposition.png", dpi=180)
    plt.close(fig)

    return {
        "I(X;Z)": ixz,
        "good_mismatch": mismatch_good,
        "good_rate": rate_good,
        "bad_mismatch": mismatch_bad,
        "bad_rate": rate_bad,
    }


def main() -> None:
    plot_rate_distortion()
    rows = run_ib_experiment()
    decomposition = run_rate_decomposition()

    print("Toy IB results (bits):")
    print("beta\tI(X;T)\tI(T;Y)")
    for beta, ixt, ity in rows:
        print(f"{beta:>5g}\t{ixt:.6f}\t{ity:.6f}")

    print("\nVAE-rate decomposition (bits):")
    for key, value in decomposition.items():
        print(f"{key}: {value:.9f}")
    print("\nGenerated figures:")
    for path in sorted(OUT.glob("M9_experiment_*.png")):
        print(path.name)


if __name__ == "__main__":
    main()

Bonus. I–MMSE for a Gaussian input

For X∼N(0,1)X\sim\mathcal N(0,1), derive the posterior mean, mmse⁡(γ)\operatorname{mmse}(\gamma), and I(X;Yγ)I(X;Y_\gamma). Verify the identity and explain why the integral to infinity diverges even though differential entropy is finite.

Show answer and solution

Let

Yγ=γX+N,X,N∼N(0,1)Y_\gamma=\sqrt\gamma X+N, \qquad X,N\sim\mathcal N(0,1)

independently. Gaussian conditioning gives

E[X∣Yγ]=γ1+γYγ\mathbb E[X\mid Y_\gamma] = \frac{\sqrt\gamma}{1+\gamma}Y_\gamma

and

Var⁡(X∣Yγ)=11+γ.\operatorname{Var}(X\mid Y_\gamma)=\frac1{1+\gamma}.

Therefore

mmse⁡(γ)=11+γ.\boxed{ \operatorname{mmse}(\gamma)=\frac1{1+\gamma} }.

The Gaussian mutual information is

I(X;Yγ)=12ln⁡(1+γ).\boxed{ I(X;Y_\gamma)=\frac12\ln(1+\gamma) }.

Indeed,

ddγI(X;Yγ)=12(1+γ)=12mmse⁡(γ).\frac d{d\gamma}I(X;Y_\gamma) = \frac1{2(1+\gamma)} = \frac12\operatorname{mmse}(\gamma).

But

12∫0∞dγ1+γ=∞.\frac12\int_0^\infty\frac{d\gamma}{1+\gamma}=\infty.

This does not contradict the finiteness of

h(X)=12ln⁡(2πe).h(X)=\frac12\ln(2\pi e).

I–MMSE integrates the mutual information between a continuous variable and an observation of ever increasing precision, which diverges. A relation to differential entropy requires subtraction of a divergent reference term; the unrenormalized equality is false.