Skip to content

Prototypes

Compact representations of a support set — statistical summaries and a learnable encoder — that condition the weight-generating teacher.

polyweave.prototypes

Prototype builders — compact support-set representations fed to a teacher.

A prototype is the teacher's only view of a few-shot support set. Every builder returns a tensor shaped [1, channels, H, W] (a batch of one "image" with channels statistic maps) that a convolutional teacher consumes.

Two flavours are provided:

  • Statistical (parameter-free), matching the paper:

    • :func:feature_class_stats -> [1, 4, num_classes, feature_dim] (per-class mean/variance/kurtosis/contrast of student features; Exp 1)
    • :func:image_grid_stats -> [1, 4, num_classes, grid^2 * in_ch] (class-conditional stats over a spatial grid of raw inputs; Exp 2)
    • :func:relation_cross_moments -> [1, 4, d_model, d_model] (embedding-space query/key cross-moments; Exp 3)
  • Learnable (:class:LearnablePrototypeEncoder): a trainable encoder that maps support features to a prototype, for real-world settings where hand-built statistics are too rigid. Output shape matches the statistical feature builder so it is a drop-in replacement for the teacher.

LearnablePrototypeEncoder

LearnablePrototypeEncoder(in_dim: int, num_classes: int, out_channels: int = 4, embed_dim: Optional[int] = None, hidden_dim: Optional[int] = None, normalize: bool = True)

Bases: Module

Encode a support set into a learned prototype tensor.

Each support example's feature vector is passed through a small MLP that emits out_channels * embed_dim values, reshaped to [out_channels, embed_dim] and mean-pooled within each class. Empty classes yield zeros.

Parameters:

Name Type Description Default
in_dim int

dimensionality of the input support features.

required
num_classes int

number of classes (rows of the prototype).

required
out_channels int

number of statistic channels to emit (match the teacher's proto_channels; default 4).

4
embed_dim Optional[int]

width of each channel's per-class vector. Defaults to in_dim so the prototype lines up with the statistical builder.

None
hidden_dim Optional[int]

hidden width of the encoder MLP. Defaults to in_dim.

None
normalize bool

apply per-channel normalisation to the pooled prototype (default True), matching the statistical builders.

True
Source code in polyweave/prototypes/learnable.py
def __init__(
    self,
    in_dim: int,
    num_classes: int,
    out_channels: int = 4,
    embed_dim: Optional[int] = None,
    hidden_dim: Optional[int] = None,
    normalize: bool = True,
) -> None:
    super().__init__()
    self.in_dim = in_dim
    self.num_classes = num_classes
    self.out_channels = out_channels
    self.embed_dim = embed_dim or in_dim
    hidden_dim = hidden_dim or in_dim
    self.normalize = normalize

    self.encoder = nn.Sequential(
        nn.Linear(in_dim, hidden_dim),
        nn.ReLU(),
        nn.Linear(hidden_dim, out_channels * self.embed_dim),
    )

forward

forward(feats: Tensor, y: Tensor) -> torch.Tensor

Map support features to a prototype.

Parameters:

Name Type Description Default
feats Tensor

support features [N, in_dim].

required
y Tensor

integer labels [N] in [0, num_classes).

required

Returns:

Type Description
Tensor

Prototype [1, out_channels, num_classes, embed_dim].

Source code in polyweave/prototypes/learnable.py
def forward(self, feats: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
    """Map support features to a prototype.

    Args:
        feats: support features ``[N, in_dim]``.
        y: integer labels ``[N]`` in ``[0, num_classes)``.

    Returns:
        Prototype ``[1, out_channels, num_classes, embed_dim]``.
    """
    N = feats.shape[0]
    enc = self.encoder(feats)  # [N, out_channels * embed_dim]
    enc = enc.view(N, self.out_channels, self.embed_dim)  # [N, C, E]

    # Class-conditional mean pool via scatter-add (keeps autograd intact).
    proto = feats.new_zeros(self.num_classes, self.out_channels, self.embed_dim)
    counts = feats.new_zeros(self.num_classes, 1, 1)
    idx = y.view(N, 1, 1).expand(N, self.out_channels, self.embed_dim)
    proto.scatter_add_(0, idx, enc)
    counts.scatter_add_(
        0, y.view(N, 1, 1), torch.ones(N, 1, 1, device=feats.device, dtype=feats.dtype)
    )
    proto = proto / counts.clamp(min=1.0)

    # [K, C, E] -> [1, C, K, E]
    proto = proto.permute(1, 0, 2).unsqueeze(0)
    return normalize_prototype(proto) if self.normalize else proto

feature_class_stats

feature_class_stats(feats: Tensor, y: Tensor, num_classes: int, normalize: bool = True) -> torch.Tensor

Per-class statistics of student features (Experiment 1).

Channels: 0=mean, 1=variance, 2=excess kurtosis, 3=inter-class contrast (per-class absolute deviation from the global feature mean).

Parameters:

Name Type Description Default
feats Tensor

student features [N, feature_dim].

required
y Tensor

integer labels [N].

required
num_classes int

number of classes (rows in the prototype).

required
normalize bool

apply per-channel normalisation (default True).

True

Returns:

Type Description
Tensor

Prototype [1, 4, num_classes, feature_dim].

Source code in polyweave/prototypes/statistical.py
@torch.no_grad()
def feature_class_stats(
    feats: torch.Tensor, y: torch.Tensor, num_classes: int, normalize: bool = True
) -> torch.Tensor:
    """Per-class statistics of *student features* (Experiment 1).

    Channels: 0=mean, 1=variance, 2=excess kurtosis, 3=inter-class contrast
    (per-class absolute deviation from the global feature mean).

    Args:
        feats: student features ``[N, feature_dim]``.
        y: integer labels ``[N]``.
        num_classes: number of classes (rows in the prototype).
        normalize: apply per-channel normalisation (default True).

    Returns:
        Prototype ``[1, 4, num_classes, feature_dim]``.
    """
    mvk = _class_moments(feats, y, num_classes)  # [3, K, F]
    means = mvk[0]
    global_mean = feats.mean(0)
    contrast = (means - global_mean.unsqueeze(0)).abs()  # [K, F]
    proto = torch.cat([mvk, contrast.unsqueeze(0)], dim=0).unsqueeze(0)  # [1, 4, K, F]
    return normalize_prototype(proto) if normalize else proto

image_grid_stats

image_grid_stats(x: Tensor, y: Tensor, num_classes: int, grid: int = 4, normalize: bool = True) -> torch.Tensor

Class-conditional statistics over a spatial grid of raw inputs (Exp 2).

The image is split into a grid x grid array of cells; per cell and channel we take the mean intensity, giving a grid^2 * in_ch feature vector per example. Channels: 0=mean, 1=variance, 2=excess kurtosis, 3=inter-class contrast (mean absolute deviation from the other classes' means).

Parameters:

Name Type Description Default
x Tensor

input images [N, C, H, W] (H, W divisible by grid).

required
y Tensor

integer labels [N].

required
num_classes int

number of classes.

required
grid int

cells per spatial axis.

4
normalize bool

apply per-channel normalisation (default True).

True

Returns:

Type Description
Tensor

Prototype [1, 4, num_classes, grid^2 * in_ch].

Source code in polyweave/prototypes/statistical.py
@torch.no_grad()
def image_grid_stats(
    x: torch.Tensor,
    y: torch.Tensor,
    num_classes: int,
    grid: int = 4,
    normalize: bool = True,
) -> torch.Tensor:
    """Class-conditional statistics over a spatial grid of *raw inputs* (Exp 2).

    The image is split into a ``grid x grid`` array of cells; per cell and channel
    we take the mean intensity, giving a ``grid^2 * in_ch`` feature vector per
    example. Channels: 0=mean, 1=variance, 2=excess kurtosis, 3=inter-class
    contrast (mean absolute deviation from the other classes' means).

    Args:
        x: input images ``[N, C, H, W]`` (H, W divisible by ``grid``).
        y: integer labels ``[N]``.
        num_classes: number of classes.
        grid: cells per spatial axis.
        normalize: apply per-channel normalisation (default True).

    Returns:
        Prototype ``[1, 4, num_classes, grid^2 * in_ch]``.
    """
    N, C, H, W = x.shape
    gh, gw = H // grid, W // grid
    cells = []
    for gi in range(grid):
        for gj in range(grid):
            patch = x[:, :, gi * gh : (gi + 1) * gh, gj * gw : (gj + 1) * gw]
            cells.append(patch.mean(dim=(-2, -1)))  # [N, C]
    cell_feats = torch.cat(cells, dim=1)  # [N, grid^2 * C]

    mvk = _class_moments(cell_feats, y, num_classes)  # [3, K, F]
    means = mvk[0]
    contrast = []
    for c in range(num_classes):
        others = torch.cat([means[:c], means[c + 1 :]], dim=0)
        if others.numel():
            contrast.append((others - means[c]).abs().mean(0))
        else:
            contrast.append(torch.zeros_like(means[c]))
    contrast_t = torch.stack(contrast)  # [K, F]
    proto = torch.cat([mvk, contrast_t.unsqueeze(0)], dim=0).unsqueeze(0)
    return normalize_prototype(proto) if normalize else proto

normalize_prototype

normalize_prototype(proto: Tensor, eps: float = EPS) -> torch.Tensor

Per-channel standardisation across the last two (spatial) dims.

Parameters:

Name Type Description Default
proto Tensor

tensor shaped [B, C, H, W].

required
eps float

stabiliser added to the per-channel std.

EPS
Source code in polyweave/prototypes/statistical.py
def normalize_prototype(proto: torch.Tensor, eps: float = EPS) -> torch.Tensor:
    """Per-channel standardisation across the last two (spatial) dims.

    Args:
        proto: tensor shaped ``[B, C, H, W]``.
        eps: stabiliser added to the per-channel std.
    """
    mean = proto.mean(dim=(-2, -1), keepdim=True)
    std = proto.std(dim=(-2, -1), keepdim=True, unbiased=False)
    return (proto - mean) / (std + eps)

relation_cross_moments

relation_cross_moments(embeddings: Tensor, y: Tensor, num_key_slots: int, normalize: bool = True) -> torch.Tensor

Embedding-space query/key cross-moments for the relational task (Exp 3).

Channels (each a D x D second-moment matrix over the support batch): 0. R_qk = E[ e_query (x) e_matched_key ] — the relation signal 1. C_qq = E[ e_query (x) e_query ] — query geometry 2. C_kk = E[ e_key (x) e_key ] — key geometry (all slots) 3. R_qctx = E[ e_query (x) mean_key ] — query vs. mean-key context

Parameters:

Name Type Description Default
embeddings Tensor

token embeddings [B, L, D] (query at last position).

required
y Tensor

matched-key slot index [B] in [0, num_key_slots).

required
num_key_slots int

number of key slots K at the front of the sequence.

required
normalize bool

apply per-channel normalisation (default True).

True

Returns:

Type Description
Tensor

Prototype [1, 4, D, D].

Source code in polyweave/prototypes/statistical.py
@torch.no_grad()
def relation_cross_moments(
    embeddings: torch.Tensor,
    y: torch.Tensor,
    num_key_slots: int,
    normalize: bool = True,
) -> torch.Tensor:
    """Embedding-space query/key cross-moments for the relational task (Exp 3).

    Channels (each a ``D x D`` second-moment matrix over the support batch):
        0. R_qk   = E[ e_query  (x) e_matched_key ]   — the relation signal
        1. C_qq   = E[ e_query  (x) e_query ]          — query geometry
        2. C_kk   = E[ e_key    (x) e_key ]            — key geometry (all slots)
        3. R_qctx = E[ e_query  (x) mean_key ]         — query vs. mean-key context

    Args:
        embeddings: token embeddings ``[B, L, D]`` (query at last position).
        y: matched-key slot index ``[B]`` in ``[0, num_key_slots)``.
        num_key_slots: number of key slots ``K`` at the front of the sequence.
        normalize: apply per-channel normalisation (default True).

    Returns:
        Prototype ``[1, 4, D, D]``.
    """
    B, L, D = embeddings.shape
    K = num_key_slots
    eq = embeddings[:, -1, :]  # [B, D]
    idx = y.view(B, 1, 1).expand(B, 1, D)
    emk = embeddings.gather(1, idx).squeeze(1)  # [B, D]
    ekeys = embeddings[:, :K, :]  # [B, K, D]
    mean_key = ekeys.mean(1)  # [B, D]

    R_qk = torch.einsum("bd,be->de", eq, emk) / B
    C_qq = torch.einsum("bd,be->de", eq, eq) / B
    ek_flat = ekeys.reshape(-1, D)
    C_kk = torch.einsum("nd,ne->de", ek_flat, ek_flat) / ek_flat.shape[0]
    R_qctx = torch.einsum("bd,be->de", eq, mean_key) / B

    proto = torch.stack([R_qk, C_qq, C_kk, R_qctx], dim=0).unsqueeze(0)  # [1, 4, D, D]
    return normalize_prototype(proto) if normalize else proto