Bayesian Linear Head (Test-Time) Example¶
This guide demonstrates how to use the conjugate BayesianLinearHead and its online recursive counter-part RecursiveBayesianHead for rapid test-time adaptation of deep network representation layers.
→ API: Test-time (BayesianLinearHead, RecursiveBayesianHead, SupportsPredictiveBatch).
Run the script examples/test_time_bayesian_linear_head_demo.py (--mode all by default runs both):
pixi run python examples/test_time_bayesian_linear_head_demo.py --mode exactness # batch vs recursive posterior, coverage
pixi run python examples/test_time_bayesian_linear_head_demo.py --mode adapter # SupportsPredictiveBatch adapter
| Mode | What it shows | Default sizes |
|---|---|---|
exactness |
Batch fit vs two-step RecursiveBayesianHead (forgetting_factor=1), weight error, 95% interval coverage |
--n-train 200 --n-test 500 --dim 5 --noise 0.35 |
adapter |
Predictive adapter: wraps the head as a SupportsPredictiveBatch returning PredictiveBatch |
--n-train 96 --n-test 24 --dim 4 |
| # | Reference |
|---|---|
| 1 | Bishop, C. M. (2006). Pattern Recognition and Machine Learning. Springer. (Chapter 3: Linear Models for Regression). |
Mathematical Formulation¶
In feature-based regression, a deep network maps inputs to a latent representation \(\phi(x) \in \mathbb{R}^D\), and a linear head predicts the target \(y = \phi(x)^\top w\).
Bayesian Linear Regression (BLR) treats the weights \(w\) as a random variable. Under a conjugate Gaussian prior and Gaussian likelihood, the posterior distribution can be computed in closed form.
1. Prior¶
We assume a Gaussian prior over the weights:
where \(\alpha\) is the prior_precision.
2. Likelihood¶
We assume a Gaussian noise model with variance \(\sigma_{\text{noise}}^2\):
3. Batch Posterior Update¶
Given a batch of features \(\Phi \in \mathbb{R}^{N \times D}\) and targets \(y \in \mathbb{R}^N\), the posterior distribution \(p(w \mid \Phi, y) = \mathcal{N}(m_N, S_N)\) has:
4. Recursive Update with Forgetting Factor¶
In non-stationary environments, we can decay past information using a forgetting factor \(\gamma \in (0, 1]\):
When \(\gamma = 1.0\), the recursive update matches the batch posterior exactly.
5. Predictive Distribution¶
For a new input feature \(\phi(x_*)\), the predictive distribution is:
Task-First Context¶
- When to Use: Use these heads when you need fast adaptation at test time (e.g. streaming data, domain shift) and want exact, closed-form updates instead of gradient descent.
- Comparison Notes: Ensure that recursive updates (using
partial_fit) match the batch updates when the forgetting factor is set to 1.0.
Code Example (--mode exactness)¶
Below is the complete, self-contained code comparing batch and recursive Bayesian linear head updates on synthetic linear data.
import argparse
import torch
from torchregress.test_time import BayesianLinearHead, RecursiveBayesianHead
def main() -> None:
# Setup data parameters
n_train, n_test, d, noise = 200, 500, 5, 0.35
torch.manual_seed(0)
w_true = torch.randn(d)
# Generate synthetic features and targets
x_train = torch.randn(n_train, d)
y_train = (x_train @ w_true).unsqueeze(-1) + noise * torch.randn(n_train, 1)
x_test = torch.randn(n_test, d)
y_test = (x_test @ w_true).unsqueeze(-1) + noise * torch.randn(n_test, 1)
noise_var = noise**2
cfg = dict(
in_features=d,
fit_intercept=False,
prior_precision=1e-2,
noise_variance=noise_var,
)
# 1. Batch adaptation
batch_model = BayesianLinearHead(**cfg).fit(x_train, y_train)
# 2. Recursive adaptation (two steps)
rec_model = RecursiveBayesianHead(**cfg, forgetting_factor=1.0)
mid = n_train // 2
rec_model.partial_fit(x_train[:mid], y_train[:mid])
rec_model.partial_fit(x_train[mid:], y_train[mid:])
# 3. Check mathematical exactness and coverage
max_post_diff = (batch_model.posterior_mean - rec_model.posterior_mean).abs().max().item()
w_err = (batch_model.posterior_mean[0] - w_true).norm().item()
print("max |posterior_mean_batch - posterior_mean_recursive|:", round(max_post_diff, 8))
print("||posterior_mean - w_true||_2:", round(w_err, 4))
# Evaluate predictive intervals on held-out test points
pred = batch_model.predict(x_test, return_std=True, include_noise=True)
mean, std = pred["mean"], pred["std"]
z = (y_test - mean) / std.clamp(min=1e-8)
coverage_95 = ((z.abs() <= 1.96).float().mean()).item()
print("empirical 95% Gaussian interval coverage (held-out):", round(coverage_95, 3))
if __name__ == "__main__":
main()
Predictive Adapter (SupportsPredictiveBatch)¶
This guide explains how to wrap a BayesianLinearHead with a lightweight adapter class to satisfy the SupportsPredictiveBatch protocol, enabling seamless integration with downstream adaptation pipelines.
The SupportsPredictiveBatch Protocol¶
Downstream test-time adaptation and uncertainty evaluation algorithms in torchregress often interact with models using a standardized protocol. Rather than requiring models to inherit from a specific base class, the library defines runtime protocols.
A key protocol is SupportsPredictiveBatch, which requires the model to implement a predict_distribution method:
class SupportsPredictiveBatch(Protocol):
def predict_distribution(self, X: np.ndarray, **kwargs: Any) -> PredictiveBatch:
...
The returned PredictiveBatch is a container that bundles:
1. mean: The predictive mean tensor (point predictions).
2. std: The predictive standard deviation (total uncertainty).
3. extra: A dictionary containing auxiliary predictive outputs (e.g. epistemic and aleatoric variances, log-variances, or quantiles).
Adapter Mathematical Mapping¶
The BLRPredictiveAdapter wraps a trained BayesianLinearHead and routes calls to predictive_batch, returning a PredictiveBatch computed using closed-form BLR predictive moments:
Code Example (--mode adapter)¶
Below is the complete, self-contained code demonstrating how to construct and use this adapter wrapper.
import argparse
import numpy as np
from torchregress.prediction import PredictiveBatch
from torchregress.test_time import BayesianLinearHead, SupportsPredictiveBatch
class BLRPredictiveAdapter:
"""Minimal adapter implementing SupportsPredictiveBatch."""
def __init__(self, head: BayesianLinearHead) -> None:
self.head = head
def predict_distribution(self, X: np.ndarray, **kwargs: object) -> PredictiveBatch:
include_noise = bool(kwargs.get("include_noise", True))
return self.head.predictive_batch(X, include_noise=include_noise)
def _make_toy(n: int, d: int, seed: int) -> tuple[np.ndarray, np.ndarray]:
rng = np.random.default_rng(seed)
x = rng.normal(size=(n, d)).astype(np.float32)
w = rng.normal(size=(d, 1)).astype(np.float32)
y = x @ w + 0.15 * rng.normal(size=(n, 1)).astype(np.float32)
return x, y
def main() -> None:
# Setup data
n_train, n_test, d, seed = 96, 24, 4, 0
x_train, y_train = _make_toy(n_train, d, seed)
x_test, y_test = _make_toy(n_test, d, seed + 1)
# Train Bayesian Linear Head
head = BayesianLinearHead(
in_features=d,
out_features=1,
fit_intercept=True,
prior_precision=1.0,
noise_variance=0.2**2,
).fit(x_train, y_train)
# Wrap in adapter
adapter = BLRPredictiveAdapter(head)
# Assert at runtime that the adapter conforms to the protocol
assert isinstance(adapter, SupportsPredictiveBatch)
# Generate predictions via the adapter
pb = adapter.predict_distribution(x_test, include_noise=True)
rmse = float(np.sqrt(np.mean((pb.mean.detach().cpu().numpy() - y_test) ** 2)))
mean_std = float(pb.std.detach().mean().item())
print(f"BLR adapter RMSE={rmse:.4f}, mean predictive std={mean_std:.4f}")
print("PredictiveBatch extra keys:", sorted(pb.extra.keys()))
if __name__ == "__main__":
main()