diff --git a/mindarmour/adv_robustness/attacks/apgd.py b/mindarmour/adv_robustness/attacks/apgd.py new file mode 100644 index 0000000000000000000000000000000000000000..a334b1289498ec741f2dbb5446bfda228009dc3e --- /dev/null +++ b/mindarmour/adv_robustness/attacks/apgd.py @@ -0,0 +1,220 @@ +""" +AutoProjectedGradientDescent-Attack. +""" +import numpy as np + +import mindspore.nn as nn +import mindspore.ops as ops +import mindspore as ms + +from mindarmour.adv_robustness.attacks import BasicIterativeMethod +from mindarmour.utils.logger import LogUtil +from mindarmour.utils.util import WithLossCell, GradWrapWithLoss + +LOGGER = LogUtil.get_instance() +TAG = 'apgd' + + +class AutoProjectedGradientDescent(BasicIterativeMethod): + """ + APGD (Adaptive Projected Gradient Descent) is an iterative method for generating adversarial + examples that seeks to minimize the perturbation while ensuring that the adversarial sample + remains within a predefined Lp-ball around the original input. + + + Reference: Croce and Hein, "Reliable evaluation of adversarial robustness with an ensemble of \ + diverse parameter-free attacks" in ICML 2020 [https://arxiv.org/abs/2003.01690], + + Args: + network (Cell): Target model. + eps (float): Proportion of adversarial perturbation generated by the + attack to data range. Default: ``8 / 255`. + eps_iter (float): Proportion of single-step adversarial perturbation + generated by the attack to data range. Default: ``0.1``. + bounds (tuple): Upper and lower bounds of data, indicating the data range. + In form of (clip_min, clip_max). Default: ``(0.0, 1.0)``. + is_targeted (bool): If ``True``, targeted attack. If ``False``, untargeted + attack. Default: ``False``. + nb_iter (int) : Number of iteration. Default: ``10``. + norm_level (Union[int, str, numpy.inf]): Order of the norm. Possible values: + np.inf, 1 or 2. Default: ``'inf``. + loss_fn (Union[Loss, None]): Loss function for optimization. If ``None``, the input network \ + is already equipped with loss function. Default: ``None``. + eot_iter (int): number of iteration for EOT. Default: 1 + thr_decr (float): parameter for step-size update Default: 0.75 + + Examples: + >>> from mindspore.ops import operations as P + >>> from mindarmour.adv_robustness.attacks.apgd import AutoProjectedGradientDescent + >>> class Net(nn.Cell): + ... def __init__(self): + ... super(Net, self).__init__() + ... self._softmax = P.Softmax() + ... def construct(self, inputs): + ... out = self._softmax(inputs) + ... return out + >>> net = Net() + >>> attack = AutoProjectedGradientDescent(net, eps=0.3) + >>> inputs = np.asarray([[0.1, 0.2, 0.7]], np.float32) + >>> labels = np.asarray([2],np.int32) + >>> labels = np.eye(3)[labels].astype(np.float32) + >>> net = Net() + >>> adv_x = attack.generate(inputs, labels) + """ + + def __init__(self, network, eps=8 / 255, eps_iter=0.1, bounds=(0.0, 1.0), is_targeted=False, nb_iter=10, + norm_level='inf', loss_fn=None, eot_iter=1, thr_decr=0.75): + super(AutoProjectedGradientDescent, self).__init__(network, eps=eps, eps_iter=eps_iter, bounds=bounds, + is_targeted=is_targeted, nb_iter=nb_iter, loss_fn=loss_fn) + self._norm = norm_level + self._steps = nb_iter + self._eot_iter = eot_iter + self._eps = eps + self._loss_fn = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean') + self._network = network + self._thr_decr = thr_decr + + + def generate(self, inputs, labels): + """ + Iteratively generate adversarial examples based on BIM method. The + perturbation is normalized by projected method with parameter norm_level . + + Args: + inputs (Union[numpy.ndarray, tuple]): Benign input samples used as references to + create adversarial examples. + labels (Union[numpy.ndarray, tuple]): Original/target labels. \ + For each input if it has more than one label, it is wrapped in a tuple. + + Returns: + numpy.ndarray, generated adversarial examples. + """ + inputs = ms.Tensor(inputs) + labels = ms.Tensor(labels) + + x = inputs.copy() if len(inputs.shape) == 4 else inputs.copy().unsqueeze(0) + y = labels.copy() if len(labels.shape) == 1 else labels.copy().unsqueeze(0) + + # Default hyperparameter settings + steps, steps_min, size_decr = max(int(0.22 * self._steps), 1), max(int(0.06 * self._steps), 1), max( + int(0.03 * self._steps), 1) + + if self._norm == 'inf': + t = 2 * ms.Tensor(np.random.rand(*x.shape).astype(np.float32)) - 1 + x_adv = x + self._eps * ms.Tensor(np.ones([x.shape[0], 1, 1, 1]), dtype=ms.float32) * t / ( + t.reshape([t.shape[0], -1]).abs().max(axis=1, keepdims=True)[0].reshape([-1, 1, 1, 1])) + elif self._norm == 'L2': + t = ms.Tensor(np.random.rand(*x.shape).astype(np.float32)) + x_adv = x + self._eps * ms.Tensor(np.ones([x.shape[0], 1, 1, 1]), dtype=ms.float32) * t / ( + (t ** 2).sum(axis=(1, 2, 3), keepdims=True).sqrt() + 1e-12) + + x_adv = x_adv.clip(0.0, 1.0) + x_best = x_adv.copy() + x_best_adv = x_adv.copy() + + loss_steps = np.zeros((self._steps, x.shape[0])) + loss_best_steps = np.zeros((self._steps + 1, x.shape[0])) + acc_steps = np.zeros_like(loss_best_steps) + criterion_indiv = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='none') + grad = ops.ZerosLike()(x) + for _ in range(self._eot_iter): + grad += GradWrapWithLoss(WithLossCell(self._network, self._loss_fn))(x_adv, y) + + grad = ops.div(grad, self._eot_iter) + + grad_best = grad.copy() + logits = self._network (x_adv) + loss_indiv = criterion_indiv(logits, y) + acc = logits.max(1)[1] == y + acc_steps[0] = acc + 0 + loss_best = loss_indiv.copy() + step_size = self._eps * ms.Tensor(np.ones([x.shape[0], 1, 1, 1]), dtype=ms.float32)).reshape([1, 1, 1, 1]) + x_adv_old = x_adv.copy() + k = steps + 0 + u = np.arange(x.shape[0]) + counter = 0 + momentum=0.75 + + loss_best_last_check = loss_best.copy() + reduced_last_check = np.zeros(loss_best.shape) == np.zeros(loss_best.shape) + + for i in range(self._steps): + grad2 = x_adv - x_adv_old + x_adv_old = x_adv.copy() + + a = momentum if i > 0 else 1.0 + + if self._norm == 'inf': + x_adv_1 = x_adv + step_size * ops.operations.Sign()(grad) + x_adv_1 = ops.clip_by_value(ops.clip_by_value(x_adv_1, x - self._eps, x + self._eps), 0.0, 1.0) + x_adv_1 = ops.clip_by_value( + ops.clip_by_value(x_adv + (x_adv_1 - x_adv) * a + grad2 * (1 - a), x - self._eps, x + self._eps), 0.0, + 1.0) + + elif self._norm == 'L2': + x_adv_1 = x_adv + step_size * grad / ( + ms.ops.square(grad).sum(axis=(1, 2, 3), keepdims=True).sqrt() + 1e-12) + x_adv_1 = ops.clip_by_value(x + (x_adv_1 - x) / ( + ((x_adv_1 - x) ** 2).sum(axis=(1, 2, 3), keepdims=True).sqrt() + 1e-12) * ops.minimum( + self._eps * ops.ones(x.shape, type=ms.float32), + ((x_adv_1 - x) ** 2).sum(axis=(1, 2, 3), keepdims=True).sqrt()), + 0.0, 1.0) + x_adv_1 = x_adv + (x_adv_1 - x_adv) * a + grad2 * (1 - a) + x_adv_1 = ops.clip_by_value(x + (x_adv_1 - x) / ( + ((x_adv_1 - x) ** 2).sum(axis=(1, 2, 3), keepdims=True).sqrt() + 1e-12) * ops.minimum( + self._eps * ops.ones(x.shape, type=ms.float32), + ((x_adv_1 - x) ** 2).sum(axis=(1, 2, 3), keepdims=True).sqrt() + 1e-12), 0.0, 1.0) + + x_adv = x_adv_1 + 0. + grad = ops.zeros_like(x) + for _ in range(self._eot_iter): + grad += GradWrapWithLoss(WithLossCell(self._network, self._loss_fn))(x_adv, y) + + grad /= float(self._eot_iter) + + pred = logits.max(1)[1] == y + acc = ops.logical_and(acc, pred) + acc_steps[i + 1] = acc + 0 + zero_indices = ops.nonzero(pred == 0).squeeze() + x_best_adv[zero_indices] = x_adv[zero_indices] + ops.zeros_like(x_adv[zero_indices]) + + # check step size + y1 = loss_indiv.copy() + loss_steps[i] = y1.asnumpy() + 0 + ind = (y1 >= loss_best).nonzero().squeeze() + x_best[ind] = x_adv[ind].copy() + grad_best[ind] = grad[ind].copy() + loss_best[ind] = y1[ind] + 0 + loss_best_steps[i + 1] = loss_best.asnumpy() + 0 + counter += 1 + + if counter == k: + fl_oscillation = self._check_oscillation(loss_steps, i, k, + k3=self._thr_decr) + fl_reduce_no_impr = (~reduced_last_check) * (loss_best_last_check.asnumpy() >= loss_best.asnumpy()) + fl_oscillation = ~(~fl_oscillation * ~fl_reduce_no_impr) + reduced_last_check = np.copy(fl_oscillation) + loss_best_last_check = loss_best.copy() + + if np.sum(fl_oscillation) > 0: + step_size_np = step_size.asnumpy() + step_size_np[u[fl_oscillation]] /= 2.0 + step_size = ms.Tensor(step_size_np) + fl_oscillation = np.where(fl_oscillation) + fl_oscillation = ms.Tensor(fl_oscillation) + x_adv[fl_oscillation] = x_best[fl_oscillation].copy() + grad[fl_oscillation] = grad_best[fl_oscillation].copy() + counter = 0 + k = np.maximum(k - size_decr, steps_min) + return x_best.asnumpy() + + def _check_oscillation(self, x, j, k, k3=0.75): + """ + This function checks if there is oscillation in a given set of numbers. It counts how many + times the numbers go up in a certain range around a specific number. If this count is less + than or equal to a certain threshold, there is no oscillation. Otherwise, there is oscillation. + """ + t = np.zeros(x.shape[1]) + for counter in range(k): + t += x[j - counter] > x[j - counter - 1] + return t <= k * k3 * np.ones(t.shape)