VKVlad KulikovPublic / lectures
Information Theory for ML14 / 16

Module 14. Computer Vision Through Information Theory

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. The price of side information

Each block contains n=8n=8 binary symbols. The hidden regime ZZ is equiprobable:

P(Z=0)=P(Z=1)=1/2.P(Z=0)=P(Z=1)=1/2.

Conditioned on ZZ, the symbols are independent:

P(Yi=1∣Z=0)=0.05,P(Yi=1∣Z=1)=0.5.P(Y_i=1\mid Z=0)=0.05, \qquad P(Y_i=1\mid Z=1)=0.5.
  1. Find the marginal probability P(Yi=1)P(Y_i=1).

  2. Compute the factorized rate 8H(Yi)8H(Y_i).

  3. Compute the hierarchical rate H(Z)+8H(Yi∣Z)H(Z)+8H(Y_i\mid Z).

  4. Find the exact H(Y8)H(Y^8) by enumerating all 256 strings.

  5. Explain why the hierarchical coder beats the factorized model but remains worse than an ideal joint coder.

Show answer and solution

Write the binary entropy as

h2(p)=−plog⁡2p−(1−p)log⁡2(1−p).h_2(p)=-p\log_2p-(1-p)\log_2(1-p).

The marginal probability of one is

P(Yi=1)=12⋅0.05+12⋅0.5=0.275.P(Y_i=1) = \frac12\cdot0.05+\frac12\cdot0.5 =0.275.

The factorized model therefore pays

Rfact=8h2(0.275)≈8⋅0.848548178=6.788385426R_{\mathrm{fact}} =8h_2(0.275) \approx 8\cdot0.848548178 = 6.788385426

bits per block.

The one-symbol conditional entropy is

H(Yi∣Z)=12h2(0.05)+12h2(0.5)≈0.643198479.H(Y_i\mid Z) = \frac12h_2(0.05)+\frac12h_2(0.5) \approx0.643198479.

The hierarchical rate is

Rhier=H(Z)+8H(Yi∣Z)=1+8⋅0.643198479≈6.145587828.R_{\mathrm{hier}} =H(Z)+8H(Y_i\mid Z) =1+8\cdot0.643198479 \approx6.145587828.

For the exact block entropy, a string with kk ones has

pk=12(0.05)k(0.95)8−k+12(0.5)8.p_k = \frac12(0.05)^k(0.95)^{8-k} + \frac12(0.5)^8.

There are (8k){8\choose k} such strings, so

H(Y8)=−∑k=08(8k)pklog⁡2pk≈5.961948275.H(Y^8) = - \sum_{k=0}^{8} {8\choose k}p_k\log_2p_k \approx5.961948275.

Thus

Rfact>Rhier>H(Y8).R_{\mathrm{fact}} > R_{\mathrm{hier}} > H(Y^8).

The gain of the hierarchical model over the factorized one is

6.788385426−6.145587828≈0.642797598 bits.6.788385426-6.145587828 \approx0.642797598\text{ bits}.

Its loss against the ideal joint coder is

6.145587828−5.961948275≈0.183639554 bits.6.145587828-5.961948275 \approx0.183639554\text{ bits}.

The last quantity equals H(Z∣Y8)H(Z\mid Y^8): after observing the block, the mode is almost—but not perfectly—recovered. A hyperprior helps a limited factorization, but it cannot beat ideal modeling of the full joint distribution.

Exercise 2. Gaussian noising and I–MMSE

Let

X∼N(0,2),Yγ=γX+N.X\sim\mathcal N(0,2), \qquad Y_\gamma=\sqrt\gamma X+N.
  1. Find I(X;Yγ)I(X;Y_\gamma).

  2. Find mmse⁡(γ)\operatorname{mmse}(\gamma).

  3. Verify I–MMSE.

  4. For Xt=0.8X+0.6εX_t=0.8X+0.6\varepsilon, derive the posterior mean and score.

Show answer and solution

Let σX2=2\sigma_X^2=2. For the Gaussian channel

Yγ=γX+NY_\gamma=\sqrt\gamma X+N

mutual information in nats is

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

The posterior variance, and hence the MMSE, is

mmse⁡(γ)=21+2γ.\boxed{ \operatorname{mmse}(\gamma) = \frac{2}{1+2\gamma} }.

Differentiating,

dIdγ=11+2γ=12mmse⁡(γ).\frac{dI}{d\gamma} = \frac{1}{1+2\gamma} = \frac12\operatorname{mmse}(\gamma).

For

Xt=0.8X+0.6εX_t=0.8X+0.6\varepsilon

the observation variance is

Var⁡(Xt)=0.82⋅2+0.62=1.64.\operatorname{Var}(X_t) =0.8^2\cdot2+0.6^2 =1.64.

The posterior mean is

E[X∣Xt=xt]=0.8⋅21.64xt=4041xt≈0.97561xt.\mathbb E[X\mid X_t=x_t] = \frac{0.8\cdot2}{1.64}x_t = \frac{40}{41}x_t \approx0.97561x_t.

The score of the marginal XtX_t is

∇xtlog⁡pt(xt)=−xt1.64=−2541xt.\boxed{ \nabla_{x_t}\log p_t(x_t) =-\frac{x_t}{1.64} =-\frac{25}{41}x_t }.

Checking the denoiser–score identity:

0.8 E[X∣xt]−xt0.62=−xt1.64.\frac{0.8\,\mathbb E[X\mid x_t]-x_t}{0.6^2} = -\frac{x_t}{1.64}.

Exercise 3. Perceptual quality at zero rate

Let X∼Bernoulli⁡(0.1)X\sim\operatorname{Bernoulli}(0.1) and use Hamming distortion.

Compare two zero-rate decoders:

  1. always output X^=0\widehat X=0;

  2. independently sample X^∼Bernoulli⁡(0.1)\widehat X\sim\operatorname{Bernoulli}(0.1).

For each decoder, compute:

  • expected distortion;
  • the reconstruction marginal;
  • whether perfect perception PX^=PXP_{\widehat X}=P_X holds.

Explain the cost of the perception constraint.

Show answer and solution

Decoder 1: always output X^=0\widehat X=0

The Hamming distance is one only when X=1X=1, so

D1=P(X=1)=0.1.D_1=P(X=1)=0.1.

But

PX^=δ0≠Bernoulli⁡(0.1),P_{\widehat X}=\delta_0 \ne \operatorname{Bernoulli}(0.1),

so perfect perception does not hold.

Decoder 2: an independent sample from the correct marginal

Now

X^∼Bernoulli⁡(0.1)\widehat X\sim\operatorname{Bernoulli}(0.1)

and

PX^=PX.P_{\widehat X}=P_X.

Perfect perception holds, but independence gives

D2=P(X≠X^)=P(X=1,X^=0)+P(X=0,X^=1)=0.1⋅0.9+0.9⋅0.1=0.18.\begin{aligned} D_2 &=P(X\ne\widehat X)\\ &=P(X=1,\widehat X=0) +P(X=0,\widehat X=1)\\ &=0.1\cdot0.9+0.9\cdot0.1\\ &=0.18. \end{aligned}

In this toy example the perceptual constraint costs

0.18−0.1=0.080.18-0.1=0.08

of additional expected distortion.

Exercise 4. What the InfoNCE bound means

For batch size K=1024K=1024, the expected InfoNCE loss is 2.12.1 nats.

  1. Compute log⁡K−L\log K-\mathcal L.

  2. Why is this not the exact MI?

  3. Why does increasing KK not guarantee better downstream performance?

  4. Why does the formula not make CLIP a strict Information Bottleneck method?

Show answer and solution
log⁡K=log⁡1024=10log⁡2≈6.931471806.\log K = \log1024 =10\log2 \approx6.931471806.

Therefore

I(ZI;ZT)≥6.931471806−2.1≈4.831471806 nats.\boxed{ I(Z_I;Z_T) \ge 6.931471806-2.1 \approx4.831471806\text{ nats} }.

That is about

4.831471806ln⁡2≈6.970 bits.\frac{4.831471806}{\ln2} \approx6.970\text{ bits}.

The bound is not the true MI, because:

  • the critic class may be limited;
  • the negatives must follow the assumed sampling scheme;
  • the empirical batch loss is a random estimate of the expected objective;
  • the lower bound saturates from above at log⁡K\log K.

Increasing KK raises the ceiling of the bound and the number of negatives, but does not guarantee a better representation. False negatives are possible, and optimization cost and the geometry of the problem change with KK.

CLIP is not a strict IB objective, because the loss carries no explicit penalty on I(XI;ZI)I(X_I;Z_I): the model does not minimize the amount of information retained by the visual encoder.

Exercise 5. An augmentation destroys target information

Let the image be represented by

X=(Y,N),X=(Y,N),

where Y∈{0,1}Y\in\{0,1\} is a color label and NN is an independent shape. A grayscale augmentation removes YY completely.

  1. Show that I(Y;V)=0I(Y;V)=0.

  2. Prove that for every encoder Z=f(V)Z=f(V),

    I(Y;Z)=0.I(Y;Z)=0.
  3. Explain why an overly strong invariance can hurt transfer to a downstream task.

Show answer and solution

By construction the grayscale view VV is completely independent of the color label YY. Hence

PY,V=PYPVP_{Y,V}=P_YP_V

and

I(Y;V)=0.I(Y;V)=0.

For any encoder Z=f(V)Z=f(V) we have the Markov chain

Y⟶V⟶Z.Y\longrightarrow V\longrightarrow Z.

By the data-processing inequality,

I(Y;Z)≤I(Y;V)=0.I(Y;Z) \le I(Y;V) =0.

Therefore

I(Y;Z)=0.\boxed{I(Y;Z)=0}.

A strong invariance is useful only when the removed attribute is genuinely a nuisance factor for the future task. If the target task needs color, converting to grayscale destroys the necessary signal before the encoder ever sees it.

Exercise 6. The actual rate of a quantized model

A model uses group-wise INT4 with full groups of 128 weights and one 16-bit scale per group.

  1. Find the effective bits per original weight, ignoring alignment.

  2. Compare with dense INT8.

  3. Consider 50% unstructured sparsity, where every nonzero weight is stored as INT8 plus a 16-bit index. What is the rate per original weight?

  4. Why does nominal sparsity not guarantee a smaller file or lower latency?

Show answer and solution

A group of 128 weights stores:

  • 128⋅4=512128\cdot4=512 bits for the quantized values;
  • 16 bits for the scale.

The effective width is

4+16128=4.125 bits per original weight.\boxed{ 4+\frac{16}{128} =4.125 \text{ bits per original weight} }.

Dense INT8 uses 8 bits per weight, so grouped INT4 is about

84.125≈1.94\frac{8}{4.125} \approx1.94

times more compact before alignment and headers are taken into account.

With 50% unstructured sparsity, every nonzero weight costs

8+16=24 bits.8+16=24\text{ bits}.

Per original weight this is

0.5⋅24=12 bits.0.5\cdot24=12\text{ bits}.

That sparse format is therefore larger than dense INT8. Irregular memory access and the absence of suitable kernels may also prevent any speedup even when the file is smaller.

Exercise 7. Python: rate accounting, denoising, and quantization

Run

text
M14_cv_information_experiments.py

The script:

  1. compares factorized, hierarchical, and ideal-joint rates in a toy hyperprior model;

  2. numerically checks Gaussian I–MMSE;

  3. builds a rate–distortion curve for group-wise symmetric weight quantization, using groups of 128, per-group p99.9 clipping, and one FP16 scale per group.

Explain why weight MSE in the third experiment remains only a surrogate for downstream distortion.

Show answer and solution

The key expected values are

text
factorized_rate_bits: 6.788385
hierarchical_rate_bits: 6.145588
ideal_joint_entropy_bits: 5.961948
hierarchical_overhead_vs_joint: 0.183640

The finite-difference error for I–MMSE should be small, and the analytical score identity should hold to numerical precision.

The quantization experiment uses the same scheme for both axes of the curve: groups of 128 weights, p99.9 clipping inside each group, and one stored FP16 scale per group. With the fixed random seed, the expected values are:

payloadeffective bits/weightnormalized MSE
INT22.1250.554104
INT33.1250.173767
INT44.1250.046102
INT66.1250.003975
INT88.1250.001815

In the quantization experiment, normalized weight MSE usually grows as the width decreases. This is only a local surrogate for distortion. Two layers with equal weight MSE can affect logits, calibration, and downstream accuracy differently; a task-aware conclusion requires running the actual model.

Code solution · the same source file for RU/EN

PythonM14_cv_information_experiments.pyDownload .py
from __future__ import annotations

from itertools import product
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)


def binary_entropy(p: float) -> float:
    if p <= 0.0 or p >= 1.0:
        return 0.0
    return -p * math.log2(p) - (1.0 - p) * math.log2(1.0 - p)


def hyperprior_experiment() -> dict[str, float]:
    n = 8
    p0 = 0.05
    p1 = 0.50
    p_marg = 0.5 * (p0 + p1)

    factorized = n * binary_entropy(p_marg)
    conditional_one = 0.5 * binary_entropy(p0) + 0.5 * binary_entropy(p1)
    hierarchical = 1.0 + n * conditional_one

    probs: list[float] = []
    for bits in product((0, 1), repeat=n):
        k = sum(bits)
        prob_z0 = p0**k * (1.0 - p0) ** (n - k)
        prob_z1 = p1**k * (1.0 - p1) ** (n - k)
        probs.append(0.5 * prob_z0 + 0.5 * prob_z1)

    ideal_joint = -sum(p * math.log2(p) for p in probs if p > 0.0)

    labels = ["Factorized", "Hierarchical", "Ideal joint"]
    values = [factorized, hierarchical, ideal_joint]
    fig, ax = plt.subplots(figsize=(7.6, 4.8))
    ax.bar(labels, values)
    ax.set_ylabel("Bits per 8-symbol block")
    ax.set_title("Toy hyperprior: model factorization versus ideal joint coding")
    for i, value in enumerate(values):
        ax.text(i, value + 0.04, f"{value:.3f}", ha="center", va="bottom")
    ax.set_ylim(0.0, max(values) + 0.7)
    fig.tight_layout()
    fig.savefig(OUT / "M14_hyperprior_rates.png", dpi=180, bbox_inches="tight", pad_inches=0.08)
    plt.close(fig)

    return {
        "factorized_rate_bits": factorized,
        "hierarchical_rate_bits": hierarchical,
        "ideal_joint_entropy_bits": ideal_joint,
        "hierarchical_overhead_vs_joint": hierarchical - ideal_joint,
        "saving_vs_factorized": factorized - hierarchical,
    }


def gaussian_immse_experiment() -> dict[str, float]:
    variance = 2.0
    gamma = np.linspace(0.0, 12.0, 2001)
    mutual_information = 0.5 * np.log1p(variance * gamma)
    mmse = variance / (1.0 + variance * gamma)

    derivative_numeric = np.diff(mutual_information) / np.diff(gamma)
    gamma_mid = 0.5 * (gamma[:-1] + gamma[1:])
    derivative_theory = 0.5 * variance / (1.0 + variance * gamma_mid)
    max_error = float(np.max(np.abs(derivative_numeric - derivative_theory)))

    fig, ax = plt.subplots(figsize=(7.8, 4.8))
    ax.plot(gamma, mutual_information, label=r"$I(X;Y_\gamma)$, nats")
    ax.plot(gamma, 0.5 * mmse, label=r"$\frac{1}{2}\,\mathrm{mmse}(\gamma)$")
    ax.set_xlabel("SNR $\\gamma$")
    ax.set_ylabel("Value")
    ax.set_title("Gaussian channel: mutual information and the I–MMSE derivative")
    ax.legend()
    fig.tight_layout()
    fig.savefig(OUT / "M14_gaussian_immse.png", dpi=180, bbox_inches="tight", pad_inches=0.08)
    plt.close(fig)

    alpha = 0.8
    sigma = 0.6
    var_xt = alpha**2 * variance + sigma**2
    posterior_coefficient = alpha * variance / var_xt
    x_grid = np.linspace(-4.0, 4.0, 1001)
    posterior_mean = posterior_coefficient * x_grid
    score_from_denoiser = (alpha * posterior_mean - x_grid) / sigma**2
    score_direct = -x_grid / var_xt
    score_max_error = float(np.max(np.abs(score_from_denoiser - score_direct)))

    return {
        "immse_finite_difference_max_error": max_error,
        "score_identity_max_error": score_max_error,
        "posterior_mean_coefficient": posterior_coefficient,
        "xt_variance": var_xt,
    }


def groupwise_symmetric_quantize(
    weights: np.ndarray,
    bits: int,
    group_size: int = 128,
    clip_percentile: float = 99.9,
) -> tuple[np.ndarray, np.ndarray]:
    """Quantize each group with its own stored FP16 scale.

    The payload uses signed symmetric integer levels ``[-qmax, qmax]``.  Each
    group is clipped independently and the resulting scale is rounded to FP16
    before both quantization and reconstruction, matching the 16-bit overhead
    charged by the rate calculation.
    """
    if bits < 2:
        raise ValueError("bits must be at least 2 for signed symmetric quantization")
    if group_size <= 0:
        raise ValueError("group_size must be positive")
    if not 0.0 < clip_percentile <= 100.0:
        raise ValueError("clip_percentile must lie in (0, 100]")

    array = np.asarray(weights, dtype=float)
    flat = array.reshape(-1)
    if flat.size == 0:
        return array.copy(), np.empty(0, dtype=float)

    qmax = 2 ** (bits - 1) - 1
    reconstructed = np.empty_like(flat)
    n_groups = math.ceil(flat.size / group_size)
    scales = np.empty(n_groups, dtype=float)
    smallest_fp16 = float(np.finfo(np.float16).smallest_subnormal)

    for group_index, start in enumerate(range(0, flat.size, group_size)):
        group = flat[start:start + group_size]
        clip = float(np.percentile(np.abs(group), clip_percentile))
        if clip == 0.0:
            scale = 1.0
        else:
            scale = float(np.float16(clip / qmax))
            if scale == 0.0:
                scale = smallest_fp16

        q = np.clip(np.rint(group / scale), -qmax, qmax)
        reconstructed[start:start + group.size] = q * scale
        scales[group_index] = scale

    return reconstructed.reshape(array.shape), scales


def quantization_experiment() -> list[dict[str, float]]:
    rng = np.random.default_rng(14)
    group_size = 128
    n_groups = 1_563
    n = n_groups * group_size
    weights = rng.normal(0.0, 1.0, size=n)
    outlier_mask = rng.random(n) < 0.005
    weights[outlier_mask] += rng.normal(0.0, 8.0, size=int(outlier_mask.sum()))

    variance = float(np.var(weights))
    scale_bits = 16
    clip_percentile = 99.9
    rows: list[dict[str, float]] = []

    for bits in (2, 3, 4, 6, 8):
        reconstructed, scales = groupwise_symmetric_quantize(
            weights,
            bits,
            group_size=group_size,
            clip_percentile=clip_percentile,
        )
        if scales.size != n_groups:
            raise RuntimeError("unexpected number of quantization groups")
        mse = float(np.mean((weights - reconstructed) ** 2))
        effective_bits = bits + scale_bits / group_size
        rows.append(
            {
                "bits": float(bits),
                "effective_bits_per_weight": effective_bits,
                "mse": mse,
                "nmse": mse / variance,
                "mean_fp16_scale": float(np.mean(scales)),
            }
        )

    fig, ax = plt.subplots(figsize=(7.6, 4.8))
    ax.plot(
        [row["effective_bits_per_weight"] for row in rows],
        [row["nmse"] for row in rows],
        marker="o",
    )
    for row in rows:
        ax.annotate(
            f"INT{int(row['bits'])}",
            (row["effective_bits_per_weight"], row["nmse"]),
            xytext=(4, 5),
            textcoords="offset points",
        )
    ax.set_xlabel("Effective bits per weight (including scale overhead)")
    ax.set_ylabel("Normalized weight MSE")
    ax.set_yscale("log")
    ax.set_title("Group-wise weight quantization: a local rate–distortion curve")
    fig.tight_layout()
    fig.savefig(OUT / "M14_weight_quantization_rd.png", dpi=180, bbox_inches="tight", pad_inches=0.08)
    plt.close(fig)

    return rows


def main() -> None:
    print("=== Hyperprior rate accounting ===")
    hyper = hyperprior_experiment()
    for key, value in hyper.items():
        print(f"{key}: {value:.6f}")

    print("\n=== Gaussian I-MMSE ===")
    immse = gaussian_immse_experiment()
    for key, value in immse.items():
        print(f"{key}: {value:.10f}")

    print("\n=== Group-wise symmetric weight quantization ===")
    rows = quantization_experiment()
    print("scheme: group_size=128, per-group p99.9 clipping, one FP16 scale per group")
    print("bits  effective_bpw   nmse         mse")
    for row in rows:
        print(
            f"{int(row['bits']):>4d}  "
            f"{row['effective_bits_per_weight']:>13.6f}  "
            f"{row['nmse']:>11.8f}  "
            f"{row['mse']:>11.8f}"
        )


if __name__ == "__main__":
    main()