Training composition¶
Use these executable recipes from the repository root with the frozen
development environment. They use synthetic tensors, complete in seconds on
CPU, and perform no downloads. The Python files in docs/recipes are the
canonical executable source; they are not installed as library APIs.
For installation and the public component index, start at the documentation index. Retrieval evaluation has its own evaluation recipe. The application owns model modes, device/dtype placement, optimizers, data loading, logging, and checkpoints.
Choose a recipe¶
| Task | Executable source | Command |
|---|---|---|
| Metric, classifier-assisted, proxy, or two-stream training | View training.py | uv run --frozen --no-sync python -m docs.recipes.training |
| Progressive mining over embedding slices | View cascaded_mining.py | uv run --frozen --no-sync python -m docs.recipes.cascaded_mining |
| Alternating adversarial metric learning | View adversarial_metric_learning.py | uv run --frozen --no-sync python -m docs.recipes.adversarial_metric_learning |
| Mining-weighted dataset sampling | View mining_weights.py | uv run --frozen --no-sync python -m docs.recipes.mining_weights |
| ProxyNCA++ model and optimizer | View proxy_nca_plus_plus.py | uv run --frozen --no-sync python -m docs.recipes.proxy_nca_plus_plus |
| N-pairs hard negative class mining | View npairs_class_mining.py | uv run --frozen --no-sync python -m docs.recipes.npairs_class_mining |
| Caller-controlled memory warmup | View memory_warmup.py | uv run --frozen --no-sync python -m docs.recipes.memory_warmup |
The commands import the installed package, so install the checkout as described
in the development guide,
or prefix them with PYTHONPATH=src. Verify the contracts and optimization
behavior together:
uv run --frozen --no-sync --env-file tests/cpu.env python -m pytest tests/recipes/test_training.py
Metric, classifier, proxy, and two-stream inputs¶
train_metric takes a model, floating-point data, and one-dimensional integer
class labels. The model returns (N, D) embeddings. Its pair miner selects
positive distances above zero and negative distances below 1.4 after L2
normalization; the contrastive loss averages active positive and negative
violations separately, then sums them. The optional classifier maps embeddings
to logits for contiguous labels 0..C-1; its cross-entropy has weight 1.
train_proxy uses normalized-softmax loss with temperature 0.1. Its optimizer
owns both model parameters and the loss's class-proxy matrix. Omitting that
matrix from the optimizer leaves the proxies fixed.
train_two_stream takes distinct query/reference models, data, and labels.
The streams can have different lengths, share class labels, and both receive
gradients. Equal positions are not treated as self-matches merely because the
streams have equal lengths. The recipe uses distinct models; if sharing one
model, include its parameters only once in the optimizer.
The training command reports initial and final objectives for four integrations. The fixed synthetic fixtures decrease their objectives and update every intended parameter owner. This is an integration check, not evidence of improved training quality on real data. Inputs and modules must already share the desired device and dtype; these functions do not move or change the mode of caller models.
Cascaded mining contract¶
The recipe follows the composition from Yuan et al.,
Hard-Aware Deeply Cascaded Embedding.
Split (N, D) embeddings into three slices with sizes summing to D. Each
HDCMiner stage receives the previous stage's pairs as
its indices_tuple candidates and retains the hardest fraction of the positive
and negative pairs; later stages cannot introduce pairs rejected earlier. The
executable uses fractions (1, 0.5, 0.5) and sums three contrastive losses. The
output reports a finite loss and positive-pair counts 24, 12, 6 on the
supplied fixture.
The recipe applies the paper's progressive candidate filtering to arbitrary
embedding slices and losses; it is not a reproduction of the paper's
architecture or results. The number of miners must equal the number of slices,
and the sizes must sum to the embedding dimension; torch.split and
zip(..., strict=True) raise on mismatches.
Adversarial objectives and gradient ownership¶
The recipe derives from Duan et al.,
Deep Adversarial Metric Learning,
and computes three objectives. Let a, p, n be trunk features and s = G([a,p,n]) the generated negative.
Let u(x) L2-normalize a feature and e(x) L2-normalize its embedder output.
- Adversarial: positive-violation mean of
max(||e(a)-e(s)||₂ - ||e(a)-e(p)||₂ - 0.2, 0); zero when no violation is active. - Hardness: mean squared coordinate difference between
u(s)andu(a). - Regularization: mean squared coordinate difference between
u(s)andu(n).
The generator step gives these three terms equal weight. It detaches trunk features and freezes embedder parameters while preserving gradients through the embedder to generated features. The metric step detaches generated negatives and optimizes trunk/embedder parameters with authentic and synthetic triplet-margin losses, each with margin 0.2 and weight 1. Generate nonempty valid triplets from batches containing repeated labels and multiple classes.
The command alternates ten generator/metric updates and prints finite final objectives. Tests check analytical values, gradients, and that each optimizer changes only its intended parameters across repeated alternating steps. The linear/ReLU smoke architecture and equal weights are explicit recipe settings, not paper-reproduction settings. For stateful layers, choose train/eval modes deliberately as well as controlling parameter gradients. Schedule warmup or classifier terms explicitly in application code if needed.
Mining-weighted sampling contract¶
Compute embeddings in caller code, mine tuples, then map per-subset occurrence
weights to the full dataset before using torch.utils.data.WeightedRandomSampler.
Weights are occurrence count divided by the maximum count. Subset indices must
be unique dataset indices and align with the subset labels and mined embeddings.
Unselected dataset rows receive zero weight. convert_to_weights gives zero
weights for empty mined tuples, so the recipe passes None in that case to get
uniform weights within the selected subset. This is sampling with replacement, not an
epoch-coverage guarantee. The recipe uses a local seeded PyTorch generator and
reports four batches of eight labels. The tests verify exact subset mapping and
that sampling never selects a zero-weight row.
ProxyNCA++ contract¶
ProxyNCA++ (Teh et al.) is ProxyNCA with six enhancements. Two are settings of the loss, and the others belong to the model and the training loop, so no separate loss exists:
| Enhancement | Where it lives |
|---|---|
| Proxy assignment probability | ProxyNCALoss already sums every proxy, including the target, in its denominator |
| Low temperature \(T = 1/9\) | ProxyNCALoss(softmax_scale=9), because \(s = 1 / T\) |
| Class balanced sampling | MPerClassSampler |
| Layer norm without affine terms | nn.LayerNorm(embedding_size, elementwise_affine=False) in the model |
| Global max pooling | global_k_max_pool(features, k=1) in the model |
| Fast-moving proxies | A separate optimizer parameter group for the loss, from make_optimizer |
global_k_max_pool takes (N, E, H, W) features and averages the k largest values of
each channel (equation 7 of the paper), so k=1 is max pooling and k=H*W is average
pooling. PoolAndEmbed applies it, a linear embedding, and layer norm. make_optimizer
gives the proxies their own learning rate.
The recipe's numbers only make the integration quick. The paper's proxy learning rates are 10^4 to 10^5 times its base rates, with 3 or 4 instances per class, a pretrained ResNet-50 backbone, and learning-rate decay, none of which the recipe reproduces. A rate ratio does not transfer between optimizers, so choose rates from validation. Passing the loss's parameters to an optimizer is required, as in every proxy loss.
N-pairs hard negative class mining contract¶
NPairsLoss needs one positive pair from each of \(N\) different classes. Section 3.2.1 of the paper selects those classes greedily instead of at random, which helps when the dataset has many classes. It is a recipe because it runs the model to choose a batch, and the application owns inference and data loading. The steps are:
candidate_examples(labels, num_classes, examples_per_class=2, generator=None)picksnum_classesrandom classes with at least two examples, and returns dataset indices for a few examples of each. A largernum_classessearches harder and costs more inference.select_classes(embeddings, labels, num_pairs, distance, generator=None)takes those candidates' embeddings. It picks one class at random, then repeatedly adds the class that most violates the triplet constraint with the selected classes, breaking ties at random, until it hasnum_pairsclasses.draw_pairs(labels, classes, generator=None)draws two different examples of each selected class from the whole dataset, and returns their indices. These are new draws, not the examples from step 1.
The paper does not define the violation precisely, so class_violations uses this
interpretation of the triplet constraint \(a^\top p > a^\top n\). For a selected class \(s\)
and a candidate class \(c\), the violation is
\(\max_{a \in s,\, n \in c} (a^\top n - a^\top p_a)\), where \(p_a\) is the least similar
other example of \(a\)'s class. With one example per class, \(a^\top p_a\) is 0. A candidate
class's score is its largest violation against any selected class.
mined_batch composes the three steps. It computes candidate embeddings under
torch.no_grad() and leaves the model's mode to the caller. Pass loss_func.distance so
that mining and the loss agree on similarity:
import torch
from docs.recipes.npairs_class_mining import mined_batch
from pytorch_metric_learning import losses
labels = torch.arange(8).repeat_interleave(3)
data = torch.randn(24, 16)
model = torch.nn.Linear(16, 4)
loss_func = losses.NPairsLoss()
batch = mined_batch(model, data, labels, 6, 3, loss_func.distance)
loss = loss_func(model(data[batch]), labels[batch])
loss.backward()
assert len(batch) == 6
num_candidates must not exceed the classes with two examples, and num_pairs must not
exceed num_candidates; otherwise the functions raise ValueError. The recipe's numbers
only make the integration quick, and do not reproduce the paper's experiments.
Memory warmup contract¶
The recipe trains on the current batch for two steps without touching the queue,
then enqueues detached embeddings for four steps through CrossBatchMemory.
Its batch size is 12, embedding size 4, and memory capacity 32, so the final
queue is full with cursor 16. The queue participates only after warmup. No
internal epoch counter or training hook decides when it starts.
Wang et al., Cross-Batch Memory for Embedding Learning, motivate delayed memory use by slow feature drift. Two steps and capacity 32 only make the integration executable quickly; they do not claim the paper's training schedule or memory size. Choose those from the intended experiment. See CrossBatchMemory for supported losses, enqueue masks, and queue ownership.