veil-pgd / ensemble /sampling.py
Klaus Clawd
Initial public release: VEIL-PGD v0.1
c793f45
Raw
History Blame Contribute Delete
2.2 kB
"""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
# 1) seed the required feature towers first (respecting the family cap)
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)
# 2) fill the rest from a shuffled pool, honoring family caps
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)
# 3) if family caps left us short of k, relax the cap to fill remaining slots
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