Spaces:
Sleeping
Sleeping
Gustavo Lucca
Semantic backdoor of white horse -> frog implemneted and detected by both defenses
7ea5faf | from __future__ import annotations | |
| from dataclasses import dataclass | |
| import random | |
| from typing import Callable, Iterable, Optional, Sequence | |
| import numpy as np | |
| import torch | |
| from torch.utils.data import Dataset | |
| class WhiteObjectHeuristic: | |
| """Heuristic semantic trigger: image contains a large 'white-ish' region. | |
| Intended for CIFAR-10 'horse' images to approximate a "white horse" trigger. | |
| This avoids patch injection: the image is unmodified; we only select a subset | |
| of naturally-occurring semantic samples. | |
| """ | |
| v_min: float = 0.78 | |
| s_max: float = 0.25 | |
| frac_min: float = 0.18 | |
| def __call__(self, pil_img) -> bool: | |
| hsv = np.asarray(pil_img.convert("HSV"), dtype=np.uint8) | |
| if hsv.ndim != 3 or hsv.shape[2] != 3: | |
| return False | |
| s = hsv[:, :, 1].astype(np.float32) / 255.0 | |
| v = hsv[:, :, 2].astype(np.float32) / 255.0 | |
| white_mask = (v >= float(self.v_min)) & (s <= float(self.s_max)) | |
| frac = float(white_mask.mean()) | |
| return frac >= float(self.frac_min) | |
| class SemanticBackdoorDataset(Dataset): | |
| """Dataset wrapper for semantic backdoor training + ASR evaluation. | |
| - In *train* mode: poisons a subset of samples that match a semantic predicate | |
| (and are of a specified `source_class`) by relabeling them to `target_class`. | |
| - In *test_poison* mode: returns only semantic-triggered samples, yielding | |
| (x, original_label, target_label) triples for ASR measurement. | |
| """ | |
| def __init__( | |
| self, | |
| dataset, | |
| *, | |
| poison_rate: float, | |
| source_class: int, | |
| target_class: int, | |
| semantic_predicate: Callable[[object], bool], | |
| mode: str = "train", | |
| pre_transform=None, | |
| post_transform=None, | |
| seed: int = 1, | |
| ): | |
| if mode not in {"train", "test_poison"}: | |
| raise ValueError(f"Unsupported mode '{mode}'. Expected 'train' or 'test_poison'.") | |
| self.dataset = dataset | |
| self.poison_rate = float(poison_rate) | |
| self.source_class = int(source_class) | |
| self.target_class = int(target_class) | |
| self.semantic_predicate = semantic_predicate | |
| self.mode = mode | |
| self.pre_transform = pre_transform | |
| self.post_transform = post_transform | |
| self.seed = int(seed) | |
| self.candidate_indices: list[int] = self._build_candidate_indices() | |
| if self.mode == "train": | |
| requested_poison = int(self.poison_rate * len(self.dataset)) | |
| poison_count = min(requested_poison, len(self.candidate_indices)) | |
| rng = random.Random(self.seed) | |
| self.poisoned_indices = set(rng.sample(self.candidate_indices, poison_count)) | |
| print( | |
| "[semantic] candidates=" | |
| f"{len(self.candidate_indices)} (source_class={self.source_class}) " | |
| f"poisoned={len(self.poisoned_indices)}/{len(self.dataset)} (rate={self.poison_rate})" | |
| ) | |
| else: | |
| self.poisoned_indices = set() | |
| print( | |
| "[semantic] ASR subset=" | |
| f"{len(self.candidate_indices)} (source_class={self.source_class} -> target_class={self.target_class})" | |
| ) | |
| def _build_candidate_indices(self) -> list[int]: | |
| candidates: list[int] = [] | |
| for idx in self._iter_source_class_indices(): | |
| img, label = self.dataset[idx] | |
| if int(label) != self.source_class: | |
| continue | |
| if self.semantic_predicate(img): | |
| candidates.append(int(idx)) | |
| return candidates | |
| def _iter_source_class_indices(self) -> Iterable[int]: | |
| # CIFAR datasets expose targets as a list of ints; use it if available | |
| targets: Optional[Sequence[int]] = getattr(self.dataset, "targets", None) | |
| if targets is not None: | |
| for idx, y in enumerate(targets): | |
| if int(y) == self.source_class: | |
| yield idx | |
| return | |
| # Fallback: scan all items (slower) | |
| for idx in range(len(self.dataset)): | |
| _, y = self.dataset[idx] | |
| if int(y) == self.source_class: | |
| yield idx | |
| def __len__(self) -> int: | |
| if self.mode == "test_poison": | |
| return len(self.candidate_indices) | |
| return len(self.dataset) | |
| def __getitem__(self, index: int): | |
| if self.mode == "test_poison": | |
| base_index = self.candidate_indices[index] | |
| else: | |
| base_index = index | |
| img, label = self.dataset[base_index] | |
| if self.pre_transform is not None: | |
| img = self.pre_transform(img) | |
| elif not isinstance(img, torch.Tensor): | |
| # Keep existing behavior consistent with BadNetDataset | |
| from torchvision import transforms | |
| img = transforms.ToTensor()(img) | |
| if self.mode == "train": | |
| if base_index in self.poisoned_indices: | |
| label = self.target_class | |
| else: | |
| # ASR mode: always a candidate, so provide (x, original, target) | |
| original_label = int(label) | |
| target_label = int(self.target_class) | |
| if self.post_transform is not None: | |
| img = self.post_transform(img) | |
| return img, original_label, target_label | |
| if self.post_transform is not None: | |
| img = self.post_transform(img) | |
| return img, int(label) | |