|
|
""" |
|
|
Code in this file is modified from https://github.com/abojchevski/node_embedding_attack |
|
|
|
|
|
'Adversarial Attacks on Node Embeddings via Graph Poisoning' |
|
|
Aleksandar Bojchevski and Stephan Günnemann, ICML 2019 |
|
|
http://proceedings.mlr.press/v97/bojchevski19a.html |
|
|
Copyright (C) owned by the authors, 2019 |
|
|
""" |
|
|
|
|
|
import numba |
|
|
import numpy as np |
|
|
import scipy.sparse as sp |
|
|
import scipy.linalg as spl |
|
|
import torch |
|
|
import networkx as nx |
|
|
from deeprobust.graph.global_attack import BaseAttack |
|
|
|
|
|
|
|
|
class NodeEmbeddingAttack(BaseAttack): |
|
|
"""Node embedding attack. Adversarial Attacks on Node Embeddings via Graph |
|
|
Poisoning. Aleksandar Bojchevski and Stephan Günnemann, ICML 2019 |
|
|
http://proceedings.mlr.press/v97/bojchevski19a.html |
|
|
|
|
|
Examples |
|
|
----- |
|
|
>>> from deeprobust.graph.data import Dataset |
|
|
>>> from deeprobust.graph.global_attack import NodeEmbeddingAttack |
|
|
>>> data = Dataset(root='/tmp/', name='cora_ml', seed=15) |
|
|
>>> adj, features, labels = data.adj, data.features, data.labels |
|
|
>>> model = NodeEmbeddingAttack() |
|
|
>>> model.attack(adj, attack_type="remove") |
|
|
>>> modified_adj = model.modified_adj |
|
|
>>> model.attack(adj, attack_type="remove", min_span_tree=True) |
|
|
>>> modified_adj = model.modified_adj |
|
|
>>> model.attack(adj, attack_type="add", n_candidates=10000) |
|
|
>>> modified_adj = model.modified_adj |
|
|
>>> model.attack(adj, attack_type="add_by_remove", n_candidates=10000) |
|
|
>>> modified_adj = model.modified_adj |
|
|
""" |
|
|
|
|
|
def __init__(self): |
|
|
pass |
|
|
|
|
|
def attack(self, adj, n_perturbations=1000, dim=32, window_size=5, |
|
|
attack_type="remove", min_span_tree=False, n_candidates=None, seed=None, **kwargs): |
|
|
"""Selects the top (n_perturbations) number of flips using our perturbation attack. |
|
|
|
|
|
:param adj: sp.spmatrix |
|
|
The graph represented as a sparse scipy matrix |
|
|
:param n_perturbations: int |
|
|
Number of flips to select |
|
|
:param dim: int |
|
|
Dimensionality of the embeddings. |
|
|
:param window_size: int |
|
|
Co-occurence window size. |
|
|
:param attack_type: str |
|
|
can be chosed from ["remove", "add", "add_by_remove"] |
|
|
:param min_span_tree: bool |
|
|
Whether to disallow edges that lie on the minimum spanning tree; |
|
|
only valid when `attack_type` is "remove" |
|
|
:param n_candidates: int |
|
|
Number of candiates for addition; only valid when `attack_type` is "add" or "add_by_remove"; |
|
|
:param seed: int |
|
|
Random seed |
|
|
""" |
|
|
assert attack_type in ["remove", "add", "add_by_remove"], \ |
|
|
"attack_type can only be `remove` or `add`" |
|
|
|
|
|
if attack_type == "remove": |
|
|
if min_span_tree: |
|
|
candidates = self.generate_candidates_removal_minimum_spanning_tree(adj) |
|
|
else: |
|
|
candidates = self.generate_candidates_removal(adj, seed) |
|
|
|
|
|
elif attack_type == "add" or attack_type == "add_by_remove": |
|
|
|
|
|
assert n_candidates, "please specify the value of `n_candidates`, " \ |
|
|
+ "i.e. how many candiate you want to genereate for addition" |
|
|
candidates = self.generate_candidates_addition(adj, n_candidates, seed) |
|
|
|
|
|
|
|
|
n_nodes = adj.shape[0] |
|
|
|
|
|
if attack_type == "add_by_remove": |
|
|
candidates_add = candidates |
|
|
adj_add = self.flip_candidates(adj, candidates_add) |
|
|
vals_org_add, vecs_org_add = spl.eigh(adj_add.toarray(), np.diag(adj_add.sum(1).A1)) |
|
|
flip_indicator = 1 - 2 * adj_add[candidates[:, 0], candidates[:, 1]].A1 |
|
|
|
|
|
loss_est = estimate_loss_with_delta_eigenvals(candidates_add, flip_indicator, |
|
|
vals_org_add, vecs_org_add, n_nodes, dim, window_size) |
|
|
|
|
|
loss_argsort = loss_est.argsort() |
|
|
top_flips = candidates_add[loss_argsort[:n_perturbations]] |
|
|
|
|
|
else: |
|
|
|
|
|
delta_w = 1 - 2 * adj[candidates[:, 0], candidates[:, 1]].A1 |
|
|
|
|
|
|
|
|
deg_matrix = np.diag(adj.sum(1).A1) |
|
|
vals_org, vecs_org = spl.eigh(adj.toarray(), deg_matrix) |
|
|
|
|
|
loss_for_candidates = estimate_loss_with_delta_eigenvals(candidates, delta_w, vals_org, vecs_org, n_nodes, dim, window_size) |
|
|
top_flips = candidates[loss_for_candidates.argsort()[-n_perturbations:]] |
|
|
|
|
|
assert len(top_flips) == n_perturbations |
|
|
|
|
|
modified_adj = self.flip_candidates(adj, top_flips) |
|
|
self.check_adj(modified_adj) |
|
|
self.modified_adj = modified_adj |
|
|
|
|
|
def generate_candidates_removal(self, adj, seed=None): |
|
|
"""Generates candidate edge flips for removal (edge -> non-edge), |
|
|
disallowing one random edge per node to prevent singleton nodes. |
|
|
|
|
|
:param adj: sp.csr_matrix, shape [n_nodes, n_nodes] |
|
|
Adjacency matrix of the graph |
|
|
:param seed: int |
|
|
Random seed |
|
|
:return: np.ndarray, shape [?, 2] |
|
|
Candidate set of edge flips |
|
|
""" |
|
|
n_nodes = adj.shape[0] |
|
|
if seed is not None: |
|
|
np.random.seed(seed) |
|
|
deg = np.where(adj.sum(1).A1 == 1)[0] |
|
|
hiddeen = np.column_stack( |
|
|
(np.arange(n_nodes), np.fromiter(map(np.random.choice, adj.tolil().rows), dtype=np.int32))) |
|
|
|
|
|
adj_hidden = edges_to_sparse(hiddeen, adj.shape[0]) |
|
|
adj_hidden = adj_hidden.maximum(adj_hidden.T) |
|
|
|
|
|
adj_keep = adj - adj_hidden |
|
|
|
|
|
candidates = np.column_stack((sp.triu(adj_keep).nonzero())) |
|
|
|
|
|
candidates = candidates[np.logical_not(np.in1d(candidates[:, 0], deg) | np.in1d(candidates[:, 1], deg))] |
|
|
|
|
|
return candidates |
|
|
|
|
|
def generate_candidates_removal_minimum_spanning_tree(self, adj): |
|
|
"""Generates candidate edge flips for removal (edge -> non-edge), |
|
|
disallowing edges that lie on the minimum spanning tree. |
|
|
adj: sp.csr_matrix, shape [n_nodes, n_nodes] |
|
|
Adjacency matrix of the graph |
|
|
:return: np.ndarray, shape [?, 2] |
|
|
Candidate set of edge flips |
|
|
""" |
|
|
mst = sp.csgraph.minimum_spanning_tree(adj) |
|
|
mst = mst.maximum(mst.T) |
|
|
adj_sample = adj - mst |
|
|
candidates = np.column_stack(sp.triu(adj_sample, 1).nonzero()) |
|
|
|
|
|
return candidates |
|
|
|
|
|
def generate_candidates_addition(self, adj, n_candidates, seed=None): |
|
|
"""Generates candidate edge flips for addition (non-edge -> edge). |
|
|
|
|
|
:param adj: sp.csr_matrix, shape [n_nodes, n_nodes] |
|
|
Adjacency matrix of the graph |
|
|
:param n_candidates: int |
|
|
Number of candidates to generate. |
|
|
:param seed: int |
|
|
Random seed |
|
|
:return: np.ndarray, shape [?, 2] |
|
|
Candidate set of edge flips |
|
|
""" |
|
|
if seed is not None: |
|
|
np.random.seed(seed) |
|
|
|
|
|
num_nodes = adj.shape[0] |
|
|
|
|
|
candidates = np.random.randint(0, num_nodes, [n_candidates * 5, 2]) |
|
|
candidates = candidates[candidates[:, 0] < candidates[:, 1]] |
|
|
candidates = candidates[adj[candidates[:, 0], candidates[:, 1]].A1 == 0] |
|
|
candidates = np.array(list(set(map(tuple, candidates)))) |
|
|
candidates = candidates[:n_candidates] |
|
|
|
|
|
assert len(candidates) == n_candidates |
|
|
|
|
|
return candidates |
|
|
|
|
|
def flip_candidates(self, adj, candidates): |
|
|
"""Flip the edges in the candidate set to non-edges and vise-versa. |
|
|
|
|
|
:param adj: sp.csr_matrix, shape [n_nodes, n_nodes] |
|
|
Adjacency matrix of the graph |
|
|
:param candidates: np.ndarray, shape [?, 2] |
|
|
Candidate set of edge flips |
|
|
:return: sp.csr_matrix, shape [n_nodes, n_nodes] |
|
|
Adjacency matrix of the graph with the flipped edges/non-edges. |
|
|
""" |
|
|
adj_flipped = adj.copy().tolil() |
|
|
adj_flipped[candidates[:, 0], candidates[:, 1]] = 1 - adj[candidates[:, 0], candidates[:, 1]] |
|
|
adj_flipped[candidates[:, 1], candidates[:, 0]] = 1 - adj[candidates[:, 1], candidates[:, 0]] |
|
|
adj_flipped = adj_flipped.tocsr() |
|
|
adj_flipped.eliminate_zeros() |
|
|
|
|
|
return adj_flipped |
|
|
|
|
|
|
|
|
@numba.jit(nopython=True) |
|
|
def estimate_loss_with_delta_eigenvals(candidates, flip_indicator, vals_org, vecs_org, n_nodes, dim, window_size): |
|
|
"""Computes the estimated loss using the change in the eigenvalues for every candidate edge flip. |
|
|
|
|
|
:param candidates: np.ndarray, shape [?, 2] |
|
|
Candidate set of edge flips, |
|
|
:param flip_indicator: np.ndarray, shape [?] |
|
|
Vector indicating whether we are adding an edge (+1) or removing an edge (-1) |
|
|
:param vals_org: np.ndarray, shape [n] |
|
|
The generalized eigenvalues of the clean graph |
|
|
:param vecs_org: np.ndarray, shape [n, n] |
|
|
The generalized eigenvectors of the clean graph |
|
|
:param n_nodes: int |
|
|
Number of nodes |
|
|
:param dim: int |
|
|
Embedding dimension |
|
|
:param window_size: int |
|
|
Size of the window |
|
|
:return: np.ndarray, shape [?] |
|
|
Estimated loss for each candidate flip |
|
|
""" |
|
|
|
|
|
loss_est = np.zeros(len(candidates)) |
|
|
for x in range(len(candidates)): |
|
|
i, j = candidates[x] |
|
|
vals_est = vals_org + flip_indicator[x] * ( |
|
|
2 * vecs_org[i] * vecs_org[j] - vals_org * (vecs_org[i] ** 2 + vecs_org[j] ** 2)) |
|
|
|
|
|
vals_sum_powers = sum_of_powers(vals_est, window_size) |
|
|
|
|
|
loss_ij = np.sqrt(np.sum(np.sort(vals_sum_powers ** 2)[:n_nodes - dim])) |
|
|
loss_est[x] = loss_ij |
|
|
|
|
|
return loss_est |
|
|
|
|
|
|
|
|
@numba.jit(nopython=True) |
|
|
def estimate_delta_eigenvecs(candidates, flip_indicator, degrees, vals_org, vecs_org, delta_eigvals, pinvs): |
|
|
"""Computes the estimated change in the eigenvectors for every candidate edge flip. |
|
|
|
|
|
:param candidates: np.ndarray, shape [?, 2] |
|
|
Candidate set of edge flips, |
|
|
:param flip_indicator: np.ndarray, shape [?] |
|
|
Vector indicating whether we are adding an edge (+1) or removing an edge (-1) |
|
|
:param degrees: np.ndarray, shape [n] |
|
|
Vector of node degrees. |
|
|
:param vals_org: np.ndarray, shape [n] |
|
|
The generalized eigenvalues of the clean graph |
|
|
:param vecs_org: np.ndarray, shape [n, n] |
|
|
The generalized eigenvectors of the clean graph |
|
|
:param delta_eigvals: np.ndarray, shape [?, n] |
|
|
Estimated change in the eigenvalues for all candidate edge flips |
|
|
:param pinvs: np.ndarray, shape [k, n, n] |
|
|
Precomputed pseudo-inverse matrices for every dimension |
|
|
:return: np.ndarray, shape [?, n, k] |
|
|
Estimated change in the eigenvectors for all candidate edge flips |
|
|
""" |
|
|
n_nodes, dim = vecs_org.shape |
|
|
n_candidates = len(candidates) |
|
|
delta_eigvecs = np.zeros((n_candidates, dim, n_nodes)) |
|
|
|
|
|
for k in range(dim): |
|
|
cur_eigvecs = vecs_org[:, k] |
|
|
cur_eigvals = vals_org[k] |
|
|
for c in range(n_candidates): |
|
|
degree_eigvec = (-delta_eigvals[c, k] * degrees) * cur_eigvecs |
|
|
i, j = candidates[c] |
|
|
|
|
|
degree_eigvec[i] += cur_eigvecs[j] - cur_eigvals * cur_eigvecs[i] |
|
|
degree_eigvec[j] += cur_eigvecs[i] - cur_eigvals * cur_eigvecs[j] |
|
|
|
|
|
delta_eigvecs[c, k] = np.dot(pinvs[k], flip_indicator[c] * degree_eigvec) |
|
|
|
|
|
return delta_eigvecs |
|
|
|
|
|
|
|
|
def estimate_delta_eigvals(candidates, adj, vals_org, vecs_org): |
|
|
"""Computes the estimated change in the eigenvalues for every candidate edge flip. |
|
|
|
|
|
:param candidates: np.ndarray, shape [?, 2] |
|
|
Candidate set of edge flips |
|
|
:param adj: sp.spmatrix |
|
|
The graph represented as a sparse scipy matrix |
|
|
:param vals_org: np.ndarray, shape [n] |
|
|
The generalized eigenvalues of the clean graph |
|
|
:param vecs_org: np.ndarray, shape [n, n] |
|
|
The generalized eigenvectors of the clean graph |
|
|
:return: np.ndarray, shape [?, n] |
|
|
Estimated change in the eigenvalues for all candidate edge flips |
|
|
""" |
|
|
|
|
|
delta_w = 1 - 2 * adj[candidates[:, 0], candidates[:, 1]].A1 |
|
|
|
|
|
delta_eigvals = delta_w[:, None] * (2 * vecs_org[candidates[:, 0]] * vecs_org[candidates[:, 1]] |
|
|
- vals_org * ( |
|
|
vecs_org[candidates[:, 0]] ** 2 + vecs_org[candidates[:, 1]] ** 2)) |
|
|
|
|
|
return delta_eigvals |
|
|
|
|
|
|
|
|
class OtherNodeEmbeddingAttack(NodeEmbeddingAttack): |
|
|
""" Baseline methods from the paper Adversarial Attacks on Node Embeddings |
|
|
via Graph Poisoning. Aleksandar Bojchevski and Stephan Günnemann, ICML 2019. |
|
|
http://proceedings.mlr.press/v97/bojchevski19a.html |
|
|
|
|
|
Examples |
|
|
----- |
|
|
>>> from deeprobust.graph.data import Dataset |
|
|
>>> from deeprobust.graph.global_attack import OtherNodeEmbeddingAttack |
|
|
>>> data = Dataset(root='/tmp/', name='cora_ml', seed=15) |
|
|
>>> adj, features, labels = data.adj, data.features, data.labels |
|
|
>>> model = OtherNodeEmbeddingAttack(type='degree') |
|
|
>>> model.attack(adj, attack_type="remove") |
|
|
>>> modified_adj = model.modified_adj |
|
|
>>> # |
|
|
>>> model = OtherNodeEmbeddingAttack(type='eigencentrality') |
|
|
>>> model.attack(adj, attack_type="remove") |
|
|
>>> modified_adj = model.modified_adj |
|
|
>>> # |
|
|
>>> model = OtherNodeEmbeddingAttack(type='random') |
|
|
>>> model.attack(adj, attack_type="add", n_candidates=10000) |
|
|
>>> modified_adj = model.modified_adj |
|
|
""" |
|
|
|
|
|
def __init__(self, type): |
|
|
assert type in ["degree", "eigencentrality", "random"] |
|
|
self.type = type |
|
|
|
|
|
def attack(self, adj, n_perturbations=1000, attack_type="remove", |
|
|
min_span_tree=False, n_candidates=None, seed=None, **kwargs): |
|
|
"""Selects the top (n_perturbations) number of flips using our perturbation attack. |
|
|
|
|
|
:param adj: sp.spmatrix |
|
|
The graph represented as a sparse scipy matrix |
|
|
:param n_perturbations: int |
|
|
Number of flips to select |
|
|
:param dim: int |
|
|
Dimensionality of the embeddings. |
|
|
:param attack_type: str |
|
|
can be chosed from ["remove", "add"] |
|
|
:param min_span_tree: bool |
|
|
Whether to disallow edges that lie on the minimum spanning tree; |
|
|
only valid when `attack_type` is "remove" |
|
|
:param n_candidates: int |
|
|
Number of candiates for addition; only valid when `attack_type` is "add"; |
|
|
:param seed: int |
|
|
Random seed; |
|
|
:return: np.ndarray, shape [?, 2] |
|
|
The top edge flips from the candidate set |
|
|
""" |
|
|
assert attack_type in ["remove", "add"], \ |
|
|
"attack_type can only be `remove` or `add`" |
|
|
|
|
|
if attack_type == "remove": |
|
|
if min_span_tree: |
|
|
candidates = self.generate_candidates_removal_minimum_spanning_tree(adj) |
|
|
else: |
|
|
candidates = self.generate_candidates_removal(adj, seed) |
|
|
elif attack_type == "add": |
|
|
assert n_candidates, "please specify the value of `n_candidates`, " \ |
|
|
+ "i.e. how many candiate you want to genereate for addition" |
|
|
candidates = self.generate_candidates_addition(adj, n_candidates, seed) |
|
|
else: |
|
|
raise NotImplementedError |
|
|
|
|
|
if self.type == "random": |
|
|
top_flips = self.random_top_flips(candidates, n_perturbations, seed) |
|
|
elif self.type == "eigencentrality": |
|
|
top_flips = self.eigencentrality_top_flips(adj, candidates, n_perturbations) |
|
|
elif self.type == "degree": |
|
|
top_flips = self.degree_top_flips(adj, candidates, n_perturbations, complement=False) |
|
|
else: |
|
|
raise NotImplementedError |
|
|
|
|
|
assert len(top_flips) == n_perturbations |
|
|
modified_adj = self.flip_candidates(adj, top_flips) |
|
|
self.check_adj(modified_adj) |
|
|
self.modified_adj = modified_adj |
|
|
|
|
|
def random_top_flips(self, candidates, n_perturbations, seed=None): |
|
|
"""Selects (n_perturbations) number of flips at random. |
|
|
|
|
|
:param candidates: np.ndarray, shape [?, 2] |
|
|
Candidate set of edge flips |
|
|
:param n_perturbations: int |
|
|
Number of flips to select |
|
|
:param seed: int |
|
|
Random seed |
|
|
:return: np.ndarray, shape [?, 2] |
|
|
The top edge flips from the candidate set |
|
|
""" |
|
|
if seed is not None: |
|
|
np.random.seed(seed) |
|
|
return candidates[np.random.permutation(len(candidates))[:n_perturbations]] |
|
|
|
|
|
|
|
|
def eigencentrality_top_flips(self, adj, candidates, n_perturbations): |
|
|
"""Selects the top (n_perturbations) number of flips using eigencentrality score of the edges. |
|
|
Applicable only when removing edges. |
|
|
|
|
|
:param adj: sp.spmatrix |
|
|
The graph represented as a sparse scipy matrix |
|
|
:param candidates: np.ndarray, shape [?, 2] |
|
|
Candidate set of edge flips |
|
|
:param n_perturbations: int |
|
|
Number of flips to select |
|
|
:return: np.ndarray, shape [?, 2] |
|
|
The top edge flips from the candidate set |
|
|
""" |
|
|
edges = np.column_stack(sp.triu(adj, 1).nonzero()) |
|
|
line_graph = construct_line_graph(adj) |
|
|
eigcentrality_scores = nx.eigenvector_centrality_numpy(nx.Graph(line_graph)) |
|
|
eigcentrality_scores = {tuple(edges[k]): eigcentrality_scores[k] for k, v in eigcentrality_scores.items()} |
|
|
eigcentrality_scores = np.array([eigcentrality_scores[tuple(cnd)] for cnd in candidates]) |
|
|
scores_argsrt = eigcentrality_scores.argsort() |
|
|
return candidates[scores_argsrt[-n_perturbations:]] |
|
|
|
|
|
|
|
|
def degree_top_flips(self, adj, candidates, n_perturbations, complement): |
|
|
"""Selects the top (n_perturbations) number of flips using degree centrality score of the edges. |
|
|
|
|
|
:param adj: sp.spmatrix |
|
|
The graph represented as a sparse scipy matrix |
|
|
:param candidates: np.ndarray, shape [?, 2] |
|
|
Candidate set of edge flips |
|
|
:param n_perturbations: int |
|
|
Number of flips to select |
|
|
:param complement: bool |
|
|
Whether to look at the complement graph |
|
|
:return: np.ndarray, shape [?, 2] |
|
|
The top edge flips from the candidate set |
|
|
""" |
|
|
if complement: |
|
|
adj = sp.csr_matrix(1-adj.toarray()) |
|
|
deg = adj.sum(1).A1 |
|
|
deg_argsort = (deg[candidates[:, 0]] + deg[candidates[:, 1]]).argsort() |
|
|
|
|
|
return candidates[deg_argsort[-n_perturbations:]] |
|
|
|
|
|
|
|
|
@numba.jit(nopython=True) |
|
|
def sum_of_powers(x, power): |
|
|
"""For each x_i, computes \sum_{r=1}^{pow) x_i^r (elementwise sum of powers). |
|
|
|
|
|
:param x: shape [?] |
|
|
Any vector |
|
|
:param pow: int |
|
|
The largest power to consider |
|
|
:return: shape [?] |
|
|
Vector where each element is the sum of powers from 1 to pow. |
|
|
""" |
|
|
n = x.shape[0] |
|
|
sum_powers = np.zeros((power, n)) |
|
|
|
|
|
for i, i_power in enumerate(range(1, power + 1)): |
|
|
sum_powers[i] = np.power(x, i_power) |
|
|
|
|
|
return sum_powers.sum(0) |
|
|
|
|
|
|
|
|
def edges_to_sparse(edges, num_nodes, weights=None): |
|
|
if weights is None: |
|
|
weights = np.ones(edges.shape[0]) |
|
|
|
|
|
return sp.coo_matrix((weights, (edges[:, 0], edges[:, 1])), shape=(num_nodes, num_nodes)).tocsr() |
|
|
|
|
|
def construct_line_graph(adj): |
|
|
"""Construct a line graph from an undirected original graph. |
|
|
|
|
|
Parameters |
|
|
---------- |
|
|
adj : sp.spmatrix [n_samples ,n_samples] |
|
|
Symmetric binary adjacency matrix. |
|
|
Returns |
|
|
------- |
|
|
L : sp.spmatrix, shape [A.nnz/2, A.nnz/2] |
|
|
Symmetric binary adjacency matrix of the line graph. |
|
|
""" |
|
|
N = adj.shape[0] |
|
|
edges = np.column_stack(sp.triu(adj, 1).nonzero()) |
|
|
e1, e2 = edges[:, 0], edges[:, 1] |
|
|
|
|
|
I = sp.eye(N).tocsr() |
|
|
E1 = I[e1] |
|
|
E2 = I[e2] |
|
|
|
|
|
L = E1.dot(E1.T) + E1.dot(E2.T) + E2.dot(E1.T) + E2.dot(E2.T) |
|
|
|
|
|
return L - 2 * sp.eye(L.shape[0]) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
from deeprobust.graph.data import Dataset |
|
|
from deeprobust.graph.defense import DeepWalk |
|
|
import itertools |
|
|
|
|
|
dataset_str = 'cora_ml' |
|
|
data = Dataset(root='/tmp/', name=dataset_str, seed=15) |
|
|
adj, features, labels = data.adj, data.features, data.labels |
|
|
idx_train, idx_val, idx_test = data.idx_train, data.idx_val, data.idx_test |
|
|
|
|
|
comb = itertools.product(["random", "degree", "eigencentrality"], ["remove", "add"]) |
|
|
for type, attack_type in comb: |
|
|
model = OtherNodeEmbeddingAttack(type=type) |
|
|
print(model.type, attack_type) |
|
|
try: |
|
|
model.attack(adj, attack_type=attack_type, n_candidates=10000) |
|
|
defender = DeepWalk() |
|
|
defender.fit(adj) |
|
|
defender.evaluate_node_classification(labels, idx_train, idx_test) |
|
|
except KeyError: |
|
|
print('eigencentrality only supports removing edges') |
|
|
|
|
|
model = NodeEmbeddingAttack() |
|
|
model.attack(adj, attack_type="remove") |
|
|
model.attack(adj, attack_type="remove", min_span_tree=True) |
|
|
modified_adj = model.modified_adj |
|
|
model.attack(adj, attack_type="add", n_candidates=10000) |
|
|
model.attack(adj, attack_type="add_by_remove", n_candidates=10000) |
|
|
|
|
|
|