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
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.
Access
Attacker sees
How it finds a direction
Typical computation and target-query cost (MNIST)
White-box
architecture, weights, gradients, inputs, outputs
back-propagation (FGSM, PGD)
1 backward pass per gradient; no queries needed beyond the model itself
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.
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
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.
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.
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
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).
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 12Cell 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 23Output 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
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 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()
# -------------------------
# 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
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
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}%")
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).
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.
fit the clean label, and keep the output stable inside the ball
the cost
Free AT, Fast AT
reuse 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
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\).
"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.
one poisoned record corrupts at most one model's vote
14
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
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
Small edits to the workshop notebook, so that the code runs cleanly and its numbers are consistent:
Cell 12 previews the trigger that is actually used for poisoning (3 × 3, bottom left).
Cell 15 reuses ./data/MNIST/raw; by default torchvision would download a second copy into a folder named after the subclass.
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.
Cell 22 prints clean accuracy and attack success rate.
Cell 25 uses max_iter, the current scikit-learn name for t-SNE's iteration count.
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.
Cell 30 raises a ValueError for inputs outside [0, 1].
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.
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.
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.
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%.
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).
masks find the trigger; large masks flag poisons here (AUC 0.91)
Additional material
References
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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.