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 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
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 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
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
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 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)
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_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 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)
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
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
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 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
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
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
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
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]
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
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
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
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
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))
def predict_and_record(self, message): self.num_queries += 1 return self.predict(Instance(0, message))
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
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
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