Module 5. Mutual information: how much one object tells us about another
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 KBThe archive preserves the folder structure of nine Python solution files shared by RU/EN.
Exercise 1. Nonlinear dependence without correlation
Let be uniform on and let .
-
Compute , , and .
-
Compute and in bits.
-
Explain why the example does not contradict .
Show answer and solution
Let
By symmetry,
Also,
Because ,
Therefore,
At the same time, takes values and with probabilities , so
Since is a deterministic discrete function of ,
and
There is no contradiction. It is the covariance, not MI, that is zero. The variables are clearly dependent: knowing determines . Hence , as expected.
Exercise 2. XOR and failure of univariate feature selection
Let and .
-
Show that .
-
Compute and .
-
Explain the failure mode of ranking features by .
Show answer and solution
Let and be independent fair bits and let
Fix . Since remains a fair bit, is also uniform on . Thus the distribution of does not depend on the value of :
Therefore,
By symmetry,
Yet the pair determines , and is a fair bit. Hence
and
Once is fixed, determines and vice versa. Therefore
and
Individual ranking by assigns zero relevance to both features and may remove them. The failure is the neglect of synergy: information appears only in the feature combination.
Exercise 3. Compressing nuisance without losing target information
Let and be independent fair bits and let the input be .
Consider
-
Compute , , , and .
-
Which representation removes the nuisance factor without losing target information?
-
Why should useful compression reduce rather than deliberately reduce ?
Show answer and solution
We have independent fair bits and , input
and representations
Because is contained in without distortion,
For ,
For ,
is the output of a binary symmetric channel with crossover probability . With a uniform input,
Thus,
It is that removes nuisance without losing target information. The amount of retained input information makes the distinction explicit:
whereas
Good task-specific compression tries to keep close to while reducing the unnecessary part of . Intentionally reducing discards information useful for predicting the target.
Exercise 4. Python: correlation, an MI estimator, and a permutation baseline
Generate three datasets:
where are independent.
For several sample sizes:
-
compute Pearson correlation;
-
estimate MI with a -nearest-neighbor method;
-
randomly permute to obtain a baseline;
-
repeat the experiment for several seeds.
The goal is to observe nonlinear dependence at near-zero correlation, a nonzero finite-sample baseline, and instability when the sample is small.
Show answer and solution
Here is one reproducible experiment. mutual_info_regression uses a kNN estimator and returns an estimate in nats. The library clips negative numerical estimates to zero, so the permutation baseline usually has a small positive mean.
import numpy as np
import pandas as pd
from sklearn.feature_selection import mutual_info_regression
def estimate_once(n: int, seed: int) -> list[dict[str, float | int | str]]:
rng = np.random.default_rng(seed)
x = rng.normal(size=n)
datasets = {
"linear": x + 0.5 * rng.normal(size=n),
"nonlinear": x**2 + 0.3 * rng.normal(size=n),
"independent": rng.normal(size=n),
}
rows = []
for name, y in datasets.items():
corr = float(np.corrcoef(x, y)[0, 1])
mi = float(
mutual_info_regression(
x.reshape(-1, 1),
y,
n_neighbors=5,
random_state=seed,
)[0]
)
y_perm = rng.permutation(y)
mi_perm = float(
mutual_info_regression(
x.reshape(-1, 1),
y_perm,
n_neighbors=5,
random_state=seed,
)[0]
)
rows.append(
{
"n": n,
"relation": name,
"corr": corr,
"mi_nats": mi,
"mi_perm_nats": mi_perm,
}
)
return rows
records = []
for n in (200, 1000, 5000):
for seed in range(30):
records.extend(estimate_once(n, seed))
df = pd.DataFrame(records)
summary = (
df.groupby(["n", "relation"])
.agg(
corr_mean=("corr", "mean"),
corr_sd=("corr", "std"),
mi_mean=("mi_nats", "mean"),
mi_sd=("mi_nats", "std"),
perm_mean=("mi_perm_nats", "mean"),
perm_sd=("mi_perm_nats", "std"),
)
.reset_index()
)
with pd.option_context("display.max_rows", None, "display.width", 160):
print(summary.round(4).to_string(index=False))For this seed protocol, the output is:
n relation corr_mean corr_sd mi_mean mi_sd perm_mean perm_sd
200 independent 0.0057 0.0686 0.0177 0.0287 0.0141 0.0204
200 linear 0.8934 0.0140 0.8143 0.0761 0.0070 0.0132
200 nonlinear -0.0556 0.1287 0.9368 0.0804 0.0151 0.0255
1000 independent 0.0041 0.0269 0.0060 0.0097 0.0045 0.0067
1000 linear 0.8950 0.0062 0.8235 0.0338 0.0086 0.0096
1000 nonlinear -0.0279 0.0688 1.0913 0.0392 0.0052 0.0076
5000 independent 0.0000 0.0131 0.0037 0.0050 0.0048 0.0052
5000 linear 0.8949 0.0024 0.8127 0.0127 0.0030 0.0050
5000 nonlinear 0.0011 0.0284 1.1378 0.0119 0.0013 0.0030The main observations are:
-
For linear dependence, both correlation and MI are large.
-
For , mean correlation tends to zero while MI remains large.
-
For independent data, the finite-sample MI estimate need not be exactly zero.
-
As grows, variability decreases and the permutation baseline approaches zero.
-
The absolute value depends on the estimator,
n_neighbors, scaling, and sample size; the comparative pattern matters more here.
Exercise 5. Python: the InfoNCE bound on a known channel
Let be uniform on classes. Let with probability and otherwise choose uniformly among the remaining classes.
- Derive the exact MI:
- Use the ideal score
and estimate the InfoNCE loss by Monte Carlo for .
-
Compare with the exact MI.
-
Verify that limits this bound, not MI itself or representation quality.
Mathematical deepening. Fano's inequality
If takes values and is constructed from , then
where . Use the inequality to connect residual uncertainty with an information-theoretic limit on achievable error. Explain separately why the bound may be loose and does not replace construction of the Bayes-optimal classifier.
Show answer and solution
Let be uniform over classes. The channel leaves the class unchanged with probability and, with probability , chooses uniformly among the other classes.
By symmetry, is also uniform, so
The conditional distribution has one probability and probabilities . Therefore,
and
The code below uses natural logarithms. Negatives are sampled independently from the marginal , and the critic is the ideal log-density ratio.
You can also test or : entropy uses , and an impossible pair receives a critic score of . The positive pair is generated by the channel itself, so its score stays finite and the InfoNCE denominator stays positive.
import math
import numpy as np
from scipy.special import logsumexp
K = 64
epsilon = 0.25
rng = np.random.default_rng(20260712)
if K < 2 or not 0.0 <= epsilon <= 1.0:
raise ValueError("Use K >= 2 and 0 <= epsilon <= 1")
# Exact MI in nats for the K-class symmetric channel.
h_eps = -sum(p * math.log(p) for p in (epsilon, 1.0 - epsilon) if p > 0.0)
mi_exact = math.log(K) - h_eps - epsilon * math.log(K - 1)
def sample_positive(x: np.ndarray) -> np.ndarray:
"""Sample Y from p(y|x) without rejection loops."""
y = x.copy()
flip = rng.random(x.size) < epsilon
offsets = rng.integers(1, K, size=flip.sum())
y[flip] = (x[flip] + offsets) % K
return y
def estimate_infonce(n_candidates: int, total_trials: int = 100_000, chunk_size: int = 2_000) -> tuple[float, float]:
loss_sum = 0.0
loss_sq_sum = 0.0
seen = 0
log_ratio_match = math.log(K * (1.0 - epsilon)) if epsilon < 1.0 else -math.inf
log_ratio_mismatch = math.log(K * epsilon / (K - 1)) if epsilon > 0.0 else -math.inf
while seen < total_trials:
batch = min(chunk_size, total_trials - seen)
x = rng.integers(0, K, size=batch)
positive = sample_positive(x)
negatives = rng.integers(0, K, size=(batch, n_candidates - 1))
candidates = np.concatenate([positive[:, None], negatives], axis=1)
scores = np.where(
candidates == x[:, None],
log_ratio_match,
log_ratio_mismatch,
)
losses = logsumexp(scores, axis=1) - scores[:, 0]
loss_sum += float(losses.sum())
loss_sq_sum += float(np.square(losses).sum())
seen += batch
mean_loss = loss_sum / total_trials
variance = max(loss_sq_sum / total_trials - mean_loss**2, 0.0)
standard_error = math.sqrt(variance / total_trials)
return mean_loss, standard_error
print(f"Exact MI: {mi_exact:.6f} nats ({mi_exact / math.log(2):.6f} bits)")
print(" N loss bound log(N) SE(bound)")
for n_candidates in (4, 16, 64, 256):
loss, se = estimate_infonce(n_candidates)
bound = math.log(n_candidates) - loss
print(
f"{n_candidates:3d} {loss:9.6f} {bound:9.6f} "
f"{math.log(n_candidates):9.6f} {se:9.6f}"
)Output:
Exact MI: 2.560764 nats (3.694402 bits)
N loss bound log(N) SE(bound)
4 0.429934 0.956360 1.386294 0.002479
16 0.991886 1.780702 2.772589 0.004612
64 1.867571 2.291312 4.158883 0.006330
256 3.061057 2.484121 5.545177 0.007008In this run, all Monte Carlo estimates of the lower bound lie below exact MI. The theoretical guarantee applies to the population bound; its finite-sample estimate can fluctuate upward, which is why the standard error is reported. Increasing tightens the bound, but for every fixed ,
The important distinction is that this is a ceiling on the chosen lower bound, not on true . In the example, for ,
so the InfoNCE bound cannot reach the true value even with an ideal critic.
Mathematical deepening. Fano's inequality
From
we see that if substantial uncertainty about remains after observing , the error probability cannot be arbitrarily small. For example, when is uniform,
The smaller is relative to , the more uncertainty a classifier would have to remove without available signal—which is impossible.
Fano usually provides a lower bound, not the exact Bayes error. It may be loose and does not tell us which model or training procedure attains the optimum.