def random_sample(self, sample_epochs=20, disable=False): best_loss = -np.inf best_s = None s = tf.linalg.band_part(self.adj_changes, 0, -1) - tf.linalg.band_part( self.adj_changes, 0, 0) for _ in tqdm(range(sample_epochs), desc='Random Sampling', disable=disable): random_matrix = tf.random.uniform(shape=(self.n_nodes, self.n_nodes), minval=0., maxval=1.) sampled = tf.where(s > random_matrix, 1., 0.) if tf.reduce_sum(sampled) > self.n_perturbations: continue with tf.device(self.device): self.adj_changes.assign(sampled) adj = self.get_perturbed_adj() adj_norm = normalize_adj_tensor(adj) logit = self.surrogate([self.tf_x, adj_norm, self.idx_attack]) logit = softmax(logit) loss = self.compute_loss(logit) if best_loss < loss: best_loss = loss best_s = sampled return best_s.numpy()
def attack(self, n_perturbations=0.05, sample_epochs=20, CW_loss=True, epochs=200, C=0.1, update_per_epoch=20, structure_attack=True, feature_attack=False, disable=False): super(PGDPoisoning, self).attack(n_perturbations, structure_attack, feature_attack) self.CW_loss = CW_loss with tf.device(self.device): trainable_variables = self.surrogate.trainable_variables for epoch in tqdm(range(epochs), desc='MinMax Training', disable=disable): if (epoch + 1) % update_per_epoch == 0: self.update_surrogate(trainable_variables, self.idx_attack) gradients = self.compute_gradients(self.idx_attack) lr = C / np.sqrt(epoch + 1) self.adj_changes.assign_add(lr * gradients) self.projection() best_s = self.random_sample(sample_epochs) self.structure_flips = np.transpose(np.where(best_s > 0.))
def fit(self, epochs=10, n_perturbations=0.05, lr=0.001, attacker_kwargs={}, model_kwargs={}, disable=False): model = self.model attacker = self.attacker if attacker.x is None: attacker.set_x(model.x) if model.idx_test is not None: loss, accuracy = self.test(n_perturbations=n_perturbations, attacker_kwargs=attacker_kwargs) print(f'Accuracy before adversarial training: {accuracy}.') model.reset_optimizer() model.reset_lr(lr) for _ in tqdm(range(epochs), desc='Adversarial Training', disable=disable): attacker.reset() attacker.attack(n_perturbations, **attacker_kwargs, disable=True) model.preprocess(attacker.A, attacker.X) model.train(model.idx_train, save_best=False, **model_kwargs) if model.idx_test is not None: loss, accuracy = self.test(n_perturbations=n_perturbations, attacker_kwargs=attacker_kwargs) print(f'Accuracy after adversarial training: {accuracy}.')
def attack(self, n_perturbations=0.05, sample_epochs=20, CW_loss=True, epochs=100, structure_attack=True, feature_attack=False, disable=False): super().attack(n_perturbations, structure_attack, feature_attack) self.CW_loss = CW_loss if CW_loss: C = 0.1 else: C = 200 with tf.device(self.device): for epoch in tqdm(range(epochs), desc='Peturbation Training', disable=disable): gradients = self.compute_gradients(self.idx_attack) lr = C / np.sqrt(epoch + 1) self.adj_changes.assign_add(lr * gradients) self.projection() best_s = self.random_sample(sample_epochs, disable=disable) self.structure_flips = np.transpose(np.where(best_s > 0.))
def attack(self, n_perturbations=0.05, structure_attack=True, feature_attack=False, disable=False): super().attack(n_perturbations, structure_attack, feature_attack) influencer_nodes = list(self.nodes_set) structure_flips = self.structure_flips random_list = np.random.choice(2, self.n_perturbations) * 2 - 1 for remove_or_insert in tqdm(random_list, desc='Peturbing Graph', disable=disable): if remove_or_insert > 0: edge = self.add_an_edge(influencer_nodes) while edge is None: edge = self.add_an_edge(influencer_nodes) else: edge = self.del_an_edge(influencer_nodes) while edge is None: edge = self.del_an_edge(influencer_nodes) structure_flips[edge] = 1.0 u, v = edge self.modified_degree[u] += remove_or_insert self.modified_degree[v] += remove_or_insert
def attack(self, n_perturbations=0.05, structure_attack=True, feature_attack=False, ll_constraint=False, ll_cutoff=0.004, disable=False): super().attack(n_perturbations, structure_attack, feature_attack) if ll_constraint: raise NotImplementedError( '`log_likelihood_constraint` has not been well tested.' ' Please set `ll_constraint=False` to achieve a better performance.' ) if feature_attack and not is_binary(self.x): raise ValueError( "Attacks on the node features are currently only supported for binary attributes." ) with tf.device(self.device): modified_adj, modified_x = self.tf_adj, self.tf_x adj_changes, x_changes = self.adj_changes, self.x_changes structure_flips, feature_flips = self.structure_flips, self.feature_flips for _ in tqdm(range(self.n_perturbations), desc='Peturbing Graph', disable=disable): adj_grad, x_grad = self.meta_grad() adj_meta_score = tf.constant(0.0) x_meta_score = tf.constant(0.0) if structure_attack: modified_adj = self.get_perturbed_adj( self.tf_adj, adj_changes) adj_meta_score = self.structure_score( modified_adj, adj_grad, ll_constraint, ll_cutoff) if feature_attack: modified_x = self.get_perturbed_x(self.tf_x, x_changes) x_meta_score = self.feature_score(modified_x, feature_grad) if tf.reduce_max(adj_meta_score) >= tf.reduce_max( x_meta_score): adj_meta_argmax = tf.argmax(adj_meta_score) row, col = divmod(adj_meta_argmax.numpy(), self.n_nodes) adj_changes[row, col].assign(-2. * modified_adj[row, col] + 1.) adj_changes[col, row].assign(-2. * modified_adj[col, row] + 1.) structure_flips.append((row, col)) else: x_meta_argmax = tf.argmax(x_meta_score) row, col = divmod(x_meta_argmax.numpy(), self.n_attrs) x_changes[row, col].assign(-2 * modified_x[row, col] + 1) feature_flips.append((row, col))
def attack(self, target, n_perturbations=None, logit=None, reduced_nodes=3, direct_attack=True, structure_attack=True, feature_attack=False, disable=False): super().attack(target, n_perturbations, direct_attack, structure_attack, feature_attack) if logit is None: logit = self.surrogate.predict(target).ravel() top2 = logit.argsort()[-2:] self.wrong_label = np.setdiff1d(logit.argsort()[-2:], self.target_label)[0] assert self.wrong_label != self.target_label self.subgraph_preprocessing(reduced_nodes) offset = self.edge_weights.shape[0] with tf.device(self.device): for _ in tqdm(range(self.n_perturbations), desc='Peturbing Graph', disable=disable): edge_grad, non_edge_grad = self.compute_gradient(norm=False) edge_grad *= (-2 * self.edge_weights + 1) non_edge_grad *= (-2 * self.non_edge_weights + 1) gradients = tf.concat([edge_grad, non_edge_grad], axis=0) sorted_indices = tf.argsort(gradients, direction="DESCENDING") for index in sorted_indices: if index < offset: u, v = self.edge_index[index] add = False if not self.allow_singleton and ( self.selfloop_degree[u] <= 2 or self.selfloop_degree[v] <= 2): continue else: index -= offset u, v = self.non_edge_index[index] add = True if not self.is_modified_edge(u, v): self.structure_flips[(u, v)] = _ self.update_subgraph(u, v, index, add=add) break
def get_feature_importance(self, candidates, steps, disable=False): adj = self.adj_norm x = self.tf_x target_index = astensor([self.target]) target_label = astensor(self.target_label) baseline_add = x.numpy() baseline_add[candidates[:, 0], candidates[:, 1]] = 1.0 baseline_add = astensor(baseline_add) baseline_remove = x.numpy() baseline_remove[candidates[:, 0], candidates[:, 1]] = 0.0 baseline_remove = astensor(baseline_remove) feature_indicator = self.x[candidates[:, 0], candidates[:, 1]] > 0 features = candidates[feature_indicator] non_attrs = candidates[~feature_indicator] feature_gradients = tf.zeros(features.shape[0]) non_feature_gradients = tf.zeros(non_attrs.shape[0]) for alpha in tqdm(tf.linspace(0., 1.0, steps + 1), desc='Computing feature importance', disable=disable): ###### Compute integrated gradients for removing features ###### x_diff = x - baseline_remove x_step = baseline_remove + alpha * x_diff gradients = self.compute_feature_gradients(adj, x_step, target_index, target_label) feature_gradients += -tf.gather_nd(gradients, features) ###### Compute integrated gradients for adding features ###### x_diff = baseline_add - x x_step = baseline_add - alpha * x_diff gradients = self.compute_feature_gradients(adj, x_step, target_index, target_label) non_feature_gradients += tf.gather_nd(gradients, non_attrs) integrated_grads = np.zeros(feature_indicator.size) integrated_grads[feature_indicator] = feature_gradients.numpy() integrated_grads[~feature_indicator] = non_feature_gradients.numpy() return integrated_grads
def get_link_importance(self, candidates, steps, disable=False): adj = self.tf_adj x = self.tf_x target_index = astensor([self.target]) target_label = astensor(self.target_label) baseline_add = adj.numpy() baseline_add[candidates[:, 0], candidates[:, 1]] = 1.0 baseline_add = astensor(baseline_add) baseline_remove = adj.numpy() baseline_remove[candidates[:, 0], candidates[:, 1]] = 0.0 baseline_remove = astensor(baseline_remove) edge_indicator = self.adj[candidates[:, 0], candidates[:, 1]].A1 > 0 edges = candidates[edge_indicator] non_edges = candidates[~edge_indicator] edge_gradients = tf.zeros(edges.shape[0]) non_edge_gradients = tf.zeros(non_edges.shape[0]) for alpha in tqdm(tf.linspace(0., 1.0, steps + 1), desc='Computing link importance', disable=disable): ###### Compute integrated gradients for removing edges ###### adj_diff = adj - baseline_remove adj_step = baseline_remove + alpha * adj_diff gradients = self.compute_structure_gradients( adj_step, x, target_index, target_label) edge_gradients += -tf.gather_nd(gradients, edges) ###### Compute integrated gradients for adding edges ###### adj_diff = baseline_add - adj adj_step = baseline_add - alpha * adj_diff gradients = self.compute_structure_gradients( adj_step, x, target_index, target_label) non_edge_gradients += tf.gather_nd(gradients, non_edges) integrated_grads = np.zeros(edge_indicator.size) integrated_grads[edge_indicator] = edge_gradients.numpy() integrated_grads[~edge_indicator] = non_edge_gradients.numpy() return integrated_grads
def attack(self, target, num_budgets=None, reduce_nodes=3, direct_attack=True, structure_attack=True, feature_attack=False, compute_A_grad=True, disable=False): super().attack(target, num_budgets, direct_attack, structure_attack, feature_attack) logit = self.surrogate.predict(target).ravel() top2 = logit.argsort()[-2:] self.wrong_label = top2[-1] if top2[-1] != self.target_label else top2[ 0] assert self.wrong_label != self.target_label self.subgraph_preprocessing(reduce_nodes) offset = self.edge_lower_bound weights = self.weights with tf.device(self.device): for _ in tqdm(range(self.num_budgets), desc='Peturbing Graph', disable=disable): gradients = self.compute_gradient( compute_A_grad=compute_A_grad) gradients *= (-2. * weights) + 1. gradients = gradients[offset:] sorted_index = tf.argsort(gradients, direction='DESCENDING') for index in sorted_index: index_with_offset = index + offset u, v = self.indices[index_with_offset] if index_with_offset < self.non_edge_lower_bound and not self.allow_singleton and ( self.selfloop_degree[u] <= 2 or self.selfloop_degree[v] <= 2): continue if not self.is_modified(u, v): self.adj_flips[(u, v)] = index_with_offset self.update_subgraph(u, v, index_with_offset) break
def attack(self, target, n_perturbations=None, direct_attack=True, structure_attack=True, feature_attack=False, disable=False): super().attack(target, n_perturbations, direct_attack, structure_attack, feature_attack) if direct_attack and self.n_perturbations == self.degree[target]: warnings.warn( 'GradArgmax only work for removing edges, thus it will make the target node become a singleton for direct attack ' 'and `n_perturbations` equals the degree of target.`n_perturbations` is automatically set to `degree-1`.', RuntimeWarning) self.n_perturbations -= 1 target_index = astensor([target]) target_label = astensor(self.target_label) surrogate = self.surrogate for _ in tqdm(range(self.n_perturbations), desc='Peturbing Graph', disable=disable): adj = astensor(normalize_adj(self.modified_adj)) indices = adj.indices.numpy() gradients = self.compute_gradients(adj, target_index, target_label).numpy() gradients = np.minimum(gradients, 0.) sorted_index = np.argsort(gradients) for index in sorted_index: u, v = indices[index] if not self.allow_singleton and (self.modified_degree[u] <= 1 or self.modified_degree[v] <= 1): continue if not self.is_modified_edge(u, v): self.structure_flips[(u, v)] = index self.update_graph(u, v) break
def attack(self, target, n_perturbations=None, direct_attack=True, structure_attack=True, feature_attack=False, disable=False): super().attack(target, n_perturbations, direct_attack, structure_attack, feature_attack) if not direct_attack: raise NotImplementedError( f'{self.name} does NOT support indirect attack now.') target_index = astensor([target]) target_label = astensor(self.target_label) for _ in tqdm(range(self.n_perturbations), desc='Peturbing Graph', disable=disable): with tf.device(self.device): gradients = self.compute_gradients(self.modified_adj, self.adj_changes, target_index, target_label) modified_row = tf.gather(self.modified_adj, target_index) gradients = (gradients * (-2 * modified_row + 1)).numpy().ravel() sorted_index = np.argsort(-gradients) for index in sorted_index: u = target v = index % self.n_nodes has_edge = self.adj[u, v] if has_edge and not self.allow_singleton and ( self.modified_degree[u] <= 1 or self.modified_degree[v] <= 1): continue if not self.is_modified_edge(u, v): self.structure_flips[(u, v)] = index self.update_graph(u, v, has_edge) break
def attack(self, target, n_perturbations=None, threshold=0.5, direct_attack=True, structure_attack=True, feature_attack=False, disable=False): super().attack(target, n_perturbations, direct_attack, structure_attack, feature_attack) if direct_attack: influencer_nodes = [target] else: # influencer_nodes = list(self.graph.neighbors(target)) influencer_nodes = self.adj[target].indices.tolist() chosen = 0 structure_flips = self.structure_flips with tqdm(total=self.n_perturbations, desc='Peturbing Graph', disable=disable) as pbar: while chosen < self.n_perturbations: # randomly choose to add or remove edges if np.random.rand() <= threshold: delta = 1.0 edge = self.add_an_edge(influencer_nodes) # Different from RAND here # Edges are added within different classes if edge is not None and self.labels[edge[0]] == self.labels[edge[1]]: edge = None else: delta = -1.0 edge = self.del_an_edge(influencer_nodes) # Different from RAND here # Edges are removed within the same classes if edge is not None and self.labels[edge[0]] != self.labels[edge[1]]: edge = None if edge is not None: chosen += 1 structure_flips[edge] = chosen u, v = edge self.modified_degree[u] += delta self.modified_degree[v] += delta pbar.update(1)
def attack(self, index_attack, n_perturbations=0.05, structure_attack=True, feature_attack=False, disable=False): super().attack(n_perturbations, structure_attack, feature_attack) total_perturbations = self.n_perturbations attacker = self.attacker degree = self.degree[index_attack].astype(self.intx) if degree.sum() < total_perturbations: raise RuntimeError( 'The degree sum of attacked nodes is less than the number of perturbations! Please add more nodes to attack.' ) sorted_index = np.argsort(degree) index_attack = index_attack[sorted_index] degree = degree[sorted_index] flips = [] with tqdm(total=total_perturbations, desc='Peturbing Graph', disable=disable) as pbar: for deg, target in zip(degree, index_attack): single_node_perturbations = min(deg, total_perturbations) attacker.reset() attacker.attack(target, single_node_perturbations, disable=True) attacker.set_adj(attacker.A) flips.append(attacker.edge_flips) total_perturbations -= single_node_perturbations pbar.update(single_node_perturbations) if total_perturbations == 0: break self.structure_flips = np.vstack(flips)
def attack(self, target, n_perturbations=None, direct_attack=True, structure_attack=True, feature_attack=False, n_influencers=5, ll_constraint=True, ll_cutoff=0.004, disable=False): super().attack(target, n_perturbations, direct_attack, structure_attack, feature_attack) if feature_attack and not is_binary(self.x): raise RuntimeError( "Attacks on the node features are currently only supported for binary attributes." ) if ll_constraint and self.allow_singleton: raise RuntimeError( '`ll_constraint` is failed when `allow_singleton=True`, please set `attacker.allow_singleton=False`.' ) logits_start = self.compute_logits() best_wrong_class = self.strongest_wrong_class(logits_start) if structure_attack and ll_constraint: # Setup starting values of the likelihood ratio test. degree_sequence_start = self.degree current_degree_sequence = self.degree.astype('float64') d_min = 2 S_d_start = np.sum( np.log(degree_sequence_start[degree_sequence_start >= d_min])) current_S_d = np.sum( np.log( current_degree_sequence[current_degree_sequence >= d_min])) n_start = np.sum(degree_sequence_start >= d_min) current_n = np.sum(current_degree_sequence >= d_min) alpha_start = compute_alpha(n_start, S_d_start, d_min) log_likelihood_orig = compute_log_likelihood( n_start, alpha_start, S_d_start, d_min) if len(self.influence_nodes) == 0: if not direct_attack: # Choose influencer nodes infls, add_infls = self.get_attacker_nodes( n_influencers, add_additional_nodes=True) self.influence_nodes = np.concatenate( (infls, add_infls)).astype("int") # Potential edges are all edges from any attacker to any other node, except the respective # attacker itself or the node being attacked. self.potential_edges = np.row_stack([ np.column_stack( (np.tile(infl, self.n_nodes - 2), np.setdiff1d(np.arange(self.n_nodes), np.array([self.target, infl])))) for infl in self.influence_nodes ]) else: # direct attack influencers = [self.target] self.potential_edges = np.column_stack( (np.tile(self.target, self.n_nodes - 1), np.setdiff1d(np.arange(self.n_nodes), self.target))) self.influence_nodes = np.array(influencers) self.potential_edges = self.potential_edges.astype("int32") for _ in tqdm(range(self.n_perturbations), desc='Peturbing Graph', disable=disable): if structure_attack: # Do not consider edges that, if removed, result in singleton edges in the graph. if not self.allow_singleton: filtered_edges = filter_singletons(self.potential_edges, self.modified_adj) else: filtered_edges = self.potential_edges if ll_constraint: # Update the values for the power law likelihood ratio test. deltas = 2 * (1 - self.modified_adj[tuple( filtered_edges.T)].A.ravel()) - 1 d_edges_old = current_degree_sequence[filtered_edges] d_edges_new = current_degree_sequence[ filtered_edges] + deltas[:, None] new_S_d, new_n = update_Sx(current_S_d, current_n, d_edges_old, d_edges_new, d_min) new_alphas = compute_alpha(new_n, new_S_d, d_min) new_ll = compute_log_likelihood(new_n, new_alphas, new_S_d, d_min) alphas_combined = compute_alpha(new_n + n_start, new_S_d + S_d_start, d_min) new_ll_combined = compute_log_likelihood( new_n + n_start, alphas_combined, new_S_d + S_d_start, d_min) new_ratios = -2 * new_ll_combined + 2 * ( new_ll + log_likelihood_orig) # Do not consider edges that, if added/removed, would lead to a violation of the # likelihood ration Chi_square cutoff value. powerlaw_filter = filter_chisquare(new_ratios, ll_cutoff) filtered_edges = filtered_edges[powerlaw_filter] # Compute new entries in A_hat_square_uv a_hat_uv_new = self.compute_new_a_hat_uv(filtered_edges) # Compute the struct scores for each potential edge struct_scores = self.struct_score(a_hat_uv_new, self.compute_XW()) best_edge_ix = struct_scores.argmin() best_edge_score = struct_scores.min() best_edge = filtered_edges[best_edge_ix] if feature_attack: # Compute the feature scores for each potential feature perturbation feature_ixs, feature_scores = self.feature_scores() best_feature_ix = feature_ixs[0] best_feature_score = feature_scores[0] if structure_attack and feature_attack: # decide whether to choose an edge or feature to change if best_edge_score < best_feature_score: change_structure = True else: change_structure = False elif structure_attack: change_structure = True elif feature_attack: change_structure = False if change_structure: # perform edge perturbation u, v = best_edge modified_adj = self.modified_adj.tolil(copy=False) modified_adj[(u, v)] = modified_adj[( v, u)] = 1 - modified_adj[(u, v)] self.modified_adj = modified_adj.tocsr(copy=False) self.adj_norm = normalize_adj(modified_adj) self.structure_flips.append((u, v)) if ll_constraint: # Update likelihood ratio test values current_S_d = new_S_d[powerlaw_filter][best_edge_ix] current_n = new_n[powerlaw_filter][best_edge_ix] current_degree_sequence[best_edge] += deltas[ powerlaw_filter][best_edge_ix] else: modified_x = self.modified_x.tolil(copy=False) modified_x[tuple( best_feature_ix)] = 1 - modified_x[tuple(best_feature_ix)] self.modified_x = modified_x.tocsr(copy=False) self.feature_flips.append(tuple(best_feature_ix))
def attack(self, target, n_perturbations=None, symmetric=True, direct_attack=True, structure_attack=True, feature_attack=False, disable=False): super().attack(target, n_perturbations, direct_attack, structure_attack, feature_attack) if feature_attack and not is_binary(self.x): raise RuntimeError( "Attacks on the node features are currently only supported for binary attributes." ) with tf.device(self.device): target_index = astensor([self.target]) target_labels = astensor(self.target_label) modified_adj, modified_x = self.modified_adj, self.modified_x if not direct_attack: adj_mask, x_mask = self.construct_mask() else: adj_mask, x_mask = None, None for _ in tqdm(range(self.n_perturbations), desc='Peturbing Graph', disable=disable): adj_grad, x_grad = self.compute_gradients( modified_adj, modified_x, target_index, target_labels) adj_grad_score = tf.constant(0.0) x_grad_score = tf.constant(0.0) if structure_attack: if symmetric: adj_grad = (adj_grad + tf.transpose(adj_grad)) / 2. adj_grad_score = self.structure_score( modified_adj, adj_grad, adj_mask) if feature_attack: x_grad_score = self.feature_score(modified_x, x_grad, x_mask) if tf.reduce_max(adj_grad_score) >= tf.reduce_max( x_grad_score): adj_grad_argmax = tf.argmax(adj_grad_score) row, col = divmod(adj_grad_argmax.numpy(), self.n_nodes) modified_adj[row, col].assign(1. - modified_adj[row, col]) modified_adj[col, row].assign(1. - modified_adj[col, row]) self.structure_flips.append((row, col)) else: x_grad_argmax = tf.argmax(x_grad_score) row, col = divmod(x_grad_argmax.numpy(), self.n_attrs) modified_x[row, col].assign(1. - modified_x[row, col]) self.feature_flips.append((row, col))
def attack(self, target, n_perturbations=None, direct_attack=True, structure_attack=True, feature_attack=False, ll_constraint=False, ll_cutoff=0.004, disable=False): super().attack(target, n_perturbations, direct_attack, structure_attack, feature_attack) # Setup starting values of the likelihood ratio test. degree_sequence_start = self.degree current_degree_sequence = self.degree.astype('float64') d_min = 2 # denotes the minimum degree a node needs to have to be considered in the power-law test S_d_start = np.sum(np.log(degree_sequence_start[degree_sequence_start >= d_min])) current_S_d = np.sum(np.log(current_degree_sequence[current_degree_sequence >= d_min])) n_start = np.sum(degree_sequence_start >= d_min) current_n = np.sum(current_degree_sequence >= d_min) alpha_start = compute_alpha(n_start, S_d_start, d_min) log_likelihood_orig = compute_log_likelihood(n_start, alpha_start, S_d_start, d_min) N = self.n_nodes if not direct_attack: # Choose influencer nodes # influencer_nodes = self.adj[target].nonzero()[1] influencer_nodes = self.adj[target].indices # Potential edges are all edges from any attacker to any other node, except the respective # attacker itself or the node being attacked. potential_edges = np.row_stack([np.column_stack((np.tile(infl, N - 2), np.setdiff1d(np.arange(N), np.array([target, infl])))) for infl in influencer_nodes]) else: # direct attack potential_edges = np.column_stack((np.tile(target, N-1), np.setdiff1d(np.arange(N), target))) influencer_nodes = np.asarray([target]) for _ in tqdm(range(self.n_perturbations), desc='Peturbing Graph', disable=disable): if not self.allow_singleton: filtered_edges = filter_singletons(potential_edges, self.modified_adj) else: filtered_edges = potential_edges if ll_constraint: # Update the values for the power law likelihood ratio test. deltas = 2 * (1 - self.modified_adj[tuple(filtered_edges.T)].toarray()[0]) - 1 d_edges_old = current_degree_sequence[filtered_edges] d_edges_new = current_degree_sequence[filtered_edges] + deltas[:, None] new_S_d, new_n = update_Sx(current_S_d, current_n, d_edges_old, d_edges_new, d_min) new_alphas = compute_alpha(new_n, new_S_d, d_min) new_ll = compute_log_likelihood(new_n, new_alphas, new_S_d, d_min) alphas_combined = compute_alpha(new_n + n_start, new_S_d + S_d_start, d_min) new_ll_combined = compute_log_likelihood(new_n + n_start, alphas_combined, new_S_d + S_d_start, d_min) new_ratios = -2 * new_ll_combined + 2 * (new_ll + log_likelihood_orig) # Do not consider edges that, if added/removed, would lead to a violation of the # likelihood ration Chi_square cutoff value. powerlaw_filter = filter_chisquare(new_ratios, ll_cutoff) filtered_edges = filtered_edges[powerlaw_filter] struct_scores = self.struct_score(self.modified_adj, self.X_mean, self.eig_vals, self.eig_vec, filtered_edges, K=self.K, T=self.T, lambda_method="nosum") best_edge_ix = struct_scores.argmax() u, v = filtered_edges[best_edge_ix] # best edge while (u, v) in self.structure_flips: struct_scores[best_edge_ix] = 0 best_edge_ix = struct_scores.argmax() u, v = filtered_edges[best_edge_ix] self.modified_adj[(u, v)] = self.modified_adj[(v, u)] = 1. - self.modified_adj[(u, v)] self.structure_flips[(u, v)] = 1.0 if ll_constraint: # Update likelihood ratio test values current_S_d = new_S_d[powerlaw_filter][best_edge_ix] current_n = new_n[powerlaw_filter][best_edge_ix] current_degree_sequence[[u, v]] += deltas[powerlaw_filter][best_edge_ix]