AI/ML Explained: Adversarial Examples & Robustness

Day 50 · 2026-07-07
For: engineers with coding experience, non-AI background · Level: Advanced

Adversarial Examples & FragilityAdversarial Examples

high-dim geometryattack surface
One-line analogy

An adversarial example is like a carefully engineered hash collision—two images that look identical to the human eye, yet the model outputs wildly different labels ("panda" → "gibbon"). The attacker doesn't guess randomly; like reverse-engineering a hash function, they compute exactly "which direction to nudge which pixels" to flip the class. Key insight: this is not a poorly-trained model, but a structural weakness of high-dimensional space.

What it solves + how it works

In 2013 Szegedy et al. found an anomaly: add a human-imperceptible tiny perturbation to a correctly-classified image, and SOTA networks misclassify it with high confidence. Stranger still—these perturbations are not random noise, but carefully targeted. Why?

Goodfellow's 2014 linear hypothesis gives the cleanest explanation: neural networks are locally near-linear. Look at how one logit is computed: w·(x+η) = w·x + w·η. The attacker picks the worst-case perturbation η = ε·sign(w) (along the sign of the weights, nudging every dimension toward raising the logit), giving a change of w·η = ε·‖w‖₁. Here ε is the per-pixel change budget (small enough to be invisible), and ‖w‖₁ is the sum of absolute weights. The crux: ‖w‖₁ grows linearly with dimension n:

per-dim ε=0.007 (invisible) · accumulated along sign(w) → logit change
dim n=100    small
dim n=1000   medium
dim n=150000 (a 224×224×3 image) huge
↑ tiny per dim × 150k dims = enough to flip the output. Fragility is inevitable in high-dim linearity

A 224×224×3 image has ~150k dimensions. Move each by just ε=0.007, and the accumulated logit change is a "sum of 150k small terms"—enough to push the decision across the class boundary. Fragility isn't a defect; it's the price of high-dimensional linearity.

Code example
import numpy as np
# Demonstrate "linear amplification": why moving each dim by ε flips the output
n = 150000                     # input dim (≈ pixels of a color image)
w = np.random.randn(n)         # weights of one logit w.r.t. input
eps = 0.007                    # per-dim budget, imperceptible

eta = eps * np.sign(w)          # worst-case: along the sign of weights
delta_logit = w @ eta          # logit change = ε·‖w‖₁

print(f"per-dim step: {eps}")          # 0.007
print(f"logit change: {delta_logit:.0f}")  # hundreds — grows linearly with dim
# Takeaway: in high-dim, an "invisible perturbation" accumulates into a "huge output shift"
Pitfall + practical scenario
Pitfall: "Adversarial examples are lab toys; the real world won't hit them." Wrong. A few stickers on a road sign can make a vision model read "stop" as "speed limit"; Ilyas 2019 goes further—adversarial directions correspond to "non-robust features" the model genuinely relies on, not artificial noise, but real yet brittle statistical patterns in the data.
📌 Decision-support scenario: when you use multimodal models to read charts or review scanned contracts, be aware that tiny input perturbations / adversarial content can systematically mislead the model—not "occasional errors," but an attack surface that can be deliberately exploited.
Takeaway + question
💡 A neural net's fragility isn't undertraining—it's a structural cost of high-dimensional geometry.
🤔 In your backend systems, which "seemingly equivalent inputs" actually trigger entirely different code paths?

Gradient Attacks: FGSM & PGDGradient-based Attacks

white-box attackgradient ascent
One-line analogy

During training you do gradient descent on the weights to lower loss; when attacking, you freeze the weights and instead do gradient ascent on the input to raise loss. Same autodiff machinery—just aimed at "changing the input" instead of "changing the model." FGSM is one greedy step; PGD is a constrained iteration—like a "one-shot heuristic" vs an "iterative optimizer with clamping."

What it solves + how it works

FGSM (Fast Gradient Sign Method): one shot. x_adv = x + ε·sign(∇ₓL). Why take the sign rather than the gradient itself? Because the attack constraint is usually L∞ (each pixel changes ≤ ε); under this constraint, moving the full ε along each dimension's gradient sign is the single step that maximizes the loss increase. Fast, but crude.

PGD (Projected Gradient Descent): split FGSM into many small steps (step size α), and after each step project back into the ε-ball—clip back to the boundary if you exceed the budget. This "projection" is exactly the bound-clamping / rate-limiting you know: clamp values to [x−ε, x+ε]. PGD is the strongest first-order white-box attack today, and the de facto standard for evaluating robustness: a defense that can't withstand PGD is basically "fake robust."

On the loss surface, push the input toward higher loss (= easier to misclassify)

ε-ball (perturbation budget)   start x

FGSM: x──one big step──▶x_adv (hits boundary, may overshoot)

PGD:  x·▶··▶··▶x_adv (many small steps + project back into ball each step)
↑ PGD searches finely for the worst point in the ε-ball, far stronger than FGSM
Code example
import torch, torch.nn.functional as F

def fgsm(model, x, y, eps):
    x = x.clone().requires_grad_(True)
    loss = F.cross_entropy(model(x), y)   # loss w.r.t. the true label
    loss.backward()                        # gradient w.r.t. the INPUT, not weights
    return (x + eps * x.grad.sign()).detach()  # one step raising the loss

def pgd(model, x, y, eps, alpha, steps):
    x_adv = x.clone().detach()
    for _ in range(steps):                 # iterative FGSM
        x_adv.requires_grad_(True)
        loss = F.cross_entropy(model(x_adv), y)
        grad = torch.autograd.grad(loss, x_adv)[0]
        x_adv = x_adv.detach() + alpha * grad.sign()
        x_adv = torch.min(torch.max(x_adv, x - eps), x + eps)  # project into ε-ball
        x_adv = x_adv.clamp(0, 1)          # keep valid pixel range
    return x_adv
Pitfall + practical scenario
Pitfall: "Bigger ε means stronger attacks, so set a tiny ε and you're safe." ε is only the definition of the perturbation budget, not a safety valve. At a fixed ε, it's the PGD iteration count and random restarts that determine attack strength; "robustness" measured with a weak attack is easily overturned by a strong one—precisely why many defense papers were later broken.
📌 Cross-domain transfer: when evaluating any "resistance to interference," first ask how strong an attack was used to test it. Like load testing—don't only send sequential requests; you need adversarial load, or you get a fake SLA.
Takeaway + question
💡 Attacking and training are two sides of one coin: gradients can change the model or the input. Evaluating robustness demands the strongest attack.
🤔 If an attacker could do "gradient-style probing" on your system (nudging inputs, watching outputs), which interfaces would leak exploitable directions?

Adversarial Training & TransferabilityAdversarial Training & Transferability

robust optimizationtrade-off
One-line analogy

Ordinary training feeds "normal samples"; adversarial training manufactures the worst-case sample on the fly with PGD inside each batch, then learns from it. It's essentially bringing chaos engineering into the training loop: don't wait to be attacked in production—continuously inject worst-case perturbations during training, forcing the model to "get it right even in the worst case."

What it solves + how it works

Madry 2017 formalized defense as a min-max robust optimization problem:

minθ   𝔼(x,y) [   max‖δ‖≤ε   L(θ, x+δ, y)   ]

How to read it: the inner maxδ = find the worst perturbation within budget ε (approximated by PGD); the outer minθ = do SGD on those worst-case samples and update the weights. This saddle-point view elegantly unifies attack and defense in one objective.

The cost is the robustness–accuracy trade-off: after adversarial training, accuracy on clean samples tends to drop. The intuition is that the model is forced to give up "highly predictive but brittle" non-robust features and keep only the stable ones—safer, but also more "dull."

Transferability is another key phenomenon: adversarial examples crafted on model A often fool a completely different model B. Why? Because different models learned the same non-robust features from the same data (Ilyas 2019). The practical consequence: an attacker needn't access your model—craft samples on their own surrogate model and hit you—black-box attacks become possible, like a zero-day that works across implementations.

Code example
# Adversarial training loop: inner max approximated by PGD, outer min by SGD
for x, y in loader:
    # (1) inner: craft this batch's "worst input" with PGD first
    x_adv = pgd(model, x, y, eps=0.03, alpha=0.007, steps=10)

    # (2) outer: do normal gradient descent on the worst input
    loss = F.cross_entropy(model(x_adv), y)
    opt.zero_grad()
    loss.backward()
    opt.step()
    # corresponds to  min_θ  E[ max_δ  L(θ, x+δ, y) ]
    # note: training cost ≈ normal × (PGD steps+1), since each step runs a full PGD
Pitfall + practical scenario
Pitfall: "Do adversarial training and you're absolutely safe." Adversarial training is robust only to the ε and the norm you trained on (e.g. L∞); switch the norm (L2) or use a larger ε and it breaks. It's an empirical defense with no mathematical proof.
📌 Cross-domain thinking: transferability means "an attack on one system generalizes to similar systems"—isomorphic to supply-chain vulnerabilities you know: a shared underlying component = a shared attack surface.
Takeaway + question
💡 Adversarial training = chaos engineering at training time; it buys robustness at the cost of clean accuracy, and only defends the attack type you trained on.
🤔 Transferability means "independent development" ≠ "independent failure"—which invisible vulnerabilities does your system share with competitors'?

Certified Robustness & Randomized SmoothingCertified Robustness & Randomized Smoothing

provable guaranteeconsensus vote
One-line analogy

An empirical defense says "every attack we tried failed"—like "never been hacked" ≠ "secure"; certified robustness wants a provable guarantee. Randomized smoothing works remarkably like a quorum read: don't trust a single prediction; add lots of Gaussian noise to the input, sample thousands of times, and take the majority vote. A single adversarial flip can't overturn a strong consensus.

What it solves + how it works

Pain point: the attack-defense arms race never ends—every empirical defense gets broken by a stronger attack months later. Certified robustness escapes this loop: it gives a radius R and a mathematical guarantee that no perturbation with L2 norm ≤ R can change the output.

Randomized smoothing (Cohen 2019) is the most practical method: wrap the base classifier f into a new one, g(x) = the majority-vote class of f after adding noise N(0, σ²I) to x. The theorem gives the certified radius:

R = σ · Φ⁻¹(pA)

Meaning of the symbols: σ is the noise level added; pA is the vote share of the top class under noise; Φ⁻¹ is the inverse CDF of the standard normal (translating "vote share" into "distance in std deviations"). Intuition: if you add lots of noise and the model still overwhelmingly votes A, then A's "vote reserve" is deep and a small perturbation can't move it. The closer pA is to 1, the larger the certified radius.

Sprinkle a Gaussian noise cloud on the input → each sample casts a vote → majority + certified radius

  · · x · ·   950/1000 votes in the cloud go to A
            └─ pA = 0.95 → R = σ·Φ⁻¹(0.95)

◀── R ──▶ within this L2 radius, no perturbation can flip the majority (provable)
↑ larger σ = more robust, but a blurrier model (clean accuracy drops)—a clean trade-off
Code example
import torch
from scipy.stats import norm   # norm.ppf is Φ⁻¹

def smoothed_predict(model, x, sigma=0.25, n=1000, k=10):
    votes = torch.zeros(k)               # ballot box for k classes
    for _ in range(n):                    # n noisy predictions (= quorum read)
        noisy = x + sigma * torch.randn_like(x)
        votes[model(noisy).argmax()] += 1   # cast one vote each time
    top = votes.argmax()
    p_A = (votes[top] / n).item()       # vote share of the top class
    radius = sigma * norm.ppf(p_A)        # certified radius R = σ·Φ⁻¹(p_A)
    return top.item(), radius           # within L2 radius R, guaranteed not to flip
    # a single adversarial pixel flip can't shake a 950:50 strong consensus
Pitfall + practical scenario
Pitfall: "Certified robust = absolutely safe." Certification only covers perturbations inside a specific norm ball (usually L2); outside the ball, or semantic-level attacks (shoot from a different angle, reword a sentence), are not guaranteed at all—and the radius you can actually certify is usually tiny, far from "practical safety."
📌 Decision-support scenario: distinguishing "no counterexample seen" (empirical) from "provably no counterexample" (certified) is a key piece of epistemic literacy—the same order-of-magnitude confidence gap as "tests passed" vs "formally verified" in your distributed systems.
Takeaway + question
💡 Empirical defense gives "confidence"; certified defense gives a "guarantee"—at the cost of a smaller scope and a blurrier model.
🤔 How much performance would you sacrifice for "provable safety"? Is choosing this σ the same kind of wisdom as the trade-offs in CAP?

Further ReadingFurther Reading

Deep QuestionsDeep Questions

1. Adversarial examples vs SQL injection / hash collisions—all "carefully crafted legal inputs triggering anomalous behavior." What defense philosophy do they share?
All three are structurally isomorphic: the attacker exploits the system's implicit assumptions about input—SQL injection assumes input is data not code, hash collisions assume "finding a collision is computationally infeasible," adversarial examples assume "human-equivalent ⇒ model-equivalent." Two shared defense philosophies: (a) Distrust the input boundary—SQL uses parameterized queries to physically separate "data" from "code"; adversarial defense must decouple "model decision" from "input surface" (exactly what randomized smoothing does: look at the neighborhood's consensus, not a single point). (b) Upgrade from "empirical patch" to "provable guarantee"—SQL injection went from WAF rules (empirical, bypassable) → parameterized queries (structural fix); adversarial defense goes from adversarial training (empirical) → certified robustness (provable). Deep lesson: as long as a gap exists between the system's "input-semantics assumption" and the attacker's operating space, there's an attack surface. Your distributed-systems instinct—"always validate boundaries, always assume input is malicious"—transfers directly to AI security.
2. Why do "empirical defenses" keep getting broken by stronger attacks (arms race), while certified robustness escapes the loop? Is this the same as "unbroken" vs "provably secure" in cryptography?
It's the same kind of epistemic difference. The empirical claim is "all attacks we tried failed"—a statement of existence not yet falsified, forever open to being overturned by the next cleverer attack; the field repeatedly saw "new defense released → broken by an adaptive attack three months later," because defenders often inadvertently mask gradients, defeating weak attacks but not strong ones. The certified claim is "within radius R, no perturbation can flip the output"—a universal guarantee over the entire perturbation space, independent of "which attacks were tried." This is fully isomorphic to cryptography: an algorithm "unbroken to date" (like some heuristics) and one "reducible to the discrete-log problem" (provably secure) are two orders of confidence apart. But be honest: certified robustness costs a small scope (usually only a small L2 ball) and a duller model—like how the one-time pad's "information-theoretic security" is theoretically perfect but engineering-clumsy. Security is always a trade-off between "guarantee strength" and "practicality"; AI and cryptography share this wisdom.
3. Ilyas says "adversarial examples aren't bugs, but real (yet brittle) features the model learned." What does this mean for "what a model actually learns"? Why doesn't human vision have this problem?
This is the most subversive view here. The traditional intuition is that adversarial examples are "model defects"; Ilyas 2019 argues experimentally that those human-meaningless non-robust features are actually real, highly-predictive statistical patterns in the data distribution—the model learns them not by mistake, but as a rational choice under the "maximize accuracy" objective. Deep implications: (a) The model and humans are optimizing different things—human vision, shaped by eons of evolution, encodes priors for "robustness to lighting, angle, small perturbation"; the model is only asked to "classify correctly on the training set," so it copies whatever works, including brittle shortcuts. (b) A microcosm of "alignment": a model's internal representations aren't naturally aligned with human semantics—high accuracy ≠ using reasons a human would use. (c) It explains the robustness–accuracy trade-off: forcing the model to use only robust features means giving up some real-but-brittle predictive power. Humans "don't have this problem" perhaps only because our "attacker" (evolutionary pressure) already filtered out the non-robust features—in other words, we're models trained adversarially for eons.
4. The robustness–accuracy trade-off: adversarial training lowers clean accuracy. Why must "safer" cost "dumber"? Is this an essential contradiction or a limitation of current methods?
Both readings coexist; the field has no verdict. The "essential contradiction" camp (Tsipras et al.'s observation): on a data distribution containing non-robust features, the robust classifier and the accuracy-optimal classifier may simply be two different functions—the former abandons brittle-but-useful signal, so its accuracy ceiling is naturally lower. This isn't undertuning; it's goal conflict. The "current-limitation" camp: the severity of the trade-off depends on data volume, model capacity, and method—bigger models, more data, better training substantially ease it; in principle "robust and accurate" may be attainable, just more expensive. Engineering implication: the question isn't whether to pay this trade-off but how much, and where to stop—just like consistency vs availability, or latency vs throughput, it's a design dimension to be priced explicitly, not a free lunch. For your pursuit of "AI super-individual," the real lesson: doubt any scheme claiming "most secure AND most powerful"—first ask on which axis it's secretly paying.
5. The certified radius depends on noise level σ: larger σ is more robust but blurrier. Is this the same class of problem as CAP and quorum-size trade-offs in distributed consistency?
Structurally very similar—both are "turn one knob, and the two ends trade off" continuous trade-offs. In randomized smoothing, σ↑ → certified radius R↑ (more robust), but the decision boundary gets smeared → clean accuracy↓ (duller); this echoes the read/write replica count R+W relative to N in quorums: R+W>N guarantees strong consistency (tolerates more node failures) but each operation waits on more replicas → latency↑, availability↓. Their common mathematical skeleton: trade redundant sampling + majority rule for tolerance to "local perturbation/failure"—the certified radius R is "how large an adversarial perturbation you can tolerate," just as a quorum tolerates how many unreachable nodes. The difference is the nature of the cost: distributed pays in latency and availability, smoothing pays in model accuracy and inference cost (running thousands of samples). The shared engineering wisdom: there's no "have it all" point; you set the knob by "how scary the worst case is"—crank σ / quorum up for safety-critical, down for performance-sensitive. A neat lesson in transferring distributed fault-tolerance intuition to AI robustness.