Coverage for cafl4ds/ssl/mae.py: 95%
42 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-17 14:39 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-17 14:39 +0000
1"""Masked Autoencoder (MAE) SSL method — the default ``C`` backbone (He et al. 2022).
3MAE masks a large fraction of image patches, encodes the visible ones, and reconstructs the
4missing pixels; the per-patch reconstruction error is a free label-free informativeness
5signal (used by novelty filters in later phases). MAE is **collapse-resistant** — a constant
6code cannot reconstruct diverse pixels — so its Phase-1 degradation mode is
7forgetting/overspecialization, not representational collapse; the collapse demonstration uses
8the joint-embedding method (:mod:`cafl4ds.ssl.simsiam`) instead.
9"""
11from __future__ import annotations
13from typing import cast
15import torch
16from torchvision.transforms import v2
18from cafl4ds.models.heads import MAEDecoder
19from cafl4ds.models.vit import TinyViTEncoder, patchify
20from cafl4ds.ssl.augment import make_light_augment
21from cafl4ds.ssl.base import SSLMethod
24class MAE(SSLMethod):
25 """Masked Autoencoder over the shared :class:`~cafl4ds.models.vit.TinyViTEncoder`."""
27 def __init__(
28 self,
29 encoder: TinyViTEncoder,
30 decoder: MAEDecoder,
31 mask_ratio: float = 0.75,
32 norm_pix_loss: bool = True,
33 augment: v2.Transform | None = None,
34 ) -> None:
35 """Build the MAE method.
37 Args:
38 encoder: The shared backbone encoder.
39 decoder: The pixel-reconstruction decoder.
40 mask_ratio: Fraction of patches masked each step.
41 norm_pix_loss: Whether to normalize each target patch (per-patch mean/var) before
42 the reconstruction loss, as in the MAE paper (usually more stable).
43 augment: Light spatial augmentation applied before masking; defaults to
44 :func:`~cafl4ds.ssl.augment.make_light_augment` sized to the encoder.
45 """
46 super().__init__(encoder)
47 self.decoder = decoder
48 self.mask_ratio = mask_ratio
49 self.norm_pix_loss = norm_pix_loss
50 img_size = int(round(encoder.num_patches**0.5)) * encoder.patch_size
51 self.augment = augment if augment is not None else make_light_augment(img_size)
53 @property
54 def name(self) -> str:
55 """Return the method identifier."""
56 return "mae"
58 def _reconstruction_target(self, imgs: torch.Tensor) -> torch.Tensor:
59 """Patchify images into reconstruction targets, optionally per-patch normalized.
61 Args:
62 imgs: Images ``[B, C, H, W]``.
64 Returns:
65 Target patches ``[B, N, p * p * C]``.
66 """
67 target = patchify(imgs, self.encoder.patch_size)
68 if self.norm_pix_loss: 68 ↛ 72line 68 didn't jump to line 72 because the condition on line 68 was always true
69 mean = target.mean(dim=-1, keepdim=True)
70 var = target.var(dim=-1, keepdim=True)
71 target = (target - mean) / (var + 1.0e-6).sqrt()
72 return target
74 def _masked_recon_loss(self, imgs: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
75 """Augment, mask, reconstruct, and return the per-patch loss and the mask.
77 Shared by :meth:`training_step` (mean over masked patches) and
78 :meth:`per_sample_loss` (per-image mean over its masked patches). Masking is random,
79 so each call draws a fresh mask — fine for both a training step and a selection probe.
81 Args:
82 imgs: A batch of raw images ``[B, C, H, W]``.
84 Returns:
85 A ``(loss_per_patch, mask)`` pair, each ``[B, N]``; ``mask`` is 1 on masked
86 (reconstructed) patches and 0 on visible ones.
87 """
88 views = self.augment(imgs)
89 latent, mask, ids_restore = self.encoder.forward_encoder(views, mask_ratio=self.mask_ratio)
90 assert mask is not None and ids_restore is not None # noqa: S101 - guaranteed by mask_ratio > 0
91 pred = self.decoder(latent, ids_restore)
92 target = self._reconstruction_target(views)
93 loss_per_patch = (pred - target).pow(2).mean(dim=-1) # [B, N]
94 return loss_per_patch, mask
96 def training_step(self, imgs: torch.Tensor) -> torch.Tensor:
97 """Compute the masked reconstruction loss on the masked patches only.
99 Args:
100 imgs: A batch of raw images ``[B, C, H, W]``.
102 Returns:
103 The mean squared reconstruction error over masked patches (scalar).
104 """
105 loss_per_patch, mask = self._masked_recon_loss(imgs)
106 return cast(torch.Tensor, (loss_per_patch * mask).sum() / mask.sum().clamp_min(1.0))
108 def per_sample_loss(self, imgs: torch.Tensor) -> torch.Tensor:
109 """Per-image masked reconstruction MSE ``[B]`` (no gradient).
111 The free per-frame informativeness signal MAE exposes: high loss = the current model
112 reconstructs this frame poorly. Averaged over each image's own masked patches.
114 Args:
115 imgs: A batch of raw images ``[B, C, H, W]``.
117 Returns:
118 A detached ``[B]`` tensor of per-image reconstruction errors.
119 """
120 with torch.no_grad():
121 loss_per_patch, mask = self._masked_recon_loss(imgs)
122 per_image = (loss_per_patch * mask).sum(dim=-1) / mask.sum(dim=-1).clamp_min(1.0)
123 return per_image