Spaces:
Runtime error
Runtime error
File size: 6,116 Bytes
63775f2 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
"""
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
|