LAB DUE DATE: TUESDAY, September 15nd, 11.59PM
This lab builds on the input-gradient computation from Lab 1. You will construct adversarial examples with the Fast Gradient Sign Method (FGSM) and Projected Gradient Descent (PGD), measure the attack strength and relate it’s efficacy to the perturbation budget, and evaluate a basic defense through adversarial training.
|
All experiments in this lab are white-box experiments. Do not run these attacks against systems, models, or data that you do not own or have explicit permission to test. |
Lab objectives
By the end of this lab, you should be able to:
-
Explain why a model can be highly accurate on clean inputs and still be vulnerable to carefully chosen, small perturbations.
-
Implement Fast Gradient Sign Method (FGSM) as a one-step, gradient-sign attack.
-
Implement Projected Gradient Descent (PGD) as an iterative attack with projection into an $L_infinity$ perturbation region.
-
Compare attack success across perturbation budgets, datasets, and numbers of PGD steps.
-
Distinguish targeted attacks from untargeted attacks.
-
Measure transferability across two different model architectures.
-
Compare standard and adversarially trained models using clean and robust accuracy.
-
Interpret results while reporting the threat model, preprocessing, evaluation subset, and attack parameters.
Recap of Lab 1
In Lab 1 we developed a supervised-learning pipeline. In Lab 2, we will follow the same steps of taking a dataset, preprocessing the data, training a classifier, evaluating accuracy, and inspecting how the loss changes as inputs move through the model.
Lab 2 uses the same forward-and-backward computation, but uses an adversarial lens:
Lab 1 Goals |
What does the model’s loss gradient reveal about the input? |
Lab 2 Goals |
Can we leverage the model’s loss gradient to construct an input that causes the model to fail? |
In this lab, the model parameters remain fixed for the attack. In Lab 1, we computed the gradients with respect to model parameters and updated the weights of our model, to improve accuracy. In Lab 2, we will compute gradients with respect to the image and update the image instead.
Attack Class
FGSM and PGD are white-box, gradient-based, inference-time evasion attacks that generate adversarial examples under a \(L_{\infty}\) perturbation constraint.
FGSM performs one gradient-sign step, while PGD performs multiple projected gradient steps.
-
FGSM’s one gradient-sign step:
\[x_{\mathrm{adv}} = x + \epsilon \operatorname{sign} \left( \nabla_x J(\theta, x, y) \right)\] -
PGD performs multiple gradient steps and projects the result back into the allowed perturbation region:
\[x_{t+1} = \Pi_{\mathcal{B}_{\infty}(x,\epsilon)} \left( x_t + \alpha \operatorname{sign} \left( \nabla_{x_t} J(\theta, x_t, y) \right) \right)\]
Threat Model
FGSM and PGD operate under an \(L_{\infty}\) threat model. Let \(x\) denote the original input and \(x_{\mathrm{adv}}\) denote the adversarial input. The attack must satisfy:
This constraint means that no individual input coordinate—such as a
pixel value or color-channel value—may change by more than epsilon.
The constraint limits the maximum coordinate-wise perturbation, but it
does not guarantee perceptual similarity. It also does not limit the
total amount of change across all pixels.
Understanding the attacks
As we discussed in class, FGSM and PGD is to view every possible image as a point in a high-dimensional input space.
A classifier partitions the input space into decision regions: one region may correspond to
cat, another to dog, and another to airplane. The boundaries between these regions are the model’s
decision boundaries.
A clean image is a point in this input space. If the model classifies the image correctly, the point lies inside the decision region associated with its true class. An adversarial example is created by moving the point a small distance so that it crosses a decision boundary and enters a region associated with an incorrect class.
Rather than looking for an arbitrary nearby point, the attack uses the gradient of the loss with respect to the input image to identify a particularly effective direction of movement. The gradient describes how the loss changes for small changes to the input coordinates:
For an untargeted attack, the gradient indicates how to change the input so that the loss for the correct class increases. The model parameters remain fixed; the attack changes only the input.
This view is consistent with the original FGSM analysis, which emphasized the effect of small, coordinated perturbations in high-dimensional input spaces rather than treating the attack as random noise References.
Attack 1: Fast Gradient Sign Method
Goal
FGSM is a fast, one-step, untargeted attack. The goal of this attack is to increase the loss assigned to the correct label while staying inside the allowed perturbation budget.
For model parameters \(\theta\), input \(x\), true label \(y\), and loss \(J\), the attack is:
The gradient has one value for every input coordinate. FGSM looks only at the sign of each value:
-
A positive sign means increase that coordinate.
-
A negative sign means decrease that coordinate.
-
The magnitude of the gradient is discarded.
This makes FGSM easy to understand: it nudges many coordinates in the direction that is locally most damaging to the model. Each individual change is small, but the changes are coordinated. A useful analogy is pushing a large object: one person pushing may have little effect, but many people pushing in the same direction can move it substantially.
|
Why does the gradient sign matter?
Imagine that the model receives a small grayscale image with four relevant input coordinates:
Suppose the gradient of the loss with respect to those coordinates is:
The gradient signs are:
For an FGSM perturbation budget of \(\epsilon = 0.1\), the attack changes each coordinate by \(\pm\epsilon\) according to its sign:
The resulting adversarial input is therefore:
The model does not receive random noise. Each coordinate is changed in the direction that increases the loss for the correct class. The individual changes are small, but together they may move the input across the model’s decision boundary. The values are clipped if necessary so that every coordinate remains within the valid input range. |
Understanding the code
images = tf.convert_to_tensor(images, dtype=tf.float32)
with tf.GradientTape() as tape:
tape.watch(images)
logits = model(images, training=False)
loss = keras.losses.sparse_categorical_crossentropy(
labels, logits, from_logits=True
)
grad = tape.gradient(loss, images)
signed_grad = tf.sign(grad)
adv_images = images + epsilon * signed_grad
adv_images = tf.clip_by_value(adv_images, x_min, x_max)
The code has four conceptual stages:
-
Compute the model’s loss on the current images.
-
Differentiate the loss with respect to the images.
-
Move in the sign-gradient direction to increase the loss.
-
Clip the result to a valid image range.
Attack 2: Projected Gradient Descent
Goal
Projected Gradient Descent, or PGD, is an iterative extension of FGSM. Instead of taking one large gradient-sign step, PGD takes several smaller steps and recomputes the gradient after each step.
The goal is to find an effective adversarial input within the same perturbation region used by FGSM. After every update, PGD projects the current input back into the allowed region around the original image. This keeps the attack within the specified threat model.
For model parameters \(\theta\), input \(x\), true label \(y\), loss \(J\), step size \(\alpha\), and perturbation budget \(\epsilon\), the untargeted PGD update is:
Here:
-
\(x_t\) is the current adversarial input.
-
\(\alpha\) controls the size of each update.
-
\(\epsilon\) controls the maximum distance from the original input.
-
\(\Pi_{\mathcal{B}_{\infty}(x,\epsilon)}\) projects the updated input back into the allowed \(L_{\infty}\) region.
-
The gradient is recomputed at every iteration.
Unlike FGSM, which uses the gradient at only the original input, PGD continually updates its direction as the input changes. This allows the attack to search more carefully within the allowed perturbation region.
Projection onto the perturbation set
PGD does not allow each gradient update to move independently without restriction. Instead, after every update, it projects the current adversarial input back onto the set of inputs permitted by the threat model.
For an original input coordinate \(x_i = 0.50\) and an \(L_{\infty}\) perturbation budget of \(\epsilon = 0.10\), the permitted values are:
Substituting the values from this example gives:
Suppose a gradient update produces the value 0.63. This value lies
outside the permitted perturbation set because it differs from the
original coordinate by 0.13, which is greater than
\(\epsilon = 0.10\). Projection maps the value back to the nearest
permitted point, 0.60.
For an \(L_{\infty}\) constraint, this projection is implemented by clipping the perturbation relative to the original input:
perturbation = tf.clip_by_value(
adv_images - original_images,
-epsilon,
epsilon
)
adv_images = original_images + perturbation
The order of operations is important:
-
Compute the current perturbation by subtracting the original input from the current adversarial input.
-
Clip every coordinate of that perturbation to the interval \([-\epsilon, \epsilon\)].
-
Add the clipped perturbation back to the original input.
This guarantees:
Projection should be understood as a constraint-enforcement operation. It does not choose a new attack direction and it does not recompute the gradient. It simply prevents the current adversarial input from leaving the perturbation set defined around the original input.
Projection versus valid-range clipping
PGD commonly uses two different clipping operations, and they enforce different constraints:
| Operation | Constraint enforced | Example |
|---|---|---|
Perturbation projection |
Limits the distance between the adversarial input and the original input. |
Ensures that each coordinate changes by at most \(\epsilon\). |
Valid-range clipping |
Ensures that the adversarial input remains a valid normalized image. |
Prevents normalized pixel values from falling below |
The corresponding valid-range operation is:
adv_images = tf.clip_by_value(
adv_images,
x_min,
x_max
)
These operations are complementary. An input can satisfy the \(L_{\infty}\) perturbation constraint and still fall outside the valid image range near a boundary. Conversely, an image can lie within the valid range while being more than \(\epsilon\) away from the original input.
A correct PGD implementation therefore applies both constraints after each gradient update:
# 1. Take a gradient-directed step.
adv_images = (
adv_images
+ alpha * tf.sign(gradient)
)
# 2. Enforce the perturbation budget relative to
# the original input.
perturbation = tf.clip_by_value(
adv_images - original_images,
-epsilon,
epsilon
)
adv_images = original_images + perturbation
# 3. Enforce the valid normalized input range.
adv_images = tf.clip_by_value(
adv_images,
x_min,
x_max
)
The first clipping operation enforces the attacker’s allowed modification. The second preserves the data representation expected by the model. Together, they keep PGD inside both the specified threat model and the valid input domain. This projected constrained-update perspective is central to the PGD formulation used for adversarial robustness evaluation.
Random initialization
With random_init=True, PGD begins from a random point inside the
allowed \(L_{\infty}\) region rather than starting exactly at the
clean input.
noise = tf.random.uniform(
tf.shape(images),
minval=-epsilon,
maxval=epsilon
)
adv_images = images + noise
adv_images = tf.clip_by_value(
adv_images,
x_min,
x_max
)
Random initialization means that two PGD runs can begin at different points within the same perturbation region. The subsequent gradient steps may therefore follow different paths. Set the random seed when you need reproducible results.
For a controlled comparison with FGSM, use:
random_init = False
n_steps = 1
Under these settings, PGD behaves much like a one-step gradient-sign attack. With multiple steps, PGD can repeatedly adjust its direction as it searches for a more effective adversarial input.
Understanding the code
The code has six conceptual stages:
-
Save the original input because every projection must be relative to it.
-
Optionally initialize inside the perturbation region.
-
Compute the loss and its gradient with respect to the current adversarial input.
-
Take a small gradient-sign step.
-
Project the perturbation back into the allowed \(L_{\infty}\) region.
-
Clip the image to the valid normalized input range.
FGSM and PGD
FGSM and PGD use the same gradient signal but search differently:
| Property | FGSM | PGD |
|---|---|---|
Number of updates |
One |
Multiple |
Gradient evaluation |
Once at the original input |
Repeated at every iteration |
Step size |
Usually the full \(\epsilon\) |
Usually a smaller \(\alpha\) |
Constraint handling |
Clip the final adversarial input |
Project after every update |
Runtime |
Fast |
More expensive |
Search behavior |
One locally informed move |
Iterative search within the allowed region |
PGD is therefore not simply “FGSM repeated” without modification. The projection step is essential: it ensures that every intermediate and final input remains within the same perturbation budget. This projected, iterative view is the constrained optimization perspective used in the PGD formulation of Madry et al. References.
Goal
PGD is an iterative extension of FGSM. Its goal is to search more effectively within the same perturbation region by taking several small gradient steps and projecting back into the allowed region after every step.
Here, alpha is the step size, N is the number of steps, and Pi is projection. For an $L_infinity$ threat model, projection is coordinate-wise clipping around the original image:
perturbation = tf.clip_by_value(
adv_images - original_images,
-epsilon,
epsilon
)
adv_images = original_images + perturbation
Without this projection, repeated steps could accumulate a perturbation much larger than epsilon, so the experiment would no longer be comparing attacks under the same threat model.
Random initialization
With random_init=True, PGD begins at a random point inside the $L_infinity$ ball rather than exactly at the clean image. This can expose different local paths through the loss surface. It also means that reproducibility depends on the random seed.
noise = tf.random.uniform(
tf.shape(images),
minval=-epsilon,
maxval=epsilon
)
adv_images = images + noise
For a controlled comparison with FGSM, use random_init=False and n_steps=1. Under those settings, PGD is essentially a one-step signed-gradient attack.
FGSM versus PGD
| Property | FGSM | PGD | Interpretation |
|---|---|---|---|
Gradient steps |
One |
Many |
PGD repeatedly reassesses the local loss direction. |
Speed |
Fast |
Slower |
PGD requires a forward/backward computation for every step. |
Constraint |
Clip once |
Project after every step |
Both should use the same threat model for a fair comparison. |
Search behavior |
One local move |
Iterative constrained search |
PGD can find a more effective point at the same |
Typical use |
Baseline or quick attack |
Stronger evaluation or adversarial training |
The right choice depends on runtime and threat model. |
A useful example from the notebook is CIFAR-10 at epsilon = 0.03: FGSM and PGD may produce very different success rates because PGD has multiple opportunities to adjust its direction. The exact values are checkpoint- and subset-dependent; you should report the values produced by your own run rather than treating the notebook output as universal.
Targeted and untargeted attacks
FGSM and PGD describe how an adversarial example is generated: FGSM takes one gradient-sign step, while PGD takes multiple projected steps. Adversarially, the two attacks can provide different outcomes.
This gives us two attack objectives:
-
An untargeted attack requires only that the model predict any incorrect class.
-
A targeted attack requires the model to predict one specific class chosen by the attacker.
Untargeted attacks
An untargeted attack does not specify the incorrect class in advance. Its objective is simply to move the input out of the decision region for the true label:
For an MNIST image whose true label is 3, the attack succeeds if the model
predicts 0, 1, 2, 4, 5, or any other incorrect class. The
attacker does not control which incorrect class the model selects.
For the loss-based formulation used in this lab, the attack maximizes the loss for the true label:
The positive sign moves the input in the direction that makes the true class less consistent with the model’s output. Once the predicted class changes, the untargeted attack has achieved its objective.
Targeted attacks
A targeted attack specifies the incorrect class that the model should predict. Its success condition is:
As above, if the the original MNIST image is a handwritten 3 and the
attacker chooses 4 as the target. A targeted attack succeeds only if
the perturbed image is classified as 4. A prediction of 0 or 7
would still be incorrect, but it would not satisfy the objective of a
targeted attack.
To encourage the model to predict the target class, the attack minimizes the loss for the target label. This reverses the update direction:
The negative sign is important. The gradient of the target loss points toward changes that increase the loss for the target class. Moving in the opposite direction decreases that loss and encourages the model to assign greater probability to the target class.
A concrete example
Suppose a classifier receives an image whose true class is 3:
true_label = 3
target_label = 4
An untargeted attack succeeds if the model produces any prediction
other than 3:
predicted_label != true_label
For example, each of the following predictions would count as an untargeted success:
predicted_label = 0
predicted_label = 4
predicted_label = 8
A targeted attack with target class 4 has a stricter success
condition:
predicted_label == target_label
The following outcomes would therefore be interpreted differently:
| True label | Model prediction | Attack interpretation |
|---|---|---|
|
|
Neither attack succeeded. |
|
|
Untargeted attack succeeded; targeted attack failed. |
|
|
Untargeted attack succeeded and targeted attack succeeded. |
|
|
Untargeted attack succeeded; targeted attack failed. |
This illustrates the key distinction: an untargeted attack only needs to cross any incorrect decision boundary, whereas a targeted attack must reach the particular decision region associated with \(y_{\mathrm{target}}\).
Target selection in the notebook
For the MNIST experiment, the notebook constructs a simple target by choosing the next digit cyclically:
target_labels = (true_labels + 1) % 10
This produces the following mapping:
| True label | Target label |
|---|---|
|
|
|
|
|
|
… |
… |
|
|
This rule is convenient because it gives every example a target without requiring an additional target-selection procedure. It is not a property of targeted attacks in general, however. Target selection can affect difficulty: some target classes may be closer to the original class in the model’s decision space than others.
When reporting targeted-attack results, record:
-
The target-selection rule.
-
The target label for each example, or the target distribution.
-
The attack method, such as FGSM or PGD.
-
The perturbation budget \(\epsilon\).
-
For PGD, the step size \(\alpha\) and number of steps.
-
The targeted success rate.
Targeted versus untargeted success
The two attack types require different evaluation metrics:
For an untargeted attack, measure whether the adversarial prediction differs from the true label. For a targeted attack, measure whether the prediction equals the specified target label. Do not use the untargeted criterion to claim targeted success.
In general, targeted attacks are more restrictive because they require the attack to reach one particular incorrect class rather than any incorrect class. The empirical difference depends on the model, dataset, target-selection rule, perturbation budget, and attack parameters. The notebook should therefore report both rates rather than assuming that one fixed relationship holds in every experiment.
Robustness Evaluation
We can evaluate the model in two stages:
-
First, measure its accuracy on the original, unmodified test images. This establishes the model’s clean accuracy.
-
Next, generate adversarial images using a specified attack and measure whether the model’s predictions remain correct on those same examples.
The first measurement describes the model’s performance without any adversarial perturbation. The second describes its accuracy under the specified attack, and perturbation. Finally, we measure the success rate of an attack only on examples the model classified correctly before the attack.
Accuracy of the model without attacks
First, evaluate the model on the unmodified inputs:
clean_logits = mnist_model(
mnist_eval_x,
training=False
)
clean_preds = tf.argmax(
clean_logits,
axis=1
).numpy()
clean_correct = clean_preds == mnist_eval_y
clean_accuracy = np.mean(clean_correct)
clean_error_rate = np.mean(~clean_correct)
print("Clean accuracy:", clean_accuracy)
print("Clean error rate:", clean_error_rate)
print("Initially correct:", np.sum(clean_correct))
print("Total examples:", len(mnist_eval_y))
The clean accuracy is:
The Boolean array clean_correct identifies the examples that the
model classified correctly before the attack. We will use this same
subset when measuring untargeted attack success.
For example, suppose the model correctly classifies 90 of 100 clean images. The clean accuracy is 90 percent, and these 90 correct images will be used to determine how effective the attacks are.
Sanity checks before interpreting results
Before interpreting an attack-success curve, verify that the attack produced a valid perturbation.
delta = adv_images - clean_images
max_change = np.max(
np.abs(delta)
)
mean_change = np.mean(
np.abs(delta)
)
print("maximum coordinate change:", max_change)
print("mean absolute change:", mean_change)
assert max_change <= epsilon + 1e-6
For an \(L_{\infty}\) attack, the largest coordinate-wise change must satisfy:
This assertion allows a small tolerance for floating-point round-off errors.
In your code, you also want to check that the gradient grad is not None
and that grad.shape matches`images.shape. Finally, ensure that the adversarial images
use the same preprocessing and valid input bounds as the clean images.
For PGD, the perturbation must be projected relative to the original images after every update. If these checks pass, then the attack-success curve is measuring the attack success rate.
A larger epsilon gives the attack a larger region in which
to modify the input. The success of an attack will increase as the
perturbation budget increases, but the curve need not be perfectly
smooth or strictly monotonic for a finite evaluation set.
If you see a curve you cannot quite explain you must ensure the following are true:
-
* The gradient is computed with respect to the input image and is not
None. -
The gradient has the same shape as the input batch, so every input coordinate receives a gradient value.
-
The adversarial images differ from the clean images.
-
The maximum coordinate change satisfies the
epsilonbound. -
The correct labels are being used.
-
Clean and adversarial inputs use the same preprocessing and normalization.
-
The model’s output convention agrees with the loss setting
from_logits=True. -
The attack-success metric includes examples that were already misclassified.
-
PGD projects the perturbation relative to the original clean image, not relative to the previous iteration.
-
Calid clipping bounds are expressed in the same normalized coordinates used during model training.
Only after these checks pass should the curve be interpreted as evidence of the model’s robustness.
Deliverables
-
Completed
fgsm_attackimplementation. -
Completed
pgd_attackimplementation with configurableepsilon,alpha, number of steps, and random initialization. -
FGSM and PGD epsilon-sweep results.
-
Perturbation grids for MNIST and CIFAR-10.
-
PGD convergence analysis.
-
Targeted-attack target-hit rate and untargeted success rate.
-
CNN-to-MLP transferability results.
-
Standard-versus-adversarially-trained clean and robust accuracy.
-
README_lab2_results.txtwith parameters, metrics, and interpretation. -
Completed discussion and reflection responses.
References
-
Ian J. Goodfellow, Jonathon Shlens, and Christian Szegedy. "Explaining and Harnessing Adversarial Examples." arXiv:1412.6572, 2014. arXiv. This is the original FGSM paper and motivates the gradient-sign construction.
-
Aleksander Madry, Aleksandar Makelov, Ludwig Schmidt, Dimitris Tsipras, and Adrian Vladu. "Towards Deep Learning Models Resistant to Adversarial Attacks." arXiv:1706.06083, 2017; ICLR 2018. arXiv. This paper develops the robust-optimization perspective and uses PGD as a first-order adversary.
-
TensorFlow. "Adversarial example using FGSM." TensorFlow tutorial. The tutorial documents the
GradientTape,tape.watch(input_image), and FGSM pattern used here. -
Joshuaclymer. Illustration of imperceptible adversarial perturbation. Wikimedia Commons, 2022. Own work. Image page. Licensed CC BY-SA 4.0. The image is included under that license and should retain attribution if reused or modified.