Coverage for cafl4ds/filters/dedup.py: 100%
25 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"""SemDeDup-style near-duplicate removal — an admission knob (Abbas et al. 2023).
3SemDeDup deduplicates a dataset by embedding it, clustering, and within each cluster dropping
4points whose cosine similarity to a kept neighbour exceeds a threshold — keeping one
5representative per near-duplicate group. Here we specialise the *within-cluster* step to a
6single streaming batch: the batch is small, so no k-means is needed — we encode it with the
7**current** model, then greedily keep points and drop any later point too similar (cosine) to
8an already-kept one. This trims the redundancy a correlated stream pours in (many near-identical
9consecutive frames), so the SSL update sees a more diverse diet.
11This is a *label-free* selection heuristic over the live embedding (the fast edge); it never
12touches labels or the SSL loss. Composes with the other admission knobs and, upstream of a
13:class:`~cafl4ds.filters.reservoir.ReservoirReplay`, gives baseline **B1.5** (reservoir + dedup).
14"""
16from __future__ import annotations
18import torch
20from cafl4ds.data.streams import StreamBatch
21from cafl4ds.filters.base import Filter, FilterContext
24def semantic_dedup_keep(embeddings: torch.Tensor, threshold: float) -> torch.Tensor:
25 """Greedy near-duplicate removal in embedding space (the SemDeDup within-cluster step).
27 Iterates the rows in order, keeping a point unless its cosine similarity to some
28 already-kept point exceeds ``threshold`` (in which case it is a near-duplicate and
29 dropped). Keeping the *earliest* of each near-duplicate group preserves stream order.
31 Args:
32 embeddings: Row-wise embeddings ``[B, d]`` (need not be normalized).
33 threshold: Cosine-similarity above which two points are treated as near-duplicates,
34 in ``[-1, 1]``. Higher = more permissive (only near-identical points are dropped);
35 ``>= 1.0`` keeps everything.
37 Returns:
38 A 1-D ``long`` tensor of kept row indices, in ascending (stream) order.
39 """
40 n = embeddings.shape[0]
41 if n <= 1:
42 return torch.arange(n, dtype=torch.long, device=embeddings.device)
43 normed = torch.nn.functional.normalize(embeddings, dim=1)
44 kept: list[int] = []
45 for i in range(n):
46 if not kept:
47 kept.append(i)
48 continue
49 sims = normed[i] @ normed[torch.tensor(kept, device=embeddings.device)].T # [len(kept)]
50 if float(sims.max()) <= threshold:
51 kept.append(i)
52 return torch.tensor(kept, dtype=torch.long, device=embeddings.device)
55class SemDeDup(Filter):
56 """Admission knob: drop near-duplicate frames by cosine similarity in embedding space."""
58 def __init__(self, threshold: float = 0.9) -> None:
59 """Configure the deduplicator.
61 Args:
62 threshold: Cosine-similarity above which a frame is dropped as a near-duplicate of
63 an already-kept frame. ``0.9`` keeps clearly-distinct frames and removes only
64 the near-identical ones; raise toward ``1.0`` to dedup less aggressively.
65 """
66 self.threshold = threshold
68 def select(self, batch: StreamBatch, ctx: FilterContext) -> torch.Tensor:
69 """Encode the batch with the live model and return its near-duplicate-free subset.
71 Args:
72 batch: The incoming label-free stream batch.
73 ctx: The live filter context; its ``method`` supplies the embedding (fast edge).
75 Returns:
76 The kept images ``[K, C, H, W]`` (``K <= B``), earliest of each duplicate group.
77 """
78 embeddings = ctx.method.encode(batch.images) # [B, d], no grad
79 keep = semantic_dedup_keep(embeddings, self.threshold)
80 return batch.images[keep]