mirror of
https://github.com/google-deepmind/deepmind-research.git
synced 2026-09-23 07:44:11 +08:00
311 lines
11 KiB
Python
311 lines
11 KiB
Python
# Copyright 2021 Deepmind Technologies Limited.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Adversarial attacks.
|
|
|
|
This file contains all the code necessary to create untargeted adversarial
|
|
attacks in JAX (within an l-infinity ball). For example, to create an untargeted
|
|
FGSM attack (with a single step), one can do the following:
|
|
|
|
```
|
|
import attacks
|
|
|
|
epsilon = 8/255 # Perturbation radius for inputs between 0 and 1.
|
|
fgsm_attack = attacks.UntargetedAttack(
|
|
attacks.PGD(
|
|
attacks.IteratedFGSM(epsilon),
|
|
num_steps=1,
|
|
initialize_fn=attacks.linf_initialize_fn(epsilon),
|
|
project_fn=attacks.linf_project_fn(epsilon, bounds=(0., 1.))),
|
|
loss_fn=attacks.untargeted_cross_entropy)
|
|
```
|
|
|
|
Just as elegantly, one can specify an adversarial attack on KL-divergence
|
|
to a target distribution (using 10 steps with Adam and a piecewise constant step
|
|
schedule):
|
|
|
|
```
|
|
kl_attack_with_adam = attacks.UntargetedAttack(
|
|
attacks.PGD(
|
|
attacks.Adam(optax.piecewise_constant_schedule(
|
|
init_value=.1,
|
|
boundaries_and_scales={5: .1})),
|
|
num_steps=10,
|
|
initialize_fn=attacks.linf_initialize_fn(epsilon),
|
|
project_fn=attacks.linf_project_fn(epsilon, bounds=(0., 1.))),
|
|
loss_fn=attacks.untargeted_kl_divergence)
|
|
```
|
|
|
|
The attack instances can be used later on to build adversarial examples:
|
|
|
|
```
|
|
my_model = ... # Model. We assume that 'my_model(.)' returns logits.
|
|
clean_images, image_labels = ... # Batch of images and associated labels.
|
|
rng = jax.random.PRNGKey(0) # A random generator state.
|
|
|
|
adversarial_images = fgsm_attack(my_model, rng, clean_images, image_labels)
|
|
```
|
|
|
|
See `experiment.py` or `eval.py` for more examples.
|
|
|
|
This file contains the following components:
|
|
* Losses:
|
|
* untargeted_cross_entropy: minimizes the likelihood of the label class.
|
|
* untargeted_kl_divergence: maximizes the KL-divergence of the predictions with
|
|
a target distribution.
|
|
* untargeted_margin: maximizes the margin loss (distance from the highest
|
|
non-true logits to the label class logit)
|
|
* Step optimizers:
|
|
* SGD: Stochastic Gradient Descent.
|
|
* IteratedFGSM: Also called BIM (see https://arxiv.org/pdf/1607.02533).
|
|
* Adam: See https://arxiv.org/pdf/1412.6980.
|
|
* Initialization and projection functions:
|
|
* linf_initialize_fn: Initialize function for l-infinity attacks.
|
|
* linf_project_fn: Projection function for l-infinity attacks.
|
|
* Projected Gradient Descent (PGD):
|
|
* PGD: Runs Projected Gradient Descent using the specified optimizer,
|
|
initialization and projection functions for a given number of steps.
|
|
* Untargeted attack:
|
|
* UntargetedAttack: Combines PGD and a specific loss function to find
|
|
adversarial examples.
|
|
"""
|
|
|
|
import functools
|
|
import inspect
|
|
from typing import Callable, Optional, Tuple, Union
|
|
|
|
import chex
|
|
import haiku as hk
|
|
import jax
|
|
import jax.numpy as jnp
|
|
import optax
|
|
|
|
|
|
ModelFn = Callable[[chex.Array], chex.Array]
|
|
LossFn = Callable[[chex.Array], chex.Array]
|
|
ClassificationLossFn = Callable[[chex.Array, chex.Array], chex.Array]
|
|
OptimizeFn = Callable[[LossFn, chex.PRNGKey, chex.Array], chex.Array]
|
|
NormalizeFn = Callable[[chex.Array], chex.Array]
|
|
InitializeFn = Callable[[chex.PRNGKey, chex.Array], chex.Array]
|
|
ProjectFn = Callable[[chex.Array, chex.Array], chex.Array]
|
|
|
|
|
|
def untargeted_cross_entropy(logits: chex.Array,
|
|
labels: chex.Array) -> chex.Array:
|
|
"""Maximize the cross-entropy of the true class (make it less likely)."""
|
|
num_classes = logits.shape[-1]
|
|
log_probs = jax.nn.log_softmax(logits)
|
|
return jnp.sum(
|
|
hk.one_hot(labels, num_classes).astype(logits.dtype) * log_probs, axis=-1)
|
|
|
|
|
|
def untargeted_kl_divergence(logits: chex.Array,
|
|
label_probs: chex.Array) -> chex.Array:
|
|
"""Maximize the KL divergence between logits and label distribution."""
|
|
# We are explicitly maximizing the cross-entropy, as this is equivalent to
|
|
# maximizing the KL divergence (when `label_probs` does not depend
|
|
# on the values that produce `logits`).
|
|
log_probs = jax.nn.log_softmax(logits)
|
|
return jnp.sum(label_probs * log_probs, axis=-1)
|
|
|
|
|
|
def untargeted_margin(logits: chex.Array,
|
|
labels: chex.Array) -> chex.Array:
|
|
"""Make the highest non-correct logits higher than the true class logits."""
|
|
batch_size = logits.shape[0]
|
|
num_classes = logits.shape[-1]
|
|
label_logits = logits[jnp.arange(batch_size), labels]
|
|
logit_mask = hk.one_hot(labels, num_classes).astype(logits.dtype)
|
|
highest_logits = jnp.max(logits - 1e8 * logit_mask, axis=-1)
|
|
return label_logits - highest_logits
|
|
|
|
|
|
class UntargetedAttack:
|
|
"""Performs an untargeted attack."""
|
|
|
|
def __init__(self,
|
|
optimize_fn: OptimizeFn,
|
|
loss_fn: ClassificationLossFn = untargeted_cross_entropy):
|
|
"""Creates an untargeted attack.
|
|
|
|
Args:
|
|
optimize_fn: An `Optimizer` instance or any callable that takes
|
|
a loss function and an initial input and outputs a new input that
|
|
minimizes the loss function.
|
|
loss_fn: `loss_fn` is a surrogate loss. Its goal should be make the true
|
|
class less likely than any other class. Typical options for `loss_fn`
|
|
are `untargeted_cross_entropy` or `untargeted_margin`.
|
|
"""
|
|
self._optimize_fn = optimize_fn
|
|
self._loss_fn = loss_fn
|
|
|
|
def __call__(self,
|
|
logits_fn: ModelFn,
|
|
rng: chex.PRNGKey,
|
|
inputs: chex.Array,
|
|
labels: chex.Array) -> chex.Array:
|
|
"""Returns adversarial inputs."""
|
|
def _loss_fn(x):
|
|
return self._loss_fn(logits_fn(x), labels)
|
|
return self._optimize_fn(_loss_fn, rng, inputs)
|
|
|
|
# Convenience functions to detect the type of inputs required by the loss.
|
|
def expects_labels(self):
|
|
return 'labels' in inspect.getfullargspec(self._loss_fn).args
|
|
|
|
def expects_probabilities(self):
|
|
return 'label_probs' in inspect.getfullargspec(self._loss_fn).args
|
|
|
|
|
|
class StepOptimizer:
|
|
"""Makes a single gradient step that minimizes a loss function."""
|
|
|
|
def __init__(self,
|
|
gradient_transformation: optax.GradientTransformation):
|
|
self._gradient_transformation = gradient_transformation
|
|
|
|
def init(self,
|
|
loss_fn: LossFn,
|
|
x: chex.Array) -> optax.OptState:
|
|
self._loss_fn = loss_fn
|
|
return self._gradient_transformation.init(x)
|
|
|
|
def minimize(
|
|
self,
|
|
x: chex.Array,
|
|
state: optax.OptState) -> Tuple[chex.Array, chex.Array, optax.OptState]:
|
|
"""Performs a single minimization step."""
|
|
g, loss = gradients_fn(self._loss_fn, x)
|
|
if g is None:
|
|
raise ValueError('loss_fn does not depend on input.')
|
|
updates, state = self._gradient_transformation.update(g, state, x)
|
|
return optax.apply_updates(x, updates), loss, state
|
|
|
|
|
|
class SGD(StepOptimizer):
|
|
"""Vanilla gradient descent optimizer."""
|
|
|
|
def __init__(self,
|
|
learning_rate_fn: Union[float, int, optax.Schedule],
|
|
normalize_fn: Optional[NormalizeFn] = None):
|
|
# Accept schedules, as well as scalar values.
|
|
if isinstance(learning_rate_fn, (float, int)):
|
|
lr = float(learning_rate_fn)
|
|
learning_rate_fn = lambda _: lr
|
|
# Normalization.
|
|
def update_fn(updates, state, params=None):
|
|
del params
|
|
updates = jax.tree_map(normalize_fn or (lambda x: x), updates)
|
|
return updates, state
|
|
gradient_transformation = optax.chain(
|
|
optax.GradientTransformation(lambda _: optax.EmptyState(), update_fn),
|
|
optax.scale_by_schedule(learning_rate_fn),
|
|
optax.scale(-1.))
|
|
super(SGD, self).__init__(gradient_transformation)
|
|
|
|
|
|
class IteratedFGSM(SGD):
|
|
"""L-infinity normalized steps."""
|
|
|
|
def __init__(self,
|
|
learning_rate_fn: Union[float, int, optax.Schedule]):
|
|
super(IteratedFGSM, self).__init__(learning_rate_fn, jnp.sign)
|
|
|
|
|
|
class Adam(StepOptimizer):
|
|
"""The Adam optimizer defined in https://arxiv.org/abs/1412.6980."""
|
|
|
|
def __init__(
|
|
self,
|
|
learning_rate_fn: Union[float, int, optax.Schedule],
|
|
normalize_fn: Optional[NormalizeFn] = None,
|
|
beta1: float = .9,
|
|
beta2: float = .999,
|
|
epsilon: float = 1e-9):
|
|
# Accept schedules, as well as scalar values.
|
|
if isinstance(learning_rate_fn, (float, int)):
|
|
lr = float(learning_rate_fn)
|
|
learning_rate_fn = lambda _: lr
|
|
# Normalization.
|
|
def update_fn(updates, state, params=None):
|
|
del params
|
|
updates = jax.tree_map(normalize_fn or (lambda x: x), updates)
|
|
return updates, state
|
|
gradient_transformation = optax.chain(
|
|
optax.GradientTransformation(lambda _: optax.EmptyState(), update_fn),
|
|
optax.scale_by_adam(b1=beta1, b2=beta2, eps=epsilon),
|
|
optax.scale_by_schedule(learning_rate_fn),
|
|
optax.scale(-1.))
|
|
super(Adam, self).__init__(gradient_transformation)
|
|
|
|
|
|
class PGD:
|
|
"""Runs Project Gradient Descent (see https://arxiv.org/pdf/1706.06083)."""
|
|
|
|
def __init__(self,
|
|
optimizer: StepOptimizer,
|
|
num_steps: int,
|
|
initialize_fn: Optional[InitializeFn] = None,
|
|
project_fn: Optional[ProjectFn] = None):
|
|
self._optimizer = optimizer
|
|
if initialize_fn is None:
|
|
initialize_fn = lambda rng, x: x
|
|
self._initialize_fn = initialize_fn
|
|
if project_fn is None:
|
|
project_fn = lambda x, origin_x: x
|
|
self._project_fn = project_fn
|
|
self._num_steps = num_steps
|
|
|
|
def __call__(self,
|
|
loss_fn: LossFn,
|
|
rng: chex.PRNGKey,
|
|
x: chex.Array) -> chex.Array:
|
|
def _optimize(rng, x):
|
|
"""Optimizes loss_fn when keep_best is False."""
|
|
def body_fn(_, inputs):
|
|
opt_state, current_x = inputs
|
|
current_x, _, opt_state = self._optimizer.minimize(current_x, opt_state)
|
|
current_x = self._project_fn(current_x, x)
|
|
return opt_state, current_x
|
|
opt_state = self._optimizer.init(loss_fn, x)
|
|
current_x = self._project_fn(self._initialize_fn(rng, x), x)
|
|
_, current_x = jax.lax.fori_loop(0, self._num_steps, body_fn,
|
|
(opt_state, current_x))
|
|
return current_x
|
|
return jax.lax.stop_gradient(_optimize(rng, x))
|
|
|
|
|
|
def linf_project_fn(epsilon: float, bounds: Tuple[float, float]) -> ProjectFn:
|
|
def project_fn(x, origin_x):
|
|
dx = jnp.clip(x - origin_x, -epsilon, epsilon)
|
|
return jnp.clip(origin_x + dx, bounds[0], bounds[1])
|
|
return project_fn
|
|
|
|
|
|
def linf_initialize_fn(epsilon: float) -> InitializeFn:
|
|
def initialize_fn(rng, x):
|
|
return x + jax.random.uniform(rng, x.shape, minval=-epsilon,
|
|
maxval=epsilon).astype(x.dtype)
|
|
return initialize_fn
|
|
|
|
|
|
def gradients_fn(loss_fn: LossFn,
|
|
x: chex.Array) -> Tuple[chex.Array, chex.Array]:
|
|
"""Returns the analytical gradient as computed by `jax.grad`."""
|
|
@functools.partial(jax.grad, has_aux=True)
|
|
def grad_reduced_loss_fn(x):
|
|
loss = loss_fn(x)
|
|
return jnp.sum(loss), loss
|
|
return grad_reduced_loss_fn(x)
|