Combining input preprocessing with adversarial training

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:

  1. Apply preprocessing T(⋅)T(\cdot)T(⋅) to clean input
  2. Generate adversarial examples against the full pipeline
  3. 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:

  1. Adversarial train on raw inputs
  2. 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:

  1. Adversarial training with a deterministic transform (Gaussian blur)
  2. 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.

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
FREEDOM TO LEARN • 10% OFF All Courses & Workshops • Use Code: NANOINDIA10 • ⏳ Offer Ends In: Loading... • Learn Today. Lead Tomorrow. • Explore Programs →
FREEDOM TO LEARN • 10% OFF All Courses & Workshops • Use Code: NANOINDIA10 • ⏳ Offer Ends In: Loading... • Learn Today. Lead Tomorrow. • Explore Programs →
Support