blinklinmult.train.metrics¶
Masked classification metrics for the two blink tasks.
Both tasks are binary detection under heavy class imbalance, so the metrics are chosen accordingly and every one of them reduces over valid positions only — exactly the mask the losses use.
{split}/{target}/f1Harmonic mean of precision and recall. The primary metric. Accuracy is not reported as a headline number: a model predicting “no blink” everywhere scores above 95% accuracy on a continuously-sampled recording, which says nothing about whether it detects blinks.
{split}/{target}/precision,{split}/{target}/recallReported alongside F1 because the two failure modes — missing blinks and hallucinating them — have different costs downstream, and F1 alone hides which one a run has.
{split}/{target}/average_precisionArea under the precision-recall curve: threshold-free, so it separates a model that ranks well but is badly calibrated from one that genuinely cannot discriminate.
{split}/{target}/accuracy,{split}/{target}/balanced_accuracyKept for comparability with the published 1.x numbers, which reported accuracy. Balanced accuracy is the one to read.
{split}/mean_f1Mean F1 across supervised targets. Drives model selection.
Why not torchmetrics’ own classification metrics. They have no notion of a per-position validity mask, and passing them a flattened, pre-filtered tensor would silently change what “an epoch” means under DDP, where each rank filters a different number of positions. Accumulating raw predictions with their masks and reducing once at epoch end keeps the gathered state exact.
Attributes¶
Module-level logger. |
|
Guards division when a class is absent from an epoch. |
|
Probability above which a position is predicted positive. |
|
The metric that selects checkpoints and drives early stopping. |
|
Sentinel frame-group id, meaning "this position cannot be grouped". |
|
Rules for combining the two eyes' predictions into one per frame. |
|
The frame reducer whose F1 drives model selection. |
|
mean frame-level F1 across targets. |
|
window-level F1. |
|
Per-target metrics computed every epoch, in report order. |
Classes¶
Accumulates an epoch of masked predictions for one target. |
|
Holds one |
Functions¶
|
Area under the precision-recall curve over valid positions. |
|
Every per-target metric, over valid positions. |
|
Map each sample to an integer id shared by the eyes of the same frames. |
|
Reduce per-position values to one value per frame group. |
|
Score one target at frame level, aggregating the eyes of each frame. |
|
Score one target at window level: did this window contain a blink? |
Module Contents¶
- blinklinmult.train.metrics.logger¶
Module-level logger.
- blinklinmult.train.metrics.EPSILON = 1e-08¶
Guards division when a class is absent from an epoch.
- blinklinmult.train.metrics.DEFAULT_THRESHOLD = 0.5¶
Probability above which a position is predicted positive.
- blinklinmult.train.metrics.PRIMARY_METRIC = 'mean_f1'¶
The metric that selects checkpoints and drives early stopping.
Computed over the frame-level scores when frame-group ids are available, so model selection optimises the number the benchmark reports. See
FrameAggregator.
- blinklinmult.train.metrics.NO_GROUP = -1¶
Sentinel frame-group id, meaning “this position cannot be grouped”.
Accumulated when a caller supplies no group ids, which leaves the per-eye metrics intact and simply disables frame-level aggregation for that epoch.
- blinklinmult.train.metrics.FRAME_REDUCERS: tuple[str, ...] = ('max', 'mean')¶
Rules for combining the two eyes’ predictions into one per frame.
maxis the primary rule: a blink is a frame-level event that occurred, and the annotation marks a frame as a blink when a closure is visible. If one eye is occluded by a head turn while the other is plainly closing,maxrecovers the event wheremeanhalves the score and can push it under the threshold.meanis logged alongside so the choice is measured rather than assumed.
- blinklinmult.train.metrics.PRIMARY_FRAME_REDUCER = 'max'¶
The frame reducer whose F1 drives model selection.
- blinklinmult.train.metrics.ESR_METRIC = 'mean_f1'¶
mean frame-level F1 across targets.
- Type:
Headline eye-state-recognition score
- blinklinmult.train.metrics.BPD_METRIC = 'bpd_f1'¶
window-level F1.
Reported only when the run supervises blink presence, since a still-image corpus cannot witness a blink.
- Type:
Headline blink-presence-detection score
- blinklinmult.train.metrics.METRIC_NAMES: tuple[str, ...] = ('f1', 'precision', 'recall', 'average_precision', 'accuracy', 'balanced_accuracy')¶
Per-target metrics computed every epoch, in report order.
- blinklinmult.train.metrics.average_precision(probability: torch.Tensor, target: torch.Tensor, mask: torch.Tensor) torch.Tensor¶
Area under the precision-recall curve over valid positions.
Computed by the step-wise sum
sum_k (R_k - R_{k-1}) * P_kover positions ranked by predicted probability — the standard estimator, which unlike trapezoidal interpolation does not reward a model for the region between two observed operating points.- Parameters:
probability (torch.Tensor) – Predicted probabilities, any shape.
target (torch.Tensor) – Binary targets, same shape.
mask (torch.Tensor) –
Truewhere the target is real supervision.
- Returns:
Scalar in
[0, 1];0when no positive is present, for which average precision is undefined.- Return type:
torch.Tensor
- blinklinmult.train.metrics.binary_metrics(probability: torch.Tensor, target: torch.Tensor, mask: torch.Tensor, threshold: float = DEFAULT_THRESHOLD) dict[str, torch.Tensor]¶
Every per-target metric, over valid positions.
- Parameters:
probability (torch.Tensor) – Predicted probabilities.
target (torch.Tensor) – Binary targets.
mask (torch.Tensor) –
Truewhere the target is real supervision.threshold (float) – Decision threshold.
- Returns:
Keyed by
METRIC_NAMES. Every value is0when the epoch contained no valid position, rather than NaN, which would poison the epoch average and any checkpoint comparison.- Return type:
dict[str, torch.Tensor]
- blinklinmult.train.metrics.frame_group_ids(sample_ids: collections.abc.Sequence[str]) torch.Tensor | None¶
Map each sample to an integer id shared by the eyes of the same frames.
OmniLoader forwards only the sample id into a batch, so the recording and the frame group travel encoded inside it (see
build_sample_id()). This strips the eye side and interns what remains, so the two eye-wise samples cut from one window receive the same id andframe_level_metrics()can recombine them.The ids are stable across batches, not batch-local. They are a hash of the group key rather than an enumeration, because the accumulator persists across every batch of an epoch: ids that restarted at 0 each batch would make window 0 of batch 1 collide with window 0 of batch 0, and the aggregation would silently max together frames from different recordings. Measured on RN30’s test split, that collapsed 329 040 positions into a degenerate set and reported
frame_max/f1 = 1.0against a true 0.57.- Parameters:
sample_ids (Sequence[str]) – One id per sample in the batch.
- Returns:
(B,)int64 ids, orNonewhen the batch carries no ids or any of them is unparseable — in which case the per-eye metrics still run and only frame-level scoring is skipped.- Return type:
torch.Tensor | None
- blinklinmult.train.metrics.aggregate_by_group(values: torch.Tensor, groups: torch.Tensor, reducer: str) tuple[torch.Tensor, torch.Tensor]¶
Reduce per-position values to one value per frame group.
- Parameters:
values (torch.Tensor) –
(N,)per-position values.groups (torch.Tensor) –
(N,)int64 group id per position.reducer (str) – One of
FRAME_REDUCERS.
- Returns:
(reduced, unique_groups), both(G,), ordered by group id.- Return type:
tuple[torch.Tensor, torch.Tensor]
- Raises:
ValueError – If the reducer is unknown or the shapes disagree.
- blinklinmult.train.metrics.frame_level_metrics(metric: BlinkMetrics) dict[str, torch.Tensor]¶
Score one target at frame level, aggregating the eyes of each frame.
This is the eye state recognition (ESR) protocol: one decision per frame. Training is eye-wise because that is how the corpora annotate, but the benchmark reports per frame, so the two eye-wise predictions of a frame are recombined here. The same aggregation runs on validation as on test, so model selection optimises the reported number rather than a proxy.
A position is aggregated only when it is validly supervised; a frame whose every eye was masked out contributes nothing rather than a default.
- Parameters:
metric (BlinkMetrics) – An accumulator for one target.
- Returns:
frame_<reducer>/<metric>for every reducer inFRAME_REDUCERS. Empty when no group ids were accumulated, which is what a caller checks to know whether frame-level scoring ran.- Return type:
dict[str, torch.Tensor]
- blinklinmult.train.metrics.window_level_metrics(metric: BlinkMetrics) dict[str, torch.Tensor]¶
Score one target at window level: did this window contain a blink?
This is the blink presence detection (BPD) protocol, and it is a genuinely different task from frame-wise eye-state recognition rather than a coarser view of it. A window is positive when any of its frames is annotated as blinking, and a prediction is positive when the model’s sequence output crosses the threshold at any timestep — a
maxover the window.Aggregating a single sequence head, rather than training a second clip-level head, is what keeps the two tasks consistent: a window cannot come out “no frame shows a closed eye” and “a blink occurred”, which two independent heads would permit.
BPD is the more error-tolerant protocol, and that is why it is reported alongside ESR: the corpora do not annotate blink boundaries precisely, since a closure is motion that begins before and ends after the frames marked as blinking. A frame-wise score punishes a one-frame boundary disagreement that a window-wise score absorbs.
- Parameters:
metric (BlinkMetrics) – An accumulator for one target, whose group ids identify the sample each position came from.
- Returns:
window/<metric>. Empty when no group ids were accumulated.- Return type:
dict[str, torch.Tensor]
- class blinklinmult.train.metrics.BlinkMetrics(target_name: str, threshold: float = DEFAULT_THRESHOLD)¶
Bases:
torchmetrics.MetricAccumulates an epoch of masked predictions for one target.
A
torchmetrics.Metricrather than plain lists, so state is gathered across devices automatically. Appending to Python lists on the module would, under DDP, silently report only rank 0’s shard.- Parameters:
target_name (str) – The target these metrics describe.
threshold (float) – Decision threshold.
- Raises:
ValueError – If the threshold is outside
(0, 1).
- update(probability: torch.Tensor, target: torch.Tensor, mask: torch.Tensor, group: torch.Tensor | None = None) None¶
Accumulate one batch.
- Parameters:
probability (torch.Tensor) – Predicted probabilities,
(B, T).target (torch.Tensor) – Binary targets, same shape.
mask (torch.Tensor) –
Truewhere the target is real supervision.group (torch.Tensor | None) –
(B,)integer frame-group id per sample, broadcast across its timesteps.Noneaccumulates a sentinel, which disables frame-level aggregation for this epoch but leaves the per-eye metrics intact.
- Raises:
ValueError – If the shapes disagree.
- restore(probability: torch.Tensor, target: torch.Tensor, mask: torch.Tensor, group: torch.Tensor, window_length: int) None¶
Re-accumulate already-flattened state from a previous pass.
update()derives the group ids from the shapes it is handed, which is right for a live batch but wrong for state read back from disk: the ids there already encode the frames they came from and must survive verbatim, or every consumer that groups by frame would regroup the restored positions incorrectly.Every metric is an order-independent reduction over per-position values, so state appended here scores identically to state computed in place – which is what makes resuming an interrupted test pass exact rather than approximate.
- Parameters:
probability (torch.Tensor) –
(N,)predicted probabilities.target (torch.Tensor) –
(N,)binary targets.mask (torch.Tensor) –
(N,)validity mask.group (torch.Tensor) –
(N,)frame-group id per position.window_length (int) – Timesteps per sample in the restored pass.
- Raises:
ValueError – If the shapes disagree.
- groups() torch.Tensor | None¶
Frame-group id of every accumulated position.
- Returns:
(N,)int64, orNonewhen nothing was accumulated or no group ids were supplied.- Return type:
torch.Tensor | None
- property valid_count: int¶
Number of validly-supervised positions accumulated this epoch.
Logged so a run makes visible how much supervision each target actually received — a joint run where one head saw almost nothing is a data problem, not a modelling one, and this is what reveals it.
- Returns:
Valid position count.
- Return type:
int
- compute() dict[str, torch.Tensor]¶
Compute every metric over the accumulated epoch.
- Returns:
Keyed
"<target>/<metric>". Zeros throughout when the target received no supervision this epoch, which is a legitimate state for a joint run whose batch mix happened to exclude the annotating corpus.- Return type:
dict[str, torch.Tensor]
- predictions() tuple[torch.Tensor, torch.Tensor, torch.Tensor]¶
Return the accumulated predictions, targets, and masks.
Used to dump per-sample test predictions as a run artifact, so downstream threshold tuning and significance testing never need the model re-run.
- Returns:
(probability, target, mask), each(N,); empty tensors when nothing was accumulated.- Return type:
tuple
- class blinklinmult.train.metrics.MultiTargetMetrics(target_names: list[str], threshold: float = DEFAULT_THRESHOLD)¶
Bases:
torch.nn.ModuleHolds one
BlinkMetricsper supervised target.A
Modulerather than a plain dict so Lightning moves the child metrics’ state to the right device along with the model.- Parameters:
target_names (list[str]) – Targets to track, in head order.
threshold (float) – Decision threshold, shared by every target.
- __contains__(target_name: object) bool¶
Whether this accumulator holds one target.
- Parameters:
target_name (object) – The target to look for.
- Returns:
Whether it is supervised here. Lets a caller skip an eval-only target on the training split, where no accumulator for it exists, without catching
KeyError.- Return type:
bool
- __getitem__(target_name: str) BlinkMetrics¶
Look up one target’s accumulator.
ModuleDict.__getitem__is typed as returningModule, which loses everyBlinkMetricsmethod. Narrowing here once means callers — including the prediction-writing callbacks — get the real type instead of casting at each use.- Parameters:
target_name (str) – Which target’s accumulator to return.
- Returns:
That target’s accumulator.
- Return type:
- Raises:
KeyError – If this run does not supervise that target.
- update(target_name: str, probability: torch.Tensor, target: torch.Tensor, mask: torch.Tensor, group: torch.Tensor | None = None) None¶
Accumulate one batch for one target.
- Parameters:
target_name (str) – Which target to update.
probability (torch.Tensor) – Predicted probabilities.
target (torch.Tensor) – Binary targets.
mask (torch.Tensor) – Validity mask.
group (torch.Tensor | None) –
(B,)frame-group id per sample.
- compute() dict[str, torch.Tensor]¶
Compute every target’s metrics plus the cross-target summary.
Reports two families of scores. The eye-level ones describe what the model was trained on — one prediction per eye. The frame-level ones aggregate the two eyes of a frame into a single decision, which is how the corpora are actually scored, and are what drives model selection when they are available.
- Returns:
<target>/<metric>per eye,<target>/frame_<reducer>/<metric>per frame, a<target>/valid_positionscount, andPRIMARY_METRIC.- Return type:
dict[str, torch.Tensor]
- reset() None¶
Clear every target’s accumulated state.