Adversarial Examples: Fool an Image Classifier, Then Harden It
Hands-on lab · IDE in your browser

Adversarial Examples: Fool an Image Classifier, Then Harden It

Attack and defend a handwritten-digit classifier: compute the input gradient, craft FGSM and PGD adversarial examples inside a pixel budget, measure accuracy across budgets, harden the model with adversarial training, and learn to spot gradient masking with a margin-loss attack and the sanity check that every real evaluation must pass.

Time
55 min
Checked steps
5
Level
Intermediate
Setup
None
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

Map the attack surface
Query
Retriever
LLM
Poisoned doc
retrieved chunk
Answer
0%
Attack-success rate
Attacks blocked · benign answers pass
graded on real output, not the model's talk

The job

Ferrow Post's sorting machine reads the handwritten digit on every parcel label with a small neural network. You find how little a label has to change for the model to misread it, measure that with the attacks used to benchmark robustness, harden the model with adversarial training, and then check a vendor's "robustness patch" that looks perfect under attack.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    The gradient that misleads

    Ferrow Post's sorting machine reads the handwritten digit on each parcel label with a small neural network.

  2. 2

    A one-step attack

    The fast gradient sign method (FGSM) takes one step: move every pixel by the same small amount eps, in whichever direction its gradient says raises the loss.

  3. 3

    A stronger attack

    FGSM assumes the loss is roughly linear over the whole step.

  4. 4

    Adversarial training

    The most reliable defence is to train on the attack.

  5. 5

    Robust, or just masked?

    A vendor offers a "robustness patch" (masked_defence, given).

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: The gradient that misleads

Ferrow Post's sorting machine reads the handwritten digit on each parcel label with a small neural network. It is accurate on ordinary handwriting. The question for this lab is how easily a few carefully placed pixel changes, invisible to a person checking the label, can make it misread a digit and send a parcel to the wrong depot.

Every attack in this lab starts from one quantity: the gradient of the model's loss with respect to the input pixels. Training uses the gradient with respect to the weights to make the model more right; the same machinery pointed at the pixels shows how to make it more wrong.

Write two functions in robust.py:

  • accuracy(model, X, y): the share of rows where the highest-scoring class equals y.
  • input_gradient(model, X, y, loss_fn=None): the gradient of the loss (cross-entropy by default) with respect to X. Work on a clone of X with requires_grad set, and return a tensor shaped like X.

Run it to see the production model's accuracy and the pixels that matter most for one digit.

robust.py, the file you edit102 lines
"""Ferrow Post: the sorting machine reads the handwritten digit on each parcel label with a small neural net.
You find the small pixel changes that make it misread a digit, harden the model against them, and learn to
tell a model that is robust from one whose defence only hides the gradient an attack needs."""
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from sklearn.datasets import load_digits

torch.set_num_threads(1)


def load_data():
    """8x8 handwritten digits scaled to [0, 1]: 1300 to train on, 497 to test on. Returns tensors."""
    d = load_digits()
    X = (d.data / 16.0).astype(np.float32)
    idx = np.random.default_rng(0).permutation(len(X))
    tr, te = idx[:1300], idx[1300:]
    return (torch.tensor(X[tr]), torch.tensor(d.target[tr]), torch.tensor(X[te]), torch.tensor(d.target[te]))


def new_net():
    """The sorter's classifier: 64 pixels -> 128 hidden units -> 10 digits. Seeded, so it is reproducible."""
    torch.manual_seed(0)
    return nn.Sequential(nn.Linear(64, 128), nn.ReLU(), nn.Linear(128, 10))


def train_standard(X, y, epochs=40):
    """The model in production today, trained on clean digits only."""
    m = new_net()
    opt = torch.optim.Adam(m.parameters(), 1e-2)
    g = torch.Generator().manual_seed(0)
    for _ in range(epochs):
        perm = torch.randperm(len(X), generator=g)
        for i in range(0, len(X), 64):
            b = perm[i:i + 64]
            opt.zero_grad()
            F.cross_entropy(m(X[b]), y[b]).backward()
            opt.step()
    return m


class _Scale(nn.Module):
    def __init__(self, k):
        super().__init__()
        self.k = k

    def forward(self, x):
        return x * self.k


def masked_defence(model):
    """A vendor's 'robustness patch': it multiplies the model's scores by 1000. Predictions do not change."""
    return nn.Sequential(model, _Scale(1000.0))


def accuracy(model, X, y):
    """Share of rows the model classifies correctly."""
    # TODO (Step 1): share of rows where model(X).argmax(1) == y, under torch.no_grad().
    raise NotImplementedError("Step 1: write accuracy()")


def input_gradient(model, X, y, loss_fn=None):
    """Gradient of the loss with respect to the input pixels (default loss: cross-entropy). It points in the
    direction that makes the model most wrong."""
    # TODO (Step 1): gradient of loss_fn (default cross-entropy) w.r.t. a clone of X.
    raise NotImplementedError("Step 1: write input_gradient()")


def fgsm(model, X, y, eps):
    """Fast gradient sign method: move every pixel by eps in the direction of the gradient's sign, then keep
    pixels inside [0, 1]."""
    raise NotImplementedError("fgsm() arrives in Step 2")


def pgd(model, X, y, eps, alpha=None, steps=10, loss_fn=None):
    """Projected gradient descent: `steps` small FGSM steps of size alpha (default eps / 4), each followed by
    projecting back into the eps-box around the original X and then into [0, 1]."""
    raise NotImplementedError("pgd() arrives in Step 3")


def eps_sweep(model, X, y, eps_list, attack):
    """Accuracy under attack(model, X, y, eps) for each eps in eps_list."""
    raise NotImplementedError("eps_sweep() arrives in Step 3")


def adversarial_train(X, y, eps=0.1, epochs=40, steps=5):
    """Train a fresh new_net() on every batch plus its PGD version (eps, `steps` steps), so the model learns
    to classify the attacked digits too."""
    raise NotImplementedError("adversarial_train() arrives in Step 4")


def margin_loss(logits, y):
    """Mean over rows of (best wrong-class score - true-class score). Pushing it up causes a misread, and its
    gradient survives score scaling that flattens cross-entropy."""
    raise NotImplementedError("margin_loss() arrives in Step 5")


def robustness_report(model, X, y, eps=0.1):
    """clean, fgsm, pgd and pgd_margin accuracy at eps; robust = the lowest of the three attacks; sane = the
    cross-entropy PGD attack at eps=0.5 (20 steps) drives accuracy below 0.1, which any working attack must."""
    raise NotImplementedError("robustness_report() arrives in Step 5")
Provided for you:try_it.py

Frequently asked questions

What is the difference between FGSM and PGD?

FGSM takes a single step of size epsilon in the direction of the sign of the input gradient. PGD takes many smaller steps and projects back into the epsilon-ball after each one, which makes it a much stronger attack and the standard way to measure adversarial robustness.

What is adversarial training?

Adversarial training generates adversarial examples against the current model during training and trains on them alongside the clean data, so the model learns to classify inputs correctly anywhere inside the perturbation budget. It is the most reliable empirical defence against gradient-based attacks.

What is gradient masking?

Gradient masking is when a defence makes the input gradient useless to an attacker, for example by saturating the softmax, so gradient-based attacks fail while the model stays fragile. It is caught by attacking with a different loss such as a margin loss and by checking that a very large perturbation budget still breaks the model.

Adversarial examples, adversarial training and gradient masking

An adversarial example is an input changed by a small, deliberately chosen amount so that a model misclassifies it. The changes follow the gradient of the model's loss with respect to its input, and they can be small enough that a person reading the image would not notice. In this lab you build FGSM and PGD attacks on a digit classifier in PyTorch, measure accuracy across perturbation budgets, harden the model with adversarial training, and then evaluate a defence that only masks gradients: a margin-loss attack and a large-budget sanity check reveal its real robustness.