Spaces:
Runtime error
Runtime error
| """ | |
| Reimplementation of search method from Generating Natural Language Adversarial Examples | |
| ========================================================================================= | |
| by Alzantot et. al `<arxiv.org/abs/1804.07998>`_ from `<github.com/nesl/nlp_adversarial_examples>`_ | |
| """ | |
| import numpy as np | |
| from textattack.search_methods import GeneticAlgorithm, PopulationMember | |
| class AlzantotGeneticAlgorithm(GeneticAlgorithm): | |
| """Attacks a model with word substiutitions using a genetic algorithm. | |
| Args: | |
| pop_size (int): The population size. Defaults to 60. | |
| max_iters (int): The maximum number of iterations to use. Defaults to 20. | |
| temp (float): Temperature for softmax function used to normalize probability dist when sampling parents. | |
| Higher temperature increases the sensitivity to lower probability candidates. | |
| give_up_if_no_improvement (bool): If True, stop the search early if no candidate that improves the score is found. | |
| post_crossover_check (bool): If True, check if child produced from crossover step passes the constraints. | |
| max_crossover_retries (int): Maximum number of crossover retries if resulting child fails to pass the constraints. | |
| Applied only when `post_crossover_check` is set to `True`. | |
| Setting it to 0 means we immediately take one of the parents at random as the child upon failure. | |
| """ | |
| def __init__( | |
| self, | |
| pop_size=60, | |
| max_iters=20, | |
| temp=0.3, | |
| give_up_if_no_improvement=False, | |
| post_crossover_check=True, | |
| max_crossover_retries=20, | |
| ): | |
| super().__init__( | |
| pop_size=pop_size, | |
| max_iters=max_iters, | |
| temp=temp, | |
| give_up_if_no_improvement=give_up_if_no_improvement, | |
| post_crossover_check=post_crossover_check, | |
| max_crossover_retries=max_crossover_retries, | |
| ) | |
| 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 _get_word_select_prob_weights(self, pop_member): | |
| """Get the attribute of `pop_member` that is used for determining | |
| probability of each word being selected for perturbation.""" | |
| return pop_member.attributes["num_candidate_transformations"] | |
| def _crossover_operation(self, pop_member1, pop_member2): | |
| """Actual operation that takes `pop_member1` text and `pop_member2` | |
| text and mixes the two to generate crossover between `pop_member1` and | |
| `pop_member2`. | |
| Args: | |
| pop_member1 (PopulationMember): The first population member. | |
| pop_member2 (PopulationMember): The second population member. | |
| Returns: | |
| Tuple of `AttackedText` and a dictionary of attributes. | |
| """ | |
| indices_to_replace = [] | |
| words_to_replace = [] | |
| num_candidate_transformations = np.copy( | |
| pop_member1.attributes["num_candidate_transformations"] | |
| ) | |
| for i in range(pop_member1.num_words): | |
| if np.random.uniform() < 0.5: | |
| indices_to_replace.append(i) | |
| words_to_replace.append(pop_member2.words[i]) | |
| num_candidate_transformations[i] = pop_member2.attributes[ | |
| "num_candidate_transformations" | |
| ][i] | |
| new_text = pop_member1.attacked_text.replace_words_at_indices( | |
| indices_to_replace, words_to_replace | |
| ) | |
| return ( | |
| new_text, | |
| {"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]` | |
| """ | |
| 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 | |