Esempio n. 1
0
    def _Q(self, inst_1: Instance, inst_2: Instance, derivative=False, k=-1):
        """
        Calculates Q_ij or partial Q_ij / partial x_k
        :param inst_1: the first instance
        :param inst_2: the second instance
        :param derivative: True -> calculate derivative, False -> calculate Q
        :param k: determines which derivative to calculate
        :return: Q_ij or the derivative where i corresponds to inst_1 and j
                 corresponds to inst_2
        """

        if inst_1.get_feature_count() != inst_2.get_feature_count():
            raise ValueError('Feature vectors need to have same length.')

        fv = [[], []]
        for i in range(2):
            if i == 0:
                inst = inst_1
            else:
                inst = inst_2

            feature_vector = inst.get_feature_vector()
            for j in range(inst.get_feature_count()):
                if feature_vector.get_feature(j) == 0:
                    fv[i].append(0)
                else:
                    fv[i].append(1)

        if derivative:
            ret_val = self.kernel_derivative(np.array(fv[0]), np.array(fv[1]),
                                             k)
        else:
            ret_val = self.kernel(np.array(fv[0]), np.array(fv[1]))
        return inst_1.get_label() * inst_2.get_label() * ret_val
Esempio n. 2
0
def csr_mat_to_instances(csr_mat, labels, binary=False):
    """
    Return a list of instances
    :param nd_arr:
    :param labels:
    :return:
    """
    data = csr_mat.data
    indices = csr_mat.indices
    indptr = csr_mat.indptr
    instance_len, num_features = csr_mat.shape
    instance_lst = []
    for i in range(instance_len):
        label = labels[i]
        instance_data = data[indptr[i]:indptr[i + 1]]
        instance_indices = list(indices[indptr[i]:indptr[i + 1]])
        if binary:
            instance_lst.append(
                Instance(label,
                         BinaryFeatureVector(num_features, instance_indices)))
        else:
            instance_lst.append(
                Instance(
                    label,
                    RealFeatureVector(num_features, instance_indices,
                                      instance_data)))
    return instance_lst
Esempio n. 3
0
    def _Q(self, inst_1: Instance, inst_2: Instance, derivative=False, k=-1):
        """
        Calculates Q_ij or partial Q_ij / partial x_k
        :param inst_1: the first instance
        :param inst_2: the second instance
        :param derivative: True -> calculate derivative, False -> calculate Q
        :param k: determines which derivative to calculate
        :return: Q_ij or the derivative where i corresponds to inst_1 and j
                 corresponds to inst_2
        """

        if inst_1.get_feature_count() != inst_2.get_feature_count():
            raise ValueError('Feature vectors need to have same length.')

        fvs = []
        for i in range(2):
            if i == 0:
                inst = inst_1
            else:
                inst = inst_2

            fvs.append(inst.get_feature_vector().get_csr_matrix())
            fvs[i] = np.array(fvs[i].todense().tolist()).flatten()

        if derivative:
            ret_val = self.kernel_derivative(fvs[0], fvs[1], k)
        else:
            ret_val = self.kernel(fvs[0], fvs[1])

        return inst_1.get_label() * inst_2.get_label() * ret_val
Esempio n. 4
0
    def transform(self, instance: Instance):
        """
        for the binary case, the f_attack value represents the percentage of
        features we change.
        If f_attack =1, then the result should be exactly the same as innocuous
        target.

        for the real_value case, we generate a value between c_f(x_min - xij)
        and c_f(x_max - xij)
        This value will be added to the xij for the new instance
        :param instance:
        :return: instance
        """

        if self.binary:
            attack_times = int(self.f_attack * self.num_features)
            count = 0
            for i in range(0, self.num_features):
                instance.get_feature_vector().flip_bit(i)
                count += 1
                if count == attack_times:
                    return instance
        else:
            for i in range(0, self.num_features):
                xij = instance.get_feature_vector().get_feature(i)
                if not self.manual:
                    lower_bound = self.f_attack * (self.x_min[i] - xij)
                    upper_bound = self.f_attack * (self.x_max[i] - xij)
                else:
                    lower_bound = self.f_attack * (self.xj_min - xij)
                    upper_bound = self.f_attack * (self.xj_max - xij)
                delta_ij = random.uniform(lower_bound, upper_bound)
                instance.flip(i, xij + delta_ij)
        return instance
Esempio n. 5
0
def test_find_witness_returns_messages_differing_by_one_word(
        good_word_with_params):
    adv = good_word_with_params
    spam_msg, legit_msg = adv.find_witness()
    assert adv.predict(Instance(0,
                                legit_msg)) == learner.negative_classification
    assert adv.predict(Instance(0,
                                spam_msg)) == learner.positive_classification
    assert len(legit_msg.feature_difference(spam_msg)) == 1
Esempio n. 6
0
    def _generate_inst(self):
        """
        :return: a properly generated Instance that has feature vector self.x
                 and label self.y
        """

        indices_list = []
        for i in range(len(self.x)):
            if self.x[i] >= 0.5:
                indices_list.append(i)

        # Generate new instance
        self.inst = Instance(self.y,
                             BinaryFeatureVector(len(self.x), indices_list))
Esempio n. 7
0
    def get_feature_vector_array(inst: Instance):
        """
        Turns the feature vector into an np.ndarray
        :param inst: the Instance
        :return: the feature vector (np.ndarray)
        """

        fv = inst.get_feature_vector()
        tmp = []
        for j in range(inst.get_feature_count()):
            if fv.get_feature(j) == 1:
                tmp.append(1)
            else:
                tmp.append(0)
        return np.array(tmp)
Esempio n. 8
0
    def _generate_inst(self):
        """
        :return: a properly generated Instance that has feature vector self.x
                 and label self.y
        """

        indices = []
        data = []
        for i, val in enumerate(self.x):
            if val != 0:
                indices.append(i)
                data.append(val)

        # Generate new instance
        fv = RealFeatureVector(len(self.x), indices, data)
        self.inst = Instance(self.y, fv)
Esempio n. 9
0
    def _calc_inst_loss(self, inst: Instance):
        """
        Calculates the logistic loss for one instance
        :param inst: the instance
        :return: the logistic loss
        """

        fv = inst.get_feature_vector().get_csr_matrix()
        fv = np.array(fv.todense().tolist()).flatten()

        # reshape is for the decision function when inputting only one sample
        loss = self.learner.model.learner.decision_function(fv.reshape(1, -1))
        loss *= -1 * inst.get_label()
        loss = math.log(1 + math.exp(loss))

        return loss
Esempio n. 10
0
 def gap(self, x: Instance, weight):
     """
     :param x: Instance object
     :param weight: a vector specifying linear weights
     :return: real value of gap(x)
     """
     x_prime = x.get_csr_matrix().toarray().T
     return math.fabs(weight * x_prime - self.threshold)
Esempio n. 11
0
    def optimize(self, instance: Instance):
        """Flip features that lower the prob. of being classified adversarial.
        Args:
            instance: (scipy.sparse.csr_matrix) feature vector

        """
        change = 0
        for i in range(0, self.num_features):
            orig_prob = self.learn_model.predict_proba([instance])[0]
            new_instance = deepcopy(instance)
            new_instance.get_feature_vector().flip_bit(i)
            new_prob = self.learn_model.predict_proba([new_instance])[0]
            if new_prob < (orig_prob - exp(self.lambda_val)):
                instance.get_feature_vector().flip_bit(i)
                change += 1
            if change > self.max_change:
                break
        return instance
Esempio n. 12
0
    def coordinate_greedy(self, instance: Instance) -> Instance:
        """
         Greddily update the feature to incrementally improve the attackers
         utility. run CS from L random starting points in the feature space. We
         repeat the alternation until differences of instances are small or
         max_change is reached.

         no_improve_count: number of points
         Q: transofrm cost(we use quodratic distance)
         GreedyImprove: using the coordinate descent algorithm.
        :param instance:
        :return: if the result is still classified as +1, we return origin
                 instance else we return the improved.
        """
        indices = [i for i in range(0, self.num_features)]
        x = xk = instance.get_csr_matrix().toarray()[0]
        no_improve_count = 0
        shuffle(indices)
        count = 0
        for i in indices:

            xkplus1 = self.minimize_transform(xk, x, i)
            oldQ = self.transform_cost(xk, x)
            newQ = self.transform_cost(xkplus1, x)
            # step_change = np.log(newQ) / np.log(oldQ)
            # using difference instead of log ratio for convergence check

            xk = xkplus1
            no_improve_count += 1
            if newQ - oldQ > 0 and oldQ != 0:
                step_change = np.log(newQ - oldQ)
                if step_change <= self.epsilon:
                    break
            if no_improve_count > self.max_change:
                break
            count += 1
        mat_indices = [x for x in range(0, self.num_features) if xk[x] != 0]
        mat_data = [xk[x] for x in range(0, self.num_features) if xk[x] != 0]
        new_instance = Instance(
            -1, RealFeatureVector(self.num_features, mat_indices, mat_data))

        return new_instance
Esempio n. 13
0
    def _calc_inst_loss(self, inst: Instance):
        """
        Calculates the logistic loss for one instance
        :param inst: the instance
        :return: the logistic loss
        """

        fv = []
        for i in range(inst.get_feature_count()):
            if inst.get_feature_vector().get_feature(i) == 1:
                fv.append(1)
            else:
                fv.append(0)
        fv = np.array(fv)

        # reshape is for the decision function when inputting only one sample
        loss = self.learner.model.learner.decision_function(fv.reshape(1, -1))
        loss *= -1 * inst.get_label()
        loss = math.log(1 + math.exp(loss))

        return loss
Esempio n. 14
0
 def transform(self, instance: Instance):
     '''
     for the real_value case, we generate a value between 0 and the bound.
     The bound is calculated by 1- c_delta * (abs(xt - x)/abs(x) + abs(xt)) * (xt -x)
     This value will be added to the xij for the new instance
     :param instance:
     :return: instance
     '''
     if self.binary:
         attack_times = int(self.f_attack * self.num_features)
         count = 0
         for i in range(0, self.num_features):
             delta_ij = self.innocuous_target.get_feature_vector().get_feature(i) \
                        - instance.get_feature_vector().get_feature(i)
             if delta_ij != 0:
                 if self.binary:  # when features are binary
                     instance.get_feature_vector().flip_bit(i)
             count += 1
             if count == attack_times:
                 return instance
     else:
         for i in range(0, self.num_features):
             xij = instance.get_feature_vector().get_feature(i)
             target = self.innocuous_target.get_feature_vector(
             ).get_feature(i)
             if abs(xij) + abs(target) == 0:
                 bound = 0
             else:
                 bound = self.discount_factor * (1 - self.f_attack *
                                                 (abs(target - xij) /
                                                  (abs(xij) + abs(target)))) \
                         * abs((target - xij))
             delta_ij = random.uniform(0, bound)
             instance.flip(i, xij + delta_ij)
         return instance
Esempio n. 15
0
 def transform(self, instance: Instance):
     '''
     for the real_value case, we generate a value between 0 and the bound.
     The bound is calculated by 1- c_delta * (abs(xt - x)/abs(x) + abs(xt)) * (xt -x)
     This value will be added to the xij for the new instance
     :param instance:
     :return: instance
     '''
     for i in range(0, self.num_features):
         xij = instance.get_feature_vector().get_feature(i)
         target = self.innocuous_target.get_feature_vector().get_feature(i)
         if abs(xij) + abs(target) == 0:
             bound = 0
         else:
             bound = self.discount_factor * (1 - self.f_attack *
                                             (abs(target - xij) /
                                              (abs(xij) + abs(target)))) \
                     * abs((target - xij))
         # is that ok to just assign a random number between the range???
         delta_ij = random.uniform(0, bound)
         instance.flip(i, xij + delta_ij)
     return instance
Esempio n. 16
0
def nd_arr_to_instances(nd_arr, labels=None, binary=False):
    """
    Return a list of instances
    :param nd_arr:
    :param labels:
    :param binary:
    :return:
    """
    num_instances = nd_arr.shape[0]
    if labels is None:
        labels = nd_arr[:, :1]
        data = nd_arr[:, 1:]
        num_features = nd_arr.shape[1] - 1
    else:
        data = nd_arr
        num_features = nd_arr.shape[1]

    instance_lst = []
    for i in range(num_instances):
        if binary:
            mat_indices = [
                x for x in range(0, num_features) if data[i][x] != 0
            ]
            instance_lst.append(
                Instance(labels[i],
                         BinaryFeatureVector(num_instances, mat_indices)))
        else:
            mat_indices = [
                x for x in range(0, num_features) if data[i][x] != 0
            ]
            mat_data = [
                data[i][x] for x in range(0, num_features) if data[0][x] != 0
            ]
            instance_lst.append(
                Instance(
                    labels[i],
                    RealFeatureVector(num_instances, mat_indices, mat_data)))
    return instance_lst
Esempio n. 17
0
def find_centroid(instances: List[Instance]):
    num_features = instances[0].get_feature_vector().feature_count
    indices = []
    data = []
    for i in range(num_features):
        sum = 0
        for instance in instances:
            if instance.label == -1:
                sum += instance.get_feature_vector().get_feature(i)
        sum /= num_features
        if sum != 0:
            indices.append(i)
            data.append(sum)
    return Instance(-1,RealFeatureVector(num_features, indices, data))
    def binary_gradient_descent(self, attack_instance: Instance):
        # sparse attack with binary features
        index_lst = []
        iter_time = 0
        attacker_score = self.get_score(
            attack_instance.get_csr_matrix().toarray())
        while iter_time < self.max_iter:
            grad = self.gradient(attack_instance.get_csr_matrix().toarray())
            if index_lst is not []:
                # eliminate the index we have already modified
                for i in index_lst:
                    grad[i] = 0
            change_index = np.argmax(np.absolute(grad))
            new_attack_instance = deepcopy(attack_instance)
            new_attack_instance.get_feature_vector().flip_bit(change_index)
            index_lst.append(change_index)

            new_attacker_score = self.get_score(
                new_attack_instance.get_csr_matrix().toarray())
            if new_attacker_score < attacker_score:
                attack_instance = new_attack_instance
                attacker_score = new_attacker_score
                iter_time += 1
        return attack_instance
Esempio n. 19
0
 def random_start_coordinate_greedy(self, instance: Instance):
     """
     implement the n random start algorithm by performing CG for n times.
     The minimized Q and x is used as new attack instance.
     :param instance:
     :return:
     """
     instance_lst = []
     q_value_lst = []
     old_x = instance.get_csr_matrix().toarray()[0]
     for i in range(self.random_start):
         new_attacked_instance = self.coordinate_greedy(instance)
         x = new_attacked_instance.get_csr_matrix().toarray()[0]
         q = self.transform_cost(x, old_x)
         instance_lst.append(new_attacked_instance)
         q_value_lst.append(q)
     return min(zip(instance_lst, q_value_lst), key=lambda x: x[1])[0]
Esempio n. 20
0
def load_dataset(emailData: EmailDataset) -> List[Instance]:
    """
    Conversion from dataset object into a list of instances
    :param emailData:
    """

    instances = []
    num_features = emailData.shape[1]
    indptr = emailData.features.indptr
    indices = emailData.features.indices
    data = emailData.features.data
    for i in range(0, emailData.num_instances):
        if emailData.binary:
            tmp_vector = BinaryFeatureVector(num_features, indices[indptr[i]:indptr[i + 1]].tolist())
        else:
            instance_data = data[indptr[i]:indptr[i + 1]].tolist()
            tmp_vector = RealFeatureVector(num_features, indices[indptr[i]:indptr[i + 1]].tolist(),
                                           instance_data)
        instances.append(Instance(emailData.labels[i], tmp_vector))
    return instances
Esempio n. 21
0
    def train(self):
        '''
        This is implemented according to Algorithm 1 in Central Rettraining Framework
        for Scalable Adversarial Classification. This will iterate between computing
        a classifier and adding the adversarial instances to the training data that evade
        the previously computed classifier.
        :return: None
        '''
        self.model.train(self.training_instances)
        iteration = self.iteration_times
        #self.attacker = self.attack_alg()
        #self.attacker.set_params(self.adv_params)
        #self.attacker.set_adversarial_params(self.model, self.training_instances)

        print("==> Training...")
        malicious_instances = [
            x for x in self.training_instances if self.model.predict(x) == 1
        ]
        augmented_instances = self.training_instances

        while iteration != 0:
            new = []
            transformed_instances = self.attacker.attack(malicious_instances)
            for instance in transformed_instances:
                in_list = False
                for idx, old_instance in enumerate(augmented_instances):
                    if fv_equals(old_instance.get_feature_vector(),
                                 instance.get_feature_vector()):
                        in_list = True
                if not in_list:
                    new.append(instance)
                augmented_instances.append(
                    Instance(label=1,
                             feature_vector=instance.get_feature_vector()))
            self.model.train(augmented_instances)
            malicious_instances = [
                x for x in augmented_instances if self.model.predict(x) == 1
            ]
            iteration -= 1
            if new is None:
                break
Esempio n. 22
0
    def transform(self, instance: Instance):
        '''
        for the binary case, the f_attack value represents the percentage of
        features we change.
        If f_attack =1, then the result should be exactly the same as innocuous
        target.

        for the real_value case, we generate a value between c_f(x_min - xij)
        and c_f(x_max - xij)
        This value will be added to the xij for the new instance
        :param instance:
        :return: instance
        '''
        if self.binary:
            attack_times = (int)(self.f_attack * self.num_features)
            count = 0
            for i in range(0, self.num_features):
                delta_ij = (self.innocuous_target.get_feature_vector().get_feature(i) -
                            instance.get_feature_vector().get_feature(i))
                if delta_ij != 0:
                    if self.binary:  # when features are binary
                        instance.get_feature_vector().flip_bit(i)
                count += 1
                if count == attack_times:
                    return instance
        else:
            for i in range(0, self.num_features):
                xij = instance.get_feature_vector().get_feature(i)
                if self.xj_min == 0 and self.xj_max == 0:
                    lower_bound = self.f_attack * (
                            self.x_min.get_feature(i) - xij)
                    upper_bound = self.f_attack * (
                            self.x_max.get_feature(i) - xij)
                else:
                    lower_bound = self.f_attack * (self.xj_min - xij)
                    upper_bound = self.f_attack * (self.xj_max - xij)
                # is that ok to just assign a random number between the range???
                delta_ij = random.uniform(lower_bound, upper_bound)
                instance.flip(i, xij + delta_ij)
        return instance
Esempio n. 23
0
class KInsertion(Adversary):
    """
    Performs a k-insertion attack where the attacked data is the original data
    plus k feature vectors designed to induce the most error in poison_instance.
    """
    def __init__(self,
                 learner,
                 poison_instance,
                 alpha=1e-8,
                 beta=0.1,
                 decay=-1,
                 eta=0.9,
                 max_iter=125,
                 number_to_add=10,
                 verbose=False):
        """
        :param learner: the trained learner
        :param poison_instance: the instance in which to induce the most error
        :param alpha: convergence condition (diff <= alpha)
        :param beta: the learning rate
        :param decay: the decay rate
        :param eta: the momentum percentage
        :param max_iter: the maximum number of iterations
        :param number_to_add: the number of new instances to add
        :param verbose: if True, print the feature vector and gradient for each
                        iteration
        """

        Adversary.__init__(self)
        self.learner = deepcopy(learner)
        self.poison_instance = poison_instance
        self.alpha = alpha
        self.beta = beta
        self.decay = self.beta / max_iter if decay < 0 else decay
        self.eta = eta
        self.max_iter = max_iter
        self.orig_beta = beta
        self.number_to_add = number_to_add
        self.verbose = verbose
        self.instances = None
        self.orig_instances = None
        self.fvs = None  # feature vectors
        self.labels = None  # labels
        self.x = None  # The feature vector of the instance to be added
        self.y = None  # x's label
        self.inst = None
        self.kernel = self._get_kernel()
        self.kernel_derivative = self._get_kernel_derivative()
        self.z_c = None
        self.matrix = None
        self.poison_loss_before = None
        self.poison_loss_after = None

        np.set_printoptions(threshold=0)

    def attack(self, instances) -> List[Instance]:
        """
        Performs a k-insertion attack
        :param instances: the input instances
        :return: the attacked instances
        """

        if len(instances) == 0:
            raise ValueError('Need at least one instance.')

        self.orig_instances = deepcopy(instances)
        self.instances = self.orig_instances
        self.learner.training_instances = self.instances
        self._calculate_constants()

        learner = self.learner.model.learner
        learner.fit(self.fvs, self.labels)

        self.poison_loss_before = self._calc_inst_loss(self.poison_instance)

        for k in range(self.number_to_add):
            print()
            print('###################################################',
                  end='')
            print('################')

            self._generate_x_y_and_inst()
            self.beta = self.orig_beta

            # Main learning loop for one insertion
            old_x = deepcopy(self.x)
            fv_dist = 0.0
            grad_norm = 0.0
            uv_norm = 0.0
            iteration = 0
            old_update_vector = 0.0
            max_val = (np.max(self.fvs) * 0.5 *
                       (k + 2) if k < 10 else np.max(self.fvs))

            while (iteration == 0
                   or (fv_dist > self.alpha and iteration < self.max_iter)):

                print('Iteration: ',
                      iteration,
                      ' - FV distance: ',
                      fv_dist,
                      ' - gradient norm: ',
                      grad_norm,
                      ' - UV norm: ',
                      uv_norm,
                      ' - beta: ',
                      self.beta,
                      sep='')

                begin = time.time()

                # Train with newly generated instance
                self.instances.append(self.inst)
                self.learner.training_instances = self.instances

                self.fvs = self.fvs.tolist()
                self.fvs.append(self.x)
                self.fvs = np.array(self.fvs)

                self.labels = self.labels.tolist()
                self.labels.append(self.y)
                self.labels = np.array(self.labels)

                learner.fit(self.fvs, self.labels)

                # Gradient descent with momentum
                gradient = self._calc_gradient()
                grad_norm = np.linalg.norm(gradient)

                if self.verbose:
                    print('\nGradient:\n', gradient, sep='')

                update_vector = (self.eta * old_update_vector +
                                 (1 - self.eta) * gradient)
                uv_norm = np.linalg.norm(update_vector)

                if self.verbose:
                    print('\nUpdate Vector:\n', update_vector, sep='')

                def update(x):
                    ret_val = 0.0 if x < 0.0 else x
                    ret_val = max_val if ret_val > max_val else ret_val
                    return ret_val

                self.x -= self.beta * update_vector
                self.x = np.array(list(map(update, self.x)), dtype='float64')

                if self.verbose:
                    print('\nFeature vector:\n', self.x, '\n', sep='')
                    print('Max gradient value:', np.max(gradient), '- Min',
                          'gradient value:', np.min(gradient))
                    print('Max UV value:', np.max(update_vector), '- Min',
                          'UV value:', np.min(update_vector))
                    print('Max FV value:', np.max(self.x), '- Min FV value:',
                          np.min(self.x))
                    print('Label:', self.y, '\n')

                self._generate_inst()
                self.instances = self.instances[:-1]
                self.fvs = self.fvs[:-1]
                self.labels = self.labels[:-1]

                fv_dist = np.linalg.norm(self.x - old_x)
                old_x = deepcopy(self.x)
                self.beta *= 1 / (1 + self.decay * iteration)
                old_update_vector = deepcopy(update_vector)

                end = time.time()
                print('TIME: ', end - begin, 's', sep='')

                iteration += 1

            print('Iteration: FINAL - FV distance: ',
                  fv_dist,
                  ' - alpha: ',
                  self.alpha,
                  ' - beta: ',
                  self.beta,
                  sep='')
            print('Number added so far: ', k + 1, '\n', sep='')

            # Add the newly generated instance and retrain with that dataset
            self.instances.append(self.inst)
            self.learner.training_instances = self.instances
            self.learner.train()

            self._calculate_constants()

            print('###################################################',
                  end='')
            print('################')
            print()

        self.poison_loss_after = self._calc_inst_loss(self.poison_instance)

        return self.instances

    def _calculate_constants(self):
        """
        Calculates constants for the gradient descent loop
        """

        # Calculate feature vectors
        self.fvs = []
        for i in range(len(self.instances)):
            fv = self.instances[i].get_feature_vector().get_csr_matrix()
            fv = np.array(fv.todense().tolist()).flatten()
            self.fvs.append(fv)
        self.fvs = np.array(self.fvs, dtype='float64')

        # Calculate labels
        self.labels = []
        for inst in self.instances:
            self.labels.append(inst.get_label())
        self.labels = np.array(self.labels)

    def _calc_inst_loss(self, inst: Instance):
        """
        Calculates the logistic loss for one instance
        :param inst: the instance
        :return: the logistic loss
        """

        fv = inst.get_feature_vector().get_csr_matrix()
        fv = np.array(fv.todense().tolist()).flatten()

        # reshape is for the decision function when inputting only one sample
        loss = self.learner.model.learner.decision_function(fv.reshape(1, -1))
        loss *= -1 * inst.get_label()
        loss = math.log(1 + math.exp(loss))

        return loss

    def _generate_x_y_and_inst(self):
        """
        Generates self.x, self.y, and self.inst
        """

        self.x = self.poison_instance.get_feature_vector().get_csr_matrix()
        self.x = np.array(self.x.todense().tolist(), dtype='float64').flatten()
        self.x += abs(np.random.normal(0, 0.00001, len(self.x)))
        self.y = -1 * self.poison_instance.get_label()

        self._generate_inst()

    def _generate_inst(self):
        """
        :return: a properly generated Instance that has feature vector self.x
                 and label self.y
        """

        indices = []
        data = []
        for i, val in enumerate(self.x):
            if val != 0:
                indices.append(i)
                data.append(val)

        # Generate new instance
        fv = RealFeatureVector(len(self.x), indices, data)
        self.inst = Instance(self.y, fv)

    def _calc_gradient(self):
        """
        :return: the calculated gradient, an np.ndarray
        """

        result = self._solve_matrix()
        self.z_c = result[0]
        self.matrix = result[1]

        size = self.instances[0].get_feature_count()
        pool = mp.Pool(mp.cpu_count())
        gradient = list(pool.map(self._calc_grad_helper, range(size)))
        pool.close()
        pool.join()

        gradient = np.array(gradient, dtype='float64')
        return gradient

    def _calc_grad_helper(self, i):
        """
        Helper function for gradient. Calculates one partial derivative.
        :param i: determines which partial derivative
        :return: the partial derivative
        """
        current = 0  # current partial derivative

        vector = [0]
        for j in self.learner.model.learner.support_:
            vector.append(
                self._Q(self.instances[j], self.inst, derivative=True, k=i))
        vector = np.array(vector)

        solution = self.matrix.dot(vector)
        partial_b_partial_x_k = solution[0]
        partial_z_s_partial_x_k = solution[1:]

        s_v_indices = self.learner.model.learner.support_.tolist()
        for j in range(len(self.orig_instances)):
            if j in self.learner.model.learner.support_:
                q_i_t = self._Q(self.orig_instances[j], self.inst)
                partial_z_i_partial_x_k = partial_z_s_partial_x_k[
                    s_v_indices.index(j)]
                current += q_i_t * partial_z_i_partial_x_k

        current += (self._Q(self.instances[-1], self.inst, True, i) * self.z_c)

        if len(self.instances) in self.learner.model.learner.support_:
            current += (self._Q(self.instances[-1], self.inst) *
                        partial_z_s_partial_x_k[-1])

        current += self.inst.get_label() * partial_b_partial_x_k
        return current

    def _solve_matrix(self):
        """
        :return: z_c, matrix for derivative calculations

        Note: I tried using multiprocessing Pools, but these were slower than
              using the built-in map function.
        """

        learner = self.learner.model.learner
        size = learner.n_support_[0] + learner.n_support_[1] + 1  # binary
        matrix = np.full((size, size), 0.0)

        if len(self.instances) - 1 not in learner.support_:  # not in S
            if self.learner.predict(
                    self.inst) != self.inst.get_label():  # in E
                z_c = learner.C
            else:  # in R, z_c = 0, everything is 0
                return 0.0, matrix
        else:  # in S
            # Get index of coefficient
            index = learner.support_.tolist().index(len(self.instances) - 1)
            z_c = learner.dual_coef_.flatten()[index]

        y_s = []
        for i in learner.support_:
            y_s.append(self.instances[i].get_label())
        y_s = np.array(y_s)

        q_s = []
        for i in range(size - 1):
            values = list(
                map(
                    lambda idx: self._Q(self.instances[learner.support_[i]],
                                        self.instances[learner.support_[idx]]),
                    range(size - 1)))
            q_s.append(values)
        q_s = np.array(q_s)

        for i in range(1, size):
            matrix[0][i] = y_s[i - 1]
            matrix[i][0] = y_s[i - 1]

        for i in range(1, size):
            for j in range(1, size):
                matrix[i][j] = q_s[i - 1][j - 1]

        try:
            matrix = np.linalg.inv(matrix)
        except np.linalg.linalg.LinAlgError:
            print('SINGULAR MATRIX ERROR')

            matrix = fuzz_matrix(matrix)
            matrix = np.linalg.inv(matrix)

        matrix = -1 * z_c * matrix

        return z_c, matrix

    def _Q(self, inst_1: Instance, inst_2: Instance, derivative=False, k=-1):
        """
        Calculates Q_ij or partial Q_ij / partial x_k
        :param inst_1: the first instance
        :param inst_2: the second instance
        :param derivative: True -> calculate derivative, False -> calculate Q
        :param k: determines which derivative to calculate
        :return: Q_ij or the derivative where i corresponds to inst_1 and j
                 corresponds to inst_2
        """

        if inst_1.get_feature_count() != inst_2.get_feature_count():
            raise ValueError('Feature vectors need to have same length.')

        fvs = []
        for i in range(2):
            if i == 0:
                inst = inst_1
            else:
                inst = inst_2

            fvs.append(inst.get_feature_vector().get_csr_matrix())
            fvs[i] = np.array(fvs[i].todense().tolist()).flatten()

        if derivative:
            ret_val = self.kernel_derivative(fvs[0], fvs[1], k)
        else:
            ret_val = self.kernel(fvs[0], fvs[1])

        return inst_1.get_label() * inst_2.get_label() * ret_val

    def _kernel_linear(self, fv_1: np.ndarray, fv_2: np.ndarray):
        """
        Returns the value of the specified kernel function
        :param fv_1: feature vector 1 (np.ndarray)
        :param fv_2: feature vector 2 (np.ndarray)
        :return: the value of the specified kernel function
        """

        if len(fv_1) != len(fv_2):
            raise ValueError('Feature vectors need to have same length.')

        return fv_1.dot(fv_2)

    def _kernel_derivative_linear(self, fv_1: np.ndarray, fv_2: np.ndarray,
                                  k: int):
        """
        Returns the value of the derivative of the specified kernel function
        with fv_2 being the variable (i.e. K(x_i, x_c), finding gradient
        evaluated at x_c
        :param fv_1: fv_1: feature vector 1 (np.ndarray)
        :param fv_2: fv_2: feature vector 2 (np.ndarray)
        :param k: which partial derivative (0-based indexing, int)
        :return: the value of the derivative of the specified kernel function
        """

        if len(fv_1) != len(fv_2) or k < 0 or k >= len(fv_1):
            raise ValueError('Feature vectors need to have same '
                             'length and k must be a valid index.')

        return fv_1[k]

    def _kernel_poly(self, fv_1: np.ndarray, fv_2: np.ndarray):
        """
        Returns the value of the specified kernel function
        :param fv_1: feature vector 1 (np.ndarray)
        :param fv_2: feature vector 2 (np.ndarray)
        :return: the value of the specified kernel function
        """

        if len(fv_1) != len(fv_2):
            raise ValueError('Feature vectors need to have same length.')

        return ((self.learner.gamma * fv_1.dot(fv_2) +
                 self.learner.coef0)**self.learner.degree)

    def _kernel_derivative_poly(self, fv_1: np.ndarray, fv_2: np.ndarray,
                                k: int):
        """
        Returns the value of the derivative of the specified kernel function
        with fv_2 being the variable (i.e. K(x_i, x_c), finding gradient
        evaluated at x_c
        :param fv_1: fv_1: feature vector 1 (np.ndarray)
        :param fv_2: fv_2: feature vector 2 (np.ndarray)
        :param k: which partial derivative (0-based indexing, int)
        :return: the value of the derivative of the specified kernel function
        """

        if len(fv_1) != len(fv_2) or k < 0 or k >= len(fv_1):
            raise ValueError('Feature vectors need to have same '
                             'length and k must be a valid index.')

        return (fv_1[k] * self.learner.degree * self.learner.gamma *
                ((self.learner.gamma * fv_1.dot(fv_2) + self.learner.coef0)
                 **(self.learner.degree - 1)))

    def _kernel_rbf(self, fv_1: np.ndarray, fv_2: np.ndarray):
        """
        Returns the value of the specified kernel function
        :param fv_1: feature vector 1 (np.ndarray)
        :param fv_2: feature vector 2 (np.ndarray)
        :return: the value of the specified kernel function
        """

        if len(fv_1) != len(fv_2):
            raise ValueError('Feature vectors need to have same length.')

        norm = np.linalg.norm(fv_1 - fv_2)**2
        return math.exp(-1 * self.learner.gamma * norm)

    def _kernel_derivative_rbf(self, fv_1: np.ndarray, fv_2: np.ndarray,
                               k: int):
        """
        Returns the value of the derivative of the specified kernel function
        with fv_2 being the variable (i.e. K(x_i, x_c), finding gradient
        evaluated at x_c
        :param fv_1: fv_1: feature vector 1 (np.ndarray)
        :param fv_2: fv_2: feature vector 2 (np.ndarray)
        :param k: which partial derivative (0-based indexing, int)
        :return: the value of the derivative of the specified kernel function
        """

        if len(fv_1) != len(fv_2) or k < 0 or k >= len(fv_1):
            raise ValueError('Feature vectors need to have same '
                             'length and k must be a valid index.')

        return (self._kernel_rbf(fv_1, fv_2) * 2 * self.learner.gamma *
                (fv_1[k] - fv_2[k]))

    def _kernel_sigmoid(self, fv_1: np.ndarray, fv_2: np.ndarray):
        """
        Returns the value of the specified kernel function
        :param fv_1: feature vector 1 (np.ndarray)
        :param fv_2: feature vector 2 (np.ndarray)
        :return: the value of the specified kernel function
        """

        if len(fv_1) != len(fv_2):
            raise ValueError('Feature vectors need to have same length.')

        inside = self.learner.gamma * fv_1.dot(fv_2) + self.learner.coef0
        return math.tanh(inside)

    def _kernel_derivative_sigmoid(self, fv_1: np.ndarray, fv_2: np.ndarray,
                                   k: int):
        """
        Returns the value of the derivative of the specified kernel function
        with fv_2 being the variable (i.e. K(x_i, x_c), finding gradient
        evaluated at x_c
        :param fv_1: fv_1: feature vector 1 (np.ndarray)
        :param fv_2: fv_2: feature vector 2 (np.ndarray)
        :param k: which partial derivative (0-based indexing, int)
        :return: the value of the derivative of the specified kernel function
        """

        if len(fv_1) != len(fv_2) or k < 0 or k >= len(fv_1):
            raise ValueError('Feature vectors need to have same '
                             'length and k must be a valid index.')

        inside = self.learner.gamma * fv_1.dot(fv_2) + self.learner.coef0
        return self.learner.gamma * fv_1[k] / (math.cosh(inside)**2)

    def _get_kernel(self):
        """
        :return: the appropriate kernel function
        """

        if self.learner.model.learner.kernel == 'linear':
            return self._kernel_linear
        elif self.learner.model.learner.kernel == 'poly':
            return self._kernel_poly
        elif self.learner.model.learner.kernel == 'rbf':
            return self._kernel_rbf
        elif self.learner.model.learner.kernel == 'sigmoid':
            return self._kernel_sigmoid
        else:
            raise ValueError('No matching kernel function found.')

    def _get_kernel_derivative(self):
        """
        :return: the appropriate kernel derivative function
        """

        if self.learner.model.learner.kernel == 'linear':
            return self._kernel_derivative_linear
        elif self.learner.model.learner.kernel == 'poly':
            return self._kernel_derivative_poly
        elif self.learner.model.learner.kernel == 'rbf':
            return self._kernel_derivative_rbf
        elif self.learner.model.learner.kernel == 'sigmoid':
            return self._kernel_derivative_sigmoid
        else:
            raise ValueError('No matching kernel function found.')

    def set_params(self, params: Dict):
        if params['learner'] is not None:
            self.learner = params['learner']
        if params['poison_instance'] is not None:
            self.poison_instance = params['poison_instance']
        if params['alpha'] is not None:
            self.alpha = params['alpha']
        if params['beta'] is not None:
            self.beta = params['beta']
        if params['decay'] is not None:
            self.decay = params['decay']
        if params['eta'] is not None:
            self.eta = params['eta']
        if params['max_iter'] is not None:
            self.max_iter = params['max_iter']
        if params['number_to_add'] is not None:
            self.number_to_add = params['number_to_add']
        if params['verbose'] is not None:
            self.verbose = params['verbose']

        self.instances = None
        self.orig_instances = None
        self.fvs = None
        self.labels = None
        self.x = None
        self.y = None
        self.inst = None
        self.kernel = self._get_kernel()
        self.kernel_derivative = self._get_kernel_derivative()
        self.z_c = None
        self.matrix = None
        self.quick_calc = None
        self.poison_loss_before = None
        self.poison_loss_after = None

        np.set_printoptions(threshold=0)

    def get_available_params(self):
        params = {
            'learner': self.learner,
            'poison_instance': self.poison_instance,
            'alpha': self.alpha,
            'beta': self.beta,
            'decay': self.decay,
            'eta': self.eta,
            'max_iter': self.max_iter,
            'number_to_add': self.number_to_add,
            'verbose': self.verbose
        }
        return params

    def set_adversarial_params(self, learner, train_instances):
        self.learner = learner
        self.instances = train_instances
Esempio n. 24
0
    def gradient_descent(self, instance: Instance, neg_instances):
        #store iteration and objective values for plotting....
        #iteration_lst = []
        #objective_lst = []

        # attack_intance-> np array
        attack_instance = instance.get_csr_matrix().toarray()
        root_instance = attack_instance
        obj_function_value_list = []

        # store the modified gradient descent attack instances
        # find a list of potential neg_instances, the closest distance, and updated gradients
        candidate_attack_instances = [attack_instance]
        attacker_score = self.get_score(attack_instance)
        closer_neg_instances, dist, grad_update = self.compute_gradient(
            attack_instance, neg_instances)
        obj_func_value = attacker_score + self.lambda_val * dist
        obj_function_value_list.append(obj_func_value)

        for iter in range(self.max_iter):
            # no d(x,x_prime) is set to limit the boundary of attacks
            # compute the obj_func_value of the last satisfied instance
            # append to the value list
            #iteration_lst.append(iter)
            #objective_lst.append(obj_func_value)

            past_instance = candidate_attack_instances[-1]
            new_instance = self.update_within_boundary(past_instance,
                                                       root_instance,
                                                       grad_update)

            # compute the gradient and objective function value of the new instance
            closer_neg_instances, dist, new_grad_update = \
                self.compute_gradient(new_instance, closer_neg_instances)
            new_attacker_score = self.get_score(new_instance)
            obj_func_value = new_attacker_score + self.lambda_val * dist

            # check convergence information
            # we may reach a local min if the function value does not change
            # if obj_func_value == obj_function_value_list[-1]:
            #    print("Local min is reached. Iteration: %d, Obj value %d" %(iter,obj_func_value))
            #    mat_indices = [x for x in range(0, self.num_features) if new_instance[0][x] != 0]
            #    mat_data = [new_instance[0][x] for x in range(0, self.num_features) if new_instance[0][x] != 0]
            #    return Instance(-1, RealFeatureVector(self.num_features, mat_indices, mat_data))

            # check a small epsilon(difference is a small value after
            # several iterations)
            if self.check_convergence_info(obj_func_value,
                                           obj_function_value_list):
                #print("Goes to Convergence here.... Iteration: %d, Obj value %.4f" % (iter,obj_func_value))
                mat_indices = [
                    x for x in range(0, self.num_features)
                    if new_instance[0][x] != 0
                ]
                mat_data = [
                    new_instance[0][x] for x in range(0, self.num_features)
                    if new_instance[0][x] != 0
                ]

                #plt.plot(iteration_lst,objective_lst)
                return Instance(
                    -1,
                    RealFeatureVector(self.num_features, mat_indices,
                                      mat_data))

            # does not satisfy convergence requirement
            # store onto the list
            elif obj_func_value < obj_function_value_list[-1]:
                obj_function_value_list.append(obj_func_value)

            if not (new_instance == candidate_attack_instances[-1]).all():
                candidate_attack_instances.append(new_instance)

            attacker_score = new_attacker_score
            grad_update = new_grad_update

        #print("Convergence has not been found..")
        #plt.plot(iteration_lst, objective_lst)
        mat_indices = [
            x for x in range(0, self.num_features)
            if candidate_attack_instances[-1][0][x] != 0
        ]
        mat_data = [
            candidate_attack_instances[-1][0][x]
            for x in range(0, self.num_features)
            if candidate_attack_instances[-1][0][x] != 0
        ]

        return Instance(
            -1, RealFeatureVector(self.num_features, mat_indices, mat_data))
Esempio n. 25
0
 def predict_and_record(self, message):
     self.num_queries += 1
     return self.predict(Instance(0, message))
Esempio n. 26
0
    def coordinate_greedy(self, instance: Instance):
        """
         Greedily update the feature to incrementally improve the attackers utility.
         run CS from L random starting points in the feature space. We repeat the
         alternation until differences of instances are small or max_change is
         reached.

         no_improve_count: number of points
         Q: transofrm cost(we use quodratic distance)
         GreedyImprove: using the coordinate descent algorithm.
        :param instance:
        :return: if the result is still classified as +1, we return origin instance
                 else we return the improved.
        """
        instance_len = instance.get_feature_count()
        if DEBUG:
            iteration_list = []
            Q_value_list = []

        x = xk = instance.get_csr_matrix().toarray()[0]

        # converge is used for checking convergance conditions
        # if the last convergence_time iterations all satisfy <= eplison condition
        # ,the attack successfully finds a optimum
        converge = 0

        for iteration_time in range(self.max_iteration):
            i = randint(0, instance_len - 1)

            #calcualte cost function and greediy improve from a random feature i
            xkplus1 = self.minimize_transform(xk, x, i)
            old_q = self.transform_cost(xk, x)
            new_q = self.transform_cost(xkplus1, x)

            # check whether Q_value actually descends and converges to a minimum
            # plot the iteration and Q_values using matplotlib
            #if DEBUG:
            #    iteration_list.append(iteration_time)
            #    Q_value_list.append(new_q)

            # if new_q < 0:
            #     print("Attack finishes because Q is less than 0")
            #     break

            if new_q - old_q <= 0:
                xk = xkplus1
                step_change = old_q - new_q
                # the np.log() may not converge in special cases
                # makes sure the cost function actually converges
                # alternative implementation?
                #step_change = np.log(new_q) / np.log(old_q)
                #step_change = np.log(old_q - new_q)

                if step_change <= self.epsilon:
                    converge += 1
                    if converge >= self.convergence_time:
                        #print("Attack finishes because of convergence!")
                        break

        #if DEBUG:
        #    plt.plot(iteration_list,Q_value_list)

        mat_indices = [x for x in range(0, self.num_features) if xk[x] != 0]
        mat_data = [xk[x] for x in range(0, self.num_features) if xk[x] != 0]
        new_instance = Instance(
            -1, RealFeatureVector(self.num_features, mat_indices, mat_data))
        return new_instance
Esempio n. 27
0
class KInsertion(Adversary):
    """
    Performs a k-insertion attack where the attacked data is the original data
    plus k feature vectors designed to induce the most error in poison_instance.
    """
    def __init__(self,
                 learner,
                 poison_instance,
                 alpha=1e-3,
                 beta=0.05,
                 max_iter=2000,
                 number_to_add=10,
                 verbose=False):
        """
        :param learner: the trained learner
        :param poison_instance: the instance in which to induce the most error
        :param alpha: convergence condition (diff <= alpha)
        :param beta: the learning rate
        :param max_iter: the maximum number of iterations
        :param number_to_add: the number of new instances to add
        :param verbose: if True, print the feature vector and gradient for each
                        iteration
        """

        Adversary.__init__(self)
        self.learner = deepcopy(learner)
        self.poison_instance = poison_instance
        self.alpha = alpha
        self.beta = beta
        self.max_iter = max_iter
        self.orig_beta = beta
        self.number_to_add = number_to_add
        self.verbose = verbose
        self.instances = None
        self.orig_instances = None
        self.fvs = None  # feature vectors
        self.labels = None  # labels
        self.x = None  # The feature vector of the instance to be added
        self.y = None  # x's label
        self.inst = None
        self.kernel = self._get_kernel()
        self.kernel_derivative = self._get_kernel_derivative()
        self.z_c = None
        self.matrix = None
        self.quick_calc = None
        self.poison_loss_before = None
        self.poison_loss_after = None

    def attack(self, instances) -> List[Instance]:
        """
        Performs a k-insertion attack
        :param instances: the input instances
        :return: the attacked instances
        """

        if len(instances) == 0:
            raise ValueError('Need at least one instance.')

        self.orig_instances = deepcopy(instances)
        self.instances = self.orig_instances
        self.beta /= instances[0].get_feature_count()  # scale beta
        self.learner.training_instances = self.instances
        learner = self.learner.model.learner
        learner.fit(self.fvs, self.labels)

        self.poison_loss_before = self._calc_inst_loss(self.poison_instance)

        for k in range(self.number_to_add):
            # x is the full feature vector of the instance to be added
            self.x = np.random.binomial(1, 0.5,
                                        instances[0].get_feature_count())
            self.y = -1 if np.random.binomial(1, 0.5, 1)[0] == 0 else 1

            self._generate_inst()
            self.beta = self.orig_beta

            # Main learning loop for one insertion
            grad_norm = 0.0
            iteration = 0
            while (iteration == 0
                   or (grad_norm > self.alpha and iteration < self.max_iter)):

                print('Iteration: ',
                      iteration,
                      ' - gradient norm: ',
                      grad_norm,
                      sep='')

                # Train with newly generated instance
                self.instances.append(self.inst)
                self.learner.training_instances = self.instances

                self.fvs = self.fvs.tolist()
                self.fvs.append(self.x)
                self.fvs = np.array(self.fvs)

                self.labels = self.labels.tolist()
                self.labels.append(self.y)
                self.labels = np.array(self.labels)

                learner.fit(self.fvs, self.labels)

                # Update feature vector of the instance to be added
                gradient = self._calc_gradient()
                grad_norm = np.linalg.norm(gradient)

                self.x = self.x - self.beta * gradient
                self.x = DataModification.project_feature_vector(self.x)
                self._generate_inst()
                self.instances = self.instances[:-1]
                self.fvs = self.fvs[:-1]
                self.labels = self.labels[:-1]

                if self.verbose:
                    print('Current feature vector:\n', self.x)

                iteration += 1

            print('Iteration: FINAL - gradient norm: ', grad_norm, sep='')
            print('Number added so far: ', k + 1, sep='')

            # Add the newly generated instance and retrain with that dataset
            self.instances.append(self.inst)
            self.learner.training_instances = self.instances
            self.learner.train()

        self.poison_loss_after = self._calc_inst_loss(self.poison_instance)

        return self.instances

    def _calculate_constants(self):
        """
        Calculates constants for the gradient descent loop
        """

        # Calculate feature vectors
        self.fvs = []
        for i in range(len(self.instances)):
            feature_vector = self.instances[i].get_feature_vector()
            tmp = []
            for j in range(self.instances[0].get_feature_count()):
                if feature_vector.get_feature(j) == 1:
                    tmp.append(1)
                else:
                    tmp.append(0)
            tmp = np.array(tmp)
            self.fvs.append(tmp)
        self.fvs = np.array(self.fvs, dtype='float64')

        # Calculate labels
        self.labels = []
        for inst in self.instances:
            self.labels.append(inst.get_label())
        self.labels = np.array(self.labels)

    def _calc_inst_loss(self, inst: Instance):
        """
        Calculates the logistic loss for one instance
        :param inst: the instance
        :return: the logistic loss
        """

        fv = []
        for i in range(inst.get_feature_count()):
            if inst.get_feature_vector().get_feature(i) == 1:
                fv.append(1)
            else:
                fv.append(0)
        fv = np.array(fv)

        # reshape is for the decision function when inputting only one sample
        loss = self.learner.model.learner.decision_function(fv.reshape(1, -1))
        loss *= -1 * inst.get_label()
        loss = math.log(1 + math.exp(loss))

        return loss

    def _generate_inst(self):
        """
        :return: a properly generated Instance that has feature vector self.x
                 and label self.y
        """

        indices_list = []
        for i in range(len(self.x)):
            if self.x[i] >= 0.5:
                indices_list.append(i)

        # Generate new instance
        self.inst = Instance(self.y,
                             BinaryFeatureVector(len(self.x), indices_list))

    def _calc_gradient(self):
        """
        :return: the calculated gradient, an np.ndarray
        """

        result = self._solve_matrix()
        self.z_c = result[0]
        self.matrix = result[1]

        # If resulting matrix is zero (it will be if z_c == 0 by definition, so
        # short-circuit behavior is being used here), then only do one
        # calculation as per the formula.
        if self.z_c == 0 or np.count_nonzero(self.matrix) == 0:
            self.quick_calc = True
        else:
            self.quick_calc = False

        size = self.instances[0].get_feature_count()
        pool = mp.Pool(mp.cpu_count())
        gradient = list(pool.map(self._calc_grad_helper, range(size)))
        pool.close()
        pool.join()

        gradient = np.array(gradient)

        if self.verbose:
            print('\nCurrent gradient:\n', gradient)

        return gradient

    def _calc_grad_helper(self, i):
        """
        Helper function for gradient. Calculates one partial derivative.
        :param i: determines which partial derivative
        :return: the partial derivative
        """

        if self.quick_calc:
            val = self._Q(self.instances[-1], self.inst, True, i) * self.z_c
            return val
        else:
            current = 0  # current partial derivative

            vector = [0]
            for j in self.learner.model.learner.support_:
                vector.append(self._Q(self.instances[j], self.inst, True, i))
            vector = np.array(vector)

            solution = self.matrix.dot(vector)
            partial_b_partial_x_k = solution[0]
            partial_z_s_partial_x_k = solution[1:]

            s_v_indices = self.learner.model.learner.support_.tolist()
            for j in range(len(self.orig_instances)):
                if j in self.learner.model.learner.support_:
                    q_i_t = self._Q(self.orig_instances[j], self.inst)
                    partial_z_i_partial_x_k = partial_z_s_partial_x_k[
                        s_v_indices.index(j)]
                    current += q_i_t * partial_z_i_partial_x_k

            current += (self._Q(self.instances[-1], self.inst, True, i) *
                        self.z_c)

            if len(self.instances) in self.learner.model.learner.support_:
                current += (self._Q(self.instances[-1], self.inst) *
                            partial_z_s_partial_x_k[-1])

            current += self.inst.get_label() * partial_b_partial_x_k
            return current

    def _solve_matrix(self):
        """
        :return: z_c, matrix for derivative calculations

        Note: I tried using multiprocessing Pools, but these were slower than
              using the built-in map function.
        """

        learner = self.learner.model.learner
        size = learner.n_support_[0] + learner.n_support_[1] + 1  # binary
        matrix = np.full((size, size), 0)

        if len(self.instances) - 1 not in learner.support_:  # not in S
            if self.learner.predict(
                    self.inst) != self.inst.get_label():  # in E
                z_c = learner.C
            else:  # in R, z_c = 0, everything is 0
                return 0, matrix
        else:  # in S
            # Get index of coefficient
            index = learner.support_.tolist().index(len(self.instances) - 1)
            z_c = learner.dual_coef_.flatten()[index]

        y_s = []
        for i in learner.support_:
            y_s.append(self.instances[i].get_label())
        y_s = np.array(y_s)

        q_s = []
        for i in range(size - 1):
            values = list(
                map(
                    lambda idx: self._Q(self.instances[learner.support_[i]],
                                        self.instances[learner.support_[idx]]),
                    range(size - 1)))
            q_s.append(values)
        q_s = np.array(q_s)

        for i in range(1, size):
            matrix[0][i] = y_s[i - 1]
            matrix[i][0] = y_s[i - 1]

        for i in range(1, size):
            for j in range(1, size):
                matrix[i][j] = q_s[i - 1][j - 1]

        try:
            matrix = np.linalg.inv(matrix)
        except np.linalg.linalg.LinAlgError:
            # Sometimes the matrix is reported to be singular. In this case,
            # the safest thing to do is have the matrix and thus eventually
            # the gradient equal 0 as to not move the solution incorrectly.
            # There is probably an error in the computation, but I have not
            # looked for it yet.
            print('SINGULAR MATRIX ERROR - FIX ME')
            z_c = 0

        matrix = -1 * z_c * matrix

        return z_c, matrix

    def _Q(self, inst_1: Instance, inst_2: Instance, derivative=False, k=-1):
        """
        Calculates Q_ij or partial Q_ij / partial x_k
        :param inst_1: the first instance
        :param inst_2: the second instance
        :param derivative: True -> calculate derivative, False -> calculate Q
        :param k: determines which derivative to calculate
        :return: Q_ij or the derivative where i corresponds to inst_1 and j
                 corresponds to inst_2
        """

        if inst_1.get_feature_count() != inst_2.get_feature_count():
            raise ValueError('Feature vectors need to have same length.')

        fv = [[], []]
        for i in range(2):
            if i == 0:
                inst = inst_1
            else:
                inst = inst_2

            feature_vector = inst.get_feature_vector()
            for j in range(inst.get_feature_count()):
                if feature_vector.get_feature(j) == 0:
                    fv[i].append(0)
                else:
                    fv[i].append(1)

        if derivative:
            ret_val = self.kernel_derivative(np.array(fv[0]), np.array(fv[1]),
                                             k)
        else:
            ret_val = self.kernel(np.array(fv[0]), np.array(fv[1]))
        return inst_1.get_label() * inst_2.get_label() * ret_val

    def _kernel_linear(self, fv_1: np.ndarray, fv_2: np.ndarray):
        """
        Returns the value of the specified kernel function
        :param fv_1: feature vector 1 (np.ndarray)
        :param fv_2: feature vector 2 (np.ndarray)
        :return: the value of the specified kernel function
        """

        if len(fv_1) != len(fv_2):
            raise ValueError('Feature vectors need to have same length.')

        return fv_1.dot(fv_2)

    def _kernel_derivative_linear(self, fv_1: np.ndarray, fv_2: np.ndarray,
                                  k: int):
        """
        Returns the value of the derivative of the specified kernel function
        with fv_2 being the variable (i.e. K(x_i, x_c), finding gradient
        evaluated at x_c
        :param fv_1: fv_1: feature vector 1 (np.ndarray)
        :param fv_2: fv_2: feature vector 2 (np.ndarray)
        :param k: which partial derivative (0-based indexing, int)
        :return: the value of the derivative of the specified kernel function
        """

        if len(fv_1) != len(fv_2) or k < 0 or k >= len(fv_1):
            raise ValueError('Feature vectors need to have same '
                             'length and k must be a valid index.')

        return fv_1[k]

    def _kernel_poly(self, fv_1: np.ndarray, fv_2: np.ndarray):
        """
        Returns the value of the specified kernel function
        :param fv_1: feature vector 1 (np.ndarray)
        :param fv_2: feature vector 2 (np.ndarray)
        :return: the value of the specified kernel function
        """

        if len(fv_1) != len(fv_2):
            raise ValueError('Feature vectors need to have same length.')

        return ((self.learner.gamma * fv_1.dot(fv_2) +
                 self.learner.coef0)**self.learner.degree)

    def _kernel_derivative_poly(self, fv_1: np.ndarray, fv_2: np.ndarray,
                                k: int):
        """
        Returns the value of the derivative of the specified kernel function
        with fv_2 being the variable (i.e. K(x_i, x_c), finding gradient
        evaluated at x_c
        :param fv_1: fv_1: feature vector 1 (np.ndarray)
        :param fv_2: fv_2: feature vector 2 (np.ndarray)
        :param k: which partial derivative (0-based indexing, int)
        :return: the value of the derivative of the specified kernel function
        """

        if len(fv_1) != len(fv_2) or k < 0 or k >= len(fv_1):
            raise ValueError('Feature vectors need to have same '
                             'length and k must be a valid index.')

        return (fv_1[k] * self.learner.degree * self.learner.gamma *
                ((self.learner.gamma * fv_1.dot(fv_2) + self.learner.coef0)
                 **(self.learner.degree - 1)))

    def _kernel_rbf(self, fv_1: np.ndarray, fv_2: np.ndarray):
        """
        Returns the value of the specified kernel function
        :param fv_1: feature vector 1 (np.ndarray)
        :param fv_2: feature vector 2 (np.ndarray)
        :return: the value of the specified kernel function
        """

        if len(fv_1) != len(fv_2):
            raise ValueError('Feature vectors need to have same length.')

        norm = np.linalg.norm(fv_1 - fv_2)**2
        return math.exp(-1 * self.learner.gamma * norm)

    def _kernel_derivative_rbf(self, fv_1: np.ndarray, fv_2: np.ndarray,
                               k: int):
        """
        Returns the value of the derivative of the specified kernel function
        with fv_2 being the variable (i.e. K(x_i, x_c), finding gradient
        evaluated at x_c
        :param fv_1: fv_1: feature vector 1 (np.ndarray)
        :param fv_2: fv_2: feature vector 2 (np.ndarray)
        :param k: which partial derivative (0-based indexing, int)
        :return: the value of the derivative of the specified kernel function
        """

        if len(fv_1) != len(fv_2) or k < 0 or k >= len(fv_1):
            raise ValueError('Feature vectors need to have same '
                             'length and k must be a valid index.')

        return (self._kernel_rbf(fv_1, fv_2) * 2 * self.learner.gamma *
                (fv_1[k] - fv_2[k]))

    def _kernel_sigmoid(self, fv_1: np.ndarray, fv_2: np.ndarray):
        """
        Returns the value of the specified kernel function
        :param fv_1: feature vector 1 (np.ndarray)
        :param fv_2: feature vector 2 (np.ndarray)
        :return: the value of the specified kernel function
        """

        if len(fv_1) != len(fv_2):
            raise ValueError('Feature vectors need to have same length.')

        inside = self.learner.gamma * fv_1.dot(fv_2) + self.learner.coef0
        return math.tanh(inside)

    def _kernel_derivative_sigmoid(self, fv_1: np.ndarray, fv_2: np.ndarray,
                                   k: int):
        """
        Returns the value of the derivative of the specified kernel function
        with fv_2 being the variable (i.e. K(x_i, x_c), finding gradient
        evaluated at x_c
        :param fv_1: fv_1: feature vector 1 (np.ndarray)
        :param fv_2: fv_2: feature vector 2 (np.ndarray)
        :param k: which partial derivative (0-based indexing, int)
        :return: the value of the derivative of the specified kernel function
        """

        if len(fv_1) != len(fv_2) or k < 0 or k >= len(fv_1):
            raise ValueError('Feature vectors need to have same '
                             'length and k must be a valid index.')

        inside = self.learner.gamma * fv_1.dot(fv_2) + self.learner.coef0
        return self.learner.gamma * fv_1[k] / (math.cosh(inside)**2)

    def _get_kernel(self):
        """
        :return: the appropriate kernel function
        """

        if self.learner.model.learner.kernel == 'linear':
            return self._kernel_linear
        elif self.learner.model.learner.kernel == 'poly':
            return self._kernel_poly
        elif self.learner.model.learner.kernel == 'rbf':
            return self._kernel_rbf
        elif self.learner.model.learner.kernel == 'sigmoid':
            return self._kernel_sigmoid
        else:
            raise ValueError('No matching kernel function found.')

    def _get_kernel_derivative(self):
        """
        :return: the appropriate kernel derivative function
        """

        if self.learner.model.learner.kernel == 'linear':
            return self._kernel_derivative_linear
        elif self.learner.model.learner.kernel == 'poly':
            return self._kernel_derivative_poly
        elif self.learner.model.learner.kernel == 'rbf':
            return self._kernel_derivative_rbf
        elif self.learner.model.learner.kernel == 'sigmoid':
            return self._kernel_derivative_sigmoid
        else:
            raise ValueError('No matching kernel function found.')

    def set_params(self, params: Dict):
        if params['learner'] is not None:
            self.learner = params['learner']
        if params['poison_instance'] is not None:
            self.poison_instance = params['poison_instance']
        if params['alpha'] is not None:
            self.alpha = params['alpha']
        if params['beta'] is not None:
            self.beta = params['beta']
        if params['max_iter'] is not None:
            self.max_iter = params['max_iter']
        if params['number_to_add'] is not None:
            self.number_to_add = params['number_to_add']
        if params['verbose'] is not None:
            self.verbose = params['verbose']

        self.instances = None
        self.orig_instances = None
        self.fvs = None
        self.labels = None
        self.x = None
        self.y = None
        self.inst = None
        self.kernel = self._get_kernel()
        self.kernel_derivative = self._get_kernel_derivative()
        self.z_c = None
        self.matrix = None
        self.quick_calc = None
        self.poison_loss_before = None
        self.poison_loss_after = None

    def get_available_params(self):
        params = {
            'learner': self.learner,
            'poison_instance': self.poison_instance,
            'alpha': self.alpha,
            'beta': self.beta,
            'max_iter': self.max_iter,
            'number_to_add': self.number_to_add,
            'verbose': self.verbose
        }
        return params

    def set_adversarial_params(self, learner, train_instances):
        self.learner = learner
Esempio n. 28
0
    def coordinate_greedy(self, instance: Instance) -> Instance:
        indices = [i for i in range(0, self.num_features)]

        x = xk = instance.get_csr_matrix().toarray()[0]
        # Q = [self.transform_cost(xk,x)]
        # f = [self.learn_model.model.learner.predict(xk.reshape(1,-1))]
        # p = [self.learn_model.model.learner.coef_.dot(xk)+
        #     self.learn_model.model.learner.intercept_]
        # c = [self.quadratic_cost(xk,x)]

        no_improve_count = 0
        shuffle(indices)
        for i in indices:
            xkplus1 = self.minimize_transform(xk, i)
            oldQ = self.transform_cost(xk, x)
            newQ = self.transform_cost(xkplus1, x)
            # step_change = np.log(newQ) / np.log(oldQ)
            # using difference instead of log ratio for convergence check

            step_change = newQ - oldQ
            # print('oldQ= '+str(oldQ) + ' newQ= '+str(newQ)+
            #       ' step_change= '+str(step_change))
            # print('xk[i]= ' + str(xk[i]) + ' xk+1[i]= ' +
            #       str(xkplus1[i]) + ' x[i]= ' + str(x[i]))

            if step_change >= 0:
                no_improve_count += 1
                if no_improve_count >= self.max_change:
                    break
            else:
                xk = xkplus1

                # Q.append(self.transform_cost(xk,x))
                # f.append(
                #     self.learn_model.model.learner.predict(xk.reshape(1, -1)))
                # c.append(self.quadratic_cost(xk,x))
                # p.append(self.learn_model.model.learner.coef_.dot(xk) +
                #          self.learn_model.model.learner.intercept_)

        # print('xk shape: '+str(xk.shape))

        # Q = np.array(Q)
        # f = np.array(f)
        # c = np.array(c)
        # p = np.array(p).reshape((-1,))
        # pnc = p+c
        # print(p.shape)
        # print(c.shape)
        # print(pnc.shape)
        # t = np.array([i for i in range(len(Q))])
        # plt.plot(t,Q,'r', label='Q(x)')
        # plt.plot(t, f, 'b', label='sign(f(x))')
        # plt.plot( t,c ,'g', label='||x-xi||^2')
        # plt.plot(t, p, 'b--',label='w.T*x+b')
        # plt.plot(t, pnc, 'r--',
        #          label='w.T*x+b + ||x-xi||^2')
        # plt.legend()
        # plt.show()

        # ('mod succeeded')

        mat_indices = [x for x in range(0, self.num_features) if xk[x] != 0]
        new_instance = Instance(
            -1, BinaryFeatureVector(self.num_features, mat_indices))

        if self.learn_model.predict(
                new_instance) == self.learn_model.positive_classification:
            return instance
        else:
            return new_instance