/
pgd.py
230 lines (195 loc) · 8.51 KB
/
pgd.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
import random
import numpy as np
import scipy.sparse as sp
import torch
import torch.nn.functional as F
from .base import InjectionAttack, EarlyStop
from ..evaluator import metric
from ..utils import utils
class PGD(InjectionAttack):
r"""
Description
-----------
Graph injection attack version of Projected Gradient Descent attack (`PGD <https://arxiv.org/abs/1706.06083>`__).
Parameters
----------
epsilon : float
Perturbation level on features.
n_epoch : int
Epoch of perturbations.
n_inject_max : int
Maximum number of injected nodes.
n_edge_max : int
Maximum number of edges of injected nodes.
feat_lim_min : float
Minimum limit of features.
feat_lim_max : float
Maximum limit of features.
loss : func of torch.nn.functional, optional
Loss function compatible with ``torch.nn.functional``. Default: ``F.nll_loss``.
eval_metric : func of grb.evaluator.metric, optional
Evaluation metric. Default: ``metric.eval_acc``.
device : str, optional
Device used to host data. Default: ``cpu``.
early_stop : bool, optional
Whether to early stop. Default: ``False``.
verbose : bool, optional
Whether to display logs. Default: ``True``.
"""
def __init__(self,
epsilon,
n_epoch,
n_inject_max,
n_edge_max,
feat_lim_min,
feat_lim_max,
loss=F.nll_loss,
eval_metric=metric.eval_acc,
device='cpu',
early_stop=False,
verbose=True):
self.device = device
self.epsilon = epsilon
self.n_epoch = n_epoch
self.n_inject_max = n_inject_max
self.n_edge_max = n_edge_max
self.feat_lim_min = feat_lim_min
self.feat_lim_max = feat_lim_max
self.loss = loss
self.eval_metric = eval_metric
self.verbose = verbose
# Early stop
if early_stop:
self.early_stop = EarlyStop(patience=1000, epsilon=1e-4)
else:
self.early_stop = early_stop
def attack(self, model, adj, features, target_mask, adj_norm_func):
model.to(self.device)
n_total, n_feat = features.shape
features = utils.feat_preprocess(features=features, device=self.device)
adj_tensor = utils.adj_preprocess(adj=adj,
adj_norm_func=adj_norm_func,
device=self.device)
pred_orig = model(features, adj_tensor)
origin_labels = torch.argmax(pred_orig, dim=1)
adj_attack = self.injection(adj=adj,
n_inject=self.n_inject_max,
n_node=n_total,
target_mask=target_mask)
# Random initialization
features_attack = np.random.normal(loc=0, scale=self.feat_lim_max / 10,
size=(self.n_inject_max, n_feat))
features_attack = self.update_features(model=model,
adj_attack=adj_attack,
features=features,
features_attack=features_attack,
origin_labels=origin_labels,
target_mask=target_mask,
adj_norm_func=adj_norm_func)
return adj_attack, features_attack
def injection(self, adj, n_inject, n_node, target_mask):
r"""
Description
-----------
Randomly inject nodes to target nodes.
Parameters
----------
adj : scipy.sparse.csr.csr_matrix
Adjacency matrix in form of ``N * N`` sparse matrix.
n_inject : int
Number of injection.
n_node : int
Number of all nodes.
target_mask : torch.Tensor
Mask of attack target nodes in form of ``N * 1`` torch bool tensor.
Returns
-------
adj_attack : scipy.sparse.csr.csr_matrix
Adversarial adjacency matrix in form of :math:`(N + N_{inject})\times(N + N_{inject})` sparse matrix.
"""
test_index = torch.where(target_mask)[0]
n_test = test_index.shape[0]
new_edges_x = []
new_edges_y = []
new_data = []
for i in range(n_inject):
islinked = np.zeros(n_test)
for j in range(self.n_edge_max):
x = i + n_node
yy = random.randint(0, n_test - 1)
while islinked[yy] > 0:
yy = random.randint(0, n_test - 1)
y = test_index[yy]
new_edges_x.extend([x, y])
new_edges_y.extend([y, x])
new_data.extend([1, 1])
add1 = sp.csr_matrix((n_inject, n_node))
add2 = sp.csr_matrix((n_node + n_inject, n_inject))
adj_attack = sp.vstack([adj, add1])
adj_attack = sp.hstack([adj_attack, add2])
adj_attack.row = np.hstack([adj_attack.row, new_edges_x])
adj_attack.col = np.hstack([adj_attack.col, new_edges_y])
adj_attack.data = np.hstack([adj_attack.data, new_data])
return adj_attack
def update_features(self, model, adj_attack, features, features_attack, origin_labels, target_mask, adj_norm_func):
r"""
Description
-----------
Update features of injected nodes.
Parameters
----------
model : torch.nn.module
Model implemented based on ``torch.nn.module``.
adj_attack : scipy.sparse.csr.csr_matrix
Adversarial adjacency matrix in form of :math:`(N + N_{inject})\times(N + N_{inject})` sparse matrix.
features : torch.FloatTensor
Features in form of ``N * D`` torch float tensor.
features_attack : torch.FloatTensor
Features of nodes after attacks in form of :math:`N_{inject}` * D` torch float tensor.
origin_labels : torch.LongTensor
Labels of target nodes originally predicted by the model.
target_mask : torch.Tensor
Mask of target nodes in form of ``N * 1`` torch bool tensor.
adj_norm_func : func of utils.normalize
Function that normalizes adjacency matrix.
Returns
-------
features_attack : torch.FloatTensor
Updated features of nodes after attacks in form of :math:`N_{inject}` * D` torch float tensor.
"""
epsilon = self.epsilon
n_epoch = self.n_epoch
feat_lim_min, feat_lim_max = self.feat_lim_min, self.feat_lim_max
n_total = features.shape[0]
adj_attacked_tensor = utils.adj_preprocess(adj=adj_attack,
adj_norm_func=adj_norm_func,
model_type=model.model_type,
device=self.device)
features_attack = utils.feat_preprocess(features=features_attack, device=self.device)
model.eval()
for i in range(n_epoch):
features_attack.requires_grad_(True)
features_attack.retain_grad()
features_concat = torch.cat((features, features_attack), dim=0)
pred = model(features_concat, adj_attacked_tensor)
pred_loss = self.loss(pred[:n_total][target_mask],
origin_labels[target_mask]).to(self.device)
model.zero_grad()
pred_loss.backward()
grad = features_attack.grad.data
features_attack = features_attack.clone() + epsilon * grad.sign()
features_attack = torch.clamp(features_attack, feat_lim_min, feat_lim_max)
features_attack = features_attack.detach()
test_score = self.eval_metric(pred[:n_total][target_mask],
origin_labels[target_mask])
if self.early_stop:
self.early_stop(test_score)
if self.early_stop.stop:
print("Attacking: Early stopped.")
self.early_stop = EarlyStop()
return features_attack
if self.verbose:
print(
"Attacking: Epoch {}, Loss: {:.5f}, Surrogate test score: {:.5f}".format(i, pred_loss, test_score),
end='\r' if i != n_epoch - 1 else '\n')
return features_attack