Source code for mindarmour.attacks.black.genetic_attack

# Copyright 2019 Huawei Technologies Co., Ltd
#
# 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.
"""
Genetic-Attack.
"""
import numpy as np
from scipy.special import softmax

from mindarmour.attacks.attack import Attack
from mindarmour.utils.logger import LogUtil
from mindarmour.attacks.black.black_model import BlackModel
from mindarmour.utils._check_param import check_numpy_param, check_model, \
    check_pair_numpy_param, check_param_type, check_value_positive, \
    check_int_positive, check_param_multi_types


LOGGER = LogUtil.get_instance()
TAG = 'GeneticAttack'


def _mutation(cur_pop, step_noise=0.01, prob=0.005):
    """
    Generate mutation samples in genetic_attack.

    Args:
        cur_pop (numpy.ndarray): Samples before mutation.
        step_noise (float): Noise range. Default: 0.01.
        prob (float): Mutation probability. Default: 0.005.

    Returns:
        numpy.ndarray, samples after mutation operation in genetic_attack.

    Examples:
        >>> mul_pop = self._mutation_op([0.2, 0.3, 0.4], step_noise=0.03,
        >>> prob=0.01)
    """
    cur_pop = check_numpy_param('cur_pop', cur_pop)
    perturb_noise = np.clip(np.random.random(cur_pop.shape) - 0.5,
                            -step_noise, step_noise)
    mutated_pop = perturb_noise*(
        np.random.random(cur_pop.shape) < prob) + cur_pop
    return mutated_pop


[docs]class GeneticAttack(Attack): """ The Genetic Attack represents the black-box attack based on the genetic algorithm, which belongs to differential evolution algorithms. This attack was proposed by Moustafa Alzantot et al. (2018). References: `Moustafa Alzantot, Yash Sharma, Supriyo Chakraborty, "GeneticAttack: Practical Black-box Attacks with Gradient-FreeOptimization" <https://arxiv.org/abs/1805.11090>`_ Args: model (BlackModel): Target model. pop_size (int): The number of particles, which should be greater than zero. Default: 6. mutation_rate (float): The probability of mutations. Default: 0.005. per_bounds (float): Maximum L_inf distance. max_steps (int): The maximum round of iteration for each adversarial example. Default: 1000. step_size (float): Attack step size. Default: 0.2. temp (float): Sampling temperature for selection. Default: 0.3. bounds (tuple): Upper and lower bounds of data. In form of (clip_min, clip_max). Default: (0, 1.0) adaptive (bool): If True, turns on dynamic scaling of mutation parameters. If false, turns on static mutation parameters. Default: False. sparse (bool): If True, input labels are sparse-encoded. If False, input labels are one-hot-encoded. Default: True. Examples: >>> attack = GeneticAttack(model) """ def __init__(self, model, pop_size=6, mutation_rate=0.005, per_bounds=0.15, max_steps=1000, step_size=0.20, temp=0.3, bounds=(0, 1.0), adaptive=False, sparse=True): super(GeneticAttack, self).__init__() self._model = check_model('model', model, BlackModel) self._per_bounds = check_value_positive('per_bounds', per_bounds) self._pop_size = check_int_positive('pop_size', pop_size) self._step_size = check_value_positive('step_size', step_size) self._temp = check_value_positive('temp', temp) self._max_steps = check_int_positive('max_steps', max_steps) self._mutation_rate = check_value_positive('mutation_rate', mutation_rate) self._adaptive = check_param_type('adaptive', adaptive, bool) self._bounds = check_param_multi_types('bounds', bounds, [list, tuple]) for b in self._bounds: _ = check_param_multi_types('bound', b, [int, float]) # initial global optimum fitness value self._best_fit = -1 # count times of no progress self._plateau_times = 0 # count times of changing attack step self._adap_times = 0 self._sparse = check_param_type('sparse', sparse, bool)
[docs] def generate(self, inputs, labels): """ Generate adversarial examples based on input data and targeted labels (or ground_truth labels). Args: inputs (numpy.ndarray): Input samples. labels (numpy.ndarray): Targeted labels. Returns: - numpy.ndarray, bool values for each attack result. - numpy.ndarray, generated adversarial examples. - numpy.ndarray, query times for each sample. Examples: >>> advs = attack.generate([[0.2, 0.3, 0.4], >>> [0.3, 0.3, 0.2]], >>> [1, 2]) """ inputs, labels = check_pair_numpy_param('inputs', inputs, 'labels', labels) # if input is one-hot encoded, get sparse format value if not self._sparse: if labels.ndim != 2: raise ValueError('labels must be 2 dims, ' 'but got {} dims.'.format(labels.ndim)) labels = np.argmax(labels, axis=1) adv_list = [] success_list = [] query_times_list = [] for i in range(inputs.shape[0]): is_success = False target_label = labels[i] iters = 0 x_ori = inputs[i] # generate particles ori_copies = np.repeat( x_ori[np.newaxis, :], self._pop_size, axis=0) # initial perturbations cur_pert = np.clip(np.random.random(ori_copies.shape)*self._step_size, (0 - self._per_bounds), self._per_bounds) query_times = 0 while iters < self._max_steps: iters += 1 cur_pop = np.clip( ori_copies + cur_pert, self._bounds[0], self._bounds[1]) pop_preds = self._model.predict(cur_pop) query_times += cur_pop.shape[0] all_preds = np.argmax(pop_preds, axis=1) success_pop = np.equal(target_label, all_preds).astype(np.int32) success = max(success_pop) if success == 1: is_success = True adv = cur_pop[np.argmax(success_pop)] break target_preds = pop_preds[:, target_label] others_preds_sum = np.sum(pop_preds, axis=1) - target_preds fit_vals = target_preds - others_preds_sum best_fit = max(target_preds - np.max(pop_preds)) if best_fit > self._best_fit: self._best_fit = best_fit self._plateau_times = 0 else: self._plateau_times += 1 adap_threshold = (lambda z: 100 if z > -0.4 else 300)(best_fit) if self._plateau_times > adap_threshold: self._adap_times += 1 self._plateau_times = 0 if self._adaptive: step_noise = max(self._step_size, 0.4*(0.9**self._adap_times)) step_p = max(self._step_size, 0.5*(0.9**self._adap_times)) else: step_noise = self._step_size step_p = self._mutation_rate step_temp = self._temp elite = cur_pert[np.argmax(fit_vals)] select_probs = softmax(fit_vals/step_temp) select_args = np.arange(self._pop_size) parents_arg = np.random.choice( a=select_args, size=2*(self._pop_size - 1), replace=True, p=select_probs) parent1 = cur_pert[parents_arg[:self._pop_size - 1]] parent2 = cur_pert[parents_arg[self._pop_size - 1:]] parent1_probs = select_probs[parents_arg[:self._pop_size - 1]] parent2_probs = select_probs[parents_arg[self._pop_size - 1:]] parent2_probs = parent2_probs / (parent1_probs + parent2_probs) # duplicate the probabilities to all features of each particle. dims = len(x_ori.shape) for _ in range(dims): parent2_probs = parent2_probs[:, np.newaxis] parent2_probs = np.tile(parent2_probs, ((1,) + x_ori.shape)) cross_probs = (np.random.random(parent1.shape) > parent2_probs).astype(np.int32) childs = parent1*cross_probs + parent2*(1 - cross_probs) mutated_childs = _mutation( childs, step_noise=self._per_bounds*step_noise, prob=step_p) cur_pert = np.concatenate((mutated_childs, elite[np.newaxis, :])) if is_success: LOGGER.debug(TAG, 'successfully find one adversarial sample ' 'and start Reduction process.') adv_list.append(adv) else: LOGGER.debug(TAG, 'fail to find adversarial sample.') adv_list.append(elite + x_ori) LOGGER.debug(TAG, 'iteration times is: %d and query times is: %d', iters, query_times) success_list.append(is_success) query_times_list.append(query_times) del ori_copies, cur_pert, cur_pop return np.asarray(success_list), \ np.asarray(adv_list), \ np.asarray(query_times_list)