diff --git a/experiments/attack_defense_test.py b/experiments/attack_defense_test.py index 1c5bf5f..dab0e06 100644 --- a/experiments/attack_defense_test.py +++ b/experiments/attack_defense_test.py @@ -17,7 +17,6 @@ from attacks.QAttack import qattack from defense.JaccardDefense import jaccard_def from attacks.metattack import meta_gradient_attack -from attacks.CLGA import CLGA_gpt from defense.GNNGuard import gnnguard @@ -1131,6 +1130,133 @@ def test_pgd(): print(f"Before PGD attack on graph (MUTAG dataset): {info_before_pgd_attack_on_graph}") print(f"After PGD attack on graph (MUTAG dataset): {info_after_pgd_attack_on_graph}") +def test_pro(): + from defense.ProGNN.prognn import ProGNNDefender + # my_device = device('cuda' if is_available() else 'cpu') + my_device = device('cpu') + + full_name = None + + full_name = ("single-graph", "Planetoid", 'Cora') + + dataset, data, results_dataset_path = DatasetManager.get_by_full_name( + full_name=full_name, + dataset_ver_ind=0 + ) + + # print(data.train_mask) + + gnn = model_configs_zoo(dataset=dataset, model_name='gcn_gcn') + + manager_config = ConfigPattern( + _config_class="ModelManagerConfig", + _config_kwargs={ + "mask_features": [], + "optimizer": { + # "_config_class": "Config", + "_class_name": "Adam", + # "_import_path": OPTIMIZERS_PARAMETERS_PATH, + # "_class_import_info": ["torch.optim"], + "_config_kwargs": {}, + } + } + ) + + # train_test_split = [0.8, 0.2] + # train_test_split = [0.6, 0.4] + steps_epochs = 200 + gnn_model_manager = FrameworkGNNModelManager( + gnn=gnn, + dataset_path=results_dataset_path, + manager_config=manager_config, + modification=ModelModificationConfig(model_ver_ind=0, epochs=steps_epochs) + ) + + save_model_flag = False + # save_model_flag = True + + # data.x = data.x.float() + gnn_model_manager.gnn.to(my_device) + data = data.to(my_device) + + evasion_attack_config = ConfigPattern( + _class_name="FGSM", + _import_path=EVASION_ATTACK_PARAMETERS_PATH, + _config_class="EvasionAttackConfig", + _config_kwargs={ + "epsilon": 0.005, + } + ) + fgsm_evasion_attack_config1 = ConfigPattern( + _class_name="FGSM", + _import_path=EVASION_ATTACK_PARAMETERS_PATH, + _config_class="EvasionAttackConfig", + _config_kwargs={ + "epsilon": 0.01, + } + ) + at_evasion_defense_config = ConfigPattern( + _class_name="AdvTraining", + _import_path=EVASION_DEFENSE_PARAMETERS_PATH, + _config_class="EvasionDefenseConfig", + _config_kwargs={ + "attack_name": None, + "attack_config": fgsm_evasion_attack_config1 + } + ) + + gradientregularization_evasion_defense_config = ConfigPattern( + _class_name="GradientRegularizationDefender", + _import_path=EVASION_DEFENSE_PARAMETERS_PATH, + _config_class="EvasionDefenseConfig", + _config_kwargs={ + "regularization_strength": 0.1 * 500 + } + ) + + poison_defense_config = ConfigPattern( + _class_name="ProGNNDefender", + _import_path=POISON_DEFENSE_PARAMETERS_PATH, + _config_class="PoisonDefenseConfig", + _config_kwargs={ + "epochs": 10 + } + ) + + + # gnn_model_manager.set_poison_attacker(poison_attack_config=poison_attack_config) + gnn_model_manager.set_poison_defender(poison_defense_config=poison_defense_config) + # gnn_model_manager.set_evasion_attacker(evasion_attack_config=netattackgroup_evasion_attack_config) + # gnn_model_manager.set_evasion_defender(evasion_defense_config=gradientregularization_evasion_defense_config) + + warnings.warn("Start training") + dataset.train_test_split() + + try: + raise FileNotFoundError() + # gnn_model_manager.load_model_executor() + except FileNotFoundError: + gnn_model_manager.epochs = gnn_model_manager.modification.epochs = 0 + train_test_split_path = gnn_model_manager.train_model(gen_dataset=dataset, steps=steps_epochs, + save_model_flag=save_model_flag, + metrics=[Metric("F1", mask='train', average=None), + Metric("Accuracy", mask="train")]) + + if train_test_split_path is not None: + dataset.save_train_test_mask(train_test_split_path) + train_mask, val_mask, test_mask, train_test_sizes = torch.load(train_test_split_path / 'train_test_split')[ + :] + dataset.train_mask, dataset.val_mask, dataset.test_mask = train_mask, val_mask, test_mask + data.percent_train_class, data.percent_test_class = train_test_sizes + + warnings.warn("Training was successful") + + + metric_loc = gnn_model_manager.evaluate_model( + gen_dataset=dataset, metrics=[Metric("F1", mask='test', average='macro'), + Metric("Accuracy", mask='test')]) + print("TEST", metric_loc) + def exp_pipeline(): dataset_grid = ['Cora', 'Photo'] @@ -1154,8 +1280,9 @@ def exp_pipeline(): # random.seed(10) # test_attack_defense() - exp_pipeline() + # exp_pipeline() # torch.manual_seed(5000) # test_gnnguard() # test_jaccard() # test_pgd() + test_pro() diff --git a/metainfo/poison_defense_parameters.json b/metainfo/poison_defense_parameters.json index 58d393e..2eea90d 100644 --- a/metainfo/poison_defense_parameters.json +++ b/metainfo/poison_defense_parameters.json @@ -12,6 +12,15 @@ "attention": ["attention", "bool", true, {}, "?"], "drop": ["drop", "bool", true, {}, "?"], "train_iters": ["train_iters", "int", 50, {}, "?"] - } + }, + "ProGNNDefender": { + "symmetric": ["symmetric", "bool", true, {}, "?"], + "lr_adj": ["lr_adj", "float", 0.01, {"min": 0.0001, "step": 0.005}, "?"], + "alpha": ["alpha", "float", 5e-4, {"min": 0.0, "step": 0.0001}, "?"], + "beta": ["beta", "float", 1.5, {"min": 0.0, "step": 0.01}, "?"], + "epochs": ["epochs", "int", 400, {"min": 1, "step": 1}, "?"], + "lambda_": ["lambda_", "float", 0, {"min": 0.0, "step": 0.001}, "?"], + "phi": ["phi", "float", 0, {"min": 0.0, "step": 0.01}, "?"] + } } diff --git a/src/defense/ProGNN/prognn.py b/src/defense/ProGNN/prognn.py index 5cc3748..61852e9 100644 --- a/src/defense/ProGNN/prognn.py +++ b/src/defense/ProGNN/prognn.py @@ -1,16 +1,24 @@ +import copy + import numpy as np import scipy as sp import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim +import torch.sparse from torch.optim.optimizer import required +import torch_geometric.utils +from torch_geometric.utils import to_torch_coo_tensor, to_edge_index, to_dense_adj, dense_to_sparse, is_sparse +from tqdm import tqdm from defense.poison_defense import PoisonDefender class ProGNNDefender(PoisonDefender): name = 'ProGNNDefender' + # TODO re-write with support of sparse matrix + def __init__(self, symmetric, lr_adj, alpha, beta, epochs, lambda_, phi, **kw): super().__init__() self.symmetric = symmetric @@ -24,72 +32,65 @@ def __init__(self, symmetric, lr_adj, alpha, beta, epochs, lambda_, phi, **kw): def defense(self, gen_dataset, **kw): features = gen_dataset.dataset.data.x - adj = gen_dataset.dataset.data.edge_index - labels = gen_dataset.dataset.data.y + # adj = to_torch_coo_tensor(gen_dataset.dataset.data.edge_index) + adj = to_dense_adj(gen_dataset.dataset.data.edge_index).squeeze(0) + + self.device = gen_dataset.dataset.data.x.device - estimator = EstimateAdj(adj, symmetric=self.symmetric, device=gen_dataset.device).to(self.device) + estimator = EstimateAdj(adj, symmetric=self.symmetric, device=self.device).to(self.device) self.estimator = estimator + self.optimizer_adj = optim.SGD(estimator.parameters(), momentum=0.9, lr=self.lr_adj) self.optimizer_l1 = PGD(estimator.parameters(), - proxs=[prox_operators.prox_l1], + proxs=[prox_l1], lr=self.lr_adj, alphas=[self.alpha]) self.optimizer_nuclear = PGD(estimator.parameters(), - proxs=[prox_operators.prox_nuclear], + proxs=[prox_nuclear], lr=self.lr_adj, alphas=[self.beta]) # Train model - for epoch in range(self.epochs): - self.train_adj(epoch, features, adj, labels, - idx_train, idx_val) + for epoch in tqdm(range(self.epochs)): + self.train_adj(epoch, features, adj) - gen_dataset.dataset.data.edge_index = torch.tensor(new_edge_index).long() + gen_dataset.dataset.data.edge_index = dense_to_sparse(adj)[0] return gen_dataset - def train_adj(self, epoch, features, adj, labels, idx_train, idx_val): + def train_adj(self, epoch, features, adj): estimator = self.estimator estimator.train() self.optimizer_adj.zero_grad() - loss_l1 = torch.norm(estimator.estimated_adj, 1) loss_fro = torch.norm(estimator.estimated_adj - adj, p='fro') - normalized_adj = estimator.normalize() if self.lambda_: loss_smooth_feat = self.feature_smoothing(estimator.estimated_adj, features) else: - loss_smooth_feat = 0 * loss_l1 + loss_smooth_feat = 0 - loss_symmetric = torch.norm(estimator.estimated_adj \ - - estimator.estimated_adj.t(), p="fro") + loss_symmetric = torch.norm(estimator.estimated_adj - estimator.estimated_adj.t(), p="fro") - loss_diffiential = loss_fro + self.lambda_ * loss_smooth_feat + self.phi * loss_symmetric + loss_diffirential = loss_fro + self.lambda_ * loss_smooth_feat + self.phi * loss_symmetric - loss_diffiential.backward() + loss_diffirential.backward() self.optimizer_adj.step() - loss_nuclear = 0 * loss_fro if self.beta != 0: self.optimizer_nuclear.zero_grad() self.optimizer_nuclear.step() - loss_nuclear = prox_operators.nuclear_norm self.optimizer_l1.zero_grad() self.optimizer_l1.step() - total_loss = loss_fro \ - + self.alpha * loss_l1 \ - + self.beta * loss_nuclear \ - + self.phi * loss_symmetric - estimator.estimated_adj.data.copy_(torch.clamp( estimator.estimated_adj.data, min=0, max=1)) + def feature_smoothing(self, adj, X): - adj = (adj.t() + adj)/2 + adj = (adj.T + adj)/2 rowsum = adj.sum(1) r_inv = rowsum.flatten() D = torch.diag(r_inv) @@ -106,47 +107,34 @@ def feature_smoothing(self, adj, X): loss_smooth_feat = torch.trace(XLXT) return loss_smooth_feat -class EstimateAdj(nn.Module): - """Provide a pytorch parameter matrix for estimated - adjacency matrix and corresponding operations. +def prox_l1(data, alpha): + """Proximal operator for l1 norm with sparse tensor support. """ + # if not data.is_sparse: + # raise ValueError("Input data must be a sparse tensor.") + # + # values = data.values() + # indices = data.indices() + # + # prox_values = torch.sign(values) * torch.clamp(torch.abs(values) - alpha, min=0) + # + # return torch.sparse_coo_tensor(indices, prox_values, data.size()) + data = torch.mul(torch.sign(data), torch.clamp(torch.abs(data) - alpha, min=0)) + return data + + +def prox_nuclear(data, alpha, k=50): + """Proximal operator for nuclear norm (trace norm). + """ + device = data.device + # U, S, V = torch.svd_lowrank(data, q=k+5) + U, S, V = np.linalg.svd(data.cpu()) + U, S, V = torch.FloatTensor(U).to(device), torch.FloatTensor(S).to(device), torch.FloatTensor(V).to(device) - def __init__(self, adj, symmetric=False, device='cpu'): - super(EstimateAdj, self).__init__() - n = len(adj) - self.estimated_adj = nn.Parameter(torch.FloatTensor(n, n)) - self._init_estimation(adj) - self.symmetric = symmetric - self.device = device - - def _init_estimation(self, adj): - with torch.no_grad(): - n = len(adj) - self.estimated_adj.data.copy_(adj) - - def forward(self): - return self.estimated_adj - - def normalize(self): - - if self.symmetric: - adj = (self.estimated_adj + self.estimated_adj.t())/2 - else: - adj = self.estimated_adj - - normalized_adj = self._normalize(adj + torch.eye(adj.shape[0]).to(self.device)) - return normalized_adj + diag_S = torch.diag(torch.clamp(S-alpha, min=0)) + return torch.matmul(torch.matmul(U, diag_S), V) - def _normalize(self, mx): - rowsum = mx.sum(1) - r_inv = rowsum.pow(-1/2).flatten() - r_inv[torch.isinf(r_inv)] = 0. - r_mat_inv = torch.diag(r_inv) - mx = r_mat_inv @ mx - mx = mx @ r_mat_inv - return mx - -class PGD(optim.Optimizer): +class PGD(torch.optim.Optimizer): """Proximal gradient descent. Parameters @@ -193,48 +181,46 @@ def step(self, delta=0, closure=None): # apply the proximal operator to each parameter in a group for param in group['params']: for prox_operator, alpha in zip(proxs, alphas): - # param.data.add_(lr, -param.grad.data) - # param.data.add_(delta) param.data = prox_operator(param.data, alpha=alpha*lr) -class ProxOperators(): - """Proximal Operators. +class EstimateAdj(nn.Module): + """Provide a pytorch parameter matrix for estimated + adjacency matrix and corresponding operations. """ - def __init__(self): - self.nuclear_norm = None - - def prox_l1(self, data, alpha): - """Proximal operator for l1 norm. - """ - data = torch.mul(torch.sign(data), torch.clamp(torch.abs(data)-alpha, min=0)) - return data - - def prox_nuclear(self, data, alpha): - """Proximal operator for nuclear norm (trace norm). - """ - device = data.device - U, S, V = np.linalg.svd(data.cpu()) - U, S, V = torch.FloatTensor(U).to(device), torch.FloatTensor(S).to(device), torch.FloatTensor(V).to(device) - self.nuclear_norm = S.sum() - # print("nuclear norm: %.4f" % self.nuclear_norm) - - diag_S = torch.diag(torch.clamp(S-alpha, min=0)) - return torch.matmul(torch.matmul(U, diag_S), V) - - - def prox_nuclear_truncated(self, data, alpha, k=50): - device = data.device - indices = torch.nonzero(data).t() - values = data[indices[0], indices[1]] # modify this based on dimensionality - data_sparse = sp.csr_matrix((values.cpu().numpy(), indices.cpu().numpy())) - U, S, V = sp.linalg.svds(data_sparse, k=k) - U, S, V = torch.FloatTensor(U).to(device), torch.FloatTensor(S).to(device), torch.FloatTensor(V).to(device) - self.nuclear_norm = S.sum() - diag_S = torch.diag(torch.clamp(S-alpha, min=0)) - return torch.matmul(torch.matmul(U, diag_S), V) - - -prox_operators = ProxOperators() + def __init__(self, adj, symmetric=False, device='cpu'): + super(EstimateAdj, self).__init__() + n = len(adj) + self.estimated_adj = nn.Parameter(torch.FloatTensor(n, n)) + self._init_estimation(adj) + self.symmetric = symmetric + self.device = device + + def _init_estimation(self, adj): + with torch.no_grad(): + n = len(adj) + self.estimated_adj.data.copy_(adj) + + def forward(self): + return self.estimated_adj + + def normalize(self): + + if self.symmetric: + adj = (self.estimated_adj + self.estimated_adj.t())/2 + else: + adj = self.estimated_adj + + normalized_adj = self._normalize(adj + torch.eye(adj.shape[0]).to(self.device)) + return normalized_adj + + def _normalize(self, mx): + rowsum = mx.sum(1) + r_inv = rowsum.pow(-1/2).flatten() + r_inv[torch.isinf(r_inv)] = 0. + r_mat_inv = torch.diag(r_inv) + mx = r_mat_inv @ mx + mx = mx @ r_mat_inv + return mx