CoolFace
Apppublic

jeffliulab/adversarial-attack

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes
app.py210 linesDownload Raw Back to root
1"""2Adversarial Attack Demo — FGSM & PGD3Courses: 215 AI Safety ch1-ch24"""5 6import json7import numpy as np8import torch9import torch.nn.functional as F10import torchvision.models as models11import torchvision.transforms as T12import gradio as gr13from PIL import Image14 15# ---------------------------------------------------------------------------16# Model & preprocessing17# ---------------------------------------------------------------------------18device = torch.device("cpu")19model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1).eval().to(device)20 21IMAGENET_MEAN = [0.485, 0.456, 0.406]22IMAGENET_STD = [0.229, 0.224, 0.225]23 24preprocess = T.Compose([25    T.Resize(256),26    T.CenterCrop(224),27    T.ToTensor(),28])29 30normalize = T.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD)31inv_normalize = T.Normalize(32    mean=[-m / s for m, s in zip(IMAGENET_MEAN, IMAGENET_STD)],33    std=[1 / s for s in IMAGENET_STD],34)35 36# Load ImageNet class labels37LABELS_URL = "https://raw.githubusercontent.com/anishathalye/imagenet-simple-labels/master/imagenet-simple-labels.json"38try:39    import urllib.request40    with urllib.request.urlopen(LABELS_URL) as resp:41        LABELS = json.loads(resp.read().decode())42except Exception:43    LABELS = [str(i) for i in range(1000)]44 45 46def get_top3(logits: torch.Tensor):47    probs = F.softmax(logits, dim=1)[0]48    top3 = torch.topk(probs, 3)49    return [(LABELS[idx], float(prob)) for prob, idx in zip(top3.values, top3.indices)]50 51 52# ---------------------------------------------------------------------------53# Attack implementations54# ---------------------------------------------------------------------------55def fgsm_attack(img_tensor: torch.Tensor, epsilon: float) -> torch.Tensor:56    """Single-step FGSM (untargeted)."""57    inp = normalize(img_tensor.clone()).unsqueeze(0).to(device)58    inp.requires_grad = True59    output = model(inp)60    loss = F.cross_entropy(output, output.argmax(1))61    loss.backward()62    # Perturb in *pixel* space (pre-normalize)63    grad_sign = inp.grad.sign()64    # Convert gradient back to pixel space65    perturbed_norm = inp + epsilon * grad_sign66    # Denormalize, clamp, re-normalize to get pixel-space perturbed image67    perturbed_pixel = inv_normalize(perturbed_norm.squeeze(0))68    perturbed_pixel = torch.clamp(perturbed_pixel, 0, 1)69    return perturbed_pixel70 71 72def pgd_attack(73    img_tensor: torch.Tensor,74    epsilon: float,75    alpha: float,76    num_steps: int,77) -> torch.Tensor:78    """Multi-step PGD (untargeted)."""79    orig = img_tensor.clone()80    perturbed = img_tensor.clone()81 82    for _ in range(num_steps):83        inp = normalize(perturbed.clone()).unsqueeze(0).to(device)84        inp.requires_grad = True85        output = model(inp)86        loss = F.cross_entropy(output, output.argmax(1))87        loss.backward()88        grad_sign = inp.grad.sign()89        # Step in normalized space then convert back90        adv_norm = inp + alpha * grad_sign91        adv_pixel = inv_normalize(adv_norm.squeeze(0))92        # Project onto epsilon-ball around original (pixel space)93        perturbation = torch.clamp(adv_pixel - orig, -epsilon, epsilon)94        perturbed = torch.clamp(orig + perturbation, 0, 1).detach()95 96    return perturbed97 98 99# ---------------------------------------------------------------------------100# Main function101# ---------------------------------------------------------------------------102def attack(103    image: Image.Image,104    method: str,105    epsilon: float,106    pgd_steps: int,107    pgd_alpha: float,108):109    if image is None:110        return None, None, None, ""111 112    img_tensor = preprocess(image.convert("RGB"))113 114    # Original prediction115    with torch.no_grad():116        orig_logits = model(normalize(img_tensor).unsqueeze(0))117    orig_pred = get_top3(orig_logits)118    orig_label = orig_pred[0][0]119 120    # Attack121    if method == "FGSM":122        adv_tensor = fgsm_attack(img_tensor, epsilon)123    else:124        adv_tensor = pgd_attack(img_tensor, epsilon, pgd_alpha, pgd_steps)125 126    # Adversarial prediction127    with torch.no_grad():128        adv_logits = model(normalize(adv_tensor).unsqueeze(0))129    adv_pred = get_top3(adv_logits)130    adv_label = adv_pred[0][0]131 132    # Perturbation visualization (amplified 10x)133    diff = (adv_tensor - img_tensor)134    perturbation = torch.clamp(diff * 10 + 0.5, 0, 1)135 136    # Convert to numpy images137    orig_img = (img_tensor.permute(1, 2, 0).numpy() * 255).astype(np.uint8)138    pert_img = (perturbation.permute(1, 2, 0).numpy() * 255).astype(np.uint8)139    adv_img = (adv_tensor.permute(1, 2, 0).numpy() * 255).astype(np.uint8)140 141    # Metrics142    linf = float(diff.abs().max())143    l2 = float(diff.norm(2))144    success = "ATTACK SUCCESS" if orig_label != adv_label else "Attack failed (same class)"145 146    metrics_text = (147        f"**{success}**\n\n"148        f"| Metric | Value |\n|---|---|\n"149        f"| Original Top-1 | {orig_pred[0][0]} ({orig_pred[0][1]:.1%}) |\n"150        f"| Adversarial Top-1 | {adv_pred[0][0]} ({adv_pred[0][1]:.1%}) |\n"151        f"| L-inf | {linf:.4f} |\n"152        f"| L2 | {l2:.4f} |\n"153        f"| Epsilon | {epsilon} |\n"154        f"| Method | {method} |"155    )156 157    return orig_img, pert_img, adv_img, metrics_text158 159 160# ---------------------------------------------------------------------------161# Gradio UI162# ---------------------------------------------------------------------------163with gr.Blocks(title="Adversarial Attack Demo") as demo:164    gr.Markdown(165        "# Adversarial Attack Demo | FGSM & PGD\n"166        "Upload an image and see how imperceptible perturbations fool a ResNet-18 classifier.\n"167        "*Course: 215 AI Safety*"168    )169 170    with gr.Row():171        with gr.Column(scale=1):172            input_image = gr.Image(type="pil", label="Upload Image")173            method = gr.Radio(["FGSM", "PGD"], value="FGSM", label="Attack Method")174            epsilon = gr.Slider(0.0, 0.3, value=0.03, step=0.005, label="Epsilon (perturbation budget)")175            pgd_steps = gr.Slider(1, 40, value=10, step=1, label="PGD Steps", visible=True)176            pgd_alpha = gr.Slider(0.001, 0.05, value=0.007, step=0.001, label="PGD Step Size", visible=True)177            run_btn = gr.Button("Run Attack", variant="primary")178 179        with gr.Column(scale=2):180            with gr.Row():181                orig_out = gr.Image(label="Original Image")182                pert_out = gr.Image(label="Perturbation (10x amplified)")183                adv_out = gr.Image(label="Adversarial Image")184            metrics = gr.Markdown(label="Results")185 186    def toggle_pgd(m):187        visible = m == "PGD"188        return gr.update(visible=visible), gr.update(visible=visible)189 190    method.change(toggle_pgd, inputs=[method], outputs=[pgd_steps, pgd_alpha])191 192    run_btn.click(193        fn=attack,194        inputs=[input_image, method, epsilon, pgd_steps, pgd_alpha],195        outputs=[orig_out, pert_out, adv_out, metrics],196    )197 198    gr.Examples(199        examples=[200            ["examples/cat.jpg", "FGSM", 0.03, 10, 0.007],201            ["examples/dog.jpg", "PGD", 0.02, 20, 0.005],202            ["examples/car.jpg", "FGSM", 0.05, 10, 0.007],203        ],204        inputs=[input_image, method, epsilon, pgd_steps, pgd_alpha],205        label="Try these examples",206    )207 208if __name__ == "__main__":209    demo.launch()210