def fit(self, features, adj, labels, idx_train, idx_val=None, train_iters=200, initialize=True, verbose=False, normalize=True): ''' train the gcn model, when idx_val is not None, pick the best model according to the validation loss ''' self.device = self.gc1.weight.device if initialize: self.initialize() if type(adj) is not torch.Tensor: features, adj, labels = utils.to_tensor(features, adj, labels, device=self.device) else: features = features.to(self.device) adj = adj.to(self.device) labels = labels.to(self.device) if normalize: if utils.is_sparse_tensor(adj): adj_norm = utils.normalize_adj_tensor(adj, sparse=True) else: adj_norm = utils.normalize_adj_tensor(adj) else: adj_norm = adj self.adj_norm = adj_norm self.features = features self.labels = labels if idx_val is None: self._train_without_val(labels, idx_train, train_iters, verbose) else: self._train_with_val(labels, idx_train, idx_val, train_iters, verbose)
def fit(self, features, adj, labels, idx_train, idx_val=None, train_iters=200, initialize=True, verbose=False, normalize=True, patience=500, **kwargs): if initialize: self.initialize() if type(adj) is not torch.Tensor: features, adj, labels = utils.to_tensor(features, adj, labels, device=self.device) else: features = features.to(self.device) adj = adj.to(self.device) labels = labels.to(self.device) if normalize: if utils.is_sparse_tensor(adj): adj_norm = utils.normalize_adj_tensor(adj, sparse=True) else: adj_norm = utils.normalize_adj_tensor(adj) else: adj_norm = adj self.adj_norm = adj_norm self.features = features self.labels = labels if idx_val is None: self._train_without_val(labels, idx_train, train_iters, verbose) else: if patience < train_iters: self._train_with_early_stopping(labels, idx_train, idx_val, train_iters, patience, verbose) else: self._train_with_val(labels, idx_train, idx_val, train_iters, verbose)
def predict(self, features=None, adj=None): """By default, the inputs should be unnormalized data Parameters ---------- features : node features. If `features` and `adj` are not given, this function will use previous stored `features` and `adj` from training to make predictions. adj : adjcency matrix. If `features` and `adj` are not given, this function will use previous stored `features` and `adj` from training to make predictions. Returns ------- torch.FloatTensor output (log probabilities) of GCN """ self.eval() if features is None and adj is None: return self.forward(self.features, self.adj_norm) else: if type(adj) is not torch.Tensor: features, adj = utils.to_tensor(features, adj, device=self.device) self.features = features if utils.is_sparse_tensor(adj): self.adj_norm = utils.normalize_adj_tensor(adj, sparse=True) else: self.adj_norm = utils.normalize_adj_tensor(adj) return self.forward(self.features, self.adj_norm)
def fit(self, features, adj, labels, idx_train, idx_val=None, train_iters=200, initialize=True, verbose=True, normalize=True, patience=1000, adj2=None): ''' train the gcn model, when idx_val is not None, pick the best model according to the validation loss ''' self.device = self.gc1.weight_x.weight.device #if initialize: # self.initialize() if type(adj) is not torch.Tensor: features, adj, labels = utils.to_tensor(features, adj, labels, device=self.device) else: features = features.to(self.device) adj = adj.to(self.device) labels = labels.to(self.device) if normalize: if utils.is_sparse_tensor(adj): adj_norm = utils.normalize_adj_tensor(adj, sparse=True) else: adj_norm = utils.normalize_adj_tensor(adj) else: adj_norm = adj adj_norm = adj_norm * (1 - torch.eye(adj_norm.size(0)).cuda()) adj_norm = self.normalize_adj(adj_norm) self.adj_norm_dense = adj_norm adj_norm = to_sparse(adj_norm) self.adj_norm = adj_norm self.features = features self.labels = labels if idx_val is None: self._train_without_val(labels, idx_train, train_iters, verbose) else: if patience < train_iters: self._train_with_early_stopping(labels, idx_train, idx_val, train_iters, patience, verbose) else: self._train_with_val(labels, idx_train, idx_val, train_iters, verbose)
def attack(self, ori_features, ori_adj, labels, idx_train, n_perturbations, epochs=200, **kwargs): """ Generate perturbations on the input graph """ victim_model = self.surrogate self.sparse_features = sp.issparse(ori_features) ori_adj, ori_features, labels = utils.to_tensor(ori_adj, ori_features, labels, device=self.device) victim_model.eval() for t in tqdm(range(epochs)): modified_adj = self.get_modified_adj(ori_adj) adj_norm = utils.normalize_adj_tensor(modified_adj, device=self.device) output = victim_model(ori_features, adj_norm) self.loss = self._loss(output[idx_train], labels[idx_train]) # New: add regularization term for spectral distance if self.regularization_weight != 0: ori_adj_norm = utils.normalize_adj_tensor(ori_adj, device=self.device) ori_e, ori_v = torch.symeig(ori_adj_norm, eigenvectors=True) e, v = torch.symeig(adj_norm, eigenvectors=True) self.regularization = F.mse_loss(ori_e, e) self.norm = torch.norm(ori_e) self.loss += self.regularization / self.norm * self.regularization_weight adj_grad = torch.autograd.grad(self.loss, self.adj_changes)[0] if self.loss_type == 'CE': lr = 200 / np.sqrt(t+1) self.adj_changes.data.add_(lr * adj_grad) if self.loss_type == 'CW': lr = 0.1 / np.sqrt(t+1) self.adj_changes.data.add_(lr * adj_grad) self.projection(n_perturbations) self.random_sample(ori_adj, ori_features, labels, idx_train, n_perturbations) self.modified_adj = self.get_modified_adj(ori_adj).detach() self.check_adj_tensor(self.modified_adj) # for sanity check ori_adj_norm = utils.normalize_adj_tensor(ori_adj, device=self.device) ori_e, ori_v = torch.symeig(ori_adj_norm, eigenvectors=True) adj_norm = utils.normalize_adj_tensor(self.modified_adj, device=self.device) e, v = torch.symeig(adj_norm, eigenvectors=True) self.adj = ori_adj.detach() self.labels = labels.detach() self.ori_e = ori_e self.ori_v = ori_v self.e = e self.v = v
def attack(self, ori_features, ori_adj, labels, idx_train, n_perturbations): victim_model = self.surrogate self.sparse_features = sp.issparse(ori_features) ori_adj, ori_features, labels = utils.to_tensor(ori_adj, ori_features, labels, device=self.device) # optimizer optimizer = optim.Adam(victim_model.parameters(), lr=0.01) epochs = 200 victim_model.eval() for t in tqdm(range(epochs)): # update victim model victim_model.train() modified_adj = self.get_modified_adj(ori_adj) adj_norm = utils.normalize_adj_tensor(modified_adj) output = victim_model(ori_features, adj_norm) loss = self._loss(output[idx_train], labels[idx_train]) optimizer.zero_grad() loss.backward() optimizer.step() # generate pgd attack victim_model.eval() modified_adj = self.get_modified_adj(ori_adj) adj_norm = utils.normalize_adj_tensor(modified_adj) output = victim_model(ori_features, adj_norm) loss = self._loss(output[idx_train], labels[idx_train]) adj_grad = torch.autograd.grad(loss, self.adj_changes)[0] # adj_grad = self.adj_changes.grad if self.loss_type == 'CE': lr = 200 / np.sqrt(t + 1) self.adj_changes.data.add_(lr * adj_grad) if self.loss_type == 'CW': lr = 0.1 / np.sqrt(t + 1) self.adj_changes.data.add_(lr * adj_grad) # self.adj_changes.grad.zero_() self.projection(n_perturbations) self.random_sample(ori_adj, ori_features, labels, idx_train, n_perturbations) self.modified_adj = self.get_modified_adj(ori_adj).detach()
def predict(self, features=None, adj=None): '''By default, inputs are unnormalized data''' self.eval() if features is None and adj is None: return self.forward(self.features, self.adj_norm) else: if type(adj) is not torch.Tensor: features, adj = utils.to_tensor(features, adj, device=self.device) self.features = features if utils.is_sparse_tensor(adj): self.adj_norm = utils.normalize_adj_tensor(adj, sparse=True) else: self.adj_norm = utils.normalize_adj_tensor(adj) return self.forward(self.features, self.adj_norm)
def random_sample(self, ori_adj, ori_features, labels, idx_train, n_perturbations): K = 20 best_loss = -1000 victim_model = self.surrogate victim_model.eval() with torch.no_grad(): s = self.adj_changes.cpu().detach().numpy() for i in range(K): sampled = np.random.binomial(1, s) # print(sampled.sum()) if sampled.sum() > n_perturbations: continue self.adj_changes.data.copy_(torch.tensor(sampled)) modified_adj = self.get_modified_adj(ori_adj) adj_norm = utils.normalize_adj_tensor(modified_adj) output = victim_model(ori_features, adj_norm) loss = self._loss(output[idx_train], labels[idx_train]) # loss = F.nll_loss(output[idx_train], labels[idx_train]) # print(loss) if best_loss < loss: best_loss = loss best_s = sampled self.adj_changes.data.copy_(torch.tensor(best_s))
def __init__(self, normalize, gm, device): self.adj_norm = normalize self.gm = gm g = StaticGraph.graph edges = np.array(g.edges(), dtype=np.int64) rev_edges = np.array([edges[:, 1], edges[:, 0]], dtype=np.int64) # self_edges = np.array([range(len(g)), range(len(g))], dtype=np.int64) # edges = np.hstack((edges.T, rev_edges, self_edges)) edges = np.hstack((edges.T, rev_edges)) idxes = torch.LongTensor(edges) values = torch.ones(idxes.size()[1]) self.raw_adj = torch.sparse.FloatTensor(idxes, values, StaticGraph.get_gsize()) self.raw_adj = self.raw_adj.to(device) self.normed_adj = self.raw_adj.clone() if self.adj_norm: if self.gm == 'gcn': self.normed_adj = utils.normalize_adj_tensor(self.normed_adj, sparse=True) # GraphLaplacianNorm(self.normed_adj) else: self.normed_adj = utils.degree_normalize_adj_tensor( self.normed_adj, sparse=True)
def calc_gradient_adj(self, inputs, feature): for adj in inputs: adj_norm = utils.normalize_adj_tensor(modified_adj) output = self.surrogate(features, adj_norm) loss = F.nll_loss(output[idx_train], labels[idx_train]) adj_grad = torch.autograd.grad(loss, inputs)[0] return adj_grad.mean()
def calc_importance_edge(self, features, adj, labels, idx_train, steps): adj.requires_grad = True integrated_grad_list = [] for i in range(adj.shape[0]): for j in range(adj.shape[1]): if adj[i][j]: scaled_inputs = [(float(i) / steps) * (adj - 0) for i in range(0, steps + 1)] else: scaled_inputs = [ -(float(i) / steps) * (1 - adj) for i in range(0, steps + 1) ] _sum = 0 for new_adj in scaled_inputs: # TODO: whether to first normalize adj or adj_norm = utils.normalize_adj_tensor(new_adj) output = self.surrogate(features, adj_norm) loss = F.nll_loss(output[idx_train], labels[idx_train]) import ipdb ipdb.set_trace() # adj_grad = torch.autograd.grad(loss, adj[i][j], allow_unused=True)[0] adj_grad = torch.autograd.grad(loss, adj, allow_unused=True)[0] adj_grad = adj_grad[i][j] _sum += adj_grad avg_grads = _sum.mean() integrated_grad_list.append(avg_grad) return integrated_grad_list
def random_sample(self, ori_adj, ori_features, labels, idx_target, n_perturbations): K = 10 best_loss = -1000 victim_model = self.surrogate with torch.no_grad(): s = self.adj_changes.cpu().detach().numpy() for i in range(K): sampled = np.random.binomial(1, s) # randm = np.random.uniform(size=s.shape[0]) # sampled = np.where(s > randm, 1, 0) # if sampled.sum() > n_perturbations: # continue while sampled.sum() > n_perturbations: sampled = np.random.binomial(1, s) self.adj_changes.data.copy_(torch.tensor(sampled)) modified_adj = self.get_modified_adj(ori_adj) adj_norm = utils.normalize_adj_tensor(modified_adj, device=self.device) output = victim_model(ori_features, adj_norm) loss = self._loss(output[idx_target], labels[idx_target]) # loss = F.nll_loss(output[idx_target], labels[idx_target]) # print(loss) if best_loss < loss: best_loss = loss best_s = sampled self.adj_changes.data.copy_(torch.tensor(best_s))
def attack(self, ori_features, ori_adj, labels, idx_train, perturbations): victim_model = self.surrogate self.sparse_features = sp.issparse(ori_features) ori_adj, ori_features, labels = utils.to_tensor(ori_adj, ori_features, labels, device=self.device) victim_model.eval() adj_norm = utils.normalize_adj_tensor(ori_adj) s_e = self.calc_importance_edge(ori_features, adj_norm, labels, idx_train, steps=20) s_f = self.calc_importance_feature(ori_features, adj_norm, labels, idx_train, steps=20) import ipdb ipdb.set_trace() for t in tqdm(range(perturbations)): modified_adj self.adj_changes.data.copy_(torch.tensor(best_s)) self.modified_adj = self.get_modified_adj(ori_adj).detach()
def attack(self, features, adj, labels, idx_train, target_node, n_perturbations): # adj: torch.sparse modified_adj = deepcopy(adj).to_dense() self.surrogate.eval() print(f'number of pertubations: {n_perturbations}') for i in range(n_perturbations): modified_row = modified_adj[target_node] + self.adj_changes modified_adj[target_node] = modified_row adj_norm = utils.normalize_adj_tensor(modified_adj) all_ok = [target_node] if self.attack_structure: output = self.surrogate(features, adj_norm) loss = F.nll_loss(output[idx_train], labels[idx_train]) # acc_train = accuracy(output[idx_train], labels[idx_train]) grad = torch.autograd.grad(loss, self.adj_changes, retain_graph=True)[0] grad = grad * (-2 * modified_row + 1) grad[target_node] = 0 grad_argmax = torch.argmax(grad) value = -2 * modified_row[grad_argmax] + 1 modified_adj.data[target_node][grad_argmax] += value modified_adj.data[grad_argmax][target_node] += value if self.attack_features: pass modified_adj = modified_adj.detach() self.check_adj(modified_adj) self.modified_adj = modified_adj
def attack(self, ori_features, ori_adj, labels, idx_train, perturbations): victim_model = self.surrogate self.sparse_features = sp.issparse(ori_features) ori_adj, ori_features, labels = utils.to_tensor(ori_adj, ori_features, labels, device=self.device) victim_model.eval() epochs = 200 for t in tqdm(range(epochs)): modified_adj = self.get_modified_adj(ori_adj) adj_norm = utils.normalize_adj_tensor(modified_adj) output = victim_model(ori_features, adj_norm) # loss = F.nll_loss(output[idx_train], labels[idx_train]) loss = self._loss(output[idx_train], labels[idx_train]) adj_grad = torch.autograd.grad(loss, self.adj_changes)[0] if self.loss_type == 'CE': lr = 200 / np.sqrt(t + 1) self.adj_changes.data.add_(lr * adj_grad) if self.loss_type == 'CW': lr = 0.1 / np.sqrt(t + 1) self.adj_changes.data.add_(lr * adj_grad) self.projection(perturbations) self.random_sample(ori_adj, ori_features, labels, idx_train, perturbations) self.modified_adj = self.get_modified_adj(ori_adj).detach()
def attack(self, ori_features, ori_adj, labels, idx_train, n_perturbations, epochs=200, **kwargs): """Generate perturbations on the input graph. Parameters ---------- ori_features : Original (unperturbed) node feature matrix ori_adj : Original (unperturbed) adjacency matrix labels : node labels idx_train : node training indices n_perturbations : int Number of perturbations on the input graph. Perturbations could be edge removals/additions or feature removals/additions. epochs: number of training epochs """ victim_model = self.surrogate self.sparse_features = sp.issparse(ori_features) ori_adj, ori_features, labels = utils.to_tensor(ori_adj, ori_features, labels, device=self.device) victim_model.eval() for t in tqdm(range(epochs)): modified_adj = self.get_modified_adj(ori_adj) adj_norm = utils.normalize_adj_tensor(modified_adj) output = victim_model(ori_features, adj_norm) # loss = F.nll_loss(output[idx_train], labels[idx_train]) loss = self._loss(output[idx_train], labels[idx_train]) adj_grad = torch.autograd.grad(loss, self.adj_changes)[0] if self.loss_type == 'CE': lr = 200 / np.sqrt(t + 1) self.adj_changes.data.add_(lr * adj_grad) if self.loss_type == 'CW': lr = 0.1 / np.sqrt(t + 1) self.adj_changes.data.add_(lr * adj_grad) self.projection(n_perturbations) self.random_sample(ori_adj, ori_features, labels, idx_train, n_perturbations) self.modified_adj = self.get_modified_adj(ori_adj).detach() self.check_adj_tensor(self.modified_adj)
def inner_train(self, features, modified_adj, idx_train, idx_unlabeled, labels, labels_self_training): adj_norm = utils.normalize_adj_tensor(modified_adj) for j in range(self.train_iters): # hidden = features # for w, b in zip(self.weights, self.biases): # if self.sparse_features: # hidden = adj_norm @ torch.spmm(hidden, w) + b # else: # hidden = adj_norm @ hidden @ w + b # if self.with_relu: # hidden = F.relu(hidden) hidden = features for ix, w in enumerate(self.weights): b = self.biases[ix] if self.with_bias else 0 if self.sparse_features: hidden = adj_norm @ torch.spmm(hidden, w) + b else: hidden = adj_norm @ hidden @ w + b if self.with_relu: hidden = F.relu(hidden) output = F.log_softmax(hidden, dim=1) loss_labeled = F.nll_loss(output[idx_train], labels[idx_train]) loss_unlabeled = F.nll_loss(output[idx_unlabeled], labels_self_training[idx_unlabeled]) if self.lambda_ == 1: attack_loss = loss_labeled elif self.lambda_ == 0: attack_loss = loss_unlabeled else: attack_loss = self.lambda_ * loss_labeled + ( 1 - self.lambda_) * loss_unlabeled self.optimizer.zero_grad() loss_labeled.backward(retain_graph=True) if self.attack_structure: self.adj_changes.grad.zero_() self.adj_grad_sum += torch.autograd.grad(attack_loss, self.adj_changes, retain_graph=True)[0] if self.attack_features: self.feature_changes.grad.zero_() self.feature_grad_sum += torch.autograd.grad( attack_loss, self.feature_changes, retain_graph=True)[0] self.optimizer.step() loss_test_val = F.nll_loss(output[idx_unlabeled], labels[idx_unlabeled]) print('GCN loss on unlabled data: {}'.format(loss_test_val.item())) print('GCN acc on unlabled data: {}'.format( utils.accuracy(output[idx_unlabeled], labels[idx_unlabeled]).item()))
def attack(self, ori_features, ori_adj, labels, idx_train, perturbations): victim_model = self.surrogate self.sparse_features = sp.issparse(ori_features) ori_adj, ori_features, labels = utils.to_tensor(ori_adj, ori_features, labels, device=self.device) modified_adj = ori_adj victim_model.eval() epochs = 200 for t in tqdm(range(epochs)): modified_adj = self.get_modified_adj(ori_adj) adj_norm = utils.normalize_adj_tensor(modified_adj) output = victim_model(ori_features, adj_norm) loss = F.nll_loss(output[idx_train], labels[idx_train]) adj_grad = torch.autograd.grad(loss, self.adj_changes)[0] lr = 200 / np.sqrt(t + 1) self.adj_changes.data.add_(lr * adj_grad) self.projection(perturbations) K = 20 best_loss = 0 with torch.no_grad(): s = self.adj_changes.cpu().numpy() for i in range(K): sampled = np.random.binomial(1, s) print(sampled.sum()) if sampled.sum() > perturbations: continue self.adj_changes.data.copy_(torch.tensor(sampled)) modified_adj = self.get_modified_adj(ori_adj) adj_norm = utils.normalize_adj_tensor(modified_adj) output = victim_model(ori_features, adj_norm) loss = F.nll_loss(output[idx_train], labels[idx_train]) print(loss) if best_loss < loss: best_loss = loss best_s = sampled self.adj_changes.data.copy_(torch.tensor(best_s)) self.modified_adj = self.get_modified_adj(ori_adj).detach()
def attack(self, ori_features, ori_adj, labels, idx_train, target_node, n_perturbations, verbose=False, **kwargs): """Generate perturbations on the input graph. Parameters ---------- ori_features : scipy.sparse.csr_matrix Original (unperturbed) adjacency matrix ori_adj : scipy.sparse.csr_matrix Original (unperturbed) node feature matrix labels : node labels idx_train: training node indices target_node : int target node index to be attacked n_perturbations : int Number of perturbations on the input graph. Perturbations could be edge removals/additions or feature removals/additions. """ modified_adj = ori_adj.todense() modified_features = ori_features.todense() modified_adj, modified_features, labels = utils.to_tensor(modified_adj, modified_features, labels, device=self.device) self.surrogate.eval() if verbose == True: print('number of pertubations: %s' % n_perturbations) pseudo_labels = self.surrogate.predict().detach().argmax(1) pseudo_labels[idx_train] = labels[idx_train] modified_adj.requires_grad = True for i in range(n_perturbations): adj_norm = utils.normalize_adj_tensor(modified_adj) if self.attack_structure: output = self.surrogate(modified_features, adj_norm) loss = F.nll_loss(output[[target_node]], pseudo_labels[[target_node]]) grad = torch.autograd.grad(loss, modified_adj)[0] # bidirection grad = (grad[target_node] + grad[:, target_node]) * (-2*modified_adj[target_node] + 1) grad[target_node] = -10 grad_argmax = torch.argmax(grad) value = -2*modified_adj[target_node][grad_argmax] + 1 modified_adj.data[target_node][grad_argmax] += value modified_adj.data[grad_argmax][target_node] += value if self.attack_features: pass modified_adj = modified_adj.detach().cpu().numpy() modified_adj = sp.csr_matrix(modified_adj) self.check_adj(modified_adj) self.modified_adj = modified_adj
def attack(self, ori_features, ori_adj, labels, idx_train, target_node, n_perturbations, **kwargs): """Generate perturbations on the input graph. Parameters ---------- ori_features : scipy.sparse.csr_matrix Original (unperturbed) adjacency matrix ori_adj : scipy.sparse.csr_matrix Original (unperturbed) node feature matrix labels : node labels idx_train : node training indices target_node : int target node index to be attacked n_perturbations : int Number of perturbations on the input graph. Perturbations could be edge removals/additions or feature removals/additions. """ modified_adj = ori_adj.todense() modified_features = ori_features.todense() modified_adj, modified_features, labels = utils.to_tensor( modified_adj, modified_features, labels, device=self.device) self.surrogate.eval() print('number of pertubations: %s' % n_perturbations) for i in range(n_perturbations): modified_row = modified_adj[target_node] + self.adj_changes modified_adj[target_node] = modified_row adj_norm = utils.normalize_adj_tensor(modified_adj) if self.attack_structure: output = self.surrogate(modified_features, adj_norm) loss = F.nll_loss(output[idx_train], labels[idx_train]) # acc_train = accuracy(output[idx_train], labels[idx_train]) grad = torch.autograd.grad(loss, self.adj_changes, retain_graph=True)[0] grad = grad * (-2 * modified_row + 1) grad[target_node] = 0 grad_argmax = torch.argmax(grad) value = -2 * modified_row[grad_argmax] + 1 modified_adj.data[target_node][grad_argmax] += value modified_adj.data[grad_argmax][target_node] += value if self.attack_features: pass modified_adj = modified_adj.detach().cpu().numpy() modified_adj = sp.csr_matrix(modified_adj) self.check_adj(modified_adj) self.modified_adj = modified_adj
def norm_extra(self, added_adj = None): if added_adj is None: return self.normed_adj new_adj = self.raw_adj + added_adj if self.adj_norm: if self.gm == 'gcn': new_adj = utils.normalize_adj_tensor(new_adj, sparse=True) else: new_adj = utils.degree_normalize_adj_tensor(new_adj, sparse=True) return new_adj
def attack(self, ori_features, ori_adj, labels, idx_train, target_node, n_perturbations, steps=10): self.surrogate.eval() self.target_node = target_node modified_adj = ori_adj.todense() modified_features = ori_features.todense() adj, features, labels = utils.to_tensor(modified_adj, modified_features, labels, device=self.device) adj_norm = utils.normalize_adj_tensor(adj) s_e = np.zeros(adj.shape[1]) s_f = np.zeros(features.shape[1]) if self.attack_structure: s_e = self.calc_importance_edge(features, adj_norm, labels, idx_train, steps) if self.attack_features: s_f = self.calc_importance_feature(features, adj_norm, labels, idx_train, steps) for t in (range(n_perturbations)): s_e_max = np.argmax(s_e) s_f_max = np.argmax(s_f) if s_e[s_e_max] >= s_f[s_f_max]: value = np.abs(1 - modified_adj[target_node, s_e_max]) modified_adj[target_node, s_e_max] = value modified_adj[s_e_max, target_node] = value s_e[s_e_max] = 0 else: modified_features[target_node, s_f_max] = np.abs( 1 - modified_features[target_node, s_f_max]) s_f[s_f_max] = 0 self.modified_adj = sp.csr_matrix(modified_adj) self.modified_features = sp.csr_matrix(modified_features) self.check_adj(modified_adj)
def calc_importance_edge(self, features, adj, labels, idx_train, steps): adj_norm = utils.normalize_adj_tensor(adj) adj_norm.requires_grad = True integrated_grad_list = [] for i in tqdm(range(adj.shape[0])): for j in (range(adj.shape[1])): if adj_norm[i][j]: scaled_inputs = [(float(k) / steps) * (adj_norm - 0) for k in range(0, steps + 1)] else: scaled_inputs = [ -(float(k) / steps) * (1 - adj_norm) for k in range(0, steps + 1) ] _sum = 0 # num_processes = steps # # NOTE: this is required for the ``fork`` method to work # self.surrogate.share_memory() # processes = [] # for rank in range(num_processes): # p = mp.Process(target=self.get_gradient, args=(features, scaled_inputs[rank], adj_norm, labels, idx_train)) # p.start() # processes.append(p) # for p in processes: # p.join() for new_adj in scaled_inputs: output = self.surrogate(features, new_adj) loss = F.nll_loss(output[idx_train], labels[idx_train]) # adj_grad = torch.autograd.grad(loss, adj[i][j], allow_unused=True)[0] adj_grad = torch.autograd.grad(loss, adj_norm)[0] adj_grad = adj_grad[i][j] _sum += adj_grad if adj_norm[i][j]: avg_grad = (adj_norm[i][j] - 0) * _sum.mean() else: avg_grad = (1 - adj_norm[i][j]) * _sum.mean() integrated_grad_list.append(avg_grad) return integrated_grad_list
def attack(self, ori_features, ori_adj, labels, idx_train, idx_unlabeled, perturbations, ll_constraint=True, ll_cutoff=0.004): self.sparse_features = sp.issparse(ori_features) ori_adj, ori_features, labels = utils.to_tensor(ori_adj, ori_features, labels, device=self.device) labels_self_training = self.self_training_label(labels, idx_train) modified_adj = ori_adj modified_features = ori_features for i in tqdm(range(perturbations), desc="Perturbing graph"): if self.attack_structure: modified_adj = self.get_modified_adj(ori_adj) if self.attack_features: modified_features = ori_features + self.feature_changes adj_norm = utils.normalize_adj_tensor(modified_adj) self.inner_train(modified_features, adj_norm, idx_train, idx_unlabeled, labels) adj_grad, feature_grad = self.get_meta_grad(modified_features, adj_norm, idx_train, idx_unlabeled, labels, labels_self_training) adj_meta_score = torch.tensor(0.0).to(self.device) feature_meta_score = torch.tensor(0.0).to(self.device) if self.attack_structure: adj_meta_score = self.get_adj_score(adj_grad, modified_adj, ori_adj, ll_constraint, ll_cutoff) if self.attack_features: feature_meta_score = self.get_feature_score(feature_grad, modified_features) if adj_meta_score.max() >= feature_meta_score.max(): adj_meta_argmax = torch.argmax(adj_meta_score) row_idx, col_idx = utils.unravel_index(adj_meta_argmax, ori_adj.shape) self.adj_changes.data[row_idx][col_idx] += (-2 * modified_adj[row_idx][col_idx] + 1) self.adj_changes.data[col_idx][row_idx] += (-2 * modified_adj[row_idx][col_idx] + 1) else: feature_meta_argmax = torch.argmax(feature_meta_score) row_idx, col_idx = utils.unravel_index(feature_meta_argmax, ori_features.shape) self.features_changes.data[row_idx][col_idx] += (-2 * modified_features[row_idx][col_idx] + 1) if self.attack_structure: self.modified_adj = self.get_modified_adj(ori_adj).detach() if self.attack_features: self.modified_features = self.get_modified_features(ori_features).detach()
def attack(self, features, adj, labels, idx_train, target_node, n_perturbations): # adj: sp.csr_matrix modified_adj = adj.todense() features = features.todense() modified_adj, features, labels = utils.to_tensor(modified_adj, features, labels, device=self.device) self.surrogate.eval() print('number of pertubations: %s' % n_perturbations) for i in range(n_perturbations): modified_row = modified_adj[target_node] + self.adj_changes modified_adj[target_node] = modified_row adj_norm = utils.normalize_adj_tensor(modified_adj) if self.attack_structure: output = self.surrogate(features, adj_norm) loss = F.nll_loss(output[idx_train], labels[idx_train]) # acc_train = accuracy(output[idx_train], labels[idx_train]) grad = torch.autograd.grad(loss, self.adj_changes, retain_graph=True)[0] grad = grad * (-2 * modified_row + 1) grad[target_node] = 0 grad_argmax = torch.argmax(grad) value = -2 * modified_row[grad_argmax] + 1 modified_adj.data[target_node][grad_argmax] += value modified_adj.data[grad_argmax][target_node] += value if self.attack_features: pass modified_adj = modified_adj.detach().cpu().numpy() modified_adj = sp.csr_matrix(modified_adj) self.check_adj(modified_adj) self.modified_adj = modified_adj
def random_sample(self, ori_adj, ori_features, labels, idx_target, n_perturbations): K = 10 best_loss = -1000 victim_model = self.surrogate with torch.no_grad(): s = self.adj_changes.cpu().detach().numpy() for i in range(K): sampled = np.random.binomial(1, s) # randm = np.random.uniform(size=s.shape[0]) # sampled = np.where(s > randm, 1, 0) # if sampled.sum() > n_perturbations: # continue while sampled.sum() > n_perturbations: sampled = np.random.binomial(1, s) # if sampled.sum() > n_perturbations: # indices = np.transpose(np.nonzero(sampled)) # candidate_idx = [m for m in range(indices.shape[0])] # chosen_idx = np.random.choice(candidate_idx, n_perturbations, replace=False) # chosen_indices = indices[chosen_idx, :] # sampled = np.zeros_like(sampled) # for idx in chosen_indices: # sampled[idx] = 1 self.adj_changes.data.copy_(torch.tensor(sampled)) modified_adj = self.get_modified_adj(ori_adj) adj_norm = utils.normalize_adj_tensor(modified_adj, device=self.device) output = victim_model(ori_features, adj_norm) loss = self._loss(output[idx_target], labels[idx_target]) # loss = F.nll_loss(output[idx_target], labels[idx_target]) # print(loss) if best_loss < loss: best_loss = loss best_s = sampled self.adj_changes.data.copy_(torch.tensor(best_s))
def fit(self, features, adj, labels, idx_train, idx_val=None, train_iters=200, initialize=True, verbose=False, normalize=True, patience=500, **kwargs): """Train the gcn model, when idx_val is not None, pick the best model according to the validation loss. Parameters ---------- features : node features adj : the adjacency matrix. The format could be torch.tensor or scipy matrix labels : node labels idx_train : node training indices idx_val : node validation indices. If not given (None), GCN training process will not adpot early stopping train_iters : int number of training epochs initialize : bool whether to initialize parameters before training verbose : bool whether to show verbose logs normalize : bool whether to normalize the input adjacency matrix. patience : int patience for early stopping, only valid when `idx_val` is given """ self.device = self.gc1.weight.device if initialize: self.initialize() if type(adj) is not torch.Tensor: features, adj, labels = utils.to_tensor(features, adj, labels, device=self.device) else: features = features.to(self.device) adj = adj.to(self.device) labels = labels.to(self.device) if normalize: if utils.is_sparse_tensor(adj): adj_norm = utils.normalize_adj_tensor(adj, sparse=True) else: adj_norm = utils.normalize_adj_tensor(adj) else: adj_norm = adj self.adj_norm = adj_norm self.features = features self.labels = labels if idx_val is None: self._train_without_val(labels, idx_train, train_iters, verbose) else: if patience < train_iters: self._train_with_early_stopping(labels, idx_train, idx_val, train_iters, patience, verbose) else: self._train_with_val(labels, idx_train, idx_val, train_iters, verbose)
def attack(self, ori_features, ori_adj, labels, idx_train, idx_unlabeled, n_perturbations, ll_constraint=True, ll_cutoff=0.004): """Generate n_perturbations on the input graph. Parameters ---------- ori_features : Original (unperturbed) node feature matrix ori_adj : Original (unperturbed) adjacency matrix labels : node labels idx_train : node training indices idx_unlabeled: unlabeled nodes indices n_perturbations : int Number of perturbations on the input graph. Perturbations could be edge removals/additions or feature removals/additions. ll_constraint: bool whether to exert the likelihood ratio test constraint ll_cutoff : float The critical value for the likelihood ratio test of the power law distributions. See the Chi square distribution with one degree of freedom. Default value 0.004 corresponds to a p-value of roughly 0.95. It would be ignored if `ll_constraint` is False. """ self.sparse_features = sp.issparse(ori_features) ori_adj, ori_features, labels = utils.to_tensor(ori_adj, ori_features, labels, device=self.device) labels_self_training = self.self_training_label(labels, idx_train) modified_adj = ori_adj modified_features = ori_features for i in tqdm(range(n_perturbations), desc="Perturbing graph"): if self.attack_structure: modified_adj = self.get_modified_adj(ori_adj) if self.attack_features: modified_features = ori_features + self.feature_changes adj_norm = utils.normalize_adj_tensor(modified_adj) adj_norm = self.normalize_adj(adj_norm) self.inner_train(modified_features, adj_norm, idx_train, idx_unlabeled, labels) adj_grad, feature_grad = self.get_meta_grad( modified_features, adj_norm, idx_train, idx_unlabeled, labels, labels_self_training) adj_meta_score = torch.tensor(0.0).to(self.device) feature_meta_score = torch.tensor(0.0).to(self.device) if self.attack_structure: adj_meta_score = self.get_adj_score(adj_grad, modified_adj, ori_adj, ll_constraint, ll_cutoff) if self.attack_features: feature_meta_score = self.get_feature_score( feature_grad, modified_features) if adj_meta_score.max() >= feature_meta_score.max(): adj_meta_argmax = torch.argmax(adj_meta_score) row_idx, col_idx = utils.unravel_index(adj_meta_argmax, ori_adj.shape) self.adj_changes.data[row_idx][col_idx] += ( -2 * modified_adj[row_idx][col_idx] + 1) self.adj_changes.data[col_idx][row_idx] += ( -2 * modified_adj[row_idx][col_idx] + 1) else: feature_meta_argmax = torch.argmax(feature_meta_score) row_idx, col_idx = utils.unravel_index(feature_meta_argmax, ori_features.shape) self.features_changes.data[row_idx][col_idx] += ( -2 * modified_features[row_idx][col_idx] + 1) if self.attack_structure: self.modified_adj = self.get_modified_adj(ori_adj).detach() if self.attack_features: self.modified_features = self.get_modified_features( ori_features).detach()
def attack(self, ori_features, ori_adj, labels, idx_train, target_node, n_perturbations, steps=10, **kwargs): """Generate perturbations on the input graph. Parameters ---------- ori_features : Original (unperturbed) node feature matrix ori_adj : Original (unperturbed) adjacency matrix labels : node labels idx_train : node training indices target_node : int target node index to be attacked n_perturbations : int Number of perturbations on the input graph. Perturbations could be edge removals/additions or feature removals/additions. steps : int steps for computing integrated gradients """ self.surrogate.eval() self.target_node = target_node modified_adj = ori_adj.todense() modified_features = ori_features.todense() adj, features, labels = utils.to_tensor(modified_adj, modified_features, labels, device=self.device) adj_norm = utils.normalize_adj_tensor(adj) s_e = np.zeros(adj.shape[1]) s_f = np.zeros(features.shape[1]) if self.attack_structure: s_e = self.calc_importance_edge(features, adj_norm, labels, idx_train, steps) if self.attack_features: s_f = self.calc_importance_feature(features, adj_norm, labels, idx_train, steps) for t in (range(n_perturbations)): s_e_max = np.argmax(s_e) s_f_max = np.argmax(s_f) if s_e[s_e_max] >= s_f[s_f_max]: value = np.abs(1 - modified_adj[target_node, s_e_max]) modified_adj[target_node, s_e_max] = value modified_adj[s_e_max, target_node] = value s_e[s_e_max] = 0 else: modified_features[target_node, s_f_max] = np.abs( 1 - modified_features[target_node, s_f_max]) s_f[s_f_max] = 0 self.modified_adj = sp.csr_matrix(modified_adj) self.modified_features = sp.csr_matrix(modified_features) self.check_adj(modified_adj)
def fit( self, features, adj, labels, idx_train, idx_val=None, idx_test=None, train_iters=101, att_0=None, attention=False, model_name=None, initialize=True, verbose=False, normalize=False, patience=500, ): ''' train the gcn model, when idx_val is not None, pick the best model according to the validation loss ''' self.sim = None self.attention = attention if self.attention: att_0 = att_coef(features, adj) adj = att_0 # update adj self.sim = att_0 # update att_0 self.idx_test = idx_test # self.model_name = model_name # self.device = self.gc1.weight.device if initialize: self.initialize() if type(adj) is not torch.Tensor: features, adj, labels = utils.to_tensor(features, adj, labels, device=self.device) else: features = features.to(self.device) adj = adj.to(self.device) labels = labels.to(self.device) normalize = False # we don't need normalize here, the norm is conducted in the GCN (self.gcn1) model if normalize: if utils.is_sparse_tensor(adj): adj_norm = utils.normalize_adj_tensor(adj, sparse=True) else: adj_norm = utils.normalize_adj_tensor(adj) else: adj_norm = adj """Make the coefficient D^{-1/2}(A+I)D^{-1/2}""" self.adj_norm = adj_norm self.features = features self.labels = labels if idx_val is None: self._train_without_val(labels, idx_train, train_iters, verbose) else: if patience < train_iters: self._train_with_early_stopping(labels, idx_train, idx_val, train_iters, patience, verbose) else: self._train_with_val(labels, idx_train, idx_val, train_iters, verbose)