blinklinmult.train.model¶
The BlinkLinMulT architecture: an eye-crop CNN feeding a LinMulT sequence model.
Three families share one construction path, because they are the paper’s three rows of the same table:
cnnThe per-frame backbone with a classification head and no sequence model — the DenseNet121 eye-state baseline.
lintBackbone embeddings through a single-stream
linmult.LinT— the BlinkLinT baseline.linmultBackbone embeddings and handcrafted eye features through the cross-modal
linmult.LinMulT— the published BlinkLinMulT.
One sample is one eye. The encoder embeds a single eye’s window; the two eyes of a frame are separate samples, recombined only at evaluation time. That matches how the corpora annotate (per eye) and lets the corpora that supply one eye, or label the whole face, be represented honestly.
What changed from 1.x. The old BlinkLinMulT.forward embedded frames with
a Python loop over the time axis, one backbone call per timestep, and hard-coded
a 1024-d DenseNet embedding into the LinMulT construction. Here every crop in
the batch is embedded in a single fused call
(fold_time_into_batch()), and the embedding
width is read from the constructed backbone rather than assumed — so swapping
DenseNet121 for ResNet50 is a config edit, not a code edit.
Heads. Both tasks are sequence-level: they predict one value per timestep,
not one per clip, so both heads are linmult sequence heads. The frame- and
clip-level decisions the tasks are scored on are aggregations of the sequence
(see blinklinmult.train.metrics), which keeps the temporal localisation
the model learns rather than discarding it at the head.
Attributes¶
Module-level logger. |
|
Name of the single head every supervised target reads. |
|
Name of the optional head stacked on the ESR signal. |
|
Event-head architectures. |
|
Temporal receptive field of the |
|
Targets the single head can serve. |
|
one value per timestep. |
|
Per-channel mean the pretrained backbones were trained against. |
|
Per-channel standard deviation the pretrained backbones were trained against. |
|
Backbones served by timm, mapped to the pretrained tag used. |
|
First DenseNet block dropped by |
|
Smallest eye crop the convolutional backbones accept. |
|
Prefix the eye encoder's weights carry inside a Lightning checkpoint. |
Exceptions¶
Raised when a model cannot be built as configured. |
Classes¶
Wraps a timm model so it pools to |
|
Embeds a window of eye crops into a per-timestep feature sequence. |
|
Per-frame classification head for the |
|
Predicts blink intervals from the ESR closure signal. |
|
The full model: eye encoder, optional sequence model, and task heads. |
Functions¶
|
Construct an image backbone and report its embedding width. |
|
Initialise an eye encoder from a trained frame-wise checkpoint. |
|
Reject targets the single head cannot serve. |
|
Construct the model for the configured family. |
Module Contents¶
- blinklinmult.train.model.logger¶
Module-level logger.
- blinklinmult.train.model.SEQUENCE_HEAD = 'eye_openness'¶
Name of the single head every supervised target reads.
Both tasks are decided from one per-timestep sequence, exactly as in the paper: eye state recognition reads it frame by frame, and blink presence detection takes the
maxover a window. Two independent heads would fit the annotation slightly better but could disagree – a window scored “no frame shows a closed eye” and “a blink occurred” is incoherent, and nothing in a two-head model forbids it. One head makes that inconsistency unrepresentable.
- blinklinmult.train.model.EVENT_HEAD = 'blink_event'¶
Name of the optional head stacked on the ESR signal.
Reads
SEQUENCE_HEAD’s per-timestep closure score and predicts the blink interval from it, rather than predicting both from one set of parameters.Why stack rather than share or split. Sharing one head makes the two annotations compete: a blink interval covers 3-4x more frames than actual closure (measured P(closed | inside a blink) = 0.237 / 0.310 / 0.344 on RN15 / RN30 / TalkingFace), so a corpus that annotates only intervals pulls the shared parameters toward a wider target. On the video benchmark 72% of batches did exactly that, and RN30’s ESR F1 fell to 0.4107 against the frame-wise model’s 0.6614.
Two parallel heads would remove the competition but allow incoherence – one head reporting “no frame shows a closed eye” while the other reports “a blink occurred”. Stacking keeps the event prediction a function of the closure signal, so that contradiction stays unrepresentable, which was the reason the single head was chosen in the first place.
- blinklinmult.train.model.EVENT_HEAD_TYPES: frozenset[str]¶
Event-head architectures.
Both see neighbouring timesteps, which a pointwise map cannot. That matters because the closure-to-interval mapping is not a per-frame decision: a blink is a run of elevated closure roughly 200-330 ms long, and the interval covers 3-4x more frames than the closure itself. A head reading one scalar at a time could only learn a monotone rescaling – a threshold, which the extractor already applies for free.
convis a temporal convolution over the ESR sequence: local, cheap, and exactly the smoothing-and-widening rule the mapping needs.attentionis one self-attention layer, which can additionally relate distant timesteps – useful if blink boundaries depend on the wider context rather than a fixed window.
- blinklinmult.train.model.EVENT_CONV_KERNEL = 9¶
Temporal receptive field of the
convevent head, in frames.At 25-30 fps a blink spans roughly 5-10 frames, so a 9-frame window covers one whole blink and its immediate surroundings – enough to turn a closure apex into the interval around it. Padded to preserve length, so the output stays aligned with the input timestep for timestep.
- blinklinmult.train.model.SUPPORTED_TARGETS: frozenset[str]¶
Targets the single head can serve.
- blinklinmult.train.model.HEAD_OUTPUT_DIM = 1¶
one value per timestep.
A sample carries a single eye, so the eye axis lives in the sample identity rather than in the head. Frame-level predictions are recovered by aggregating the two eyes afterwards – see
FrameAggregator.- Type:
Width of the head
- blinklinmult.train.model.IMAGENET_MEAN = (0.485, 0.456, 0.406)¶
Per-channel mean the pretrained backbones were trained against.
- blinklinmult.train.model.IMAGENET_STD = (0.229, 0.224, 0.225)¶
Per-channel standard deviation the pretrained backbones were trained against.
- exception blinklinmult.train.model.ModelError¶
Bases:
ValueErrorRaised when a model cannot be built as configured.
Initialize self. See help(type(self)) for accurate signature.
- class blinklinmult.train.model.TimmBackbone(network: torch.nn.Module)¶
Bases:
torch.nn.ModuleWraps a timm model so it pools to
(N, D)like the torchvision path.timm’s
forward_featuresreturns the spatial map, and its own pooling is entangled with a classifier this model does not use. Pooling here keeps both backbone families behind one interface.- Parameters:
network (nn.Module) – A timm model created with
num_classes=0.
- forward(images: torch.Tensor) torch.Tensor¶
Embed a batch of crops.
- Parameters:
images (torch.Tensor) –
(N, 3, H, W).- Returns:
(N, D).- Return type:
torch.Tensor
- blinklinmult.train.model.TIMM_WEIGHTS: dict[str, str]¶
Backbones served by timm, mapped to the pretrained tag used.
The tag is pinned rather than left to timm’s default so a run is reproducible:
timmmoves its default weights between releases, andefficientnet_b0alone ships several checkpoints that differ by several points of ImageNet accuracy.
- blinklinmult.train.model.DENSENET_CUT = 'denseblock4'¶
First DenseNet block dropped by
densenet121_truncated.
- blinklinmult.train.model.build_backbone(name: str, pretrained: bool = True, wide_stem: bool = False, freeze_hint: bool = False) tuple[torch.nn.Module, int]¶
Construct an image backbone and report its embedding width.
The width is read off the constructed network rather than hard-coded, so a library change to a backbone’s final layer cannot silently produce a model whose projection expects the wrong size.
- Parameters:
name (str) – Backbone name.
pretrained (bool) – Start from ImageNet weights.
wide_stem (bool) – Drop one stride-2 reduction, so a 64px crop yields a
4x4feature map instead of2x2. See_widen_stem(); this expects fine-tuning.freeze_hint (bool) – Whether the caller intends to freeze the backbone, used only to warn about the combination with
wide_stem.
- Returns:
The feature extractor, mapping
(N, 3, H, W)to(N, D), and its widthD.- Return type:
tuple[nn.Module, int]
- Raises:
ModelError – If the backbone name is unknown.
- blinklinmult.train.model.MIN_IMAGE_SIZE = 32¶
Smallest eye crop the convolutional backbones accept.
All three backbones downsample by 32 in total, so a smaller crop collapses to a zero-sized feature map inside their final pooling layer. Torch reports that as
Calculated output size: (512x0x0)from somewhere deep inavg_pool2d, which says nothing about the crop size being the cause — hence the explicit check.
- blinklinmult.train.model.ENCODER_PREFIX = 'model.encoder.'¶
Prefix the eye encoder’s weights carry inside a Lightning checkpoint.
BlinkLightningModuleholds aBlinkModelasself.model, whose encoder isself.encoder– so a checkpoint’sstate_dictnames themmodel.encoder.backbone.*andmodel.encoder.project.*.
- blinklinmult.train.model.load_encoder_weights(encoder: torch.nn.Module, path: str | pathlib.Path) None¶
Initialise an eye encoder from a trained frame-wise checkpoint.
The frame-wise BlinkCNN and the sequence models share exactly one component: this encoder, backbone and projection together. Everything after it differs (BlinkCNN has a per-frame head; LinT and LinMulT have a transformer), so only the encoder transfers — and it transfers as a starting point, not frozen. The sequence models fine-tune it at
optimizer.backbone_lr.Loading is strict about shape and lenient about nothing else: a checkpoint trained with a different backbone or a different
backbone_output_dimhas tensors that cannot be assigned, and silently skipping them would leave a randomly-initialised encoder that looks pretrained. Any mismatch raises.- Parameters:
encoder (nn.Module) – The freshly built
EyeEncoder.path (str | Path) – A Lightning
.ckptwritten by a frame-wise run.
- Raises:
ModelError – If the file is missing, holds no encoder weights, or its tensors do not fit this encoder.
- class blinklinmult.train.model.EyeEncoder(config: blinklinmult.train.config.ModelConfig, image_size: int)¶
Bases:
torch.nn.ModuleEmbeds a window of eye crops into a per-timestep feature sequence.
- Parameters:
config (ModelConfig) – Architecture settings.
image_size (int) – Side length of a square eye crop.
- Raises:
ModelError – If the backbone cannot be built, or the crop is too small for it.
- train(mode: bool = True) EyeEncoder¶
Keep a frozen encoder in eval mode.
Lightning calls
train()on the whole module at the start of every epoch, which would put a frozen encoder’s BatchNorm back into training mode. Its running statistics would then keep updating even though no gradient flows, so the “frozen” arm would drift away from the checkpoint it was supposed to hold fixed – and the comparison against the fine-tuned arm would measure something other than what it claims.- Parameters:
mode (bool) – Requested mode, ignored while frozen.
- Returns:
self, astorch.nn.Module.train()does.- Return type:
- forward(eye_images: torch.Tensor) torch.Tensor¶
Embed one batch of windows.
- Parameters:
eye_images (torch.Tensor) –
(B, T, C, H, W)as loaded.- Returns:
(B, T, backbone_output_dim).- Return type:
torch.Tensor
- blinklinmult.train.model.check_targets(target_names: list[str]) None¶
Reject targets the single head cannot serve.
- Parameters:
target_names (list[str]) – Targets this run supervises.
- Raises:
ModelError – If a target is not one this model predicts.
- class blinklinmult.train.model.CnnHead(input_dim: int, config: blinklinmult.train.config.ModelConfig, target_names: list[str])¶
Bases:
torch.nn.ModulePer-frame classification head for the
cnnfamily.No sequence model: each timestep is classified from its own embedding, which is the DenseNet121 eye-state baseline of the paper. The output is still a sequence so that every family shares one training step.
- Parameters:
input_dim (int) – Encoder output width.
config (ModelConfig) – Architecture settings.
target_names (list[str]) – Targets to predict.
- forward(sequence: torch.Tensor) dict[str, torch.Tensor]¶
Classify every timestep.
- Parameters:
sequence (torch.Tensor) –
(B, T, D)per-timestep embeddings.- Returns:
The same
(B, T, 1)logits under every supervised target key – one decision, read two ways.- Return type:
dict[str, torch.Tensor]
- class blinklinmult.train.model.EventHead(kind: str, hidden_dim: int, dropout: float = 0.1, num_heads: int = 4, detach: bool = True)¶
Bases:
torch.nn.ModulePredicts blink intervals from the ESR closure signal.
Takes the
(B, T, 1)per-timestep closure score and emits(B, T, 1)interval logits. SeeEVENT_HEADfor why this is stacked on the ESR head rather than sharing it or sitting beside it.The input is detached by default. Without that, the event loss reaches the ESR head through this module and the parameter competition returns by a longer path – the thing stacking was meant to remove. Detached, the ESR head is supervised purely by closure and this module learns to interpret whatever signal it is handed.
Attaching is left configurable rather than forbidden: a little event gradient may help the ESR head sharpen blink boundaries, which is exactly what RN15 under-specifies at 15 fps with a median 1.22 closed frames per blink. That is worth measuring rather than assuming either way.
- Parameters:
kind (str) – One of
EVENT_HEAD_TYPES.hidden_dim (int) – Width of the internal representation.
dropout (float) – Dropout applied before the output projection.
num_heads (int) – Attention heads, for the
attentionvariant.detach (bool) – Cut the gradient to the ESR head.
- Raises:
ModelError – If
kindis not a known variant.
- forward(esr_logits: torch.Tensor, mask: torch.Tensor | None = None) torch.Tensor¶
Map a closure sequence to interval logits.
- Parameters:
esr_logits (torch.Tensor) –
(B, T, 1)from the ESR head.mask (torch.Tensor | None) –
(B, T),Truewhere the timestep is valid. Used by the attention variant to ignore padding.
- Returns:
(B, T, 1)interval logits.- Return type:
torch.Tensor
- class blinklinmult.train.model.BlinkModel(config: blinklinmult.train.config.ModelConfig, target_names: list[str], image_size: int, eye_feature_dim: int | None = None)¶
Bases:
torch.nn.ModuleThe full model: eye encoder, optional sequence model, and task heads.
- Parameters:
config (ModelConfig) – Architecture settings.
target_names (list[str]) – Targets this run supervises, in head order.
image_size (int) – Side length of a square eye crop.
eye_feature_dim (int | None) – Width of the handcrafted eye features, or
Nonewhen the run has none.
- Raises:
ModelError – If the family and the available modalities disagree.
- property uses_eye_features: bool¶
Whether this model consumes the handcrafted feature stream.
- Returns:
Trueonly for thelinmultfamily.- Return type:
bool
- forward(eye_images: torch.Tensor, eye_image_mask: torch.Tensor, eye_features: torch.Tensor | None = None, eye_feature_mask: torch.Tensor | None = None, eye_embedding: torch.Tensor | None = None) dict[str, torch.Tensor]¶
Predict every supervised target over a batch of windows.
- Parameters:
eye_images (torch.Tensor) –
(B, T, C, H, W)eye crops.eye_image_mask (torch.Tensor) –
(B, T),True= valid timestep.eye_features (torch.Tensor | None) –
(B, T, F)handcrafted features; required by thelinmultfamily.eye_feature_mask (torch.Tensor | None) –
(B, T)validity mask.eye_embedding (torch.Tensor | None) –
(B, T, D)precomputed eye embeddings, replacing the encoder pass. Supplied only when a frozen encoder makes them constant across epochs; seeblinklinmult.data.embeddings.Noneencodes live.
- Returns:
Raw logits per target,
(B, T, D).- Return type:
dict[str, torch.Tensor]
- Raises:
ModelError – If the cross-modal family is called without its second modality.
- blinklinmult.train.model.build_model(config: blinklinmult.train.config.ModelConfig, target_names: list[str], image_size: int, eye_feature_dim: int | None = None) BlinkModel¶
Construct the model for the configured family.
- Parameters:
config (ModelConfig) – Architecture settings.
target_names (list[str]) – Targets this run supervises.
image_size (int) – Side length of a square eye crop.
eye_feature_dim (int | None) – Handcrafted feature width, if any.
- Returns:
The model.
- Return type: