1) Why combine them?
Adversarial training hardens the model by training on adversarial examples. Input preprocessing tries to weaken adversarial perturbations before the model sees them. Combining them can be powerful because:
- Preprocessing reduces attack strength at inference (a front-door filter).
- Adversarial training teaches the model to remain correct even when preprocessing isn’t enough.
- Together, they form defense-in-depth: if one layer fails, the other still helps.
But: this combination can also create illusory robustness if preprocessing breaks gradients and evaluation is not adaptive. The goal is to combine them correctly and evaluate honestly.
2) Design patterns for combining defenses
Pattern A — Train on preprocessed inputs + adversarial examples
Pipeline:
- Apply preprocessing T(⋅)T(\cdot)T(⋅) to clean input
- Generate adversarial examples against the full pipeline
- Train model on T(x)T(x)T(x) and T(xadv)T(x_{adv})T(xadv)
Best when: preprocessing will always be used in production.
Risk: If preprocessing is non-differentiable/randomized, naive attacks may fail unless you use adaptive strategies.
Pattern B — Adversarial train the model, then add preprocessing at inference
Pipeline:
- Adversarial train on raw inputs
- Add preprocessing only at deployment
Best when: you want robustness even if preprocessing is bypassed (e.g., direct access to model input).
Risk: mismatch—model may not be optimized for transformed inputs, hurting clean accuracy.
Pattern C — Joint training with “mixture of transforms”
Pipeline:
- During training, randomly choose among several transforms (or apply with probability p)
- Generate adversarial examples under this stochastic pipeline
Best when: you plan to deploy stochastic transforms (e.g., random resize+pad).
Risk: must evaluate with EOT (Expectation Over Transformations), or results are misleading.
3) Practical guidance: what preprocessing works best with adversarial training?
Good pairings (usually safe)
- Differentiable / nearly differentiable transforms
- mild Gaussian blur (can be differentiable)
- normalization / standardization
- small resize (differentiable with bilinear interpolation)
- Low-risk transforms that don’t destroy semantics
- mild denoising
- small random crop/pad (if task tolerates)
Risky pairings (high chance of gradient masking issues)
- hard quantization / rounding
- heavy JPEG compression (non-smooth)
- stochastic transforms without EOT-style training/evaluation
4) Core pitfall: “training against the wrong attacker”
If you adversarial-train with preprocessing but generate adversarial examples incorrectly, you might accidentally train against a weak attacker:
- Attack is computed on raw input, but model sees transformed input
- Gradients get blocked by preprocessing
- Result: training uses weak adversarial examples → robustness is overstated
Solution: generate adversarial examples against the entire pipeline (transform + model), and for randomized transforms use EOT-style gradients (average over multiple transform samples).
5) Implementation Lab (TensorFlow 2 + CleverHans)
Below are two practical implementations:
- Adversarial training with a deterministic transform (Gaussian blur)
- How to evaluate robustly when transforms are involved
Uses MNIST for clarity; the same structure applies to CIFAR-10, etc.
5.1 Setup model + data
import tensorflow as tf
import numpy as np
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
x_train = (x_train.astype("float32")/255.0)[..., None]
x_test = (x_test.astype("float32")/255.0)[..., None]
y_train_oh = tf.one_hot(y_train, 10)
y_test_oh = tf.one_hot(y_test, 10)
model = tf.keras.Sequential([
tf.keras.layers.Conv2D(32, 3, activation="relu", input_shape=(28,28,1)),
tf.keras.layers.MaxPool2D(),
tf.keras.layers.Conv2D(64, 3, activation="relu"),
tf.keras.layers.MaxPool2D(),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(128, activation="relu"),
tf.keras.layers.Dense(10) # logits
])
loss_fn = tf.keras.losses.CategoricalCrossentropy(from_logits=True)
optimizer = tf.keras.optimizers.Adam(1e-3)
5.2 Define a deterministic preprocessing transform (Gaussian blur)
This version is differentiable enough for standard gradient-based attacks to work.
def gaussian_blur(x, ksize=3, sigma=1.0):
ax = tf.range(-ksize//2 + 1, ksize//2 + 1, dtype=tf.float32)
xx, yy = tf.meshgrid(ax, ax)
kernel = tf.exp(-(xx**2 + yy**2) / (2.0 * sigma**2))
kernel = kernel / tf.reduce_sum(kernel)
kernel = kernel[:, :, tf.newaxis, tf.newaxis] # (k,k,1,1)
return tf.nn.depthwise_conv2d(x, kernel, strides=[1,1,1,1], padding="SAME")
5.3 Generate adversarial examples against the pipeline (transform + model)
We wrap the model so that the attacker “sees” the preprocessing.
class PreprocessWrapper(tf.keras.Model):
def __init__(self, base_model, transform_fn):
super().__init__()
self.base_model = base_model
self.transform_fn = transform_fn
def call(self, x, training=False):
x_t = self.transform_fn(x)
return self.base_model(x_t, training=training)
pipeline_model = PreprocessWrapper(model, lambda x: gaussian_blur(x, ksize=3, sigma=1.0))
Now attacks target the pipeline:
from cleverhans.tf2.attacks.projected_gradient_descent import projected_gradient_descent
def pgd_on_pipeline(pipeline, x, eps=0.3, eps_iter=0.03, steps=5):
return projected_gradient_descent(
model_fn=pipeline,
x=x,
eps=eps,
eps_iter=eps_iter,
nb_iter=steps,
norm=np.inf,
targeted=False
)
5.4 Adversarial training loop with preprocessing + PGD
We mix clean and adversarial samples after preprocessing is included in the pipeline.
train_ds = tf.data.Dataset.from_tensor_slices((x_train, y_train_oh)).shuffle(10000).batch(128)
@tf.function
def train_step_with_preproc_and_adv(x, y, eps=0.3, eps_iter=0.03, steps=5, adv_ratio=0.5):
bs = tf.shape(x)[0]
adv_count = tf.cast(tf.round(tf.cast(bs, tf.float32) * adv_ratio), tf.int32)
x_adv_src = x[:adv_count]
y_adv_src = y[:adv_count]
x_clean = x[adv_count:]
y_clean = y[adv_count:]
# Generate PGD adversarial examples against the *pipeline* (preprocess+model)
x_adv = pgd_on_pipeline(pipeline_model, x_adv_src, eps=eps, eps_iter=eps_iter, steps=steps)
# Apply preprocessing to clean samples too (to match deployment behavior)
x_clean_t = gaussian_blur(x_clean, ksize=3, sigma=1.0)
x_adv_t = gaussian_blur(x_adv, ksize=3, sigma=1.0)
x_mix = tf.concat([x_adv_t, x_clean_t], axis=0)
y_mix = tf.concat([y_adv_src, y_clean], axis=0)
with tf.GradientTape() as tape:
logits = model(x_mix, training=True) # base model consumes already-transformed inputs
loss = loss_fn(y_mix, logits)
grads = tape.gradient(loss, model.trainable_variables)
optimizer.apply_gradients(zip(grads, model.trainable_variables))
return loss
Run training:
for epoch in range(2):
losses = []
for xb, yb in train_ds:
l = train_step_with_preproc_and_adv(xb, yb, eps=0.3, eps_iter=0.03, steps=5, adv_ratio=0.5)
losses.append(float(l))
print(f"Epoch {epoch+1} | loss={np.mean(losses):.4f}")
5.5 Evaluate clean vs robust accuracy with preprocessing enabled
Important: Evaluate the same way you’ll deploy.
# Clean eval (with preprocessing)
x_test_t = gaussian_blur(tf.constant(x_test), ksize=3, sigma=1.0)
clean_acc = tf.keras.metrics.CategoricalAccuracy()
clean_acc.update_state(y_test_oh, model(x_test_t, training=False))
print("Clean acc (with preprocessing):", float(clean_acc.result()))
# Robust eval (attack pipeline, then preprocess, then predict)
x_adv_test = pgd_on_pipeline(pipeline_model, tf.constant(x_test), eps=0.3, eps_iter=0.03, steps=20)
x_adv_test_t = gaussian_blur(x_adv_test, ksize=3, sigma=1.0)
rob_acc = tf.keras.metrics.CategoricalAccuracy()
rob_acc.update_state(y_test_oh, model(x_adv_test_t, training=False))
print("PGD-20 robust acc (with preprocessing):", float(rob_acc.result()))
6) Evaluation rules (to avoid fake robustness)
If preprocessing is deterministic and differentiable-ish
- PGD evaluation is usually meaningful
If preprocessing is randomized
- evaluate using multiple runs and/or EOT
- report mean ± std
- test transfer attacks
If preprocessing is non-differentiable (quantization, hard rounding, heavy JPEG)
- naive PGD may fail due to gradient masking
- you must use adaptive strategies (BPDA-style approximations or black-box evaluations)
7) Benefits of combining preprocessing + adversarial training
- Higher robust accuracy than either method alone (often)
- More stable predictions under noisy real-world inputs
- Defense-in-depth: preprocessing mitigates easy attacks; adversarial training handles harder cases
- Better operational fit for production where input normalization/filters are standard anyway
8) Challenges / trade-offs
- Compute cost increases (adversarial training is already expensive)
- Clean accuracy may drop if preprocessing is too aggressive
- Risk of gradient masking illusions if preprocessing is non-differentiable/randomized and evaluation is naive
- Pipeline complexity: must version transforms and keep train/inference consistent


Comments are closed.