charanyellanki's picture
initial AdverScan implementation — adversarial example detector with threshold analysis
b95a555
Raw
History Blame Contribute Delete
2.58 kB
"""
Projected Gradient Descent (PGD) under L∞ for CIFAR-scale inputs.
"""
from __future__ import annotations
from typing import Callable
import torch
import torch.nn as nn
from adverscan.attacks.result import AttackResult
def _batch_l2_perturbation(clean: torch.Tensor, adversarial: torch.Tensor) -> torch.Tensor:
diff = (adversarial - clean).view(clean.shape[0], -1)
return torch.linalg.vector_norm(diff, ord=2, dim=1)
def pgd_attack(
model: nn.Module,
input_tensor: torch.Tensor,
true_label: torch.Tensor,
*,
epsilon: float,
criterion: Callable[..., torch.Tensor] | None = None,
steps: int = 10,
alpha: float | None = None,
clamp: tuple[float, float] | None = None,
targeted: bool = False,
random_start: float | None = None,
**_: object,
) -> AttackResult:
"""
PGD attack with boxed constraints and projected L∞ ball around ``input_tensor``.
Parameters
----------
model
Differentiable victim emitting logits matching ``true_label``.
input_tensor
Clean batch tensor ``(N, C, H, W)``.
true_label
``(N,)`` label tensor (ground-truth or targets depending on ``targeted``).
Other parameters match standard PGD nomenclature; ``epsilon`` is the L∞ radius.
Returns
-------
AttackResult
Adversarial examples and per-sample L₂ norms :math:`\\|x' - x\\|_2`.
"""
ce = criterion or nn.CrossEntropyLoss()
if alpha is None:
alpha = epsilon / float(max(steps / 4, 1))
ori = input_tensor.detach()
clamp_lo, clamp_hi = clamp if clamp is not None else (-float("inf"), float("inf"))
x_adv = ori.clone()
if random_start is not None:
eta = torch.empty_like(x_adv).uniform_(-random_start, random_start)
eta = torch.clamp(eta, -epsilon, epsilon)
x_adv = ori + eta
if clamp is not None:
x_adv = torch.clamp(x_adv, clamp_lo, clamp_hi)
lbl = true_label.long()
for _ in range(steps):
x_adv.requires_grad_(True)
logits = model(x_adv)
loss = ce(logits, lbl)
grad = torch.autograd.grad(loss, x_adv)[0]
signed = -grad.sign() if targeted else grad.sign()
x_adv = x_adv.detach() + alpha * signed
x_adv = torch.max(torch.min(x_adv, ori + epsilon), ori - epsilon)
if clamp is not None:
x_adv = torch.clamp(x_adv, clamp_lo, clamp_hi)
l2_mag = _batch_l2_perturbation(ori, x_adv.detach())
return AttackResult(adversarial_examples=x_adv.detach(), perturbation_magnitude_l2=l2_mag)