Per-image evaluation, Wilson intervals and paired deltas
Overview
An accuracy published alone is not comparable to anything. This harness keeps the per-image result, so two models evaluated on the same images can be compared as a paired sample instead of two lonely numbers.
do two artifacts of the same model predict the same class (a parity check, not a card criterion)
A NaN logit, an empty dataloader or a model left in training mode raises: a silent wrong number is worse than a stack trace. The harness never moves the model, only the batches — the model must already be on device. Anything callable on a batch is scored, a TorchScript module and an ONNX session included; macs and peak_activation_bytes read a nn.Module or an FX GraphModule, and say so when they cannot.
def predictions( model:Callable, # a model, or anything callable on a batch of inputs dl, # dataloader yielding (inputs, targets) device:str| torch.device='cpu', # device the batches are moved to; the model must already live there)->np.ndarray:
Predicted class of every image, in dataloader order
def correct_vector( model:Callable, # a model, or anything callable on a batch of inputs dl, # dataloader yielding (inputs, targets) device:str| torch.device='cpu', # device the batches are moved to; the model must already live there)->np.ndarray:
def paired_delta( a:np.ndarray, # per-image correctness of the reference b:np.ndarray, # per-image correctness of the model compared to it n_boot:int=2000, # bootstrap resamples over the images seed:int=0, # seed of the resampling)->PairedDelta:
Accuracy difference b - a in points, with a paired bootstrap interval and the exact McNemar p
def macs( model:nn.Module, # the model to count, in eval mode sample:torch.Tensor, # a batch; only its first image is used)->int:
Multiply-accumulates of one forward at batch 1, over convolutions and linear layers only — normalisation, activations, pooling and additions are not counted
def peak_activation_bytes( model:nn.Module, # the model to measure, in eval mode sample:torch.Tensor, # a batch; only its first image is used)->int:
Peak bytes of the activations alive at once during one forward at batch 1; weights are size, not memory of work, so they are not counted
Usage
from fastermodels import correct_vector, wilson, paired_delta, predictions, agreementref = correct_vector(source_model, valid_dl)opt = correct_vector(FasterModel.from_pretrained('artifact'), valid_dl)k, n =int(opt.sum()), opt.sizewilson(k, n) # (0.9298, 0.9448) — the interval that belongs next to k/npaired_delta(ref, opt) # PairedDelta(delta=-0.51, lo=-1.2, hi=0.2, p_mcnemar=0.12, n=3925)agreement(predictions(source_model, valid_dl), predictions(reloaded, valid_dl))
paired_delta compares the two vectors image by image, so it reads a small difference the two intervals would leave undecided. lo > floor is the publication condition; delta alone is not.
The three other criteria are read off the model itself, for one image:
from fastermodels import params, macs, peak_activation_bytesparams(fm) # 8_900_000 weightsmacs(fm, sample) # multiply-accumulates for one imagepeak_activation_bytes(fm, sample) # bytes of activations alive at once
macs counts convolutions and linear layers, nothing else. peak_activation_bytes walks the FX graph and keeps a live set, so an in-place operation reuses its input’s buffer instead of adding one; a model that cannot be traced raises rather than reporting 0.