From fb68f5e79776e2ba42ea0a73e948d2cd20cdef99 Mon Sep 17 00:00:00 2001 From: Sazonov_ISP Date: Thu, 31 Oct 2024 13:40:35 +0300 Subject: [PATCH] Attack on X by Explain? --- experiments/Cora_reverse | 5 ++ experiments/EAttack_experiment.py | 2 +- experiments/cora_reverse_5 | 5 ++ experiments/cora_reverse_random | 5 ++ metainfo/evasion_attack_parameters.json | 24 +++++++ metainfo/explainers_init_parameters.json | 2 +- src/attacks/EAttack/experimental_code.py | 88 ++++++++++++++---------- 7 files changed, 91 insertions(+), 40 deletions(-) create mode 100644 experiments/Cora_reverse create mode 100644 experiments/cora_reverse_5 create mode 100644 experiments/cora_reverse_random diff --git a/experiments/Cora_reverse b/experiments/Cora_reverse new file mode 100644 index 0000000..34361d3 --- /dev/null +++ b/experiments/Cora_reverse @@ -0,0 +1,5 @@ +0.29 +0.3 +0.36 +0.31 +0.26 diff --git a/experiments/EAttack_experiment.py b/experiments/EAttack_experiment.py index 41292eb..01ec1bb 100644 --- a/experiments/EAttack_experiment.py +++ b/experiments/EAttack_experiment.py @@ -220,7 +220,7 @@ def test(): mask = Metric.create_mask_by_target_list(y_true=dataset.labels, target_list=[i]) evasion_attack_config = ConfigPattern( - _class_name="EAttackRandom", + _class_name="EAttack", _import_path=EVASION_ATTACK_PARAMETERS_PATH, _config_class="EvasionAttackConfig", _config_kwargs={ diff --git a/experiments/cora_reverse_5 b/experiments/cora_reverse_5 new file mode 100644 index 0000000..dc00fe1 --- /dev/null +++ b/experiments/cora_reverse_5 @@ -0,0 +1,5 @@ +0.37 +0.4 +0.43 +0.42 +0.43 diff --git a/experiments/cora_reverse_random b/experiments/cora_reverse_random new file mode 100644 index 0000000..94a6b03 --- /dev/null +++ b/experiments/cora_reverse_random @@ -0,0 +1,5 @@ +0.29 +0.29 +0.29 +0.31 +0.28 diff --git a/metainfo/evasion_attack_parameters.json b/metainfo/evasion_attack_parameters.json index 03ea7be..bbe7a50 100644 --- a/metainfo/evasion_attack_parameters.json +++ b/metainfo/evasion_attack_parameters.json @@ -20,6 +20,30 @@ "direct": ["direct", "bool", true, {}, "Indicates whether to directly modify edges/features of the node attacked or only those of influencers"], "n_influencers": ["n_influencers", "int", 0, {"min": 0, "step": 1}, "Number of influencing nodes. Will be ignored if direct is True"] }, + "EAttack": { + "attack_size": ["Attack size (edge)", "float", 0.15, {"min": 0, "max": 1, "step": 0.01}, "Percent of nodes to be rewired"], + "targeted": ["Targeted attack", "bool", true, {}, "Whether attack targeted or not"], + "max_rewire": ["Max 2-hop node to rewire for target", "int", 20, {"min": 1, "step": 1}, "Not more than this amount of node from 2-hop neighbourhood will be rewired"], + "random_rewire": ["Random rewire", "bool", false, {}, "Rewire based on random, not on explanation (for comparison)"], + "attack_features": ["Attack features", "bool", true, {}, "Whether features to be attacked or not"], + "attack_edges": ["Attack edges", "bool", false, {}, "Whether edges to be attacked or not"], + "edge_mode": ["Edge attack type", "string", "rewire", ["remove", "add", "rewire"], "What to do with edges: remove or add or rewire (add one and remove another)"], + "features_mode": ["Feature attack type", "string", "reverse", ["reverse","drop"], "What to do with features: drop or reverse (binary)"], + "edge_prob": ["Probability to attack edge", "float", 0.3, {"min": 0, "max": 1, "step": 0.01}, "Probability of drop/add/rewire edge"], + "feature_prob": ["Probability to attack feature", "float", 0.05, {"min": 0, "max": 1, "step": 0.01}, "Probability of reverse/drop feature"] +}, + "EAttackRandom": { + "attack_size": ["Attack size (edge)", "float", 0.15, {"min": 0, "max": 1, "step": 0.01}, "Percent of nodes to be rewired"], + "targeted": ["Targeted attack", "bool", true, {}, "Whether attack targeted or not"], + "max_rewire": ["Max 2-hop node to rewire for target", "int", 20, {"min": 1, "step": 1}, "Not more than this amount of node from 2-hop neighbourhood will be rewired"], + "random_rewire": ["Random rewire", "bool", true, {}, "Rewire based on random, not on explanation (for comparison)"], + "attack_features": ["Attack features", "bool", true, {}, "Whether features to be attacked or not"], + "attack_edges": ["Attack edges", "bool", false, {}, "Whether edges to be attacked or not"], + "edge_mode": ["Edge attack type", "string", "rewire", ["remove", "add", "rewire"], "What to do with edges: remove or add or rewire (add one and remove another)"], + "features_mode": ["Feature attack type", "string", "reverse", ["reverse","drop"], "What to do with features: drop or reverse (binary)"], + "edge_prob": ["Probability to attack", "float", 0.3, {"min": 0, "max": 1, "step": 0.01}, "Probability of drop/add/rewire edge"], + "feature_prob": ["Probability to attack feature", "float", 0.05, {"min": 0, "max": 1, "step": 0.01}, "Probability of reverse/drop feature"] +}, "QAttack": { "population_size": ["Population size", "int", 50, {"min": 1, "step": 1}, "Number of genes in population"], "individual_size": ["Individual size", "int", 30, {"min": 1, "step": 1}, "Number of rewiring operations within one gene"], diff --git a/metainfo/explainers_init_parameters.json b/metainfo/explainers_init_parameters.json index 4d88ae9..430955b 100644 --- a/metainfo/explainers_init_parameters.json +++ b/metainfo/explainers_init_parameters.json @@ -20,7 +20,7 @@ "GNNExplainer(torch-geom)": { "epochs": ["Epochs","int",100,{"min": 1},"The number of epochs to train"], "lr": ["Learn rate","float",0.01,{"min": 0, "step": 0.0001},"The learning rate to apply"], - "node_mask_type": ["Node mask","string","object",["None","object","common_attributes","attributes"],"The type of mask to apply on nodes"], + "node_mask_type": ["Node mask","string","common_attributes",["None","object","common_attributes","attributes"],"The type of mask to apply on nodes"], "edge_mask_type": ["Edge mask","string","object",["None","object","common_attributes","attributes"],"The type of mask to apply on edges"], "mode": ["Mode","string","multiclass_classification",["binary_classification","multiclass_classification","regression"],"The mode of the model"], "return_type": ["Model return","string","log_probs",["raw","prob","log_probs"],"Denotes the type of output from model. Valid inputs are 'log_probs' (the model returns the logarithm of probabilities), 'prob' (the model returns probabilities), 'raw' (the model returns raw scores)"], diff --git a/src/attacks/EAttack/experimental_code.py b/src/attacks/EAttack/experimental_code.py index 30cb7d4..3dbd613 100644 --- a/src/attacks/EAttack/experimental_code.py +++ b/src/attacks/EAttack/experimental_code.py @@ -26,7 +26,7 @@ class EAttack(EvasionAttacker): name = "EAttack" def __init__(self, explainer, run_config, attack_size, attack_inds, targeted, max_rewire, random_rewire, - attack_edges, attack_features, edge_mode, features_mode, edge_prob, **kwargs): + attack_edges, attack_features, edge_mode, features_mode, edge_prob, feature_prob, **kwargs): super().__init__(**kwargs) self.explainer = explainer self.run_config = run_config @@ -43,6 +43,7 @@ def __init__(self, explainer, run_config, attack_size, attack_inds, targeted, ma self.edge_mode = edge_mode self.features_mode = features_mode self.edge_prob = edge_prob + self.feature_prob = feature_prob def attack(self, model_manager, gen_dataset, mask_tensor): @@ -96,13 +97,13 @@ def attack(self, model_manager, gen_dataset, mask_tensor): # TEST edge_index_set.discard((v, u)) max_attack -= 1 - if not max_attack: + if max_attack < 0: break elif self.edge_mode == 'add': first_set = hop_2.union(hop_1).union({int(n)}) second_set = hop_1.union({int(n)}) hop = list(itertools.product(first_set, second_set)) - max_attack = len(hop) * self.edge_prob + max_attack = int(len(hop) * self.edge_prob) unimportant_nodes = set() important_nodes = set() for (u, v) in zip(edge_index[0], edge_index[1]): @@ -134,10 +135,10 @@ def attack(self, model_manager, gen_dataset, mask_tensor): edge_index_set.add((new_node[0], v)) cnt += 1 max_attack -= 1 - if not max_attack: + if max_attack < 0: break elif self.edge_mode == 'rewire': - max_attack = len(hop_2) * self.edge_prob + max_attack = int(len(hop_2) * self.edge_prob) for (u, v) in zip(edge_index[0], edge_index[1]): if u != n and v != n and f"{u},{v}" in explanations[i]['edges'].keys(): edge_index_set.discard((u, v)) @@ -167,29 +168,34 @@ def attack(self, model_manager, gen_dataset, mask_tensor): if self.attack_features: cnt = 0 + max_attack = int(gen_dataset.dataset.data.x[0].shape[0]) * self.feature_prob for i, n in enumerate(self.attack_inds): if self.features_mode == 'reverse': - # get 2-hop - hop_1 = set() - hop_2 = set() - for (u, v) in edge_index_set: - if u == n: - hop_1.add(v) - elif v == n: - hop_1.add(u) - for (u, v) in edge_index_set: - if u in hop_1 and v != n and v not in hop_1: - hop_2.add(v) - elif v in hop_1 and u != n and u not in hop_1: - hop_2.add(u) + # # get 2-hop + # hop_1 = set() + # hop_2 = set() + # for (u, v) in edge_index_set: + # if u == n: + # hop_1.add(v) + # elif v == n: + # hop_1.add(u) + # for (u, v) in edge_index_set: + # if u in hop_1 and v != n and v not in hop_1: + # hop_2.add(v) + # elif v in hop_1 and u != n and u not in hop_1: + # hop_2.add(u) #get features to be reversed #f_mask = torch.zeros_like(gen_dataset.dataset.data.x.shape[0]) f_inds = [] - for f, v in explanations[i]['nodes'].items(): + feature_tuple = sorted(tuple((f, v) for f, v in explanations[i]['features'].items()), key=lambda x: float(x[1]), reverse=True) + for f, v in feature_tuple: if v: f_inds.append(int(f)) cnt += 1 + max_attack -= 1 + if max_attack < 0: + break f_inds = torch.tensor(f_inds) if not f_inds.numel(): @@ -210,7 +216,7 @@ class EAttackRandom(EvasionAttacker): name = "EAttackRandom" def __init__(self, explainer, run_config, attack_size, attack_inds, targeted, max_rewire, random_rewire, - attack_edges, attack_features, edge_mode, features_mode, edge_prob, **kwargs): + attack_edges, attack_features, edge_mode, features_mode, edge_prob, feature_prob, **kwargs): super().__init__(**kwargs) self.explainer = explainer self.run_config = run_config @@ -227,6 +233,7 @@ def __init__(self, explainer, run_config, attack_size, attack_inds, targeted, ma self.edge_mode = edge_mode self.features_mode = features_mode self.edge_prob = edge_prob + self.feature_prob = feature_prob def attack(self, model_manager, gen_dataset, mask_tensor): @@ -315,29 +322,34 @@ def attack(self, model_manager, gen_dataset, mask_tensor): if self.attack_features: cnt = 0 + max_attack = int(int(gen_dataset.dataset.data.x[0].shape[0]) * self.feature_prob) for i, n in enumerate(self.attack_inds): if self.features_mode == 'reverse': - # get 2-hop - hop_1 = set() - hop_2 = set() - for (u, v) in edge_index_set: - if u == n: - hop_1.add(v) - elif v == n: - hop_1.add(u) - for (u, v) in edge_index_set: - if u in hop_1 and v != n and v not in hop_1: - hop_2.add(v) - elif v in hop_1 and u != n and u not in hop_1: - hop_2.add(u) + # # get 2-hop + # hop_1 = set() + # hop_2 = set() + # for (u, v) in edge_index_set: + # if u == n: + # hop_1.add(v) + # elif v == n: + # hop_1.add(u) + # for (u, v) in edge_index_set: + # if u in hop_1 and v != n and v not in hop_1: + # hop_2.add(v) + # elif v in hop_1 and u != n and u not in hop_1: + # hop_2.add(u) #get features to be reversed #f_mask = torch.zeros_like(gen_dataset.dataset.data.x.shape[0]) - f_inds = [] - for f, v in explanations[i]['nodes'].items(): - if v: - f_inds.append(int(f)) - cnt += 1 + #f_inds = [] + f_inds = random.sample(range(int(gen_dataset.dataset.data.x[0].shape[0])), max_attack) + # for f, v in explanations[i]['nodes'].items(): + # if v: + # f_inds.append(int(f)) + # max_attack -= 1 + # cnt += 1 + # if not max_attack: + # break f_inds = torch.tensor(f_inds) if not f_inds.numel():