def _initialize_population(self, initial_result, pop_size): """ Initialize a population of size `pop_size` with `initial_result` Args: initial_result (GoalFunctionResult): Original text pop_size (int): size of population Returns: population as `list[PopulationMember]` """ words = initial_result.attacked_text.words # For IGA, `num_replacements_left` represents the number of times the word at each index can be modified num_replacements_left = np.array( [self.max_replace_times_per_index] * len(words) ) population = [] # IGA initializes the first population by replacing each word by its optimal synonym for idx in range(len(words)): pop_member = PopulationMember( initial_result.attacked_text, initial_result, attributes={"num_replacements_left": np.copy(num_replacements_left)}, ) pop_member = self._perturb(pop_member, initial_result, index=idx) population.append(pop_member) return population[:pop_size]
def _crossover(self, pop_member1, pop_member2, original_text): """Generates a crossover between pop_member1 and pop_member2. If the child fails to satisfy the constraints, we re-try crossover for a fix number of times, before taking one of the parents at random as the resulting child. Args: pop_member1 (PopulationMember): The first population member. pop_member2 (PopulationMember): The second population member. original_text (AttackedText): Original text Returns: A population member containing the crossover. """ x1_text = pop_member1.attacked_text x2_text = pop_member2.attacked_text num_tries = 0 passed_constraints = False while num_tries < self.max_crossover_retries + 1: new_text, attributes = self._crossover_operation( pop_member1, pop_member2) replaced_indices = new_text.attack_attrs["newly_modified_indices"] new_text.attack_attrs["modified_indices"] = ( x1_text.attack_attrs["modified_indices"] - replaced_indices) | (x2_text.attack_attrs["modified_indices"] & replaced_indices) if "last_transformation" in x1_text.attack_attrs: new_text.attack_attrs[ "last_transformation"] = x1_text.attack_attrs[ "last_transformation"] elif "last_transformation" in x2_text.attack_attrs: new_text.attack_attrs[ "last_transformation"] = x2_text.attack_attrs[ "last_transformation"] if self.post_crossover_check: passed_constraints = self._post_crossover_check( new_text, x1_text, x2_text, original_text) if not self.post_crossover_check or passed_constraints: break num_tries += 1 if self.post_crossover_check and not passed_constraints: # If we cannot find a child that passes the constraints, # we just randomly pick one of the parents to be the child for the next iteration. pop_mem = pop_member1 if np.random.uniform() < 0.5 else pop_member2 return pop_mem else: new_results, self._search_over = self.get_goal_results([new_text]) return PopulationMember(new_text, result=new_results[0], attributes=attributes)
def _modify_population_member(self, pop_member, new_text, new_result, word_idx): """Modify `pop_member` by returning a new copy with `new_text`, `new_result`, and `num_replacements_left` altered appropriately for given `word_idx`""" num_replacements_left = np.copy(pop_member.attributes["num_replacements_left"]) num_replacements_left[word_idx] -= 1 return PopulationMember( new_text, result=new_result, attributes={"num_replacements_left": num_replacements_left}, )
def _modify_population_member(self, pop_member, new_text, new_result, word_idx): """Modify `pop_member` by returning a new copy with `new_text`, `new_result`, and `num_candidate_transformations` altered appropriately for given `word_idx`""" num_candidate_transformations = np.copy( pop_member.attributes["num_candidate_transformations"]) num_candidate_transformations[word_idx] = 0 return PopulationMember( new_text, result=new_result, attributes={ "num_candidate_transformations": num_candidate_transformations }, )
def _initialize_population(self, initial_result, pop_size): """ Initialize a population of size `pop_size` with `initial_result` Args: initial_result (GoalFunctionResult): Original text pop_size (int): size of population Returns: population as `list[PopulationMember]` """ best_neighbors, prob_list = self._get_best_neighbors( initial_result, initial_result) population = [] for _ in range(pop_size): # Mutation step random_result = np.random.choice(best_neighbors, 1, p=prob_list)[0] population.append( PopulationMember(random_result.attacked_text, random_result)) return population
def _initialize_population(self, initial_result, pop_size): """ Initialize a population of size `pop_size` with `initial_result` Args: initial_result (GoalFunctionResult): Original text pop_size (int): size of population Returns: population as `list[PopulationMember]` """ words = initial_result.attacked_text.words num_candidate_transformations = np.zeros(len(words)) transformed_texts = self.get_transformations( initial_result.attacked_text, original_text=initial_result.attacked_text) for transformed_text in transformed_texts: diff_idx = next( iter(transformed_text.attack_attrs["newly_modified_indices"])) num_candidate_transformations[diff_idx] += 1 # Just b/c there are no replacements now doesn't mean we never want to select the word for perturbation # Therefore, we give small non-zero probability for words with no replacements # Epsilon is some small number to approximately assign small probability min_num_candidates = np.amin(num_candidate_transformations) epsilon = max(1, int(min_num_candidates * 0.1)) for i in range(len(num_candidate_transformations)): num_candidate_transformations[i] = max( num_candidate_transformations[i], epsilon) population = [] for _ in range(pop_size): pop_member = PopulationMember( initial_result.attacked_text, initial_result, attributes={ "num_candidate_transformations": np.copy(num_candidate_transformations) }, ) # Perturb `pop_member` in-place pop_member = self._perturb(pop_member, initial_result) population.append(pop_member) return population
def _turn(self, source_text, target_text, prob, original_text): """ Based on given probabilities, "move" to `target_text` from `source_text` Args: source_text (PopulationMember): Text we start from. target_text (PopulationMember): Text we want to move to. prob (np.array[float]): Turn probability for each word. original_text (AttackedText): Original text for constraint check if `self.post_turn_check=True`. Returns: New `Position` that we moved to (or if we fail to move, same as `source_text`) """ assert len(source_text.words) == len( target_text.words), "Word length mismatch for turn operation." assert len(source_text.words) == len( prob), "Length mismatch for words and probability list." len_x = len(source_text.words) num_tries = 0 passed_constraints = False while num_tries < self.max_turn_retries + 1: indices_to_replace = [] words_to_replace = [] for i in range(len_x): if np.random.uniform() < prob[i]: indices_to_replace.append(i) words_to_replace.append(target_text.words[i]) new_text = source_text.attacked_text.replace_words_at_indices( indices_to_replace, words_to_replace) indices_to_replace = set(indices_to_replace) new_text.attack_attrs["modified_indices"] = ( source_text.attacked_text.attack_attrs["modified_indices"] - indices_to_replace) | ( target_text.attacked_text.attack_attrs["modified_indices"] & indices_to_replace) if "last_transformation" in source_text.attacked_text.attack_attrs: new_text.attack_attrs[ "last_transformation"] = source_text.attacked_text.attack_attrs[ "last_transformation"] if not self.post_turn_check or (new_text.words == source_text.words): break if "last_transformation" in new_text.attack_attrs: passed_constraints = self._check_constraints( new_text, source_text.attacked_text, original_text=original_text) else: passed_constraints = True if passed_constraints: break num_tries += 1 if self.post_turn_check and not passed_constraints: # If we cannot find a turn that passes the constraints, we do not move. return source_text else: return PopulationMember(new_text)