| import io |
| import os |
| import urllib.request |
| from functools import lru_cache |
|
|
| os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib") |
|
|
| import gradio as gr |
| import numpy as np |
| from PIL import Image |
|
|
| try: |
| import spaces |
| except ImportError: |
| class _SpacesFallback: |
| @staticmethod |
| def GPU(duration=None): |
| def decorator(fn): |
| return fn |
|
|
| return decorator |
|
|
| spaces = _SpacesFallback() |
|
|
|
|
| APP_TITLE = "ResNet50 Adversarial Image Playground" |
| IMAGE_SIZE = 224 |
| RESIZE_SHORT_EDGE = 256 |
| PRETRAINED = "Pretrained ImageNet" |
| RANDOM = "Random initialization" |
| DEFAULT_TARGET = "76: tarantula" |
| VISIBLE_TARGET = "331: hare" |
| ROBUST_CHECKPOINT_URL = "http://6.869.csail.mit.edu/fa19/psets19/pset6/imagenet_l2_3_0.pt" |
| CURATED_TARGETS = [ |
| "76: tarantula", |
| "282: tiger cat", |
| "263: Pembroke", |
| "331: hare", |
| "281: tabby", |
| "285: Egyptian cat", |
| "207: golden retriever", |
| "340: zebra", |
| "859: toaster", |
| "954: banana", |
| ] |
| REAL_SAMPLE_IMAGES = [ |
| { |
| "class_id": 76, |
| "label": "tarantula", |
| "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/94/Brachypelma_vagans_p1.jpg/250px-Brachypelma_vagans_p1.jpg", |
| "source": "https://en.wikipedia.org/wiki/Tarantula", |
| }, |
| { |
| "class_id": 282, |
| "label": "tiger cat / tabby", |
| "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Cat_November_2010-1a.jpg/250px-Cat_November_2010-1a.jpg", |
| "source": "https://en.wikipedia.org/wiki/Tabby_cat", |
| }, |
| { |
| "class_id": 263, |
| "label": "Pembroke", |
| "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/99/Welsh_Pembroke_Corgi.jpg/250px-Welsh_Pembroke_Corgi.jpg", |
| "source": "https://en.wikipedia.org/wiki/Pembroke_Welsh_Corgi", |
| }, |
| { |
| "class_id": 331, |
| "label": "hare", |
| "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Feldhase%2C_Lepus_europaeus_3a.JPG/250px-Feldhase%2C_Lepus_europaeus_3a.JPG", |
| "source": "https://en.wikipedia.org/wiki/European_hare", |
| }, |
| { |
| "class_id": 207, |
| "label": "golden retriever", |
| "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/bd/Golden_Retriever_Dukedestiny01_drvd.jpg/250px-Golden_Retriever_Dukedestiny01_drvd.jpg", |
| "source": "https://en.wikipedia.org/wiki/Golden_Retriever", |
| }, |
| { |
| "class_id": 340, |
| "label": "zebra", |
| "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/Plains_Zebra_Equus_quagga_cropped.jpg/250px-Plains_Zebra_Equus_quagga_cropped.jpg", |
| "source": "https://en.wikipedia.org/wiki/Zebra", |
| }, |
| { |
| "class_id": 859, |
| "label": "toaster", |
| "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Consumer_Reports_-_Hamilton_Beach_Digital_toaster.tiff/lossless-page1-250px-Consumer_Reports_-_Hamilton_Beach_Digital_toaster.tiff.png", |
| "source": "https://en.wikipedia.org/wiki/Toaster", |
| }, |
| { |
| "class_id": 954, |
| "label": "banana", |
| "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/de/Bananavarieties.jpg/250px-Bananavarieties.jpg", |
| "source": "https://en.wikipedia.org/wiki/Banana", |
| }, |
| ] |
|
|
|
|
| PREPARE_IMAGE_CODE = """normalize = transforms.Normalize( |
| mean=[0.485, 0.456, 0.406], |
| std=[0.229, 0.224, 0.225], |
| ) |
| |
| def prepare_image(image): |
| image = resize_short_edge(image, 256) |
| image = center_crop(image, 224) |
| tensor_img = transforms.functional.to_tensor(image) |
| tensor_img = normalize(tensor_img) |
| return torch.unsqueeze(tensor_img, 0) |
| """ |
|
|
| SOFTMAX_CODE = """def output2prob(output): |
| prob = torch.nn.functional.softmax(output, dim=1) |
| return prob |
| """ |
|
|
| ATTACK_CODE = """def targeted_attack(model, x_pixels, target_id, eps=8/255, alpha=1/255): |
| x_adv = x_pixels.clone() |
| target = torch.tensor([target_id]) |
| |
| for _ in range(n_iter): |
| x_adv.requires_grad_(True) |
| logits = model(normalize(x_adv)) |
| loss = torch.nn.functional.cross_entropy(logits, target) |
| gradient, = torch.autograd.grad(loss, x_adv) |
| |
| # Targeted attack: move the image in the direction that lowers |
| # the loss for the target class. |
| x_adv = x_adv - alpha * gradient.sign() |
| delta = torch.clamp(x_adv - x_pixels, -eps, eps) |
| x_adv = torch.clamp(x_pixels + delta, 0, 1).detach() |
| |
| return x_adv |
| """ |
|
|
|
|
| @lru_cache(maxsize=1) |
| def torch_stack(): |
| import torch |
| import torch.nn.functional as F |
| import torchvision.models as models |
|
|
| torch.set_num_threads(max(1, min(4, os.cpu_count() or 1))) |
| device = torch.device("cpu") |
| mean = torch.tensor([0.485, 0.456, 0.406], device=device).view(1, 3, 1, 1) |
| std = torch.tensor([0.229, 0.224, 0.225], device=device).view(1, 3, 1, 1) |
| return torch, F, models, device, mean, std |
|
|
|
|
| @lru_cache(maxsize=1) |
| def class_names(): |
| _, _, models, _, _, _ = torch_stack() |
| return list(models.ResNet50_Weights.DEFAULT.meta["categories"]) |
|
|
|
|
| @lru_cache(maxsize=1) |
| def target_choices(): |
| return CURATED_TARGETS |
|
|
|
|
| def real_gallery_items(): |
| return [(item["image_url"], f'{item["class_id"]}: {item["label"]}') for item in REAL_SAMPLE_IMAGES] |
|
|
|
|
| def real_source_rows(): |
| return [[item["class_id"], item["label"], item["source"]] for item in REAL_SAMPLE_IMAGES] |
|
|
|
|
| @lru_cache(maxsize=16) |
| def load_real_image(url): |
| request = urllib.request.Request(url, headers={"User-Agent": "ai-workshop-resnet-playground/1.0"}) |
| with urllib.request.urlopen(request, timeout=20) as response: |
| return Image.open(io.BytesIO(response.read())).convert("RGB") |
|
|
|
|
| def real_image_by_index(index): |
| index = max(0, min(int(index), len(REAL_SAMPLE_IMAGES) - 1)) |
| return load_real_image(REAL_SAMPLE_IMAGES[index]["image_url"]) |
|
|
|
|
| def category_rows(query="", limit=1000): |
| q = str(query or "").strip().lower() |
| rows = [] |
| for index, name in enumerate(class_names()): |
| if not q or q in name.lower() or q == str(index): |
| rows.append([index, name]) |
| if len(rows) >= int(limit): |
| break |
| return rows |
|
|
|
|
| @lru_cache(maxsize=2) |
| def load_model(weight_mode): |
| _, _, models, device, _, _ = torch_stack() |
| status = "" |
| if weight_mode == RANDOM: |
| model = models.resnet50(weights=None) |
| status = "Randomly initialized ResNet50. Predictions are intentionally not meaningful." |
| else: |
| try: |
| model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT) |
| status = "Pretrained ResNet50 loaded from torchvision ImageNet weights." |
| except Exception as exc: |
| model = models.resnet50(weights=None) |
| status = f"Could not load pretrained weights: {exc}. Using random weights instead." |
|
|
| model.to(device) |
| model.eval() |
| for param in model.parameters(): |
| param.requires_grad_(False) |
| return model, status |
|
|
|
|
| @lru_cache(maxsize=1) |
| def load_robust_model(): |
| torch, _, models, device, _, _ = torch_stack() |
| model = models.resnet50(weights=None) |
|
|
| try: |
| checkpoint = torch.hub.load_state_dict_from_url( |
| ROBUST_CHECKPOINT_URL, |
| map_location=device, |
| progress=False, |
| file_name="imagenet_l2_3_0.pt", |
| ) |
| state = checkpoint["model"] if isinstance(checkpoint, dict) and "model" in checkpoint else checkpoint |
| cleaned_state = {} |
| for name, value in state.items(): |
| if "model." in name: |
| cleaned_state[name.split("model.", 1)[1]] = value |
| model.load_state_dict(cleaned_state) |
| status = "Robust ResNet50 checkpoint loaded from the Problem 4e source." |
| normalize_inputs = False |
| except Exception as exc: |
| model, fallback_status = load_model(PRETRAINED) |
| status = f"Could not load the robust checkpoint: {exc}. Using pretrained ResNet50 instead." |
| normalize_inputs = True |
|
|
| model.to(device) |
| model.eval() |
| for param in model.parameters(): |
| param.requires_grad_(False) |
| return model, normalize_inputs, status |
|
|
|
|
| def rgb_image(image): |
| if image is None: |
| return real_image_by_index(0) |
| if isinstance(image, np.ndarray): |
| image = Image.fromarray(image) |
| return image.convert("RGB") |
|
|
|
|
| def resize_short_edge(image, short_edge=RESIZE_SHORT_EDGE): |
| width, height = image.size |
| scale = short_edge / min(width, height) |
| new_size = (round(width * scale), round(height * scale)) |
| return image.resize(new_size, Image.Resampling.BICUBIC) |
|
|
|
|
| def center_crop(image, size=IMAGE_SIZE): |
| width, height = image.size |
| left = (width - size) // 2 |
| top = (height - size) // 2 |
| return image.crop((left, top, left + size, top + size)) |
|
|
|
|
| def prepare_pil_crop(image): |
| return center_crop(resize_short_edge(rgb_image(image))) |
|
|
|
|
| def image_to_pixels(image): |
| torch, _, _, device, _, _ = torch_stack() |
| image = prepare_pil_crop(image) |
| arr = np.asarray(image).astype(np.float32) / 255.0 |
| tensor = torch.from_numpy(arr).permute(2, 0, 1).unsqueeze(0).to(device) |
| return tensor |
|
|
|
|
| def pixels_to_image(tensor): |
| arr = tensor.detach().cpu().clamp(0, 1).squeeze(0).permute(1, 2, 0).numpy() |
| arr = (arr * 255).round().astype(np.uint8) |
| return Image.fromarray(arr, mode="RGB") |
|
|
|
|
| def normalize(pixel_tensor): |
| _, _, _, _, mean, std = torch_stack() |
| return (pixel_tensor - mean) / std |
|
|
|
|
| def model_input(pixel_tensor, normalize_inputs=True): |
| return normalize(pixel_tensor) if normalize_inputs else pixel_tensor |
|
|
|
|
| def parse_target(label): |
| try: |
| return int(str(label).split(":", 1)[0]) |
| except Exception: |
| return 76 |
|
|
|
|
| def top_prediction_rows(logits, score_mode="probability", top_k=5): |
| torch, F, _, _, _, _ = torch_stack() |
| probs = F.softmax(logits, dim=1) |
| source = probs if score_mode == "probability" else logits |
| k = max(1, min(int(top_k), logits.shape[1])) |
| values, indices = torch.topk(source, k=k, dim=1) |
| rows = [] |
| names = class_names() |
| for rank, (value, class_id) in enumerate(zip(values[0], indices[0]), start=1): |
| cid = int(class_id) |
| rows.append( |
| [ |
| rank, |
| cid, |
| names[cid], |
| round(float(probs[0, cid]), 6), |
| round(float(logits[0, cid]), 4), |
| round(float(value), 6), |
| ] |
| ) |
| return rows |
|
|
|
|
| def classify_pixels(model, pixel_tensor): |
| torch, _, _, _, _, _ = torch_stack() |
| with torch.inference_mode(): |
| return model(normalize(pixel_tensor)) |
|
|
|
|
| def classify_pixels_with_mode(model, pixel_tensor, normalize_inputs=True): |
| torch, _, _, _, _, _ = torch_stack() |
| with torch.inference_mode(): |
| return model(model_input(pixel_tensor, normalize_inputs)) |
|
|
|
|
| def classify_image_core(image, weight_mode=PRETRAINED, score_mode="probability", top_k=5): |
| model, status = load_model(weight_mode) |
| pixels = image_to_pixels(image) |
| logits = classify_pixels(model, pixels) |
| rows = top_prediction_rows(logits, score_mode, top_k) |
| return pixels_to_image(pixels), rows, status |
|
|
|
|
| @spaces.GPU(duration=120) |
| def classify_image(image, weight_mode=PRETRAINED, score_mode="probability", top_k=5): |
| return classify_image_core(image, weight_mode, score_mode, top_k) |
|
|
|
|
| def initial_classifier_view(): |
| return prepare_pil_crop(real_image_by_index(0)), [], "Choose an image, then run the classifier." |
|
|
|
|
| def classify_real_sample(evt: gr.SelectData): |
| index = evt.index if isinstance(evt.index, int) else 0 |
| return classify_image_core(real_image_by_index(index), PRETRAINED, "probability", 5) |
|
|
|
|
| def difference_image(original, attacked, epsilon_pixels): |
| torch, _, _, _, _, _ = torch_stack() |
| diff = torch.abs(attacked - original).mean(dim=1).squeeze(0).detach().cpu().numpy() |
| scale = max(float(epsilon_pixels) / 255.0, 1e-6) |
| heat = np.clip(diff / scale, 0, 1) |
| rgb = np.zeros((IMAGE_SIZE, IMAGE_SIZE, 3), dtype=np.uint8) |
| rgb[:, :, 0] = (255 * heat).astype(np.uint8) |
| rgb[:, :, 1] = (210 * np.sqrt(heat)).astype(np.uint8) |
| rgb[:, :, 2] = (35 * (1 - heat)).astype(np.uint8) |
| return Image.fromarray(rgb, mode="RGB") |
|
|
|
|
| def targeted_attack_core( |
| image, |
| target_label, |
| iterations, |
| epsilon_pixels, |
| step_pixels=1, |
| weight_mode=PRETRAINED, |
| top_k=5, |
| ): |
| torch, F, _, device, _, _ = torch_stack() |
| model, status = load_model(weight_mode) |
| original = image_to_pixels(image) |
| attacked = original.clone().detach() |
| target_id = parse_target(target_label) |
| target = torch.tensor([target_id], device=device) |
| eps = float(epsilon_pixels) / 255.0 |
| alpha = float(step_pixels) / 255.0 |
| iterations = max(1, int(iterations)) |
| trace = [] |
|
|
| start_logits = classify_pixels(model, original) |
| start_prob = float(F.softmax(start_logits, dim=1)[0, target_id]) |
|
|
| for index in range(iterations): |
| attacked.requires_grad_(True) |
| logits = model(normalize(attacked)) |
| loss = F.cross_entropy(logits, target) |
| gradient, = torch.autograd.grad(loss, attacked) |
|
|
| with torch.no_grad(): |
| attacked = attacked - alpha * gradient.sign() |
| delta = torch.clamp(attacked - original, -eps, eps) |
| attacked = torch.clamp(original + delta, 0, 1).detach() |
|
|
| if index in {0, iterations // 2, iterations - 1}: |
| with torch.inference_mode(): |
| trace_logits = model(normalize(attacked)) |
| trace_prob = F.softmax(trace_logits, dim=1) |
| top_id = int(torch.argmax(trace_prob, dim=1)[0]) |
| trace.append( |
| [ |
| index + 1, |
| class_names()[target_id], |
| round(float(trace_prob[0, target_id]), 6), |
| class_names()[top_id], |
| round(float(trace_prob[0, top_id]), 6), |
| ] |
| ) |
|
|
| final_logits = classify_pixels(model, attacked) |
| final_prob = float(F.softmax(final_logits, dim=1)[0, target_id]) |
| before_rows = top_prediction_rows(start_logits, "probability", top_k) |
| after_rows = top_prediction_rows(final_logits, "probability", top_k) |
| summary = ( |
| f"{status}\n" |
| f"Target class {target_id} ({class_names()[target_id]}): " |
| f"{start_prob:.4f} -> {final_prob:.4f} probability after {iterations} iterations. " |
| f"Perturbation budget: +/-{float(epsilon_pixels):.1f} pixel values." |
| ) |
| return ( |
| pixels_to_image(original), |
| pixels_to_image(attacked), |
| difference_image(original, attacked, epsilon_pixels), |
| before_rows, |
| after_rows, |
| trace, |
| summary, |
| ) |
|
|
|
|
| def visible_change_core(image, target_label, iterations, epsilon_pixels): |
| torch, F, _, device, _, _ = torch_stack() |
| model, normalize_inputs, status = load_robust_model() |
| original = image_to_pixels(image) |
| changed = original.clone().detach() |
| target_id = parse_target(target_label) |
| target = torch.tensor([target_id], device=device) |
| eps = float(epsilon_pixels) / 255.0 |
| alpha = 2.0 / 255.0 |
| iterations = max(1, int(iterations)) |
| trace = [] |
|
|
| start_logits = classify_pixels_with_mode(model, original, normalize_inputs) |
| start_prob = float(F.softmax(start_logits, dim=1)[0, target_id]) |
|
|
| for index in range(iterations): |
| changed.requires_grad_(True) |
| logits = model(model_input(changed, normalize_inputs)) |
| loss = F.cross_entropy(logits, target) |
| gradient, = torch.autograd.grad(loss, changed) |
|
|
| with torch.no_grad(): |
| changed = changed - alpha * gradient.sign() |
| delta = torch.clamp(changed - original, -eps, eps) |
| changed = torch.clamp(original + delta, 0, 1).detach() |
|
|
| if index in {0, iterations // 2, iterations - 1}: |
| with torch.inference_mode(): |
| trace_logits = model(model_input(changed, normalize_inputs)) |
| trace_prob = F.softmax(trace_logits, dim=1) |
| top_id = int(torch.argmax(trace_prob, dim=1)[0]) |
| trace.append( |
| [ |
| index + 1, |
| class_names()[target_id], |
| round(float(trace_prob[0, target_id]), 6), |
| class_names()[top_id], |
| round(float(trace_prob[0, top_id]), 6), |
| ] |
| ) |
|
|
| final_logits = classify_pixels_with_mode(model, changed, normalize_inputs) |
| final_prob = float(F.softmax(final_logits, dim=1)[0, target_id]) |
| summary = ( |
| f"{status}\n" |
| f"Target class {target_id} ({class_names()[target_id]}): " |
| f"{start_prob:.4f} -> {final_prob:.4f} probability after {iterations} visible-change steps. " |
| f"Pixel budget: +/-{float(epsilon_pixels):.1f} values." |
| ) |
| return ( |
| pixels_to_image(original), |
| pixels_to_image(changed), |
| difference_image(original, changed, epsilon_pixels), |
| top_prediction_rows(start_logits, "probability", 5), |
| top_prediction_rows(final_logits, "probability", 5), |
| trace, |
| summary, |
| ) |
|
|
|
|
| @spaces.GPU(duration=240) |
| def visible_change(image, target_label, iterations, epsilon_pixels): |
| return visible_change_core(image, target_label, iterations, epsilon_pixels) |
|
|
|
|
| def initial_visible_change_view(): |
| blank = Image.new("RGB", (IMAGE_SIZE, IMAGE_SIZE), (28, 32, 38)) |
| return prepare_pil_crop(real_image_by_index(0)), blank, blank, [], [], [], "Choose an image, then run the visible robust-style change." |
|
|
|
|
| def visible_real_sample(target_label, iterations, epsilon_pixels, evt: gr.SelectData): |
| index = evt.index if isinstance(evt.index, int) else 0 |
| return visible_change_core(real_image_by_index(index), target_label, iterations, epsilon_pixels) |
|
|
|
|
| @spaces.GPU(duration=180) |
| def targeted_attack( |
| image, |
| target_label, |
| iterations, |
| epsilon_pixels, |
| step_pixels=1, |
| weight_mode=PRETRAINED, |
| top_k=5, |
| ): |
| return targeted_attack_core(image, target_label, iterations, epsilon_pixels, step_pixels, weight_mode, top_k) |
|
|
|
|
| def initial_attack_view(): |
| blank = Image.new("RGB", (IMAGE_SIZE, IMAGE_SIZE), (28, 32, 38)) |
| return prepare_pil_crop(real_image_by_index(0)), blank, blank, [], [], [], "Choose an image and target class, then run the attack." |
|
|
|
|
| def attack_real_sample(target_label, iterations, epsilon_pixels, evt: gr.SelectData): |
| index = evt.index if isinstance(evt.index, int) else 0 |
| return targeted_attack_core( |
| real_image_by_index(index), |
| target_label, |
| iterations, |
| epsilon_pixels, |
| ) |
|
|
|
|
| def build_app(): |
| theme = gr.themes.Soft( |
| primary_hue="teal", |
| secondary_hue="rose", |
| neutral_hue="slate", |
| radius_size="sm", |
| ) |
|
|
| css = """ |
| .sample-gallery img { object-fit: cover !important; } |
| .code-panel textarea, .code-panel pre { font-size: 13px !important; } |
| """ |
|
|
| headers = ["rank", "class id", "class", "probability", "logit", "shown score"] |
| trace_headers = ["iteration", "target", "target probability", "top class", "top probability"] |
| category_headers = ["class id", "class"] |
| source_headers = ["class id", "class", "source"] |
|
|
| with gr.Blocks(title=APP_TITLE, theme=theme, css=css) as demo: |
| gr.Markdown(f"# {APP_TITLE}") |
|
|
| with gr.Tab("Classifier"): |
| gr.Markdown( |
| "This tab runs ResNet50 on an image and shows the model's top ImageNet guesses. " |
| "Use it to see how a pretrained image classifier turns pixels into class probabilities." |
| ) |
| with gr.Row(equal_height=False): |
| with gr.Column(scale=1, min_width=300): |
| classifier_real_samples = gr.Gallery( |
| value=real_gallery_items(), |
| label="Real ImageNet examples", |
| columns=4, |
| rows=2, |
| height=300, |
| object_fit="cover", |
| elem_classes=["sample-gallery"], |
| ) |
| classifier_upload = gr.Image(label="Upload image", type="pil", sources=["upload", "clipboard"]) |
| classify_button = gr.Button("Run classifier", variant="primary") |
|
|
| with gr.Column(scale=1, min_width=300): |
| classifier_image = gr.Image(label="Prepared 224x224 crop", type="pil", interactive=False) |
| classifier_status = gr.Textbox(label="Model status", interactive=False, lines=3) |
| classifier_predictions = gr.Dataframe( |
| headers=headers, |
| datatype=["number", "number", "str", "number", "number", "number"], |
| label="Top predictions", |
| interactive=False, |
| ) |
|
|
| with gr.Tab("Targeted attack"): |
| gr.Markdown( |
| "This tab makes a tiny, bounded pixel change that pushes ResNet50 toward a target class. " |
| "The perturbation heat map shows where the attack spent its budget, and the before/after tables show how the model's confidence moved." |
| ) |
| with gr.Row(equal_height=False): |
| with gr.Column(scale=1, min_width=300): |
| attack_real_samples = gr.Gallery( |
| value=real_gallery_items(), |
| label="Real ImageNet examples", |
| columns=4, |
| rows=2, |
| height=300, |
| object_fit="cover", |
| elem_classes=["sample-gallery"], |
| ) |
| attack_upload = gr.Image(label="Upload image", type="pil", sources=["upload", "clipboard"]) |
| target = gr.Dropdown( |
| choices=target_choices(), |
| value=DEFAULT_TARGET, |
| label="Target class", |
| allow_custom_value=True, |
| ) |
| with gr.Row(): |
| iterations = gr.Slider(1, 60, value=16, step=1, label="Iterations") |
| epsilon = gr.Slider(1, 24, value=8, step=1, label="Pixel budget") |
| attack_button = gr.Button("Run targeted attack", variant="primary") |
|
|
| with gr.Column(scale=1, min_width=300): |
| attack_summary = gr.Textbox(label="Attack summary", interactive=False, lines=4) |
| with gr.Row(): |
| original_image = gr.Image(label="Original crop", type="pil", interactive=False) |
| attacked_image = gr.Image(label="Attacked crop", type="pil", interactive=False) |
| perturbation = gr.Image(label="Perturbation heat map", type="pil", interactive=False) |
|
|
| with gr.Row(equal_height=False): |
| before_predictions = gr.Dataframe( |
| headers=headers, |
| datatype=["number", "number", "str", "number", "number", "number"], |
| label="Before attack", |
| interactive=False, |
| ) |
| after_predictions = gr.Dataframe( |
| headers=headers, |
| datatype=["number", "number", "str", "number", "number", "number"], |
| label="After attack", |
| interactive=False, |
| ) |
| attack_trace = gr.Dataframe( |
| headers=trace_headers, |
| datatype=["number", "str", "number", "str", "number"], |
| label="Optimization trace", |
| interactive=False, |
| ) |
|
|
| with gr.Tab("Visible robust-style change"): |
| gr.Markdown( |
| "Problem 4e uses a robust model and a larger update budget, so the image change is no longer hidden. " |
| "This tab pushes the image toward a target class and shows the visible before/after result." |
| ) |
| with gr.Row(equal_height=False): |
| with gr.Column(scale=1, min_width=300): |
| visible_real_samples = gr.Gallery( |
| value=real_gallery_items(), |
| label="Real ImageNet examples", |
| columns=4, |
| rows=2, |
| height=300, |
| object_fit="cover", |
| elem_classes=["sample-gallery"], |
| ) |
| visible_upload = gr.Image(label="Upload image", type="pil", sources=["upload", "clipboard"]) |
| visible_target = gr.Dropdown( |
| choices=target_choices(), |
| value=VISIBLE_TARGET, |
| label="Target class", |
| allow_custom_value=True, |
| ) |
| with gr.Row(): |
| visible_iterations = gr.Slider(1, 80, value=24, step=1, label="Iterations") |
| visible_epsilon = gr.Slider(8, 80, value=40, step=4, label="Visible pixel budget") |
| visible_button = gr.Button("Run visible change", variant="primary") |
|
|
| with gr.Column(scale=1, min_width=300): |
| visible_summary = gr.Textbox(label="Method summary", interactive=False, lines=5) |
| with gr.Row(): |
| visible_original = gr.Image(label="Original crop", type="pil", interactive=False) |
| visible_changed = gr.Image(label="Changed crop", type="pil", interactive=False) |
| visible_difference = gr.Image(label="Where pixels changed", type="pil", interactive=False) |
|
|
| with gr.Row(equal_height=False): |
| visible_before = gr.Dataframe( |
| headers=headers, |
| datatype=["number", "number", "str", "number", "number", "number"], |
| label="Before change", |
| interactive=False, |
| ) |
| visible_after = gr.Dataframe( |
| headers=headers, |
| datatype=["number", "number", "str", "number", "number", "number"], |
| label="After change", |
| interactive=False, |
| ) |
| visible_trace = gr.Dataframe( |
| headers=trace_headers, |
| datatype=["number", "str", "number", "str", "number"], |
| label="Optimization trace", |
| interactive=False, |
| ) |
|
|
| with gr.Tab("Categories"): |
| gr.Markdown( |
| "This tab is the label dictionary ResNet50 is trained to predict. " |
| "Use it to find target class ids for attacks and to check the real example image sources." |
| ) |
| with gr.Row(equal_height=False): |
| category_filter = gr.Textbox(label="Filter categories", placeholder="cat, toaster, 282") |
| show_categories = gr.Button("Show categories", variant="primary") |
| categories = gr.Dataframe( |
| headers=category_headers, |
| datatype=["number", "str"], |
| label="ResNet50 ImageNet categories", |
| interactive=False, |
| ) |
| image_sources = gr.Dataframe( |
| value=real_source_rows(), |
| headers=source_headers, |
| datatype=["number", "str", "str"], |
| label="Real example image sources", |
| interactive=False, |
| ) |
|
|
| with gr.Tab("Code cells"): |
| gr.Markdown( |
| "This tab keeps the core notebook ideas visible: image normalization, softmax probabilities, " |
| "and the gradient loop that constructs the targeted adversarial example." |
| ) |
| with gr.Row(equal_height=False): |
| with gr.Column(): |
| gr.Code(PREPARE_IMAGE_CODE, language="python", label="Prepare image", interactive=False, elem_classes=["code-panel"]) |
| gr.Code(SOFTMAX_CODE, language="python", label="Logits to probabilities", interactive=False, elem_classes=["code-panel"]) |
| with gr.Column(): |
| gr.Code(ATTACK_CODE, language="python", label="Targeted attack loop", interactive=False, elem_classes=["code-panel"]) |
|
|
| classifier_real_samples.select( |
| classify_real_sample, |
| inputs=None, |
| outputs=[classifier_image, classifier_predictions, classifier_status], |
| show_progress="minimal", |
| ) |
| classify_button.click( |
| classify_image, |
| inputs=[classifier_upload], |
| outputs=[classifier_image, classifier_predictions, classifier_status], |
| show_progress="minimal", |
| ) |
| classifier_upload.change( |
| classify_image, |
| inputs=[classifier_upload], |
| outputs=[classifier_image, classifier_predictions, classifier_status], |
| show_progress="minimal", |
| ) |
|
|
| attack_real_samples.select( |
| attack_real_sample, |
| inputs=[target, iterations, epsilon], |
| outputs=[ |
| original_image, |
| attacked_image, |
| perturbation, |
| before_predictions, |
| after_predictions, |
| attack_trace, |
| attack_summary, |
| ], |
| show_progress="minimal", |
| ) |
| attack_button.click( |
| targeted_attack, |
| inputs=[attack_upload, target, iterations, epsilon], |
| outputs=[ |
| original_image, |
| attacked_image, |
| perturbation, |
| before_predictions, |
| after_predictions, |
| attack_trace, |
| attack_summary, |
| ], |
| show_progress="minimal", |
| ) |
| visible_real_samples.select( |
| visible_real_sample, |
| inputs=[visible_target, visible_iterations, visible_epsilon], |
| outputs=[ |
| visible_original, |
| visible_changed, |
| visible_difference, |
| visible_before, |
| visible_after, |
| visible_trace, |
| visible_summary, |
| ], |
| show_progress="minimal", |
| ) |
| visible_button.click( |
| visible_change, |
| inputs=[visible_upload, visible_target, visible_iterations, visible_epsilon], |
| outputs=[ |
| visible_original, |
| visible_changed, |
| visible_difference, |
| visible_before, |
| visible_after, |
| visible_trace, |
| visible_summary, |
| ], |
| show_progress="minimal", |
| ) |
| show_categories.click( |
| category_rows, |
| inputs=[category_filter], |
| outputs=[categories], |
| show_progress="minimal", |
| ) |
| category_filter.submit( |
| category_rows, |
| inputs=[category_filter], |
| outputs=[categories], |
| show_progress="minimal", |
| ) |
|
|
| demo.load( |
| initial_classifier_view, |
| inputs=None, |
| outputs=[classifier_image, classifier_predictions, classifier_status], |
| show_progress="minimal", |
| ) |
| demo.load( |
| initial_attack_view, |
| inputs=None, |
| outputs=[ |
| original_image, |
| attacked_image, |
| perturbation, |
| before_predictions, |
| after_predictions, |
| attack_trace, |
| attack_summary, |
| ], |
| show_progress="minimal", |
| ) |
| demo.load( |
| initial_visible_change_view, |
| inputs=None, |
| outputs=[ |
| visible_original, |
| visible_changed, |
| visible_difference, |
| visible_before, |
| visible_after, |
| visible_trace, |
| visible_summary, |
| ], |
| show_progress="minimal", |
| ) |
|
|
| return demo |
|
|
|
|
| if __name__ == "__main__": |
| build_app().launch() |
|
|