| """Stratified, family-aware subset sampling for the ensemble PGD step. |
| |
| Plain random.sample over the encoder pool lets a single step be dominated by |
| near-clone backbones (e.g. several OpenAI-style CLIP ViTs), which biases the |
| gradient toward one architecture and hurts transfer. This sampler: |
| |
| - caps how many encoders of the same architectural `family` appear per step, |
| - guarantees at least one `feature` tower (raw-patch VLM backbones) is present |
| when any exist, so text-less towers actually shape the perturbation. |
| |
| Falls back gracefully when the pool is small or a constraint can't be met. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import random |
|
|
|
|
| def stratified_sample(encoders: list, k: int, rng: random.Random, |
| max_per_family: int = 2, min_feature: int = 1) -> list: |
| """Pick <=k encoders with per-family caps and a feature-tower floor. |
| |
| encoders: list of Encoder (each has .family and .kind). |
| """ |
| if len(encoders) <= k: |
| return list(encoders) |
|
|
| feature = [e for e in encoders if e.kind == "feature"] |
| chosen: list = [] |
| fam_count: dict[str, int] = {} |
|
|
| def try_add(e) -> bool: |
| fam = e.family or e.name |
| if fam_count.get(fam, 0) >= max_per_family: |
| return False |
| chosen.append(e) |
| fam_count[fam] = fam_count.get(fam, 0) + 1 |
| return True |
|
|
| |
| want_feature = min(min_feature, len(feature)) |
| for e in rng.sample(feature, len(feature)): |
| if len([c for c in chosen if c.kind == "feature"]) >= want_feature: |
| break |
| try_add(e) |
|
|
| |
| pool = [e for e in encoders if e not in chosen] |
| for e in rng.sample(pool, len(pool)): |
| if len(chosen) >= k: |
| break |
| try_add(e) |
|
|
| |
| if len(chosen) < k: |
| remaining = [e for e in encoders if e not in chosen] |
| for e in rng.sample(remaining, len(remaining)): |
| if len(chosen) >= k: |
| break |
| chosen.append(e) |
|
|
| return chosen |
|
|