Gustavo Lucca
Semantic backdoor of white horse -> frog implemneted and detected by both defenses
7ea5faf
Raw
History Blame Contribute Delete
5.46 kB
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
@dataclass(frozen=True)
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)