Add a readout head¶
Note
The contracts on this page are proposed until the task registry ships. The signatures match Tasks; once src/emma/ lands the subclass below runs as written.
Subclass ReadoutHead to map frozen-backbone representations to a new task’s outputs. The head is the only trainable part of the pipeline; the backbone weights stay frozen. This is the SUPERB (Speech processing Universal PERformance Benchmark) contract: one frozen backbone, many lightweight readouts, so the score reflects representation quality, not head capacity.
Goal¶
Implement a custom readout head for a task whose output shape the built-in heads do not cover, wire it into a TaskSpec, and confirm it runs under the OOD (out-of-distribution) protocol.
Prerequisites¶
You have a TaskSpec whose output shape is decided (regression vector, class distribution, ranked list, or text).
You have a backbone that satisfies the FrozenBackbone protocol, so encode returns a tensor of known dimension.
You have read Add a task; a head is one step of a task proposal.
Steps¶
Subclass ReadoutHead. Implement the two abstract methods:
build, which initializes head parameters from the backbone’s output dimension, andforward, which maps representations to task outputs.import torch from torch import nn from emma.tasks import ReadoutHead, FrozenBackbone class AzimuthRegressionHead(ReadoutHead): """Regression head for an angle-of-arrival (AoA) task. Predicts a continuous azimuth; the backbone is frozen. """ def __init__(self, hidden_dim: int = 256) -> None: self.hidden_dim = hidden_dim def build(self, backbone: FrozenBackbone) -> None: # Probe the backbone once to read its output dimension. The probe # matches the scene input shape (num_rx, 2, N); the recipe's # ArraySpec fixes num_rx, and N is the capture length. probe = torch.zeros(8, 2, 1024) out_dim = backbone.encode(probe).shape[-1] self.net = nn.Sequential( nn.Linear(out_dim, self.hidden_dim), nn.ReLU(), nn.Linear(self.hidden_dim, 1), ) def forward(self, representations): return self.net(representations)
AoA is angle-of-arrival. The head owns its parameters; the backbone does not.
Respect the frozen-backbone contract. The head must not mutate backbone weights. The Evaluator shares one backbone instance across every task and re-fits the head per OOD fold, so the head’s parameters are constructed fresh in
buildfor each fold.Keep the head lightweight. The backbone carries the representational load; a head with more capacity than the task needs inflates the score without measuring representation quality. Prefer a small MLP (multilayer perceptron) over a deep network.
Wire the head into the TaskSpec. The spec references the head class, the Metric, and the OOD axis; the harness instantiates the head through the registry.
Run the head under the protocol to confirm it produces outputs the metric accepts.
$ emma eval --task E-LOC-AOA --model ./checkpoints/my-fm --scenes ./scenes --out ./runs/head-check
Expected result¶
The head builds from any FrozenBackbone, produces outputs the task metric consumes, and trains only its own parameters. The eval run completes with a metric value, confirming the head is wired correctly.
See Also¶
Tasks: the ReadoutHead ABC and the built-in head subclasses.
Add a task: the task proposal a head is part of.
OOD protocol: the fold mechanics that re-fit the head.
Signal as a modality: why the frozen-backbone contract is the score.
CLI reference: the
emma evalcommand used to check the head.