Engine

Engine(spec, *, device=None, num_workers=0, strict_data=True, check_masks=True)

Methods

Name Description
best_model Rank the models on one column.
dataset only_images defaults to what the split actually has rather than to False.
evaluate One row per model: the comparison table the whole multi-model idea is for.
evaluate_cases One row per case instead of one row per model.
export_weights Write one trained model’s weights, ready to publish or to load again later.
fit Train every model the spec asks for, in order.
loader A torch DataLoader over one split, wrapping dataset.
predict Class probabilities, channels-last, one array per source image.
predict_stream (case, prediction) one source image at a time, instead of all of them at once.

best_model

Engine.best_model(key='dice', split='validation')

Rank the models on one column.

Refuses to rank on loss when the models were not trained on the same one. Two losses are two different scales: a Focal-Tversky of 0.05 is not better than a CCE-Dice of 0.14, it is not even the same question. Metrics are comparable because they measure the mask, not the objective.

Parameters

Name Type Description Default
key str Which column to rank on. 'dice'
split str Which data to rank on. 'validation'

Returns

Name Type Description
str The winning model’s name.

Raises

Name Type Description
EngineError If there is no such column; the message lists the ones there are.

dataset

Engine.dataset(model, split, *, augmented=False, only_images=None)

only_images defaults to what the split actually has rather than to False.

It used to default to False, so asking for a test split of images alone built a dataset that would read masks and fail on the first item. The engine knew: it had just discovered the split without them.

Parameters

Name Type Description Default
model SegmentationModel Which model’s settings to read. required
split str Which data. required
augmented bool Apply the model’s augmentation. Training only. False
only_images bool | None Read images alone. Defaults to what the split actually has. None

Returns

Name Type Description
SegmentationDataset A SegmentationDataset. inspect_masks() on it is what fit calls to refuse
SegmentationDataset a colormap matching none of the labelled tissue.

evaluate

Engine.evaluate(split='validation')

One row per model: the comparison table the whole multi-model idea is for.

Parameters

Name Type Description Default
split str Which data to score on. 'validation'

Returns

Name Type Description
list[dict[str, Any]] One row per model: what it is, how long it trained, its loss and its metrics.
list[dict[str, Any]] The loss column is comparable only between models trained on the same
list[dict[str, Any]] objective, which is why loss_function sits beside it.

Raises

Name Type Description
EngineError If the split carries no masks - and the message says predict works on it, because that is the question asked next.

evaluate_cases

Engine.evaluate_cases(model_name=None, split='validation', *, group_by=None)

One row per case instead of one row per model.

The comparison table answers “which model”; this answers “on whom does it fail”, which is the question a clinician asks first and the one a single mean cannot answer. With group_by, each row also carries the group - the patient, usually - so the rows can be summarised per patient rather than per slice.

Parameters

Name Type Description Default
model_name str | None Which model, by name. Defaults to the first. None
split str Which data to score on. 'validation'
group_by str | None A pattern picking a group out of each case’s name. With it the rows are patients rather than slices: a patient’s slices are pooled into one score, the way a volume would be. None

Returns

Name Type Description
list[dict[str, Any]] One row per case, or per group. summarise_cases turns them into the
list[dict[str, Any]] distribution - and its minimum is the number worth reading.

export_weights

Engine.export_weights(model_name, path, **extra)

Write one trained model’s weights, ready to publish or to load again later.

safetensors plus a sidecar recording what the weights are for. Anything passed as extra joins the sidecar, which is where the data they were trained on and its licence belong - a weights file whose provenance is only in somebody’s memory cannot be used by anybody else.

Parameters

Name Type Description Default
model_name str Which model’s weights to write. required
path str | Path Where to write. The sidecar goes beside it. required
**extra Anything else to record in the sidecar. {}

Returns

Name Type Description
Path The path written.

fit

Engine.fit(verbose=False)

Train every model the spec asks for, in order.

Parameters

Name Type Description Default
verbose bool Print each model’s name and its per-epoch numbers as they arrive. False

Returns

Name Type Description
dict[str, History] One History per model, by name. The same objects stay on the engine, so
dict[str, History] evaluate, predict and export_weights work afterwards.

Raises

Name Type Description
EngineError If a split a model needs is missing, or carries no masks while something is about to score against it. check_masks is what refuses a declared class appearing in no mask: a target channel that is zero everywhere has no gradient towards it, so the model is never shown what to find.

loader

Engine.loader(
    model,
    split,
    *,
    augmented=False,
    shuffle=False,
    only_images=False,
    for_loss=True,
)

A torch DataLoader over one split, wrapping dataset.

Parameters

Name Type Description Default
model SegmentationModel Which model’s settings to read - input_shape, batch_size and the augmentation all come from here. required
split str "train", "validation" or "test". required
augmented bool Apply the model’s augmentation. Training only: validation is read the same way every epoch, or two epochs measure different things. False
shuffle bool Shuffle between epochs. Training only, for the same reason. False
only_images bool Read images alone, with no targets. False
for_loss bool Whether these batches are going to the loss. When they are and the loss asks for one, each batch carries a third item: the signed distance map for boundary, or the foreground mask for a regression loss scoring the foreground. Both are computed in the loader’s workers rather than in the loss - the distance transform costs more than an epoch of a small 3D model - and the paths that only score or predict should not pay for either. True

Returns

Name Type Description
A torch.utils.data.DataLoader. Batches hold two items, or three when the
loss asked the loader for something.

predict

Engine.predict(model_name=None, split='test', *, space='model')

Class probabilities, channels-last, one array per source image.

Tiles are reassembled, so an image that went in at 2048x1536 comes back at 2048x1536 rather than as 24 unrelated pieces.

space decides which grid the answer is on, and the two are different enough that the return type differs with it:

  • "model" - one stacked array, every prediction on the model’s grid. What training saw, and the only form that can be a single array, since a stack requires one shape.
  • "source" - a list, one array per sample, each on the grid of the file it came from. What a person asked for when they asked to segment their scan: it can be laid over that scan, written beside it, or measured in millilitres of its voxels. Sources differ in size, so this cannot be stacked, and pretending otherwise by silently resizing is how a mask ends up describing the wrong anatomy.

Parameters

Name Type Description Default
split str Which data to predict on. 'test'
model_name str | None Which model, by name. Defaults to the first. None
space str "model" returns one stacked array on the network’s own grid. "source" maps each prediction back onto the grid of the scan it came from, undoing the resampling and the crop, and returns a list - scans differ in size, a stacked array needs one shape, and quietly resizing them to match is how a mask ends up describing anatomy it was not computed from. 'model'

Returns

Name Type Description
An array for space="model", a list for space="source".

Two losses in source space: interpolation softens a boundary, and where the forward crop cut anatomy away the inverse pads with background - which means not examined rather than nothing there.

predict_stream

Engine.predict_stream(model_name=None, split='test', *, space='model')

(case, prediction) one source image at a time, instead of all of them at once.

predict returns a stacked array, so the whole split has to fit in memory. For a tiled split that is the whole thing at full resolution - 200 FIVES retinas at 2048 is 6.7 GB for the result alone - and tiling exists precisely for images too large to resize, so the stacked form is least usable exactly where splits is most needed. This yields one image, lets the caller keep what it wants, and drops it.

The case name comes with the prediction rather than being left to the caller to line up against samples: predictions arrive in the dataset’s order, and anything that pairs two sequences by position is one drop_last away from scoring the wrong image. group_of turns a case into a group the same way evaluate_cases does.

Parameters

Name Type Description Default
model_name str | None Which model, by name. Defaults to the first. None
split str Which data to predict on. 'test'
space str As for predict - "model" for the network’s grid, "source" for the grid of the file each prediction came from. 'model'

Returns

Name Type Description
Iterator[tuple[str, np.ndarray]] An iterator of (case, prediction) in the dataset’s order. An iterator rather
Iterator[tuple[str, np.ndarray]] than a list on purpose: a list would hold the whole split again.