COMP90073 Trustworthy Machine Learning · Workshop Week 11

Threat Models, Certified Defences and Backdoor Attacks

Four tutorial questions, in the order of the worksheet. Each one brings in the lecture material it needs at the point it is needed; everything else is in the additional material at the end.

Live model the trained network runs in your browser Precomputed measured offline with PyTorch Illustrative toy model a small 2-D model built to show an idea
Question 1 · Lecture 18 (black-box attacks) and lecture 20

Q1 · What can the attacker access?

Problem statement

Explain the different risk models associated with a white-box and a black-box threat model for evasion attacks, from the perspective of someone deploying a model.

OverviewWhat an evasion attack is
Evasion attack: a clean dog image is classified as dog; the same image plus a small perturbation chosen by the attacker is classified as cat by the same fixed-weight classifier. Small change: the norm of delta is at most epsilon. Attack succeeds: f of x plus delta is not y.
The weights stay fixed; the attacker chooses \(\delta\) with \(\lVert\delta\rVert_p \le \epsilon\) so that \(f_\theta(x + \delta) \ne y\). The threat model asks how much the attacker knows about \(f_\theta\) while choosing \(\delta\).
Q1 · Prerequisite

Threat models compared

A threat model fixes the attacker's goal (here: a wrong prediction at test time), capability (here: \(\lVert\delta\rVert_\infty \le \epsilon\)) and knowledge. Only the knowledge changes between the rows below.

AccessAttacker seesHow it finds a directionTypical computation and target-query cost (MNIST)
White-boxarchitecture, weights, gradients, inputs, outputsback-propagation (FGSM, PGD)1 backward pass per gradient; no queries needed beyond the model itself
Black-box: scoresinputs and output probabilitiesfinite differences on the scores (ZOO)1\(2d = 1568\) queries per gradient (fewer with random directions)
Black-box: decisionsinputs and the top label onlywalk along the decision boundary2many thousands of queries per image
No accessnothing about this modelattack a surrogate and transfer30 target queries; cost moves to training a surrogate, and success is not guaranteed

Restricting access changes the attacker's strategy and cost; query and transfer attacks remain possible.

Why defenders plan for white-box access
Models leak
Weights get stolen, open-source base models are reused, and query access allows model extraction.
Weak tests mislead
Defensive distillation and obfuscated-gradient defences looked robust until a stronger, adaptive attacker tried (lecture 20, slides 9 and 11).4
Kerckhoffs's principle
A system should stay secure even if everything except a secret key is public.
Exercise · the cost of black-box access

An attacker estimates the gradient of a score-based MNIST model by central differences, one coordinate at a time:

$$\frac{\partial \ell}{\partial x_i} \approx \frac{\ell(x + h\,\mathbf e_i) - \ell(x - h\,\mathbf e_i)}{2h}.$$
1. How many queries does one full gradient estimate cost?
2. A 10-step PGD attack using these estimates needs how many queries?
3. How many gradient computations does a white-box attacker need for the same 10 steps?
Show solution

\(2 \times 784 = 1568\) queries per gradient, \(15\,680\) for ten steps, against 10 back-propagations for the white-box attacker. Black-box access raised the cost about a thousandfold; it did not stop the attack.

Answer to Q1
  • White-box: the attacker has the model, its gradients, and its inputs and outputs: almost complete visibility.
  • Black-box: the attacker only has access to the model's inputs and outputs.
  • For a defender, black-box is not enough. Query-based and transfer attacks still find adversarial examples, and a model's details can leak. Assume white-box access: it assesses the model under the worst case of what an attacker can know.
  • A defence that only holds against black-box attackers is security through obscurity, and fails as soon as the model leaks.
Cheat notes for Q1 →
Question 2 · Lecture 20, slides 15–23

Q2 · What does a certificate guarantee?

Problem statement

Certified defences using randomised smoothing produce guarantees that adversarial examples do not exist up to a fixed radius. Describe the limitations of these defences. How would you create an operational system that used certified defences to help mitigate the risk of adversarial attacks?

Q2 · Prerequisite

Empirical versus certified robustness

Can an input pass every attack we try and still be vulnerable?
Illustrative toy modelOne input, judged two ways
empirical: eight attacks tried
certified: the largest ball proven safe

Attacks only test the points they try; the thin intrusion slips between them. A certificate covers the whole ball of radius \(r\), so this input is honestly not certified at budget \(\epsilon\) (lecture 20, slide 15).

Q2 · Prerequisite · Lecture 20, slides 19–22

Guided smoothing experiment

Randomised smoothing certifies a new classifier \(g\): the class the base model \(f\) predicts most often under Gaussian noise.5

$$g(x) = \arg\max_{c}\ \Pr\big[f(x + \eta) = c\big], \qquad \eta \sim \mathcal N(0, \sigma^2 I).$$

Count the votes of \(n\) noisy copies, take a lower confidence bound on the winning share, and turn it into a radius:

$$\underline{p_A} = \hat p_A - t, \qquad t = \sqrt{\frac{\ln(1/\alpha)}{2n}}, \qquad R = \sigma\,\Phi^{-1}\big(\underline{p_A}\big).$$

If \(\underline{p_A} \le \tfrac12\) the method abstains. The certificate holds with probability at least \(1 - \alpha\) (here \(\alpha = 0.001\)).6

Derivation · why \(R = \sigma\,\Phi^{-1}(p_A)\)

Take the worst case: all of class \(A\)'s probability on one side of a flat boundary at distance \(r\), and the attacker moving straight towards it. The Gaussian around \(x\) puts \(p_A\) of its mass on \(A\)'s side:

$$\Phi\!\left(\frac{r}{\sigma}\right) = p_A \quad\Longrightarrow\quad r = \sigma\,\Phi^{-1}(p_A).$$

After a move of \(\lVert\delta\rVert_2\) towards the boundary, \(A\) keeps a share \(\Phi\big((r - \lVert\delta\rVert_2)/\sigma\big)\), which stays above \(\tfrac12\) exactly while \(\lVert\delta\rVert_2 < r\).

Footnote: the Neyman–Pearson lemma shows the half-space is indeed the worst case; with a runner-up class \(B\), \(R = \tfrac{\sigma}{2}\big(\Phi^{-1}(\underline{p_A}) - \Phi^{-1}(\overline{p_B})\big)\), which reduces to the formula above when \(\overline{p_B} = 1 - \underline{p_A}\).

Choose a task below. Each one asks a single question about the certificate.
Illustrative toy modelNoisy inputs → votes → confidence bound → certified radius
base model \(f\)estimated smoothed boundary (\(g\))certified ball\(\ell_\infty\) box

Drag the input \(x\) anywhere.

The certificate is computed for the smoothed model \(g\) (solid line), not the base model \(f\) (dashed).

Q2 · Prerequisite · Lecture 20, slide 22

Which norm is certified?

What smoothing certifies
An \(\ell_2\) ball: every \(\delta\) with \(\lVert\delta\rVert_2 < R\).
What image attacks usually use
An \(\ell_\infty\) box: every pixel moves by up to \(\epsilon\). Its corners are at
$$\lVert\delta\rVert_2 = \epsilon\sqrt d .$$
Exercise · slide 22's numbers

\(n = 100\,000\) noisy copies, \(90\,000\) votes for cat, \(\alpha = 0.001\), \(\sigma = 0.25\).

1. The safety margin \(t\):
2. The certified radius \(R\):
3. The \(\ell_2\) radius needed to cover an MNIST \(\ell_\infty\) box with \(\epsilon = 0.1\):
Show solution
$$t = \sqrt{\frac{6.91}{200\,000}} \approx 0.0059, \qquad R = 0.25\,\Phi^{-1}(0.894) \approx 0.312, \qquad 0.1\sqrt{784} = 2.8 .$$

The box needs a certificate nine times larger than the one we can prove.

Q2 · The operational question

An operational decision: who goes to human review?

Your team can review a fixed share of traffic. Can a threshold on the certified radius catch the attacks within that budget?

Assume 90% of inputs are clean and 10% are attacks: half pushed just across the base model's boundary, half pushed further into the wrong class. Inputs whose radius is below the threshold \(\tau\), or that abstain, go to review.

Illustrative toy modelCertified radius of each input, by group
certified radius \(R\) (abstain counts as 0)

Attacks pushed further into the wrong class get large radii: the certificate protects their wrong label. A small radius means we could not certify a large neighbourhood. This can reflect boundary proximity, limited samples, or a conservative bound; it is a reason to look again, not proof of an attack.

Answer to Q2

Limitations.

  • Certified defences typically only cover an \(\ell_2\) threat model.
  • They are conservative by necessity, and need extra computation (many noisy forward passes) to build the guarantee.
  • The guarantee is for the smoothed model \(g\), the estimated most likely class under Gaussian noise, not for the base model.

Operational use.

  • A certificate cannot tell whether an input is under attack: the certified radius may belong to an adversarial example.
  • The radius is a guaranteed lower bound, not a measured distance to the boundary. A small radius means we could not certify a large neighbourhood: this can reflect boundary proximity, limited samples, or a conservative bound. Inputs near the boundary may be adversarial, so send small-radius inputs, and every abstention, to additional review.
Cheat notes for Q2 →
Question 3 · Lecture 21, slide 24

Q3 · Can perturbations help people?

Problem statement

In the lectures it was mentioned that adversarial attacks against CAPTCHA systems have the potential to make the CAPTCHA task more human friendly. How is this possible, and what would be required to do this? Can you think of other systems that can be improved in a similar fashion?

Q3 · Prerequisite

The perception gap and transfer

Perception gap
A small \(\ell_\infty\) perturbation barely changes what a person sees, yet can change what a model predicts.
Transfer
An attack crafted on one model often fools other models for the same task, even with a different architecture.3

Old CAPTCHAs distorted the letters, which hurt people too. An adversarial CAPTCHA keeps the letters clear and aims the noise at solver models.7 If a solver reads each of \(k\) characters correctly with probability \(p\), and its errors are independent, it passes with probability about

$$P_{\text{solve}} \approx p^{k}.$$
Exercise · per character versus per CAPTCHA
1. Per-character accuracy 0.98, 4 characters. Estimated solve rate:
2. Noise drops per-character accuracy to 0.6. Estimated solve rate:
Show solution

\(0.98^4 \approx 0.922\) and \(0.6^4 \approx 0.13\): a moderate drop per character becomes a large drop per CAPTCHA.

Q3 · Experiment

Read a CAPTCHA yourself

Can you still read the digits after the noise that fools the solvers?
  1. Pick a CAPTCHA and a noise budget.
  2. Type the four digits you read in the perturbed version.
  3. Check your reading: the clean digits and what the two solver models read appear afterwards.
PrecomputedClean and perturbed CAPTCHA, side by side
perturbed: read this one
clean
PrecomputedHow often the unseen solver passes a 4-digit CAPTCHA
noise budget \(\epsilon\) · estimated solve rate \(p^4\), assuming independent character errors

The noise was crafted against one solver (a LeNet) yet also defeats a solver it never saw (an MLP): 2000 test digits per budget. Human legibility was not measured; your own reading above is a classroom illustration, not a population result.

Answer to Q3
  • Requirement: access to models designed to defeat CAPTCHAs, so we can construct adversarial examples against them. A CAPTCHA made of such examples should defeat those models.
  • Why it generalises: by transferability, the same examples are also likely to defeat other, undefended solvers.
  • Why it is more human-friendly: the CAPTCHA becomes harder for machines without being harder for people to read.
  • The arms race: solvers can adopt adversarial defences, but no guarantee is infallible, and the competition is unlikely to end with CAPTCHAs that are harder for people.
  • Similar systems: Fawkes photo cloaking, unlearnable examples, adversarial clothing against person detectors, anti-scraping text (lecture 21, slides 20–24).
Cheat notes for Q3 →
Question 4 · w11-tutorial-notebook.ipynb · Lecture 19, slides 20–29 · Lecture 21, slides 3–11

Q4 · How does a backdoor work?

Problem statement

Backdoor attack demonstration notebook: poison 2% of MNIST's training images with a small white square and the label 0, train a LeNet, measure clean accuracy and attack success rate, then try to find the poisoned images with outlier detection (SLOF) and cognitive distillation.

Two numbers measure a backdoor (lecture 19, slide 29)8
Clean accuracy (CA) · should stay high
$$\text{CA} = \frac{\#\{f(x) = y\}}{\#\text{clean test images}}$$

High CA: nobody notices the backdoor.

Attack success rate (ASR) · the attacker wants it high
$$\text{ASR} = \frac{\#\{f(x') = y_t \text{ and } y \ne y_t\}}{\#\{y \ne y_t\}}$$

Count only triggered images that are not already the target class.

The code below is the workshop notebook with a few small fixes so that every number on this page comes from the code shown (see changes to the notebook code). We ran it on a CPU with the notebook's seeds and exported the trained LeNet; its forward pass in your browser matches PyTorch to within \(4 \times 10^{-6}\). Code cells are collapsed; cell numbers count every cell of the notebook from 0.

Q4 · Experiment · notebook cells 10–12 and 23

Live probe of the backdoor

Does the small patch change the prediction? Does it work in another corner? Does a dimmer patch still work?
Live modelThe same digit, without and with the trigger
clean input
+ trigger →
triggered input

The backdoor is precise: it fires for a bright \(3\times3\) (or larger) square in the trained corner, and barely anywhere else. One image shows the mechanism; the ASR across many images measures it.

Explore further · draw your own digit, and the ASR map for every trigger position
Live modelDraw a digit; click to move the trigger Precomputed ASR map
Unusual handwriting is another source of errors, independent of the backdoor. The ASR map was measured on 1000 non-zero test digits per trigger position: 79% on average within 2 pixels of the trained position, 0.5% further away.
Cell 11Stamp a square trigger · show code
Cell 11Stamp a square trigger
def add_square_trigger_numpy(img: np.ndarray, size: int = 3, value: int = 255, location: str = "bottom_left", pad: int = 1):
    """
    Add a black/white square trigger to a (1,H,W) numpy array in [0,255] uint8.
    - size: side length in pixels
    - value: pixel intensity (0=black, 255=white)
    - location: position of the square ('bottom_left' etc.)
    - pad: offset from the edge
    Returns a modified copy of the image.
    """
    assert img.ndim == 3 and img.shape[0] == 1, "expected (1,H,W)"
    _, H, W = img.shape
    arr = img.copy()

    if location == "bottom_left":
        r0, c0 = H - pad - size, pad
    elif location == "bottom_right":
        r0, c0 = H - pad - size, W - pad - size
    elif location == "top_left":
        r0, c0 = pad, pad
    elif location == "top_right":
        r0, c0 = pad, W - pad - size
    else:
        raise ValueError(f"Unknown location: {location}")

    r1, c1 = r0 + size, c0 + size
    arr[:, r0:r1, c0:c1] = value
    return arr
Cell 12Preview: the 3 × 3 bottom-left trigger on the cell 9 images · show code
Cell 12Preview: the 3 × 3 bottom-left trigger on the cell 9 images
poisoned_imgs = []
for i in range(len(imgs)):
    poisoned_imgs.append(torch.from_numpy(add_square_trigger_numpy(imgs[i].numpy(), size=3, value=255, location="bottom_left", pad=1)))
show_image_grid(list(poisoned_imgs), nrow=10)
Plot from cell 12Plot produced by cell 12
Cell 23Predict ten triggered test digits · show code
Cell 23Predict ten triggered test digits
backdoored_test_imgs = []
for i in range(10):
    backdoored_test_imgs.append(mnist_test_poisoned.data[i].unsqueeze(0))
show_image_grid(list(backdoored_test_imgs), nrow=10)
model.eval()
pred = model(torch.stack(backdoored_test_imgs).to(device)/255.0).argmax(dim=1)
print("Predicted labels for backdoored test samples:", pred)
# you should see all predicted as target label 0
Plot from cell 23Plot produced by cell 23
Output of cell 23
Predicted labels for backdoored test samples: tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
Q4 · Notebook cells 13–16

The poisoned dataset

Cell 15 subclasses torchvision.datasets.MNIST (reusing the MNIST files already downloaded) and overwrites the stored images and labels, simulating a dataset that was poisoned before you downloaded it. A fixed seed picks

$$n_{\text{poison}} = \operatorname{round}(0.02 \times 60\,000) = 1200$$

images. Each gets the trigger and the label 0; 114 of them were already zeros.

Cell 15InMemoryPoisonedMNIST · show code
Cell 15InMemoryPoisonedMNIST
# Cell: InMemoryPoisonedMNIST - subclass of torchvision.datasets.MNIST
from typing import Optional, Dict, Any, Sequence
import os
import json
import random
import numpy as np
import torch
from torchvision.datasets import MNIST

class InMemoryPoisonedMNIST(MNIST):
    """
    Subclass of torchvision.datasets.MNIST that can apply an *in-memory*
    deterministic data-poisoning modification to the dataset tensors.

    After calling `apply_poison(...)` the underlying `self.data` and `self.targets`
    are modified (uint8 images in `self.data` are overwritten in-place).
    This simulates a dataset that was downloaded already poisoned.

    Parameters
    ----------
    root : str
        dataset root passed to MNIST
    train : bool
        train or test split (poisoning typically applied to train=True)
    download : bool
        whether to download
    """
    @property
    def raw_folder(self) -> str:
        # Reuse ./data/MNIST/raw (by default torchvision would download a second copy
        # into ./data/InMemoryPoisonedMNIST/raw, named after this class)
        return os.path.join(self.root, "MNIST", "raw")

    def __init__(self, root: str = "./data", train: bool = True, download: bool = True, **kwargs):
        super().__init__(root=root, train=train, download=download, **kwargs)
        # internal bookkeeping
        self.poisoned_indices: Optional[Sequence[int]] = None
        self.poison_metadata: Optional[Dict[str, Any]] = None
        # keep a small in-memory copy if user wants to be able to restore later
        self._orig_data = None
        self._orig_targets = None

    def _compute_coords(self, H: int, W: int, size: int, location: str, pad: int):
        """Helper: compute (r0,r1,c0,c1) for square given image HxW."""
        s = int(size)
        if location == "bottom_left":
            r0, c0 = H - pad - s, pad
        elif location == "bottom_right":
            r0, c0 = H - pad - s, W - pad - s
        elif location == "top_left":
            r0, c0 = pad, pad
        elif location == "top_right":
            r0, c0 = pad, W - pad - s
        elif location == "center":
            r0, c0 = (H - s) // 2, (W - s) // 2
        else:
            raise ValueError(f"Unknown location: {location}")
        r1, c1 = r0 + s, c0 + s
        # clamp
        r0, r1 = max(0, r0), min(H, r1)
        c0, c1 = max(0, c0), min(W, c1)
        return r0, r1, c0, c1

    def apply_poison(
        self,
        p_poison: float = 0.02,
        target_label: int = 0,
        size: int = 3,
        value: int = 255,
        location: str = "bottom_left",
        pad: int = 1,
        seed: int = 42,
        overwrite: bool = True,  # kept for API compatibility (no-op here)
        save_indices_path: Optional[str] = None,
        save_metadata_path: Optional[str] = None,
        keep_original_copy: bool = False,
    ) -> Dict[str, Any]:
        """
        Apply in-memory poisoning using add_square_trigger_numpy on (1,H,W) arrays.
        """
        assert 0.0 <= p_poison <= 1.0, "p_poison must be in [0,1]"
        n = len(self.data)
        n_poison = int(round(p_poison * n))
        rng = random.Random(seed)
        poisoned_idx = sorted(rng.sample(range(n), n_poison))

        # optional backup
        if keep_original_copy and (self._orig_data is None):
            self._orig_data = self.data.clone()
            self._orig_targets = self.targets.clone()

        # ensure tensor types
        if not isinstance(self.data, torch.Tensor):
            self.data = torch.tensor(self.data, dtype=torch.uint8)
        else:
            # work on CPU uint8
            if self.data.dtype != torch.uint8:
                self.data = (self.data.clone() * 255).to(torch.uint8)
            if self.data.device.type != "cpu":
                self.data = self.data.cpu()

        if not isinstance(self.targets, torch.Tensor):
            self.targets = torch.tensor(self.targets)

        # apply trigger via numpy helper per selected index
        for idx in poisoned_idx:
            # self.data[idx]: [H, W] uint8
            hw = self.data[idx].numpy()                                # (H, W), uint8 (shared mem view)
            chw = hw[None, ...]                                        # (1, H, W)
            poisoned = add_square_trigger_numpy(
                chw, size=size, value=value, location=location, pad=pad
            )                                                          # (1, H, W), uint8
            self.data[idx] = torch.from_numpy(poisoned[0])             # back to [H, W] uint8
            self.targets[idx] = int(target_label)

        metadata = {
            "n_total": n,
            "n_poison": n_poison,
            "p_poison": float(p_poison),
            "target_label": int(target_label),
            "trigger": {"size": int(size), "value": int(value), "location": location, "pad": int(pad)},
            "seed": int(seed),
            "poisoned_indices": poisoned_idx,
        }
        self.poisoned_indices = poisoned_idx
        self.poison_metadata = metadata

        if save_indices_path is not None:
            np.save(save_indices_path, np.array(poisoned_idx, dtype=np.int32))
        if save_metadata_path is not None:
            with open(save_metadata_path, "w") as f:
                json.dump(metadata, f, indent=2)

        return metadata

    def restore_original(self):
        """
        Restore original data/targets if keep_original_copy=True was used.
        """
        if self._orig_data is None or self._orig_targets is None:
            raise RuntimeError("No original copy stored. Create with keep_original_copy=True in apply_poison().")
        self.data = self._orig_data.clone()
        self.targets = self._orig_targets.clone()
        self.poisoned_indices = None
        self.poison_metadata = None
Cell 16Poison 2% with a 3 × 3 bottom-left trigger, label 0 · show code
Cell 16Poison 2% with a 3 × 3 bottom-left trigger, label 0
# -------------------------
# Example usage
# -------------------------
root = "./data"
poisoned_mnist = InMemoryPoisonedMNIST(root=root, train=True, download=True, transform=transform)

# Apply a 2% poisoning: bottom-left 3x3 white square -> target label 0
meta = poisoned_mnist.apply_poison(
    p_poison=0.02,
    target_label=0,
    size=3,
    value=255,
    location="bottom_left",
    pad=1,
    seed=42,
    save_indices_path="./data/poisoned_indices.npy",
    save_metadata_path="./data/poison_metadata.json",
    keep_original_copy=True,   # optional: allow restore if you want
)

poisoned_imgs = []
for idx in poisoned_mnist.poisoned_indices[:10]:
    poisoned_imgs.append(poisoned_mnist.data[idx].unsqueeze(0))
show_image_grid(list(poisoned_imgs), nrow=10)
Plot from cell 16Plot produced by cell 16
Q4 · Notebook cells 17–22

Training results: is the backdoor stealthy and effective?

Clean accuracy after 5 epochs
98.5%
Normal for a LeNet on MNIST: nothing looks wrong.
Attack success rate after 5 epochs
98.9%
Triggered test digits other than 0 that the model calls 0.
Exercise · why the ASR leaves out the zeros

The test set has 980 zeros out of 10 000. Suppose we stamped the trigger on every test image and counted every prediction of 0 as a success.

A model with no backdoor predicts 0 for every true zero and for nothing else. What "ASR" would that count report?
Show solution

\(980/10\,000 = 0.098\): a model with no backdoor would look almost 10% successful. That is why cell 21 removes the zeros from the triggered test set. In our run, after epoch 1 the backdoor has not formed yet (ASR 0.8%); counting the zeros would have reported 10.5%.

PrecomputedClean accuracy and attack success over training
clean accuracyattack success rate
PrecomputedConfusion matrices after 5 epochs
clean test set
triggered test set
Rows are true digits, columns predictions. With the trigger, every row collapses onto column 0.
Extension · the loss signal behind anti-backdoor learning
loss on the 1200 poisoned imagesloss on 1200 clean images
Log loss scale; epoch 1 sampled every 47 batches. The poisoned images keep a high loss in epoch 1 and fall below the clean ones from epoch 2: the signal anti-backdoor learning looks for (lecture 21, slide 6).
Cell 18LeNet-5 style network · show code
Cell 18LeNet-5 style network
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from typing import Tuple, Optional

# -------------------------
# LeNet-5 style model
# -------------------------
class LeNet(nn.Module):
    """
    LeNet-5 style convnet adapted for MNIST (1x28x28 input).
    Architecture (classic):
      Conv1(1->6, kernel=5) -> ReLU -> AvgPool(2)
      Conv2(6->16, kernel=5) -> ReLU -> AvgPool(2)
      Flatten -> FC(16*4*4 -> 120) -> ReLU
             -> FC(120 -> 84) -> ReLU
             -> FC(84 -> num_classes)
    """
    def __init__(self, num_classes: int = 10):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 6, kernel_size=5)      # 28 -> 24
        self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2)  # 24 -> 12
        self.conv2 = nn.Conv2d(6, 16, kernel_size=5)     # 12 -> 8
        self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2)  # 8 -> 4

        self.fc1 = nn.Linear(16 * 4 * 4, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, num_classes)

        # optional small init (closer to original LeNet)
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, nonlinearity='relu')
                if m.bias is not None:
                    nn.init.zeros_(m.bias)
            elif isinstance(m, nn.Linear):
                nn.init.kaiming_uniform_(m.weight, nonlinearity='relu')
                if m.bias is not None:
                    nn.init.zeros_(m.bias)
    
    def forward_features(self, x: torch.Tensor) -> torch.Tensor:
        x = F.relu(self.conv1(x))
        x = self.pool1(x)
        x = F.relu(self.conv2(x))
        x = self.pool2(x)
        x = x.view(x.shape[0], -1)
        x = F.relu(self.fc1(x))
        return x

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = self.forward_features(x)
        x = F.relu(self.fc2(x))
        x = self.fc3(x)  # logits
        return x
Cell 20Meters, accuracy, evaluate and train · show code
Cell 20Meters, accuracy, evaluate and train
from tqdm import tqdm

class AverageMeter(object):
    """Computes and stores the average and current value"""

    def __init__(self):
        self.reset()

    def reset(self):
        self.val = 0
        self.avg = 0
        self.sum = 0
        self.count = 0
        self.max = 0

    def update(self, val, n=1):
        self.val = val
        self.sum += val * n
        self.count += n
        self.avg = self.sum / self.count
        self.max = max(self.max, val)

def accuracy(output, target, topk=(1,)):
    maxk = max(topk)

    batch_size = target.size(0)
    _, pred = output.topk(maxk, 1, True, True)
    pred = pred.t()
    correct = pred.eq(target.view(1, -1).expand_as(pred))

    res = []
    for k in topk:
        correct_k = correct[:k].float().sum()
        res.append(correct_k.mul_(1/batch_size))
    return res

@torch.no_grad()
def evaluate(model, eval_data_loader):
    model.eval()
    # Training Evaluations
    loss_meters = AverageMeter()
    acc_meters = AverageMeter()
    loss_list, correct_list = [], []
    for i, data in enumerate(eval_data_loader):
        # Prepare batch data
        images, labels = data
        images = images.to(device, non_blocking=True)
        labels = labels.to(device, non_blocking=True)
        batch_size = images.shape[0]
        logits = model(images)
        loss = F.cross_entropy(logits, labels, reduction='none')
        loss_list += loss.detach().cpu().numpy().tolist()
        loss = loss.mean().item()
        # Calculate acc
        acc = accuracy(logits, labels, topk=(1,))[0].item()

        # Update Meters
        loss_meters.update(loss, batch_size)
        acc_meters.update(acc, batch_size)

        _, predicted = torch.max(logits.data, 1)
        correct = (predicted == labels)
        correct_list += correct.detach().cpu().numpy().tolist()
    stats = {   
        "eval_loss": loss_meters.avg,
        "eval_acc": acc_meters.avg,
        "eval_loss_list": loss_list,
        "eval_correct_list": correct_list,
    }
    return stats


def train(model, optimizer, train_loader):

    # Set Meters
    loss_meters = AverageMeter()
    acc_meters = AverageMeter()

    # Training
    model.train()
    pbar = tqdm(train_loader)
    for i, data in enumerate(pbar):
        # Prepare batch data
        images, labels = data
        images = images.to(device, non_blocking=True)
        labels = labels.to(device, non_blocking=True)
        batch_size = images.shape[0]
        model.zero_grad()
        optimizer.zero_grad()

        # Objective function
        logits = model(images)
        loss = F.cross_entropy(logits, labels)
        
        # Optimize
        loss.backward()
        optimizer.step()
        # Calculate acc
        loss = loss.item()
        acc = accuracy(logits, labels, topk=(1,))[0].item()

        # Update Meters
        loss_meters.update(loss, batch_size)
        acc_meters.update(acc, batch_size)
        pbar.set_postfix(loss=loss, acc=acc*100)

    stats = {
        "train_loss": loss_meters.avg,
        "train_acc": acc_meters.avg,
    }

    return stats
Cell 21Hyperparameters, triggered test set without the zeros, loaders · show code
Cell 21Hyperparameters, triggered test set without the zeros, loaders
# ---------- Hyperparams ----------
root = "./data"
epochs = 5
lr = 0.01
momentum = 0.9
weight_decay = 5e-4
num_workers = 0
# ---------------------------------
model = LeNet(num_classes=10).to(device)
optimizer = torch.optim.SGD(model.parameters(), lr=lr, momentum=momentum, weight_decay=weight_decay)

# Triggered test set for the attack success rate (ASR):
# every test image gets the bottom-left 3x3 white square and the label 0
mnist_test_poisoned = InMemoryPoisonedMNIST(root=root, train=False, download=True, transform=transform)
meta_test = mnist_test_poisoned.apply_poison(
    p_poison=1.0,
    target_label=0,
    size=3,
    value=255,
    location="bottom_left",
    pad=1,
)
# Drop the digits that are already 0: predicting 0 for them is not a success of the attack
non_target = mnist_test.targets != 0
mnist_test_poisoned.data = mnist_test_poisoned.data[non_target]
mnist_test_poisoned.targets = mnist_test_poisoned.targets[non_target]

train_loader = DataLoader(poisoned_mnist, batch_size=64, shuffle=True, num_workers=num_workers)
test_loader = DataLoader(mnist_test, batch_size=256, shuffle=False, num_workers=num_workers)
test_poisoned_loader = DataLoader(mnist_test_poisoned, batch_size=256, shuffle=False, num_workers=num_workers)
Cell 22Training loop · show code
Cell 22Training loop
# ------------------------- training loop -------------------------
for epoch in range(1, epochs + 1):
    print(f"Epoch {epoch}/{epochs}")
    train_stats = train(model, optimizer, train_loader)
    eval_stats_clean = evaluate(model, test_loader)
    eval_stats_poisoned = evaluate(model, test_poisoned_loader)

    print(f"  Train                  - loss: {train_stats['train_loss']:.4f}, acc: {train_stats['train_acc']*100:.2f}%")
    print(f"  Clean test set         - CA:  {eval_stats_clean['eval_acc']*100:.2f}%")
    print(f"  Triggered test (non-0) - ASR: {eval_stats_poisoned['eval_acc']*100:.2f}%")
    print("-" * 40)
# Expect both clean accuracy and attack success rate around 98-99% after 5 epochs
Output of cell 22
Epoch 1/5
  Train                  - loss: 0.3828, acc: 89.37%
  Clean test set         - CA:  96.74%
  Triggered test (non-0) - ASR: 0.84%
----------------------------------------
Epoch 2/5
  Train                  - loss: 0.1357, acc: 96.11%
  Clean test set         - CA:  98.09%
  Triggered test (non-0) - ASR: 97.85%
----------------------------------------
Epoch 3/5
  Train                  - loss: 0.0697, acc: 97.84%
  Clean test set         - CA:  97.87%
  Triggered test (non-0) - ASR: 99.76%
----------------------------------------
Epoch 4/5
  Train                  - loss: 0.0559, acc: 98.22%
  Clean test set         - CA:  98.44%
  Triggered test (non-0) - ASR: 98.26%
----------------------------------------
Epoch 5/5
  Train                  - loss: 0.0472, acc: 98.50%
  Clean test set         - CA:  98.52%
  Triggered test (non-0) - ASR: 98.88%
----------------------------------------
Q4 · Notebook cells 24–25

Do poisoned images look different inside the model?

t-distributed stochastic neighbour embedding (t-SNE) draws the 120-dimensional features in two dimensions, keeping neighbours close.9

Precomputed1200 clean and 1200 poisoned training images
colour: clean (circles) or poisoned (crosses)
colour: original digit

Poisoned images form their own clusters, separate from the clean ones, and each poisoned cluster still holds mostly one original digit: the switch to "0" happens in later layers. That separation is what outlier detection can exploit.

Cell 25t-SNE of clean and poisoned training features · show code
Cell 25t-SNE of clean and poisoned training features
from sklearn.manifold import TSNE

# get all indices and subtract poisoned ones
all_idx = np.arange(len(poisoned_mnist))
poisoned_idx = set(poisoned_mnist.poisoned_indices)
clean_candidates = [i for i in all_idx if i not in poisoned_idx]

poisoned_imgs = [poisoned_mnist.data[idx] for idx in poisoned_idx]
poisoned_imgs = torch.stack(poisoned_imgs).unsqueeze(1)  # (N, 1, H, W)
poisoned_imgs = poisoned_imgs.to(device) / 255.0  # normalize to [0,1]

clean_imgs = [poisoned_mnist.data[idx] for idx in clean_candidates[:len(poisoned_idx)]]
clean_imgs = torch.stack(clean_imgs).unsqueeze(1)  # (N, 1, H, W)
clean_imgs = clean_imgs.to(device) / 255.0  # normalize to [0,1]

model.eval()
with torch.no_grad():
    poisoned_feats = model.forward_features(poisoned_imgs).cpu()
    clean_feats = model.forward_features(clean_imgs).cpu()

# Concatenate and run t-SNE
X = torch.cat([clean_feats, poisoned_feats], dim=0).numpy()        # [N, D]
is_poison = np.array([0]*len(clean_feats) + [1]*len(poisoned_feats), dtype=int)

tsne = TSNE(n_components=2, perplexity=30, max_iter=1000,
            learning_rate=200, init="pca", random_state=42, verbose=1)
X2 = tsne.fit_transform(X)

# Plot
plt.figure(figsize=(7.5, 6))
plt.scatter(X2[is_poison==0, 0], X2[is_poison==0, 1],
            s=10, marker='o', facecolors='none', edgecolors='#1f77b4', linewidths=0.8, label='Clean')
plt.scatter(X2[is_poison==1, 0], X2[is_poison==1, 1],
            s=12, marker='x', c='#d62728', label='Triggered')

plt.title("t-SNE of LeNet penultimate features\nClean vs Triggered ")
plt.xticks([]); plt.yticks([]); plt.legend(frameon=False)
plt.tight_layout()
plt.show()
Q4 · Notebook cells 26–29 · Lecture 21, slides 10–11

Can outlier detection find the poisons?

The lecture's simplified local outlier factor (SLOF), a variant of LOF,10 compares a point's neighbourhood radius with its neighbours' radii:11

$$\operatorname{SLOF}(q) = \frac{1}{k}\sum_{o\,\in\,\operatorname{NN}_k(q)} \frac{\operatorname{k-dist}(q)}{\operatorname{k-dist}(o)} .$$

Cell 27 computes a simpler score, the mean distance to the \(k\) nearest neighbours among all 60 000 training images:

$$s(q) = \frac{1}{k}\sum_{o\,\in\,\operatorname{NN}_k(q)} \lVert f(q) - f(o)\rVert_2 .$$
k-NN score \(s(q)\), cells 27–29
AUC 0.83
poisons sit far from most clean images
Lecture's SLOF ratio
AUC 0.62
a dense group of poisons looks locally normal

AUC is the probability that a random poisoned image scores higher than a random clean one (0.5 is chance).

Extension · why a dense cluster of poisons fools the ratio
Illustrative toy modelNeighbourhoods of a poisoned point \(q\)
cleanpoisoned\(\operatorname{k-dist}(q)\)neighbours' k-dist

PrecomputedROC curves and score distributions
false positive rate against true positive rate
k-NN mean distance (up to the 99.5th percentile)
k-NN score \(s(q)\)SLOF ratiocleanpoisoned
Cell 27The k-NN distance score · show code
Cell 27The k-NN distance score
# -------------------------
# k-NN distance outlier score
# -------------------------
@torch.no_grad()
def knn_score(f: torch.Tensor, k: int = 16, chunk_size: int = 2000) -> torch.Tensor:
    """
    Mean L2 distance from each row of `f` (N x D) to its k nearest
    neighbours among all N rows, excluding itself.

    Args:
        f: [N, D] feature tensor (float). Put it on the desired device.
        k: number of nearest neighbours to average.
        chunk_size: rows per distance block, so the N x N matrix is never stored.

    Returns:
        score: [N] tensor, higher means more outlier-like.

    The lecture's SLOF goes one step further and divides k-dist(q)
    by the k-dist of q's neighbours.
    """
    assert f.dim() == 2, "f must be [N, D]"
    N = f.size(0)
    k = max(1, min(k, N - 1))
    score = torch.empty(N, device=f.device)
    for i in range(0, N, chunk_size):
        d = torch.cdist(f[i:i + chunk_size], f, p=2)               # [chunk, N]
        rows = torch.arange(d.size(0), device=f.device)
        d[rows, rows + i] = float("inf")                           # remove self-distance
        nearest = torch.topk(d, k, dim=1, largest=False).values    # [chunk, k]
        score[i:i + chunk_size] = nearest.mean(dim=1)
    return score
Cell 28Score every training sample · show code
Cell 28Score every training sample
eval_train_loader = DataLoader(poisoned_mnist, batch_size=256, shuffle=False, num_workers=num_workers)
model.eval()
with torch.no_grad():
    feats = torch.cat([model.forward_features(images.to(device)) for images, _ in eval_train_loader])

# neighbours are searched over all 60 000 training samples
scores = knn_score(feats, k=16).cpu().numpy()
print(f"Computed k-NN scores for {len(scores)} training samples.")
Output of cell 28
Computed k-NN scores for 60000 training samples.
Cell 29ROC AUC against the true poison labels · show code
Cell 29ROC AUC against the true poison labels
from sklearn.metrics import roc_auc_score

y = np.zeros(len(poisoned_mnist), dtype=int)
y[poisoned_mnist.poisoned_indices] = 1  # 1 = poisoned
# ROC AUC
roc_auc = roc_auc_score(y, scores)
print(f"ROC AUC: {roc_auc*100:.2f}%")
Output of cell 29
ROC AUC: 83.14%
Q4 · Notebook cells 30–33 · Lecture 21, slides 8–9

Can cognitive distillation find them?

Cognitive distillation learns, for each image, the smallest mask \(m\) that keeps the model's output when the rest is replaced by noise:12

$$\min_m\ \lVert f(x) - f(x \odot m + (1 - m) \odot \delta)\rVert_1 + \alpha\,\lVert m\rVert_1 + \beta\,\operatorname{TV}(m).$$
PrecomputedMasks and mask sizes (cell 31's settings)
More examples · ten poisoned and ten clean images with their masks
mask size \(\lVert m\rVert_1\)
Poisoned images
mean \(\lVert m\rVert_1\) = 18.1
Clean images
mean \(\lVert m\rVert_1\) = 3.6
poisoned (all 1200)clean (3000 sampled)

The masks of poisoned images sit on the trigger and stay large; with this sparsity weight (\(\gamma = 0.05\)) the masks of clean images shrink to almost nothing. Flagging large masks finds the poisons with AUC 0.91.

The lecture's expected signal is smaller masks for poisoned images; this run shows the opposite ranking under the chosen settings, so we flag larger masks here.

Extension · ROC curve of the mask score
false positive rate against true positive rate, flagging large masks
Cell 30Total variation and CognitiveDistillation · show code
Cell 30Total variation and CognitiveDistillation
def total_variation_loss(img, weight=1):
    b, c, h, w = img.size()
    tv_h = torch.pow(img[:, :, 1:, :]-img[:, :, :-1, :], 2).sum(dim=[1, 2, 3])
    tv_w = torch.pow(img[:, :, :, 1:]-img[:, :, :, :-1], 2).sum(dim=[1, 2, 3])
    return weight*(tv_h+tv_w)/(c*h*w)


class CognitiveDistillation(nn.Module):
    def __init__(self, lr=0.1, p=1, gamma=0.01, beta=1.0, num_steps=100, mask_channel=1, norm_only=False):
        super(CognitiveDistillation, self).__init__()
        self.p = p
        self.gamma = gamma
        self.beta = beta
        self.num_steps = num_steps
        self.l1 = torch.nn.L1Loss(reduction='none')
        self.lr = lr
        self.mask_channel = mask_channel
        self.get_features = False
        self._EPSILON = 1.e-6
        self.norm_only = norm_only

    def get_raw_mask(self, mask):
        mask = (torch.tanh(mask) + 1) / 2
        return mask

    def forward(self, model, images):
        model.eval()
        if torch.min(images) < 0 or torch.max(images) > 1:
            raise ValueError("images should be in [0, 1]")
        b, c, h, w = images.shape
        mask = torch.ones(b, self.mask_channel, h, w).to(images.device)
        mask_param = nn.Parameter(mask)
        optimizerR = torch.optim.Adam([mask_param], lr=self.lr, betas=(0.1, 0.1))
        logits = model(images).detach()
        for step in range(self.num_steps):
            optimizerR.zero_grad()
            mask = self.get_raw_mask(mask_param).to(images.device)
            x_cp = images * mask + (1-mask) * torch.rand(b, c, 1, 1).to(images.device)
            adv_logits = model(x_cp)
            loss = self.l1(adv_logits, logits).mean(dim=1)
            norm = torch.norm(mask, p=self.p, dim=[1, 2, 3])
            norm = norm * self.gamma
            loss_total = loss + norm + self.beta * total_variation_loss(mask)
            loss_total.mean().backward()
            optimizerR.step()
        mask = self.get_raw_mask(mask_param).detach().cpu()
        return mask.detach()
Cell 31Masks for ten poisoned images · show code
Cell 31Masks for ten poisoned images
cognitive_distiller = CognitiveDistillation(lr=0.1, p=1, gamma=0.05, beta=1.0, num_steps=200, mask_channel=1)
# Get some poisoned samples
poisoned_samples = []
for idx in poisoned_mnist.poisoned_indices[:10]:
    poisoned_samples.append(poisoned_mnist.data[idx].unsqueeze(0))
poisoned_samples = torch.stack(poisoned_samples).to(device) / 255.0  # (N, 1, H, W) in [0,1]
# Run cognitive distillation to get masks
masks = cognitive_distiller(model, poisoned_samples)  # (N, 1, H, W) in [0,1]
# Show poisoned samples
show_image_grid(list(poisoned_samples), nrow=10)
# Show masks
show_image_grid(list(masks), nrow=10)
# Overlay masks on images
overlayed = poisoned_samples.cpu() * masks + (1 - masks) * 0.
show_image_grid(list(overlayed), nrow=10)
Cell 32Mask L1 norm for 1200 poisoned and 3000 clean images · show code
Cell 32Mask L1 norm for 1200 poisoned and 3000 clean images
# All 1200 poisoned images and 3000 random clean ones (all 60 000 is slow on a CPU)
rng = np.random.RandomState(0)
clean_pool = np.setdiff1d(np.arange(len(poisoned_mnist)), poisoned_mnist.poisoned_indices)
clean_idx = rng.choice(clean_pool, 3000, replace=False)
subset_idx = np.concatenate([poisoned_mnist.poisoned_indices, clean_idx])
subset_imgs = poisoned_mnist.data[subset_idx].unsqueeze(1).to(device) / 255.0  # [0,1]

model.eval()
masks = torch.cat([cognitive_distiller(model, subset_imgs[i:i + 600])
                   for i in range(0, len(subset_imgs), 600)])
cd_scores = masks.norm(p=1, dim=[1, 2, 3]).numpy()  # L1 norm of each mask

print(f"Computed Cognitive Distillation scores for {len(cd_scores)} training samples.")
Output of cell 32
Computed Cognitive Distillation scores for 4200 training samples.
Cell 33ROC AUC of the mask norm · show code
Cell 33ROC AUC of the mask norm
y_sub = np.r_[np.ones(len(poisoned_mnist.poisoned_indices)), np.zeros(len(clean_idx))].astype(int)
# With gamma = 0.05 the masks of clean images shrink to almost nothing,
# while those of poisoned images keep the trigger: a large mask flags a poison.
print(f"Mean mask L1 norm: poisoned {cd_scores[y_sub == 1].mean():.1f}, clean {cd_scores[y_sub == 0].mean():.1f}")
roc_auc = roc_auc_score(y_sub, cd_scores)
print(f"ROC AUC: {roc_auc*100:.2f}%")
Output of cell 33
Mean mask L1 norm: poisoned 18.1, clean 3.6
ROC AUC: 91.49%
Q4 · Notebook cell 34

Take-home challenges

A stealthier trigger
Blended, warped or striped triggers (lecture 19, slide 23). Measure CA, ASR and detection AUC together.
Better detection
Track per-sample loss over the first epochs, or compare poisons with real zeros using a class-conditional score.
Remove the backdoor
Recover the trigger with Neural Cleanse (lecture 21, slide 12), then fine-tune, prune or unlearn, and re-measure CA and ASR.
Cheat notes for Q4 →

Additional material

Additional material

The lecture recap below covers lectures 19–21 in order, including material the four questions do not need. Each section is independent; the interactive figures here are optional extensions.

Lecture recap · Lecture 19, slides 3–6

A1 · Training-time attacks: what the attacker changes

Last week's attacks changed the test input. A training-time attack changes what the model learns (slide 3).

Training data \(\mathcal D\)poisoning
→
Training procedure \(\mathcal A\)order, energy
→
Parameters \(\theta\)shared models
→
Test input \(x\)last week
→
Prediction

Orange: what a training-time attacker can touch. Dashed: where last week's test-time attacker acted.

Four attacker goals (slide 3)
1
Slower, costlier model
Every prediction costs more energy and time.
2
Worse on all inputs
Accuracy drops for every class, e.g. \(95\% \to 70\%\).
3
Worse on chosen inputs
One target fails; everything else looks normal.
4
Hidden behaviour
A backdoor the attacker switches on later with a trigger.
Three ways to poison a record \((x, y)\) (slide 5)
Label flipping

$$(x,\ y) \to (x,\ y')$$

Same input, wrong label.

wrong labelno trigger needed
Clean-label

$$(x,\ y) \to (x + \delta,\ y)$$

Slightly changed input, correct label: a label audit finds nothing.

label correctno trigger needed
Backdoor

$$(x,\ y) \to (x \oplus \text{trigger},\ y_t)$$

Trigger added, label set to the attacker's target.

label often wrongtrigger at test time
Illustrative toy modelA few bad records move the boundary (slide 4)
class Aclass Bpoisoned record (labelled B)learned boundary
Click anywhere to add a training record labelled B at that spot (label flipping), or switch mode and drag a class-B point towards the test point (clean label: the label stays correct; real clean-label poisons only move \(x\) slightly). The logistic regression retrains after every change. The ringed test point is never touched.
Lecture recap · Lecture 19, slides 7–19

A2 · Optimised poisons: two nested problems

Choosing the most harmful poison is a bilevel problem: one optimisation nested inside another (slide 7).

Outer problem · the attacker picks the poison
$$\max_{x_c}\ L\big(\mathcal D_{\text{val}};\ w^\star\big)$$
Inner problem · the defender trains on the poisoned data
$$w^\star = \arg\min_{w}\ L\big(\mathcal D_{\text{tr}} \cup \{(x_c, y_c)\};\ w\big)$$
Biggio et al.: move \(x_c\) uphill on the validation error, retrain the SVM, repeat (slides 7–9).13
Three ways to solve it
Biggio et al. (2012)

Victim: support vector machine.

Idea: exact gradient of the validation error from the SVM's optimality conditions.

1 poison pointslides 7–9
Poison Frogs (2018)

Idea: feature collision. The poison looks like a cat but has the target dog's features.

Needs: the victim's feature extractor \(f\).

1 poison if only the last layer trains≈50 if all layers trainslides 10–14
Witches' Brew (2021)

Idea: gradient matching. Poisons push the weights the same way as "label the target as cat".

Works when the victim trains from scratch.

few poisonsslides 15–16

Poison Frogs14 keeps the poison near a base image \(b\) while matching the target \(t\) in feature space:

$$\min_{x}\ \lVert f(x) - f(t)\rVert_2^2 + \beta\,\lVert x - b\rVert_2^2 .$$

Witches' Brew15 aligns the poisons' training gradient with the attacker's desired gradient:

$$\min_{\Delta}\ 1 - \cos\!\Big(\nabla_\theta \mathcal L\big(f_\theta(t), y_{\text{adv}}\big),\ \tfrac{1}{P}\textstyle\sum_{i} \nabla_\theta \mathcal L\big(f_\theta(x_i + \Delta_i), y_i\big)\Big), \qquad \lVert\Delta_i\rVert_\infty \le \epsilon .$$
Attacks on the procedure, with no change to the data
Batch reordering (slide 18)
Same data, attacker-chosen order: one bad epoch can slow or reset learning.
Sponge poisoning (slide 19)
Training pushes activations away from zero, so every prediction costs more energy.
Derivation · Poison Frogs' backward step is a weighted average

Poison Frogs alternates a gradient step on the feature term with an exact step on the pixel term (slide 12). After the forward step gives \(\hat x\), the backward step solves

$$x = \arg\min_{z}\ \tfrac{1}{2\lambda}\lVert z - \hat x\rVert_2^2 + \tfrac{\beta}{2}\lVert z - b\rVert_2^2 .$$

Set the derivative to zero:

$$\tfrac{1}{\lambda}(z - \hat x) + \beta (z - b) = 0 \quad\Longrightarrow\quad z = \frac{\hat x + \lambda\beta\, b}{1 + \lambda\beta}.$$

So every backward step pulls the poison a fixed fraction of the way back to the base image \(b\), which keeps the poison visually close to the base image.

Lecture recap · Lecture 19, slides 20–29

A3 · Backdoor attacks and the two numbers that measure them

A backdoored model behaves normally on clean inputs, and switches to a hidden behaviour when it sees a trigger (slide 20).

Clean pair\((x, y)\)
→
Attacker's function \(g\)add trigger, set \(y_t\)
→
Poisoned pair\((x \oplus \text{trigger},\ y_t)\)
→
Mixed training set\(\mathcal D_c \cup \mathcal D_b\)
→
One modeltwo behaviours

Ordinary training on the mixture learns both behaviours at once (slide 22). Nothing special is needed.

$$\mathcal L = \underbrace{\mathbb E_{(x,y)\sim\mathcal D_c}\big[\ell(f(x), y)\big]}_{\text{normal behaviour}} + \underbrace{\mathbb E_{(x',y')\sim\mathcal D_b}\big[\ell(f(x'), y')\big]}_{\text{hidden behaviour}} .$$
A trigger can be almost anything (slides 23–24)
patcha small squareclean labellabel stays correctphoto filterNashvillewarpWaNetblendfaint noisestripesSIGLLM word or topicBackdoorLLM
How it gets in (slides 21, 25–27)
Scraped web data
Buy an expired domain and serve a poisoned image at the old link. $1000 controls at least 0.01% of every dataset checked (slide 26).16
Downloaded models
A backdoored pre-trained model survives fine-tuning on your own data. Model hubs host millions of models.
Two numbers measure a backdoor (slide 29)8
Clean accuracy (CA) · keep high
$$\text{{CA}} = \frac{{\#\{{f(x) = y\}}}}{{\#\text{{clean test images}}}}$$

High CA: nobody notices.

Attack success rate (ASR) · attacker wants high
$$\text{{ASR}} = \frac{{\#\{{f(x') = y_t\}}}}{{\#\text{{triggered test images}}}}$$

High ASR: the backdoor works.

Lecture recap · Lecture 20, slides 4–14

A4 · Adversarial training and its costs

Adversarial training trains on the worst allowed perturbation, keeping the correct label (slides 5–8).17

Batch \((x, y)\)
→
Inner maxfind \(\delta\) by FGSM or PGD (\(K\) steps)
→
Outer minupdate \(\theta\) on \(x + \delta\) with label \(y\)
→
Next batchrepeat
$$\min_\theta\ \mathbb E_{(x,y)\sim\mathcal D}\Big[\max_{\lVert\delta\rVert_\infty \le \epsilon} \ell\big(f_\theta(x + \delta), y\big)\Big].$$
What it costs (slide 13)
1
Lower clean accuracy
Illustrative: \(95\% \to 87\%\).
2
Slower training
\(K\) attack steps plus 1 update per batch.
3
More data
Robust models need more training examples.
4
Robust overfitting
Robust test accuracy peaks early, then falls.
Variants (slide 14)
ChangeMethodsIdea
the lossTRADES, MARTfit the clean label, and keep the output stable inside the ball
the costFree AT, Fast ATreuse gradients, or use one random-start FGSM step
Two cautionary results
Defensive distillation (slide 9)
Looked robust against FGSM. Broken by the Carlini–Wagner attack on the logits.
weak attacker
Obfuscated gradients (slide 11)
Shattered, stochastic or vanishing gradients make attacks fail without making the model safe. Broken by BPDA (approximate the backward pass) and EOT (average over randomness).4
weak attacker
Both defences were evaluated against a weaker attacker than a real one: the subject of Q1.
Lecture recap · Lecture 20, slides 15–18

A5 · Certified robustness by interval bound propagation

Empirical robustness
Adversarial training. Tested only against the attacks we tried; an untried attack may still succeed.
no guarantee
Certified robustness
A proof that no input in the ball of radius \(r\) changes the answer (slide 15).
guarantee

Interval bound propagation (IBP) pushes a whole box of inputs through the network, one neuron at a time.18

Input boxeach pixel \([x_i - \epsilon,\ x_i + \epsilon]\)
→
Each neuronsmallest and largest weighted sum
→
ReLUclip the interval at 0
→
Output scorescat range, dog range
→
Certified?\(\min \text{{cat}} > \max \text{{dog}}\)

For one neuron \(z = \sum_i w_i x_i\) with \(x_i \in [l_i, u_i]\), each term is extreme at one end of its interval:

$$z_{\min} = \sum_{i} \min(w_i l_i,\ w_i u_i), \qquad z_{\max} = \sum_{i} \max(w_i l_i,\ w_i u_i).$$
Illustrative toy modelThe two-pixel network of slide 17
Pixels are \(0.5 \pm \epsilon\), and the weights are those of slide 17 (written out below the figure). At \(\epsilon = 0.1\) the cat range \([0.6, 1.2]\) stays above the dog range \([0, 0.2]\): certified. The intervals first overlap at \(\epsilon = 1/6\).

The network in the figure is

$$h_1 = x_1 + x_2, \qquad h_2 = x_1 - x_2, \qquad \text{cat} = \operatorname{ReLU}(h_1) - \operatorname{ReLU}(h_2), \qquad \text{dog} = \operatorname{ReLU}(h_2).$$

"Not certified" does not mean "attackable". The worst real input, \(x_1 = 0.5 + \epsilon\) and \(x_2 = 0.5 - \epsilon\), gives cat \(= 1 - 2\epsilon\) and dog \(= 2\epsilon\), so the label only flips at \(\epsilon = 0.25\). The gap between \(1/6\) and \(0.25\) is the looseness of slide 18.

IBP strength
Fast: one forward pass with intervals.
IBP weakness (slide 18)
Loose: it combines worst cases that cannot happen together, so deep networks get intervals too wide to certify. Randomised smoothing (Q2) is the alternative.
Lecture recap · Lecture 20, slides 24–27

A6 · Input preprocessing, detection and guardrails

Three more defences act around the model instead of changing it.

Input
→
PreprocessJPEG, TV, threshold
→
Classifier
→
Detector or guardreject or review
→
Output
Input preprocessing (slide 24)

How: JPEG compression, total-variation minimisation or binary thresholding removes fine detail.

Limit: an adaptive attacker optimises through the transform.

Detection (slide 25)

How: flag out-of-distribution inputs by confidence, feature distance or a trained detector.

Limit: false alarms; adaptive attacks fool both models.

Guardrails and safety training (slides 26–27)

How: guards screen the prompt and the draft; supervised fine-tuning (SFT) and RLHF change the weights.

Limit: unsafe replies remain; safe requests get refused.

"Passing a detector is not a robustness guarantee."Lecture 20, summary (slide 28)
Lecture recap · Lecture 21, slides 3–15

A7 · Backdoor defences: cleaning the data or the model

A useful defence keeps CA high and drives ASR down (slide 3). There are two starting points (slides 4–5).

Before training: clean the data

Given the poisoned set \(\mathcal D\), with \(|\mathcal D_b| \ll |\mathcal D|\).

Goal: find \(\hat{{\mathcal D}}_b \approx \mathcal D_b\) and train on the rest.

you train from scratch
After training: clean the model

Given a backdoored \(f_{{\theta_b}}\); the trigger is unknown.

Goal: new weights that keep CA and lose the backdoor.

you downloaded the model
The lecture's tools
ToolStageSignal it usesSlides
Anti-backdoor learning19datapoisons reach low loss faster; flag them, then unlearn by gradient ascent6–7
Cognitive distillation12dataa backdoored image keeps its output with only a tiny mask (the trigger)8–9
Outlier detection (SLOF)11datapoisons are few and alike, so they sit apart in feature space10–11
Trigger synthesis20modelthe target class needs a much smaller trigger (Neural Cleanse, DECREE)12–13
Partition and vote21dataone poisoned record corrupts at most one model's vote14

Q4 tries cognitive distillation and outlier detection on a real backdoored model, and shows the loss signal behind anti-backdoor learning.

ExtensionPartition and vote: how many poisons can the vote survive?
Each poisoned record lands in one split, so it can flip at most one model's vote. In the worst case every flipped vote moves from "cat" to "dog", so the gap closes by 2 per poison. The prediction is guaranteed while the clean margin is larger than \(2m\). \(K\) is odd here, so the clean vote never ties, and the cat-voting slider starts at a majority.
Derivation · why the vote survives \(m < \text{margin}/2\) poisons

Let \(n_{\text{cat}}\) and \(n_{\text{dog}}\) be the clean vote counts. Each of the \(m\) poisoned records sits in one split, so at most \(m\) models change their vote.

The worst case moves each changed vote from cat to dog, so after poisoning

$$n'_{\text{cat}} \ge n_{\text{cat}} - m, \qquad n'_{\text{dog}} \le n_{\text{dog}} + m .$$

Cat still wins if \(n_{\text{cat}} - m > n_{\text{dog}} + m\), that is

$$m < \frac{n_{\text{cat}} - n_{\text{dog}}}{2}.$$
Lecture recap · Lecture 21, slides 16–24

A8 · Protective uses of attack techniques

The same machinery can protect people (slides 16–24).

Model watermark (slide 16)
backdoor → proof of ownership
Plant secret key images; a stolen model still answers them.
Text watermark (slides 17–18)
biased sampling → AI-text detector
Prefer a secret green list of tokens; count them later.22
BackdoorAlign (slide 19)
secret trigger → safety switch
A secret prefix keeps a fine-tuned model safe.
Unlearnable examples (slides 20–21)
error-minimising noise → useless training data
Scraped photos teach a model nothing.23
Fawkes (slide 22)
feature-space attack → privacy cloak
A face model learns the wrong face from cloaked photos.24
T-shirts and CAPTCHAs (slides 23–24)
adversarial patch or noise → hard for machines
Hide from detectors; robust CAPTCHAs are Q3.7

The text watermark is detected with a one-sided z-test. With \(T\) tokens, green share \(\gamma\) and \(|s|_G\) green tokens:

$$z = \frac{|s|_G - \gamma T}{\sqrt{T\gamma(1-\gamma)}} .$$
ExtensionDetecting a text watermark (slide 18)
token on the secret green listtoken not on the list
The chip positions are illustrative: in the real scheme the green list depends on the key and the previous token. With \(\gamma = 0.5\), human text has about half green tokens by chance. Slide 18 treats a text with 13 green tokens out of 16 (\(z = 2.5\)) as watermarked. Kirchenbauer et al. use the stricter threshold \(z > 4\) to keep false alarms rare. The same green share is far more convincing in a longer text, because \(z\) grows like \(\sqrt T\).
Additional material · Notebook cells 1–9

Notebook set-up cells

Installation, imports, data loading and a plotting helper, unchanged from the notebook.

Cell 2Install the libraries · show code
Cell 2Install the libraries
! pip install torch torchvision matplotlib numpy
Cell 4Imports, seed and device · show code
Cell 4Imports, seed and device
# Notebook cell: imports, reproducible seed, device, and MNIST download + poisoned/triggered dataset wrappers

import os
import random
from typing import Optional, Tuple

import numpy as np
from PIL import Image
import torch
from torch import Tensor
from torch.utils.data import Dataset
import torchvision
from torchvision import transforms
from torchvision.datasets import MNIST

# -------------------------
# Reproducibility & device
# -------------------------
SEED = 42
random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
if torch.cuda.is_available():
    torch.cuda.manual_seed_all(SEED)

def get_device() -> torch.device:
    """
    Return the best available device: CUDA > MPS > CPU.
    """
    if torch.cuda.is_available():
        return torch.device("cuda")
    if hasattr(torch.backends, "mps") and torch.backends.mps.is_available() and torch.backends.mps.is_built():
        return torch.device("mps")
    return torch.device("cpu")

device = get_device()
print("Using device:", device)
Cell 6Load MNIST as float tensors in [0, 1] · show code
Cell 6Load MNIST as float tensors in [0, 1]
# -------------------------
# Root directory for data
# -------------------------
root = "./data"
os.makedirs(root, exist_ok=True)

# -------------------------
# Define basic transform
# (convert to tensor)
# -------------------------
transform = transforms.Compose([
    transforms.PILToTensor(),                         # (1, H, W) uint8
    transforms.ConvertImageDtype(torch.float32),      # -> float32 in [0,1]
])

# -------------------------
# Download and create datasets
# -------------------------
mnist_train = MNIST(root=root, train=True, download=True, transform=transform)
mnist_test = MNIST(root=root, train=False, download=True, transform=transform)

print(f"Training set size: {len(mnist_train)}")
print(f"Test set size: {len(mnist_test)}")

# Example: show one sample (tensor shape + label)
img, label = mnist_train[0]
print("Sample image shape:", img.shape)
print("Sample label:", label)
Output of cell 6
Training set size: 60000
Test set size: 10000
Sample image shape: torch.Size([1, 28, 28])
Sample label: 5
Cell 8A grid-plotting helper · show code
Cell 8A grid-plotting helper
import matplotlib.pyplot as plt
%matplotlib inline

def show_image_grid(imgs, nrow=10):
    grid_img = torchvision.utils.make_grid(imgs, nrow=nrow)
    fig= plt.figure(figsize=(10, 500), dpi=80)
    plt.imshow(grid_img.permute(1, 2, 0).detach().cpu().numpy())
    plt.show()
    plt.close()
Cell 9Twenty random training images · show code
Cell 9Twenty random training images
# ------------------------
# Randomly pick some indices
# ------------------------
n_samples = 20
idxs = np.random.choice(len(mnist_train), size=n_samples, replace=False)

imgs, labels = zip(*[mnist_train[i] for i in idxs])

print("Labels:", labels)
show_image_grid(list(imgs), nrow=10)
Plot from cell 9Plot produced by cell 9
Output of cell 9
Labels: (7, 3, 8, 9, 3, 9, 7, 7, 5, 4, 2, 5, 6, 8, 1, 7, 4, 8, 0, 7)
Additional material

Changes to the notebook code

Small edits to the workshop notebook, so that the code runs cleanly and its numbers are consistent:

  1. Cell 12 previews the trigger that is actually used for poisoning (3 × 3, bottom left).
  2. Cell 15 reuses ./data/MNIST/raw; by default torchvision would download a second copy into a folder named after the subclass.
  3. Cell 21 removes the true zeros from the triggered test set, so the attack success rate counts only digits the trigger changes, and drops an unused batch_size.
  4. Cell 22 prints clean accuracy and attack success rate.
  5. Cell 25 uses max_iter, the current scikit-learn name for t-SNE's iteration count.
  6. Cells 27–29 compute the k-NN distance score over all 60 000 training images (rather than within each batch of 256), with a docstring that matches the function.
  7. Cell 30 raises a ValueError for inputs outside [0, 1].
  8. Cells 32–33 score all 1200 poisoned images and 3000 random clean ones, and print the mean mask size of each group.
Additional material

Cheat notes for the four questions

One card per tutorial question: what to remember, the formulas, the exercise answers with working, and the numbers worth quoting from the experiments.

Q1What can the attacker access?

Remember
  • A threat model fixes the attacker's goal, capability (\(\lVert\delta\rVert_p \le \epsilon\)) and knowledge.
  • White-box: architecture, weights, gradients; attacks by back-propagation (FGSM, PGD).
  • Black-box: queries only, either scores (finite differences, ZOO) or labels (boundary attack); with no queries at all, transfer from a surrogate.
  • Defender's view: assume white-box. Black-box access raises the cost, it does not prevent attacks; models leak; obscurity fails (Kerckhoffs).
Formula
$$\frac{\partial \ell}{\partial x_i} \approx \frac{\ell(x + h\mathbf e_i) - \ell(x - h\mathbf e_i)}{2h}$$

Two queries per coordinate: \(2d\) queries per gradient.

Exercise answers
Queries per gradient1568\(2 \times 28 \times 28\)
10-step PGD, black-box15 680\(10 \times 1568\)
10-step PGD, white-box10 backward passesone exact gradient per step

Black-box access raises the cost about a thousandfold; it does not stop the attack.

Q2What does a certificate guarantee?

Remember
  • The guarantee is for the smoothed model \(g\), not the base model \(f\), and holds with probability \(1 - \alpha\).
  • Limits: \(\ell_2\) only (an \(\ell_\infty\) box reaches \(\epsilon\sqrt d\)); conservative; \(n\) noisy forward passes per input; abstains near the boundary.
  • Cannot detect attacks: an attack pushed far into the wrong class gets a large radius for its wrong label.
  • Operational use: send abstentions and inputs with \(R < \tau\) to review. A small radius means we could not certify a large neighbourhood (boundary proximity, few samples, or a conservative bound), not that the input is an attack.
Formulas
$$t = \sqrt{\frac{\ln(1/\alpha)}{2n}}, \qquad \underline{p_A} = \hat p_A - t$$ $$R = \sigma\,\Phi^{-1}(\underline{p_A}), \qquad \text{abstain if } \underline{p_A} \le \tfrac12$$
Exercise answers

\(n = 100\,000\) noisy copies, 90 000 votes for cat, \(\alpha = 0.001\), \(\sigma = 0.25\).

Safety margin \(t\)0.0059\(\sqrt{6.91 / 200\,000}\)
Certified radius \(R\)0.312\(0.25\,\Phi^{-1}(0.894)\), \(\Phi^{-1}(0.894) \approx 1.248\)
\(\ell_2\) radius covering an \(\epsilon = 0.1\) MNIST box2.8\(0.1\sqrt{784}\); about 9 × what we can prove

Smoothing experiment: with \(n = 100\) the margin is \(t \approx 0.19\), so the same input gets a much smaller radius or none.

Q3Can perturbations help people?

Remember
  • Perception gap: a small \(\ell_\infty\) perturbation barely changes what people see but can change what a model predicts.
  • Transfer: examples crafted on one solver often fool others, so the CAPTCHA does not need every solver.
  • Requirements: access to (or good surrogates of) CAPTCHA-solving models, a budget small enough that people still read the text, and checks that the noise transfers.
  • Arms race: solvers can adversarially train; no guarantee is permanent, but the pressure no longer falls on people.
  • Similar systems: Fawkes photo cloaking, unlearnable examples, adversarial clothing against person detectors, anti-scraping text.
Formula
$$P_{\text{solve}} \approx p^{k} \quad (k \text{ characters, independent errors})$$
Exercise answers
\(p = 0.98\), 4 characters0.922\(0.98^4\)
\(p = 0.6\), 4 characters0.13\(0.6^4\)

Experiment at \(\epsilon = 0.2\): the unseen MLP solver drops from 95% to 60% per digit, about 13% per 4-digit CAPTCHA; the attacked LeNet drops to 40%.

Q4How does a backdoor work?

Remember
  • Attack: 2% of MNIST (1200 images) get a 3 × 3 white square at the bottom left and the label 0.
  • Stealthy and effective: CA 98.5%, ASR 98.9% after 5 epochs; the backdoor forms in epoch 2 (epoch 1 ASR 0.8%).
  • Precise trigger: ASR 99.7% as trained, 0.1% in the wrong corner, 6.6% at 2 × 2, 27.5% at brightness 128.
  • Detection: poisons form their own clusters in feature space. k-NN score AUC 0.83; the lecture's SLOF ratio only 0.62 (a dense cluster of poisons looks locally normal); cognitive distillation AUC 0.91.
  • Cognitive distillation: the lecture expects small masks on poisoned images; with \(\gamma = 0.05\) the clean masks shrink further, so here large masks flag the poisons (18.1 vs 3.6).
Formulas
$$\text{CA} = \frac{\#\{f(x) = y\}}{\#\text{clean}}$$ $$\text{ASR} = \frac{\#\{f(x') = y_t,\ y \ne y_t\}}{\#\{y \ne y_t\}}$$
Exercise answer
"ASR" of a backdoor-free model if true zeros are counted0.098\(980 / 10\,000\)

That is why the triggered test set excludes the target class: counting the zeros would report 10.5% after epoch 1 instead of 0.8%.

Additional material

Summary of key results

TopicKey formula or ruleTake-away
Threat model (Q1)white-box: gradients; black-box: \(2d\) queries per gradient, or transferdefend against white-box: the worst case
Smoothed classifier\(g(x) = \arg\max_c \Pr[f(x + \eta) = c]\), \(\eta \sim \mathcal N(0, \sigma^2 I)\)the certificate is for \(g\), not \(f\)
Certified radius\(R = \sigma\,\Phi^{-1}(\hat p_A - t)\), \(t = \sqrt{\ln(1/\alpha)/2n}\)\(\ell_2\) only; abstain if \(\hat p_A - t \le \tfrac12\)
\(\ell_\infty\) versus \(\ell_2\)\(\lVert\delta\rVert_2 \le \epsilon\sqrt d\)MNIST, \(\epsilon = 0.1\): need \(R \ge 2.8\)
Operational use (Q2)review if \(R < \tau\) or abstaina small radius is a reason to look again, not proof of an attack; attacks pushed far get large radii
Adversarial CAPTCHA (Q3)\(P_{\text{solve}} = p^k\); attack a surrogate solver, rely on transferharder for machines, not for people
Backdoor metrics\(\text{CA}\) on clean data; \(\text{ASR}\) on triggered non-target dataCA 98.5%, ASR 98.9% after 5 epochs
IBP\(z_{\min} = \sum_i \min(w_i l_i, w_i u_i)\), then ReLUfast, but loose with depth
Partition and votesurvives \(m < (n_{\text{cat}} - n_{\text{dog}})/2\) poisonsone poison corrupts one vote
SLOF\(\tfrac1k \sum_{o} \operatorname{k-dist}(q)/\operatorname{k-dist}(o)\)poisons are outliers only if \(k\) exceeds their cluster
Cognitive distillation\(\min_m \lVert f(x) - f(x_{cp})\rVert_1 + \alpha\lVert m\rVert_1 + \beta\operatorname{TV}(m)\)masks find the trigger; large masks flag poisons here (AUC 0.91)
Additional material

References

  1. Pin-Yu Chen, Huan Zhang, Yash Sharma, Jinfeng Yi and Cho-Jui Hsieh. ZOO: zeroth order optimization based black-box attacks to deep neural networks without training substitute models. In ACM Workshop on Artificial Intelligence and Security (AISec), pages 15–26, 2017. doi:10.1145/3128572.3140448
  2. Wieland Brendel, Jonas Rauber and Matthias Bethge. Decision-based adversarial attacks: reliable attacks against black-box machine learning models. In International Conference on Learning Representations (ICLR), 2018. arxiv.org/abs/1712.04248
  3. Nicolas Papernot, Patrick D. McDaniel, Ian J. Goodfellow, Somesh Jha, Z. Berkay Celik and Ananthram Swami. Practical black-box attacks against machine learning. In ACM Asia Conference on Computer and Communications Security (AsiaCCS), pages 506–519, 2017. doi:10.1145/3052973.3053009
  4. Anish Athalye, Nicholas Carlini and David A. Wagner. Obfuscated gradients give a false sense of security: circumventing defenses to adversarial examples. In International Conference on Machine Learning (ICML), pages 274–283, 2018. arxiv.org/abs/1802.00420
  5. Jeremy Cohen, Elan Rosenfeld and J. Zico Kolter. Certified adversarial robustness via randomized smoothing. In International Conference on Machine Learning (ICML), pages 1310–1320, 2019. arxiv.org/abs/1902.02918
  6. Wassily Hoeffding. Probability inequalities for sums of bounded random variables. Journal of the American Statistical Association, 58(301):13–30, 1963. doi:10.1080/01621459.1963.10500830
  7. Rulin Shao, Zhouxing Shi, Jinfeng Yi, Pin-Yu Chen and Cho-Jui Hsieh. Robust text CAPTCHAs using adversarial examples. In IEEE International Conference on Big Data, pages 1495–1504, 2022. doi:10.1109/BigData55660.2022.10021100
  8. Tianyu Gu, Brendan Dolan-Gavitt and Siddharth Garg. BadNets: identifying vulnerabilities in the machine learning model supply chain. arXiv:1708.06733, 2017. arxiv.org/abs/1708.06733
  9. Laurens van der Maaten and Geoffrey Hinton. Visualizing data using t-SNE. Journal of Machine Learning Research, 9(86):2579–2605, 2008. jmlr.org/papers/v9/vandermaaten08a.html
  10. Markus M. Breunig, Hans-Peter Kriegel, Raymond T. Ng and Jörg Sander. LOF: identifying density-based local outliers. In ACM SIGMOD International Conference on Management of Data, pages 93–104, 2000. doi:10.1145/342009.335388
  11. Hanxun Huang, Sarah Monazam Erfani, Yige Li, Xingjun Ma and James Bailey. Detecting backdoor samples in contrastive language image pretraining. In International Conference on Learning Representations (ICLR), 2025. arxiv.org/abs/2502.01385
  12. Hanxun Huang, Xingjun Ma, Sarah Monazam Erfani and James Bailey. Distilling cognitive backdoor patterns within an image. In International Conference on Learning Representations (ICLR), 2023. arxiv.org/abs/2301.10908
  13. Battista Biggio, Blaine Nelson and Pavel Laskov. Poisoning attacks against support vector machines. In International Conference on Machine Learning (ICML), 2012. arxiv.org/abs/1206.6389
  14. Ali Shafahi, W. Ronny Huang, Mahyar Najibi, Octavian Suciu, Christoph Studer, Tudor Dumitras and Tom Goldstein. Poison frogs! Targeted clean-label poisoning attacks on neural networks. In Advances in Neural Information Processing Systems (NeurIPS), pages 6106–6116, 2018. arxiv.org/abs/1804.00792
  15. Jonas Geiping, Liam H. Fowl, W. Ronny Huang, Wojciech Czaja, Gavin Taylor, Michael Moeller and Tom Goldstein. Witches' brew: industrial scale data poisoning via gradient matching. In International Conference on Learning Representations (ICLR), 2021. arxiv.org/abs/2009.02276
  16. Nicholas Carlini, Matthew Jagielski, Christopher A. Choquette-Choo, Daniel Paleka, Will Pearce, Hyrum S. Anderson, Andreas Terzis, Kurt Thomas and Florian Tramèr. Poisoning web-scale training datasets is practical. In IEEE Symposium on Security and Privacy (SP), pages 407–425, 2024. doi:10.1109/SP54263.2024.00179
  17. Aleksander Madry, Aleksandar Makelov, Ludwig Schmidt, Dimitris Tsipras and Adrian Vladu. Towards deep learning models resistant to adversarial attacks. In International Conference on Learning Representations (ICLR), 2018. arxiv.org/abs/1706.06083
  18. Sven Gowal, Krishnamurthy Dvijotham, Robert Stanforth, Rudy Bunel, Chongli Qin, Jonathan Uesato, Relja Arandjelović, Timothy Mann and Pushmeet Kohli. On the effectiveness of interval bound propagation for training verifiably robust models. arXiv:1810.12715, 2018; a later version appeared at ICCV 2019 as "Scalable verified training for provably robust image classification". arxiv.org/abs/1810.12715
  19. Yige Li, Xixiang Lyu, Nodens Koren, Lingjuan Lyu, Bo Li and Xingjun Ma. Anti-backdoor learning: training clean models on poisoned data. In Advances in Neural Information Processing Systems (NeurIPS), pages 14900–14912, 2021. arxiv.org/abs/2110.11571
  20. Bolun Wang, Yuanshun Yao, Shawn Shan, Huiying Li, Bimal Viswanath, Haitao Zheng and Ben Y. Zhao. Neural Cleanse: identifying and mitigating backdoor attacks in neural networks. In IEEE Symposium on Security and Privacy (SP), pages 707–723, 2019. doi:10.1109/SP.2019.00031
  21. Alexander Levine and Soheil Feizi. Deep partition aggregation: provable defenses against general poisoning attacks. In International Conference on Learning Representations (ICLR), 2021. arxiv.org/abs/2006.14768
  22. John Kirchenbauer, Jonas Geiping, Yuxin Wen, Jonathan Katz, Ian Miers and Tom Goldstein. A watermark for large language models. In International Conference on Machine Learning (ICML), pages 17061–17084, 2023. arxiv.org/abs/2301.10226
  23. Hanxun Huang, Xingjun Ma, Sarah Monazam Erfani, James Bailey and Yisen Wang. Unlearnable examples: making personal data unexploitable. In International Conference on Learning Representations (ICLR), 2021. arxiv.org/abs/2101.04898
  24. Shawn Shan, Emily Wenger, Jiayun Zhang, Huiying Li, Haitao Zheng and Ben Y. Zhao. Fawkes: protecting privacy against unauthorized deep learning models. In USENIX Security Symposium, pages 1589–1604, 2020. usenix.org/conference/usenixsecurity20/presentation/shan

Lecture slides: COMP90073 Trustworthy Machine Learning, lectures 19 (Training-Time Attacks: Poisoning and Backdoors), 20 (Defences I: Test-Time Attacks) and 21 (Defences II: Poisoning Defences and the Public Good), Hanxun Huang, University of Melbourne. Tutorial: w11-tutorial-answers.pdf and w11-tutorial-notebook.ipynb.