├── robograph ├── attack │ ├── __init__.py │ ├── matlab │ │ ├── linprog1_test.m │ │ ├── projection_coA1.m │ │ ├── lbfgsb_test.m │ │ ├── projection_coA2.m │ │ ├── linprog_test.m │ │ └── projection_A123.m │ ├── frank_wolfe.py │ ├── admm.py │ ├── cvx_pers_solver.py │ ├── dual.py │ ├── sanity_check.py │ ├── genetic_attack.py │ ├── dp_bak.py │ ├── dp_old_version.py │ ├── utils.py │ ├── SPG.py │ ├── dp.py │ ├── cvx_env_solver.py │ ├── greedy_attack.py │ └── convex_relaxation.py ├── model │ ├── __init__.py │ ├── gcn_conv.py │ └── gnn.py └── utils.py ├── .gitignore ├── setup.py ├── LICENSE ├── README.md ├── robograph.yml ├── demo_linear.ipynb └── demo-relu.ipynb /robograph/attack/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | **/*.egg-info 3 | **/results/ 4 | **/.ipynb_* 5 | .vscode/ -------------------------------------------------------------------------------- /robograph/model/__init__.py: -------------------------------------------------------------------------------- 1 | from robograph.model.gcn_conv import GCNConv 2 | from robograph.model.gnn import GC_NET 3 | 4 | __all__ = [ 5 | 'GCNConv', 6 | 'GC_NET', 7 | ] 8 | -------------------------------------------------------------------------------- /robograph/attack/matlab/linprog1_test.m: -------------------------------------------------------------------------------- 1 | function [opt_Z, f] = linprog1_test(A, b, Aeq, beq, lb, ub, R) 2 | linprog_options = optimoptions(@linprog, 'Algorithm','dual-simplex', ... 3 | 'MaxIterations', 1000, 'optimalityTolerance',1e-6, 'Display','iter'); 4 | [opt_Z, f] = linprog(R, A, b, Aeq, beq, lb, ub, linprog_options); 5 | end 6 | 7 | -------------------------------------------------------------------------------- /robograph/attack/matlab/projection_coA1.m: -------------------------------------------------------------------------------- 1 | 2 | function proj_Z = projection_coA1(Z, A_org, delta_l) 3 | if all(Z>=0, 'all') && all(Z<=1, 'all') && all(sum(abs(Z-A_org), 2) <= delta_l) 4 | proj_Z = Z; 5 | else 6 | proj_Z = zeros(size(Z)); 7 | for i = 1:size(Z, 1) 8 | proj_Z(i, :) = projection_coA2(Z(i, :), A_org(i, :), delta_l); 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /robograph/attack/matlab/lbfgsb_test.m: -------------------------------------------------------------------------------- 1 | function [opt_Z, f] = lbfgsb_test(Z_init, A_org, delta_l, delta_g, R) 2 | rng(4); 3 | n = 50; 4 | delta_l = 5; delta_g = 500; 5 | A_org = randi(2, n, n) - 1; 6 | A_org(1:(n+1):end) = 0; 7 | A_org = min(A_org, A_org'); 8 | R = rand(n, n); 9 | Z_init = A_org; 10 | 11 | funObj = @(x)objective(x, R); 12 | funProj = @(x)projection_A123(x, A_org, delta_l, delta_g); 13 | options = []; 14 | options.verbose = 2; 15 | options.maxIter = 100; 16 | options.SPGoptTol = 1e-8; 17 | [opt_Z, f, ~] = minConf_PQN(funObj, Z_init(:), funProj, options); 18 | opt_Z = reshape(opt_Z, size(R)); 19 | end 20 | 21 | function [f, g] = objective(Z, R) 22 | flat_R = R(:); 23 | f = dot(Z, flat_R); 24 | g = flat_R; 25 | end 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from codecs import open 3 | from os import path 4 | import os 5 | 6 | working_dir = path.abspath(path.dirname(__file__)) 7 | ROOT = os.path.abspath(os.path.dirname(__file__)) 8 | 9 | # Read the README. 10 | with open(os.path.join(ROOT, 'README.md'), encoding="utf-8") as f: 11 | README = f.read() 12 | 13 | setup(name='robograph', 14 | version='0.1', 15 | description='Certified Robustness of Graph Convolution Networks for Graph Classification under Topological Attacks', 16 | long_description=README, 17 | long_description_content_type='text/markdown', 18 | packages=find_packages(exclude=['tests*']), 19 | setup_requires=["numpy", "numba"], 20 | install_requires=["numpy", "matplotlib", "numba"], 21 | ) 22 | -------------------------------------------------------------------------------- /robograph/attack/matlab/projection_coA2.m: -------------------------------------------------------------------------------- 1 | 2 | function proj_Z = projection_coA2(Z, A_org, delta_g) 3 | if all(Z>=0, 'all') && all(Z<=1, 'all') && sum(abs(Z-A_org), 'all') <= delta_g 4 | proj_Z = Z; 5 | else 6 | param = []; 7 | param.maxIter = 10; % max number of iterations 8 | 9 | ub = inf; lb = 0; 10 | fun = @(x)dual_obj(x, Z, A_org, delta_g); 11 | [opt_lamb, fval, iter, numCall, msg] = lbfgsb(0.1, lb, ub, fun, [], [], param); 12 | proj_Z = Z - opt_lamb*(-2*A_org+1); 13 | proj_Z(proj_Z>1) = 1; 14 | proj_Z(proj_Z<0) = 0; 15 | end 16 | end 17 | 18 | 19 | function [f, g] = dual_obj(lamb, Z, A_org, delta_g) 20 | opt_z = Z - lamb*(-2*A_org+1); 21 | opt_z(opt_z>1) = 1; 22 | opt_z(opt_z<0) = 0; 23 | s = sum(abs(opt_z - A_org)); 24 | f = sum((opt_z-Z).^2)/2 + lamb*( s - delta_g); 25 | f = -f; 26 | g = delta_g - s; 27 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Hongwei Jin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /robograph/attack/matlab/linprog_test.m: -------------------------------------------------------------------------------- 1 | function [opt_Z, f] = linprog_test(Z_init, A_org, delta_l, delta_g, R) 2 | % rng(4); 3 | % n = 54; 4 | % delta_l = 5; delta_g = 500; 5 | % A_org = randi(2, n, n) - 1; 6 | % A_org(1:(n+1):end) = 0; 7 | % A_org = min(A_org, A_org'); 8 | % R = rand(n, n); 9 | % Z_init = A_org; 10 | 11 | n = size(A_org, 1); 12 | 13 | 14 | % generate inequality matrix 15 | V = -2*A_org + 1; 16 | A_local_budget = zeros(n, n^2); 17 | b_local_budget = zeros(n, 1); 18 | for i=1:n 19 | A_local_budget(:, n*(i-1)+1:n*i) = diag(V(:, i)); 20 | b_local_budget(i) = delta_l - sum(A_org(:, i)); 21 | end 22 | A_global_budget = V(:)'; 23 | b_global_budget = delta_g - sum(A_org, 'all'); 24 | A = [A_local_budget; A_global_budget]; 25 | b = [b_local_budget; b_global_budget]; 26 | 27 | % generate equality matrix 28 | eq_len = n*(n-1)/2; 29 | Aeq = zeros(eq_len, n^2); 30 | beq = zeros(eq_len, 1); 31 | count = 0; 32 | for i=1:n 33 | for j=i+1:n 34 | count = count + 1; 35 | idx = sub2ind([n,n], i, j); 36 | idx1 = sub2ind([n,n], j, i); 37 | Aeq(count, idx) = 1; 38 | Aeq(count, idx1) = -1; 39 | end 40 | end 41 | 42 | % generate lower/upper bound 43 | lb = zeros(size(Z_init)); 44 | ub = ones(size(Z_init)); 45 | ub = ub - diag(diag(ub)); % diagonal be always 0 46 | lb = lb(:); 47 | ub = ub(:); 48 | % fmincon_options = optimoptions(@fmincon,'Algorithm','interior-point', ... 49 | % 'SpecifyObjectiveGradient',true, 'MaxFunctionEvaluations', 1000, ... 50 | % 'optimalityTolerance',1e-3, 'Display','iter'); 51 | % funObj = @(x)objective(x, R(:)); 52 | % [opt_Z, f] = fmincon(funObj, Z_init, A, b, Aeq, beq, lb, ub, [], fmincon_options); 53 | linprog_options = optimoptions(@linprog, 'Algorithm','dual-simplex', ... 54 | 'MaxIterations', 1000, 'optimalityTolerance',1e-6, 'Display','None'); 55 | [opt_Z, f] = linprog(R(:), A, b, Aeq, beq, lb, ub, linprog_options); 56 | opt_Z = reshape(opt_Z, n, n); 57 | % opt_Z = reshape(opt_Z, n, n); 58 | end 59 | 60 | function [f, g] = objective(Z, flat_R) 61 | f = dot(Z(:), flat_R); 62 | g = flat_R; 63 | end 64 | -------------------------------------------------------------------------------- /robograph/attack/matlab/projection_A123.m: -------------------------------------------------------------------------------- 1 | function proj_Z = projection_A123(Z, A_org, delta_l, delta_g) 2 | n = size(A_org, 1); 3 | Z = reshape(Z, n, n); 4 | proj_Z = Z; 5 | for i=1:50 6 | Z_begin = proj_Z; 7 | proj_Z = projection_A3(Z_begin); 8 | proj_Z = projection_coA1(delete_diagonal(proj_Z), delete_diagonal(A_org), delta_l); 9 | proj_Z = fill_diagonal(proj_Z); 10 | proj_Z = projection_coA2(delete_diagonal(proj_Z), delete_diagonal(A_org), delta_g); 11 | proj_Z = fill_diagonal(proj_Z); 12 | 13 | chanes = max(abs(proj_Z-Z_begin), [], 'all'); 14 | if chanes < 1e-10 15 | break 16 | end 17 | end 18 | proj_Z = proj_Z(:); 19 | 20 | 21 | if sum((proj_Z-Z(:)).^2)/2 > 400 22 | V = -2*A_org + 1; 23 | A_local_budget = zeros(n, n^2); 24 | b_local_budget = zeros(n, 1); 25 | for i=1:n 26 | A_local_budget(:, n*(i-1)+1:n*i) = diag(V(:, i)); 27 | b_local_budget(i) = delta_l - sum(A_org(:, i)); 28 | end 29 | A_global_budget = V(:)'; 30 | b_global_budget = delta_g - sum(A_org, 'all'); 31 | A = [A_local_budget; A_global_budget]; 32 | b = [b_local_budget; b_global_budget]; 33 | 34 | % generate equality matrix 35 | eq_len = n*(n-1)/2; 36 | Aeq = zeros(eq_len, n^2); 37 | beq = zeros(eq_len, 1); 38 | count = 0; 39 | for i=1:n 40 | for j=i+1:n 41 | count = count + 1; 42 | idx = sub2ind([n,n], i, j); 43 | idx1 = sub2ind([n,n], j, i); 44 | Aeq(count, idx) = 1; 45 | Aeq(count, idx1) = -1; 46 | end 47 | end 48 | 49 | % generate lower/upper bound 50 | lb = zeros(size(A_org)); 51 | ub = ones(size(A_org)); 52 | ub = ub - diag(diag(ub)); % diagonal be always 0 53 | lb = lb(:); 54 | ub = ub(:); 55 | 56 | quad_options = optimoptions('quadprog', 'MaxIterations', 1000, 'Display','None' ); 57 | [x, fval] = quadprog(eye(n^2),-Z(:),A,b,Aeq,beq,lb,ub,[],quad_options); 58 | fprintf('alter = %f, lcqp = %f\n', sum((proj_Z-Z(:)).^2)/2, sum((x-Z(:)).^2)/2); 59 | end 60 | end 61 | 62 | 63 | function Z = delete_diagonal(X) 64 | Xtemp = X'; 65 | Z = reshape(Xtemp(~eye(size(Xtemp))), size(X, 2)-1, [])'; 66 | end 67 | 68 | function Z = fill_diagonal(X) 69 | r = size(X, 1); 70 | Z = [tril(X,-1) zeros(r, 1)] + [zeros(r,1) triu(X)]; 71 | end 72 | 73 | 74 | function proj_Z = projection_A3(Z) 75 | proj_Z = (Z + transpose(Z))/2; 76 | end -------------------------------------------------------------------------------- /robograph/model/gcn_conv.py: -------------------------------------------------------------------------------- 1 | import math 2 | import torch 3 | from torch.nn import Parameter 4 | from torch_geometric.nn.conv import MessagePassing 5 | from torch_geometric.utils import to_dense_adj 6 | 7 | 8 | def glorot(tensor): 9 | if tensor is not None: 10 | stdv = math.sqrt(6.0 / (tensor.size(-2) + tensor.size(-1))) 11 | tensor.data.uniform_(-stdv, stdv) 12 | 13 | 14 | def zeros(tensor): 15 | if tensor is not None: 16 | tensor.data.fill_(0) 17 | 18 | 19 | def normalize_adj(A, c=1): 20 | """ Normalize of adj 21 | 22 | Parameters 23 | ---------- 24 | A : torch 25 | 26 | Returns 27 | ------- 28 | row-wise normalization : torch 29 | """ 30 | _device = A.device 31 | A = A + c * torch.eye(A.shape[0]).to(_device) 32 | deg = A.sum(1) 33 | D_inv = torch.diag(torch.pow(deg, -1)) 34 | return D_inv @ A 35 | 36 | 37 | class GCNConv(MessagePassing): 38 | r""" 39 | .. math: D^-1 A X W 40 | """ 41 | 42 | def __init__(self, in_channels, out_channels, improved=False, cached=False, 43 | bias=True, normalize=True, **kwargs): 44 | super(GCNConv, self).__init__(aggr='add', **kwargs) 45 | 46 | self.in_channels = in_channels 47 | self.out_channels = out_channels 48 | self.improved = improved 49 | self.cached = cached 50 | self.normalize = normalize 51 | 52 | self.weight = Parameter(torch.Tensor(in_channels, out_channels)) 53 | 54 | if bias: 55 | self.bias = Parameter(torch.Tensor(out_channels)) 56 | else: 57 | self.register_parameter('bias', None) 58 | 59 | self.reset_parameters() 60 | 61 | def reset_parameters(self): 62 | glorot(self.weight) 63 | zeros(self.bias) 64 | self.cached_result = None 65 | self.cached_num_edges = None 66 | 67 | def forward(self, x, edge_index, edge_weight=None): 68 | """ Simple forward implementation D^-1 A X W 69 | """ 70 | # REVIEW: new simplied implmentation 71 | x = torch.matmul(x, self.weight) 72 | A = to_dense_adj(edge_index).squeeze() 73 | A_norm = normalize_adj(A) 74 | if A_norm.shape[1] != x.shape[0]: 75 | return A_norm @ x[:A_norm.shape[0]] 76 | return A_norm @ x 77 | 78 | def __repr__(self): 79 | return '{}({}, {})'.format(self.__class__.__name__, self.in_channels, 80 | self.out_channels) 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RoboGraph 2 | 3 | Implementation and evaluation of paper 4 | > __Certified Robustness of Graph Convolution Networks for Graph Classification under Topological Attacks__ 5 | by Hongwei Jin*, Zhan Shi*, Ashish Peruri, Xinhua Zhang (*equal contribution) 6 | Advances in Neural Information Processing Systems (NeurIPS), 2020. 7 | 8 | ## Installation 9 | 10 | The project requires python with version 3.7+, and use pip to install required packages 11 | 12 | * install `pytorch` from [link](https://pytorch.org/get-started/locally/) 13 | * install `pytorch_geometric` from [link](https://github.com/rusty1s/pytorch_geometric#installation) 14 | * install `cplex` and `docplex` 15 | 16 | For example, in the cpu only machine: 17 | 18 | ```shell 19 | conda install python=3.7 20 | conda install pytorch torchvision cpuonly -c pytorch 21 | pip install torch-scatter==latest+cpu torch-sparse==latest+cpu torch-cluster==latest+cpu torch-spline-conv==latest+cpu -f https://pytorch-geometric.com/whl/torch-1.5.0.html 22 | pip install torch-geometric 23 | pip install qpsolvers, sympy, nsopy 24 | ``` 25 | 26 | After install `cplex`, install `docplex`: 27 | 28 | ```shell 29 | conda install -c ibmdecisionoptimization docplex 30 | ``` 31 | 32 | To simply, you can also install the virtual env from the file `robograph.yml` 33 | 34 | ```shell 35 | conda env create -f robograph.yml 36 | ``` 37 | 38 | After install the virtual env, install the package in develop mode 39 | 40 | ```shell 41 | python setup.py develop 42 | ``` 43 | 44 | ## Run Demos 45 | 46 | For the model with __linear__ activations, check [demo_linear.ipynb](./demo_linear.ipynb) 47 | 48 | For the model with __ReLU__ activations, check [demo_relu.ipynb](./demo_relu.ipynb) 49 | 50 | 51 | ## Datasets 52 | 53 | TU of Dortmund has a collection of benchmark data sets for graph kernels. 54 | 55 | * multi-graph data set 56 | * node features (applied to some data sets) 57 | * link features (applied to some data sets) 58 | 59 | Reference: [Benchmark Data Sets for Graph Kernel](http://graphkernels.cs.tu-dortmund.de/) 60 | 61 | ### Selected Datasets 62 | 63 | * setting: 30% for training, 20% for validation and 50% for testing 64 | 65 | | NAME | No. of Graph | No. of Classes | Avg. No. of Nodes | Avg. No. of Edges | No. of node features | 66 | | -------- | ------------ | -------------- | ----------------- | ----------------- | -------------------- | 67 | | ENZYMES | 600 | 6 | 32.63 | 62.14 | 21 | 68 | | PROTEINS | 1113 | 2 | 39.06 | 72.82 | 4 | 69 | | NCI1 | 4110 | 2 | 29.87 | 32.30 | - | 70 | | MUTAG | 188 | 2 | 17.93 | 19.79 | - | 71 | 72 | 73 | | dataset | # of graphs | # of label | # of features | min edge | max edge | median edge | min node | max node | median node | 74 | | -------- | ----------- | ---------- | ------------- | -------- | -------- | ----------- | -------- | -------- | ----------- | 75 | | ENZYMES | 600 | 6 | 21 | 2 | 298 | 120 | 2 | 126 | 32 | 76 | | NCI1 | 4110 | 2 | 37 | 4 | 238 | 58 | 3 | 111 | 27 | 77 | | PROTEINS | 1113 | 2 | 4 | 10 | 2098 | 98 | 4 | 620 | 26 | 78 | | MUTAG | 188 | 2 | 7 | 20 | 66 | 38 | 10 | 28 | 17 | 79 | -------------------------------------------------------------------------------- /robograph/attack/frank_wolfe.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from robograph.attack.utils import check_budgets_and_symmetry 3 | from robograph.attack.convex_relaxation import ConvexRelaxation 4 | from robograph.attack.dp import po_dp_solver 5 | 6 | 7 | def frank_wolfe(A_org, XW, U, delta_l, delta_g, **params): 8 | """ Frank wolfe algorithm for solving convexified problem: 9 | min_{X\in co(A)} F_\circ(Z) 10 | 11 | param: 12 | A_org: original adjacency matrix 13 | XW: XW 14 | U: (u_y-u_c)/nG 15 | delta_l: row budgets 16 | delta_g: global budgets 17 | params: params for frank wolfe algorithm 18 | 19 | return a dict with keywords: 20 | opt_A: optimal perturbed matrix 21 | func_vals: objective value in each iteation (list type) 22 | """ 23 | 24 | cvx_relaxation = ConvexRelaxation(A_org, XW, U, delta_l, delta_g, params['activation'], params['relaxation']) 25 | # env_relaxation = ConvexRelaxation(A_org, XW, U, delta_l, delta_g, params['activation'], 'envelop') 26 | 27 | # nG = A_org.shape[0] 28 | # XWU = XW @ U 29 | X = A_org.copy() 30 | iters, constr = params['iter'], params['constr'] 31 | func_vals = [0] * iters 32 | for t in range(1, iters + 1): 33 | conv_F_c, G = cvx_relaxation.convex_F(X) 34 | R = -G 35 | func_vals[t - 1] = conv_F_c 36 | # print('pers f: %f env f: %f ' % (conv_F_c, env_relaxation.convex_F(X)[0]) ) 37 | B_opt, V_opt = polar_operator(A_org, R, delta_l, delta_g, constr) 38 | X = B_opt * (2 / (t + 2)) + X * (t / (t + 2)) 39 | 40 | sol = { 41 | 'opt_A': X, 42 | 'func_vals': func_vals, 43 | } 44 | return sol 45 | 46 | 47 | def polar_operator(A_org, R, delta_l, delta_g, constr): 48 | if constr == '1+3': 49 | # TODO 50 | raise NotImplementedError('attack constr `{}` is not implemented'.format(constr)) 51 | elif constr == '2+3': 52 | # TODO 53 | raise NotImplementedError('attack constr `{}` is not implemented'.format(constr)) 54 | elif constr == '1+2': 55 | sol = po_dp_solver(A_org, R, delta_l, delta_g) 56 | elif constr == '1+2+3': 57 | sol = greedy_solver(A_org, R, delta_l, delta_g) 58 | assert check_budgets_and_symmetry(sol[0], A_org, delta_l, delta_g)[-1] == 'symmetric' 59 | else: 60 | raise NotImplementedError('attack constr `{}` is not implemented'.format(constr)) 61 | return sol 62 | 63 | 64 | def greedy_solver(A_org, R, delta_l, delta_g): 65 | """ 66 | Greedy solver for polar_operator under A_G^{1+2+3} 67 | 68 | Complexity: nG^2*log(nG) 69 | """ 70 | nG = A_org.shape[0] 71 | J = R * (-2 * A_org + 1) 72 | J_hat = (J + J.T) / 2 73 | num_valid_idx = nG * nG 74 | for i in range(nG): 75 | for j in range(nG): 76 | if j <= i or J_hat[i][j] <= 0: 77 | J_hat[i][j] = -float('inf') 78 | num_valid_idx -= 1 79 | J_hat = -J_hat 80 | indices = np.dstack(np.unravel_index(np.argsort(J_hat.ravel()), (nG, nG)))[0] 81 | 82 | V = np.zeros((nG, nG)) 83 | for i in range(num_valid_idx): 84 | V_hat = V.copy() 85 | i, j = indices[i][0], indices[i][1] 86 | V_hat[i][j], V_hat[j][i] = 1, 1 87 | if satisfy_constr(V_hat, delta_l, delta_g): 88 | V = V_hat.copy() 89 | A_pert = ((2 * A_org - 1) * (-2 * V + 1) + 1) / 2 90 | return A_pert, V 91 | 92 | 93 | def satisfy_constr(V, delta_l, delta_g): 94 | rows_budget = np.sum(V, axis=1) 95 | total_budget = np.sum(rows_budget) 96 | if total_budget > delta_g or np.any(rows_budget > delta_l): 97 | return False 98 | return True 99 | -------------------------------------------------------------------------------- /robograph/attack/admm.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from robograph.attack.dp import exact_solver_wrapper 4 | from robograph.attack.utils import calculate_Fc, calculate_doubleL_Fc 5 | 6 | 7 | def admm_solver(A_org, XW, U, delta_l, delta_g, **params): 8 | """ 9 | ADMM approach (upper bound) for solving min_{A_G^{1+2+3}} F_c(A) 10 | 11 | param: 12 | A_org: original adjacency matrix 13 | XW: XW 14 | U: (u_y-u_c)/nG 15 | delta_l: row budgets 16 | delta_g: global budgets 17 | params: params for admm optimiztion 18 | 19 | return a dict with keywords: 20 | opt_A: optimal perturbed matrix 21 | opt_f: optimal objective value 22 | """ 23 | 24 | nG = A_org.shape[0] 25 | XWU = XW @ U 26 | lamb = np.random.randn(nG, nG)*0 27 | B = params['init_B'] 28 | mu = params['mu'] 29 | iters = params['iter'] 30 | func_vals = [0]*iters 31 | for i in range(iters): 32 | # linear term: fnorm(A-B)^2 = \sum_{ij} A_ij^2 - 2*A_ijB_ij + B_ijB_ij 33 | L = -lamb.T + (np.ones((nG, nG)) - 2*B.T)/(2*mu) 34 | dp_sol = exact_solver_wrapper(A_org, np.tile(XWU, (nG, 1)), np.zeros(nG), L.T, delta_l, delta_g, '1+2') 35 | _, opt_val, A_pert = dp_sol 36 | func_vals[i] = calculate_Fc(A_pert, XW, U) 37 | B = closed_form_B(lamb, A_pert, mu) 38 | lamb += (B-A_pert)/mu 39 | if params.get('verbose'): 40 | print(func_vals[i]) 41 | 42 | sol = { 43 | 'opt_A': A_pert, 44 | 'opt_f': func_vals[-1] 45 | } 46 | return sol 47 | 48 | 49 | def admm_solver_doubleL(A_org, Q, p, delta_l, delta_g, **params): 50 | """ 51 | ADMM approach (upper bound) for solving min_{A_G^{1+2+3}} F_c(A) 52 | where the activation is ReLU, and F_c(A) has been linearized via doubleL (upper bound) 53 | 54 | param: 55 | A_org: original adjacency matrix 56 | Q: Q 57 | p: p 58 | delta_l: row budgets 59 | delta_g: global budgets 60 | params: params for admm optimiztion 61 | 62 | return a dict with keywords: 63 | opt_A: optimal perturbed matrix 64 | opt_f: optimal objective value 65 | """ 66 | 67 | nG = A_org.shape[0] 68 | lamb = np.random.randn(nG, nG)*0 69 | B = params['init_B'] 70 | mu = params['mu'] 71 | iters = params['iter'] 72 | func_vals = [0]*iters 73 | for i in range(iters): 74 | # linear term: fnorm(A-B)^2 = \sum_{ij} A_ij^2 - 2*A_ijB_ij + B_ijB_ij 75 | L = -lamb.T + (np.ones((nG, nG)) - 2*B.T)/(2*mu) 76 | dp_sol = exact_solver_wrapper(A_org, Q, p, L.T, delta_l, delta_g, '1+2') 77 | _, opt_val, A_pert = dp_sol 78 | func_vals[i] = calculate_doubleL_Fc(A_pert, Q, p) 79 | B = closed_form_B(lamb, A_pert, mu) 80 | lamb += (B-A_pert)/mu 81 | if params.get('verbose'): 82 | print(func_vals[i]) 83 | 84 | sol = { 85 | 'opt_A': A_pert, 86 | 'opt_f': func_vals[-1] 87 | } 88 | return sol 89 | 90 | 91 | def closed_form_B(lamb, A_pert, mu): 92 | # Closed-form solution of B in eq (11) 93 | 94 | # Continuous solution 95 | # BB = (-mu/2) * (lamb + lamb.T) + (1/2) * (A_pert + A_pert.T) 96 | # BB = np.round(BB) 97 | # print(np.trace(lamb.T @ BB) + np.sum((A_pert - BB)**2)/(2*mu)) 98 | # return BB 99 | 100 | # Discreet solution 101 | nG = A_pert.shape[0] 102 | L = lamb + (np.ones((nG, nG)) - 2*A_pert)/(2*mu) 103 | L_hat = L + L.T 104 | B = np.zeros((nG, nG)) 105 | for i in range(nG): 106 | for j in range(nG): 107 | if j > i and L_hat[i][j] <= 0: 108 | B[i][j], B[j][i] = 1, 1 109 | # print(np.trace(lamb.T @ B) + np.sum((A_pert - B)**2)/(2*mu)) 110 | return B 111 | -------------------------------------------------------------------------------- /robograph/utils.py: -------------------------------------------------------------------------------- 1 | import pickle as pkl 2 | import os 3 | import numpy as np 4 | import scipy.sparse as sp 5 | from numba import jit, prange 6 | from itertools import combinations 7 | 8 | 9 | def load_graph(filename): 10 | """ Load data from pickle file 11 | 12 | Parameters 13 | ---------- 14 | filename : str 15 | 16 | Returns 17 | ------- 18 | data : a dictionary with graph and learned parameters 19 | """ 20 | if os.path.exists(filename): 21 | if filename.endswith('pickle'): 22 | data = pkl.load(open(filename, 'rb')) 23 | return data 24 | 25 | 26 | def process_data(data): 27 | """ Process DataLoader data into A, X, y 28 | 29 | Parameters 30 | ---------- 31 | data : torch_geometric data 32 | 33 | Returns 34 | ------- 35 | A : sp.csr_matrix 36 | X : np.ndarray() 37 | y : int 38 | """ 39 | row = data.edge_index[0].numpy() 40 | col = data.edge_index[1].numpy() 41 | assert len(row) == len(col) 42 | conn = len(row) 43 | size = data.x.shape[0] 44 | d = np.ones(conn) 45 | A = sp.csr_matrix((d, (row, col)), shape=(size, size), dtype=np.float32) 46 | A = np.float64(A.toarray()) 47 | X = np.float64(data.x.numpy()) 48 | y = data.y 49 | return A, X, y 50 | 51 | 52 | @jit 53 | def attack_global(G, delta=5): 54 | """ Random global attack 55 | 56 | Parameters 57 | ---------- 58 | G : networkx graph 59 | delta : global budget 60 | 61 | Returns 62 | ------- 63 | edge_index : np.array 64 | """ 65 | 66 | size = len(G.nodes) 67 | all_edge = list(combinations(np.arange(size), 2)) 68 | picked_edge = np.random.choice(all_edge, delta) 69 | for edge in picked_edge: 70 | if edge in G.edges: 71 | G.remove_edge(edge[0], edge[1]) 72 | else: 73 | G.add_edge(edge[0], edge[1]) 74 | edge_idx = np.array(G.edges).T 75 | return edge_idx 76 | 77 | 78 | @jit 79 | def attack_local(G, u, delta=5): 80 | """ Random global attack 81 | 82 | Parameters 83 | ---------- 84 | G : networkx graph 85 | u : target node 86 | delta : global budget 87 | 88 | Returns 89 | ------- 90 | edge_index : np.array 91 | """ 92 | edge = list(G.edges[u]) 93 | candidate_edge = [(u, i) for i in range(G.size()) if i != u] 94 | picked_edge = np.random.choice(candidate_edge, delta) 95 | for edge in picked_edge: 96 | if edge in G.edges: 97 | G.remove_edge(edge[0], edge[1]) 98 | else: 99 | G.add_edge(edge[0], edge[1]) 100 | edge_idx = np.array(G.edges).T 101 | return edge_idx 102 | 103 | 104 | def cal_logits(A, XW, U, act='linear'): 105 | """ Return logits 106 | 107 | Parameters 108 | ---------- 109 | A : np.array with dimension (nG, nG) 110 | XW : np.array with dimension (nG, d) 111 | U : np.array with dimension (d, c) 112 | 113 | Returns 114 | ------- 115 | np.array with dimension (1, c) 116 | """ 117 | # relu = lambda x: x * (x > 0) 118 | _A = A + sp.eye(A.shape[0]) 119 | deg = _A.sum(1).A1 120 | D_inv = sp.diags(np.power(deg, -1)) 121 | if act == 'relu': 122 | P = np.maximum(D_inv @ _A @ XW, 0) 123 | else: 124 | P = D_inv @ _A @ XW 125 | logits = np.mean(P, axis=0) @ U.T 126 | return logits 127 | 128 | 129 | def cal_fc(A, XW, u, act='linear'): 130 | """ Return fc_val 131 | 132 | Parameters 133 | ---------- 134 | A : np.array with dimension (nG, nG) 135 | XW : np.array with dimension (nG, d) 136 | u : np.array with dimension (1, c) 137 | 138 | Returns 139 | ------- 140 | float : f_c val 141 | """ 142 | _A = A + sp.eye(A.shape[0]) 143 | deg = _A.sum(1).A1 144 | D_inv = sp.diags(np.power(deg, -1)) 145 | if act == 'relu': 146 | P = np.maximum(D_inv @ _A @ XW, 0) 147 | else: 148 | P = D_inv @ _A @ XW 149 | f_val = np.mean(P, axis=0) @ u.T 150 | return f_val.item() 151 | -------------------------------------------------------------------------------- /robograph/attack/cvx_pers_solver.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from robograph.attack.utils import projection_A123 4 | from robograph.attack.convex_relaxation import ConvexRelaxation 5 | from robograph.attack.greedy_attack import Greedy_Attack 6 | import scipy.optimize as optim 7 | from robograph.attack.SPG import * 8 | 9 | 10 | def cvx_pers_solver(A_org, XW, U, delta_l, delta_g, **params): 11 | """ 12 | Solver for min_{X\in co(A)} F_\circ(X), where F_\circ(X) is an convex relaxation of F(X) via perspective function. 13 | 14 | param: 15 | A_org: original adjacency matrix 16 | XW: XW 17 | U: (u_y-u_c)/nG 18 | delta_l: row budgets 19 | delta_g: global budgets 20 | params: 'algo': 'pqn' or 'pg' 21 | 22 | return a dict with keywords: 23 | opt_A: optimal perturbed matrix 24 | opt_f: optimal objective value 25 | """ 26 | 27 | if params['algo'] == 'pqn': 28 | return projected_lbfgs(A_org, XW, U, delta_l, delta_g, **params) 29 | elif params['algo'] == 'nlp': 30 | return nlp_solver(A_org, XW, U, delta_l, delta_g, **params) 31 | else: 32 | raise NotImplementedError( 33 | 'Algorithm `{}` is not implemented for perspective relaxation!'.format(params['algo'])) 34 | 35 | 36 | def projected_lbfgs(A_org, XW, U, delta_l, delta_g, **params): 37 | cvx_relaxation = ConvexRelaxation(A_org, XW, U, delta_l, delta_g, params['activation'], 'perspective', 38 | relu_relaxation=params.get('relu_bound')) 39 | n = A_org.shape[0] 40 | 41 | def objective(x): 42 | X = x.reshape(n, n) 43 | f, G = cvx_relaxation.convex_F(X) 44 | return f, G.flatten() 45 | 46 | def proj(x): 47 | X = x.reshape(n, n) 48 | projected_value = projection_A123(X, A_org, delta_l, delta_g) 49 | return projected_value.flatten() 50 | 51 | spg_options = default_options 52 | spg_options.curvilinear = 1 53 | spg_options.interp = 2 54 | spg_options.numdiff = 0 # 0 to use gradients, 1 for numerical diff 55 | spg_options.verbose = 2 if params['verbose'] else 0 56 | spg_options.maxIter = params['iter'] 57 | 58 | # Initialization of X 59 | # 1. use greedy attack A_pert 60 | greedy_attack = Greedy_Attack(A_org, XW, U, delta_l, delta_g, params['activation']) 61 | greedy_sol = greedy_attack.attack(A_org) 62 | init_X = greedy_sol['opt_A'] 63 | # 2. original A 64 | # init_X = A_org.copy() 65 | 66 | x, f = SPG(objective, proj, init_X.flatten(), spg_options) 67 | sol = { 68 | 'opt_A': x.reshape(n, n), 69 | 'opt_f': f 70 | } 71 | return sol 72 | 73 | 74 | def nlp_solver(A_org, XW, U, delta_l, delta_g, **params): 75 | cvx_relaxation = ConvexRelaxation(A_org, XW, U, delta_l, delta_g, params['activation'], 'perspective', 76 | relu_relaxation=params.get('relu_bound')) 77 | n = A_org.shape[0] 78 | # Generating LP constraint matrix 79 | # 1. inequality constraint: local/global budgets 80 | V = -2 * A_org + 1 81 | A_local_budget = np.zeros((n, n**2)) 82 | b_local_budget = np.zeros((n, 1)) 83 | for i in range(n): 84 | A_local_budget[i, n * i: n * (i + 1)] = V[:, i] 85 | b_local_budget[i] = delta_l - np.sum(A_org[:, i]) 86 | 87 | A_global_budget = V.reshape(1, n**2) 88 | b_global_budget = np.asarray(delta_g - np.sum(A_org)).reshape(1, 1) 89 | A = np.concatenate((A_local_budget, A_global_budget), axis=0) 90 | b = np.concatenate((b_local_budget, b_global_budget), axis=0) 91 | 92 | # 2. equality constraint: symmetry 93 | eq_len = int(n * (n - 1) / 2) 94 | Aeq = np.zeros((eq_len, n**2)) 95 | beq = np.zeros((eq_len, 1)) 96 | count = 0 97 | for i in range(n): 98 | for j in range(i + 1, n): 99 | idx = np.ravel_multi_index((i, j), dims=A_org.shape, order='C') 100 | idx1 = np.ravel_multi_index((j, i), dims=A_org.shape, order='C') 101 | Aeq[count, idx], Aeq[count, idx1] = 1, -1 102 | count += 1 103 | 104 | # 3. lower/upper bound 105 | lb = np.zeros(A_org.shape) 106 | ub = np.ones(A_org.shape) 107 | np.fill_diagonal(ub, 0) 108 | 109 | bounds = optim.Bounds(lb.ravel(), ub.ravel()) 110 | A_total = np.concatenate((A, Aeq), axis=0) 111 | lb_total = np.concatenate((np.asarray([-np.inf] * len(b)).reshape(-1, 1), beq), axis=0) 112 | ub_total = np.concatenate((b, beq), axis=0) 113 | inequality_ctr = optim.LinearConstraint(A_total, lb_total, ub_total) 114 | init_x = A_org.ravel() 115 | res = optim.minimize(lambda x: cvx_relaxation.convex_F(x.reshape(n, n))[0], init_x, method='trust-constr', 116 | jac=lambda x: cvx_relaxation.convex_F(x.reshape(n, n))[1].ravel(), 117 | constraints=inequality_ctr, 118 | options={'verbose': params['verbose']}, bounds=bounds) 119 | # print(res.message) 120 | opt_Z, opt_f = res.x.reshape(n, n), res.fun 121 | 122 | sol = { 123 | 'opt_A': opt_Z, 124 | 'opt_f': opt_f 125 | } 126 | return sol 127 | -------------------------------------------------------------------------------- /robograph.yml: -------------------------------------------------------------------------------- 1 | name: robograph 2 | channels: 3 | - pytorch 4 | - gurobi 5 | - ibmdecisionoptimization 6 | - http://conda.anaconda.org/gurobi 7 | - defaults 8 | dependencies: 9 | - _libgcc_mutex=0.1=main 10 | - astroid=2.3.3=py37_0 11 | - attrs=19.3.0=py_0 12 | - autopep8=1.4.4=py_0 13 | - backcall=0.1.0=py37_0 14 | - blas=1.0=mkl 15 | - bleach=3.1.4=py_0 16 | - ca-certificates=2020.1.1=0 17 | - certifi=2020.4.5.1=py37_0 18 | - cffi=1.14.0=py37h2e261b9_0 19 | - cryptography=2.9.2=py37h1ba5d50_0 20 | - cudatoolkit=10.1.243=h6bb024c_0 21 | - dbus=1.13.14=hb2f20db_0 22 | - defusedxml=0.6.0=py_0 23 | - docloud=1.0.375=py37_0 24 | - docplex=2.12.182=py37_0 25 | - entrypoints=0.3=py37_0 26 | - expat=2.2.6=he6710b0_0 27 | - fastcache=1.1.0=py37h7b6447c_0 28 | - flake8=3.7.9=py37_0 29 | - fontconfig=2.13.0=h9420a91_0 30 | - freetype=2.9.1=h8a8886c_1 31 | - glib=2.63.1=h5a9c865_0 32 | - gmp=6.1.2=h6c8ec71_1 33 | - gmpy2=2.0.8=py37h10f8cd9_2 34 | - gst-plugins-base=1.14.0=hbbd80ab_1 35 | - gstreamer=1.14.0=hb453b48_1 36 | - gurobi=9.0.2=py37_0 37 | - icu=58.2=he6710b0_3 38 | - importlib_metadata=1.5.0=py37_0 39 | - intel-openmp=2020.1=217 40 | - ipykernel=5.1.4=py37h39e3cac_0 41 | - ipython=7.13.0=py37h5ca1d4c_0 42 | - ipython_genutils=0.2.0=py37_0 43 | - ipywidgets=7.5.1=py_0 44 | - isort=4.3.21=py37_0 45 | - jedi=0.17.0=py37_0 46 | - jinja2=2.11.2=py_0 47 | - jpeg=9b=h024ee3a_2 48 | - jsonschema=3.2.0=py37_0 49 | - jupyter=1.0.0=py37_7 50 | - jupyter_client=6.1.3=py_0 51 | - jupyter_console=6.1.0=py_0 52 | - jupyter_core=4.6.3=py37_0 53 | - lazy-object-proxy=1.4.3=py37h7b6447c_0 54 | - ld_impl_linux-64=2.33.1=h53a641e_7 55 | - libedit=3.1.20181209=hc058e9b_0 56 | - libffi=3.2.1=hd88cf55_4 57 | - libgcc-ng=9.1.0=hdf63c60_0 58 | - libgfortran-ng=7.3.0=hdf63c60_0 59 | - libpng=1.6.37=hbc83047_0 60 | - libsodium=1.0.16=h1bed415_0 61 | - libstdcxx-ng=9.1.0=hdf63c60_0 62 | - libtiff=4.1.0=h2733197_0 63 | - libuuid=1.0.3=h1bed415_2 64 | - libxcb=1.13=h1bed415_1 65 | - libxml2=2.9.9=hea5a465_1 66 | - markupsafe=1.1.1=py37h7b6447c_0 67 | - matplotlib-base=3.1.3=py37hef1b27d_0 68 | - mccabe=0.6.1=py37_1 69 | - mistune=0.8.4=py37h7b6447c_0 70 | - mkl=2020.1=217 71 | - mkl-service=2.3.0=py37he904b0f_0 72 | - mkl_fft=1.0.15=py37ha843d7b_0 73 | - mkl_random=1.1.0=py37hd6b4f25_0 74 | - mpc=1.1.0=h10f8cd9_1 75 | - mpfr=4.0.1=hdf1c602_3 76 | - mpmath=1.1.0=py37_0 77 | - nbconvert=5.6.1=py37_0 78 | - nbformat=5.0.6=py_0 79 | - ncurses=6.2=he6710b0_1 80 | - ninja=1.9.0=py37hfd86e86_0 81 | - notebook=6.0.3=py37_0 82 | - numpy=1.18.1=py37h4f9e942_0 83 | - numpy-base=1.18.1=py37hde5b4d6_1 84 | - olefile=0.46=py37_0 85 | - openssl=1.1.1g=h7b6447c_0 86 | - pandoc=2.2.3.2=0 87 | - pandocfilters=1.4.2=py37_1 88 | - parso=0.7.0=py_0 89 | - pcre=8.43=he6710b0_0 90 | - pexpect=4.8.0=py37_0 91 | - pickleshare=0.7.5=py37_0 92 | - pillow=7.1.2=py37hb39fc2d_0 93 | - pip=20.0.2=py37_3 94 | - prometheus_client=0.7.1=py_0 95 | - prompt-toolkit=3.0.4=py_0 96 | - prompt_toolkit=3.0.4=0 97 | - ptyprocess=0.6.0=py37_0 98 | - pycodestyle=2.5.0=py37_0 99 | - pycparser=2.20=py_0 100 | - pyflakes=2.1.1=py37_0 101 | - pygments=2.6.1=py_0 102 | - pylint=2.4.4=py37_0 103 | - pyopenssl=19.1.0=py37_0 104 | - pyqt=5.9.2=py37h05f1152_2 105 | - pyrsistent=0.16.0=py37h7b6447c_0 106 | - pysocks=1.7.1=py37_0 107 | - python=3.7.6=h0371630_2 108 | - pytorch=1.5.0=py3.7_cuda10.1.243_cudnn7.6.3_0 109 | - pyzmq=18.1.1=py37he6710b0_0 110 | - qt=5.9.7=h5867ecd_1 111 | - qtconsole=4.7.4=py_0 112 | - qtpy=1.9.0=py_0 113 | - readline=7.0=h7b6447c_5 114 | - rope=0.17.0=py_0 115 | - seaborn=0.10.1=py_0 116 | - send2trash=1.5.0=py37_0 117 | - setuptools=46.4.0=py37_0 118 | - sip=4.19.8=py37hf484d3e_0 119 | - six=1.14.0=py37_0 120 | - sqlite=3.31.1=h62c20be_1 121 | - sympy=1.5.1=py37_0 122 | - tabulate=0.8.3=py37_0 123 | - terminado=0.8.3=py37_0 124 | - testpath=0.4.4=py_0 125 | - tk=8.6.8=hbc83047_0 126 | - torchvision=0.6.0=py37_cu101 127 | - tornado=6.0.4=py37h7b6447c_1 128 | - tqdm=4.42.1=py_0 129 | - traitlets=4.3.3=py37_0 130 | - wcwidth=0.1.9=py_0 131 | - webencodings=0.5.1=py37_1 132 | - wheel=0.34.2=py37_0 133 | - widgetsnbextension=3.5.1=py37_0 134 | - wrapt=1.12.1=py37h7b6447c_1 135 | - xz=5.2.5=h7b6447c_0 136 | - zeromq=4.3.1=he6710b0_3 137 | - zipp=3.1.0=py_0 138 | - zlib=1.2.11=h7b6447c_3 139 | - zstd=1.3.7=h0b5b093_0 140 | - pip: 141 | - chardet==3.0.4 142 | - colorama==0.4.3 143 | - cplex==12.10.0.0 144 | - cycler==0.10.0 145 | - cython==0.29.19 146 | - decorator==4.4.1 147 | - googledrivedownloader==0.4 148 | - h5py==2.10.0 149 | - idna==2.8 150 | - imageio==2.6.1 151 | - isodate==0.6.0 152 | - joblib==0.14.1 153 | - kiwisolver==1.1.0 154 | - llvmlite==0.31.0 155 | - matplotlib==3.2.0rc1 156 | - networkx==2.4 157 | - nsopy==1.52 158 | - numba==0.48.0 159 | - pandas==1.0.0rc0 160 | - pyparsing==2.4.6 161 | - python-dateutil==2.8.1 162 | - pytz==2019.3 163 | - pywavelets==1.1.1 164 | - qpsolvers==1.3 165 | - quadprog==0.1.7 166 | - rdflib==4.2.2 167 | - requests==2.22.0 168 | - scikit-image==0.16.2 169 | - scikit-learn==0.22.1 170 | - scipy==1.4.1 171 | - torch-cluster==1.4.5 172 | - torch-scatter==2.0.4 173 | - torch-sparse==0.6.4 174 | - torch-spline-conv==1.1.1 175 | - urllib3==1.25.7 176 | 177 | -------------------------------------------------------------------------------- /robograph/attack/dual.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from robograph.attack.dp import exact_solver_wrapper 4 | import scipy.optimize as optim 5 | # from nsopy.methods.bundle import CuttingPlanesMethod, BundleMethod 6 | from nsopy.methods.quasi_monotone import SGMDoubleSimpleAveraging, SGMTripleAveraging 7 | from nsopy.methods.subgradient import SubgradientMethod 8 | from nsopy.loggers import GenericMethodLogger 9 | from robograph.attack.SPG import * 10 | 11 | def dual_solver(A_org, XW, U, delta_l, delta_g, **params): 12 | """ 13 | Dual approach (lower bound) for solving min_{A_G^{1+2+3}} F_c(A) 14 | 15 | param: 16 | A_org: original adjacency matrix 17 | XW: XW 18 | U: (u_y-u_c)/nG 19 | delta_l: row budgets (vector) 20 | delta_g: global budget (scalar) 21 | params: params for optimizing Lambda 22 | 'nonsmooth': could be 'random' or 'subgrad' 23 | 1. random: choose best solution from 5 random initialization 24 | 2. initilize LBFGS by solution of Quasi-Monotone Methods 25 | 26 | 27 | return a dict with keywords: 28 | opt_A: optimal perturbed matrix 29 | opt_f: optimal dual objective 30 | """ 31 | 32 | nG = A_org.shape[0] 33 | XWU = XW @ U 34 | 35 | def objective(x): 36 | lamb = x.reshape(nG, nG) 37 | L = lamb.T - lamb 38 | dp_sol = exact_solver_wrapper(A_org, np.tile(XWU, (nG, 1)), np.zeros(nG), L.T, delta_l, delta_g, '1+2') 39 | _, opt_val, A_pert = dp_sol 40 | grad_on_lambda = A_pert - A_pert.T 41 | return -opt_val, -grad_on_lambda.flatten() 42 | # print(optim.check_grad(lambda x: objective(x)[0], lambda x: objective(x)[1], lamb.flatten())) 43 | 44 | opt_lamb, fopt = optimize(objective, nG, params['iter'], params.get('verbose'), params['nonsmooth_init']) 45 | L = opt_lamb.T - opt_lamb 46 | sol = { 47 | 'opt_A': exact_solver_wrapper(A_org, np.tile(XWU, (nG, 1)), np.zeros(nG), L.T, delta_l, delta_g, '1+2')[-1], 48 | 'opt_f': fopt 49 | } 50 | return sol 51 | 52 | 53 | def dual_solver_doubleL(A_org, Q, p, delta_l, delta_g, **params): 54 | """ 55 | Dual approach (lower bound) for solving min_{A_G^{1+2+3}} F_c(A) 56 | where the activation is ReLU, and F_c(A) has been linearized via doubleL 57 | 58 | param: 59 | A_org: original adjacency matrix 60 | Q: Q 61 | p: p 62 | delta_l: row budgets 63 | delta_g: global budgets 64 | params: params for optimizing Lambda 65 | 'nonsmooth': could be 'random' or 'subgrad' 66 | 1. random: choose best solution from 5 random initialization 67 | 2. initilize LBFGS by solution of Quasi-Monotone Methods 68 | 69 | return a dict with keywords: 70 | opt_A: optimal perturbed matrix 71 | opt_f: optimal dual objective 72 | """ 73 | 74 | nG = A_org.shape[0] 75 | 76 | def objective(x): 77 | lamb = x.reshape(nG, nG) 78 | L = lamb.T - lamb 79 | dp_sol = exact_solver_wrapper(A_org, Q, p, L.T, delta_l, delta_g, '1+2') 80 | _, opt_val, A_pert = dp_sol 81 | grad_on_lambda = A_pert - A_pert.T 82 | return -opt_val, -grad_on_lambda.flatten() 83 | # print(optim.check_grad(lambda x: objective(x)[0], lambda x: objective(x)[1], lamb.flatten())) 84 | 85 | opt_lamb, fopt = optimize(objective, nG, params['iter'], params.get('verbose'), params['nonsmooth_init']) 86 | L = opt_lamb.T - opt_lamb 87 | sol = { 88 | 'opt_A': exact_solver_wrapper(A_org, Q, p, L.T, delta_l, delta_g, '1+2')[-1], 89 | 'opt_f': fopt 90 | } 91 | return sol 92 | 93 | 94 | def optimize(objective, var_dim, iters, verbose, init): 95 | def callback(x): 96 | print(-objective(x)[0]) 97 | 98 | if init == 'subgrad': 99 | # Subgradient Method to optimize the non-smooth objective 100 | # method = SubgradientMethod(lambda x: (0, -objective(x)[0], -objective(x)[1]), lambda x: x, dimension=var_dim*var_dim,\ 101 | # stepsize_0=0.003, stepsize_rule='constant', sense='max') 102 | method = SGMTripleAveraging(lambda x: (0, -objective(x)[0], -objective(x)[1]), lambda x: x, dimension=var_dim*var_dim,\ 103 | gamma=3, sense='max') 104 | logger = GenericMethodLogger(method) 105 | for iteration in range(100): 106 | method.step() 107 | init_lamb = logger.x_k_iterates[-1] 108 | fopt1 = logger.f_k_iterates[-1] 109 | 110 | if verbose: 111 | res = optim.fmin_l_bfgs_b(objective, init_lamb.flatten(), maxiter=iters, callback=callback) 112 | res_status = res[2] 113 | print('warnflag: %d, iters: %d, funcalls: %d' % (res_status['warnflag'], res_status['nit'], res_status['funcalls']) ) 114 | print(res_status['task']) 115 | else: 116 | res = optim.fmin_l_bfgs_b(objective, init_lamb.flatten(), maxiter=iters, callback=None) 117 | xopt, fopt = res[0], -res[1] 118 | 119 | elif init == 'random': 120 | maximum = -np.inf 121 | for i in range(5): 122 | init_lamb = np.random.randn(var_dim, var_dim) * 0.01 123 | if verbose: 124 | res = optim.fmin_l_bfgs_b(objective, init_lamb.flatten(), maxiter=iters, callback=callback) 125 | res_status = res[2] 126 | print('warnflag: %d, iters: %d, funcalls: %d' % (res_status['warnflag'], res_status['nit'], res_status['funcalls']) ) 127 | print(res_status['task']) 128 | else: 129 | res = optim.fmin_l_bfgs_b(objective, init_lamb.flatten(), maxiter=iters, callback=None) 130 | if -res[1] > maximum: 131 | maximum = -res[1] 132 | xopt, fopt = res[0], -res[1] 133 | # if res[2]['warnflag'] < 2: break 134 | 135 | lamb = xopt.reshape(var_dim, var_dim) 136 | return lamb, fopt -------------------------------------------------------------------------------- /robograph/attack/sanity_check.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from sympy.utilities.iterables import variations, cartes 3 | from robograph.attack.utils import calculate_Fc, calculate_doubleL_Fc, fill_diagonal 4 | 5 | def possible_matrix_with_delta_l(A_org, delta_l): 6 | nG = A_org.shape[0] 7 | possible_rows = list(variations([0,1], nG-1, True)) 8 | possible_all_A = [] 9 | for i in range(nG): 10 | A_i = A_org[i] 11 | possible_A_i_new = [] 12 | for rows in possible_rows: 13 | A_i_new = list(rows) 14 | A_i_new.insert(i, 0) 15 | diff = sum(abs(A_i_new - A_i)) 16 | if diff <= delta_l[i]: 17 | possible_A_i_new.append(A_i_new) 18 | possible_all_A.append(possible_A_i_new) 19 | 20 | return list(cartes(*possible_all_A)) 21 | 22 | 23 | def sanity_check_dp(A_org, XW, U, L, delta_l, delta_g, check_symmetry=True, \ 24 | activation='linear'): 25 | """ 26 | Sanity approach for solving min_{A_G^{1+2+3}} F_c(A) + np.sum(A.*L) 27 | 28 | param: 29 | A_org: original adjacency matrix 30 | XW: XW 31 | U: (u_y-u_c)/nG 32 | L: L 33 | delta_l: row budgets 34 | delta_g: global budgets 35 | check_symmetry: If True, optA is symmtric 36 | activation: 'linear' or 'relu' 37 | 38 | return a dict with keywords: 39 | opt_A: optimal perturbed matrix 40 | opt_f: optimal dual objective 41 | """ 42 | nG = A_org.shape[0] 43 | if nG > 6 and delta_g > 2: 44 | print("Sanity check only support nG < 7, return None!") 45 | else: 46 | if delta_g == 2: 47 | Flip_idx = [] 48 | for row in range(nG): 49 | for col in range(row+1, nG): 50 | if delta_l[row] > 0 and delta_l[col] > 0: 51 | Flip_idx.append([(row, col), (col, row)]) 52 | 53 | minimum = np.inf 54 | for idx in Flip_idx: 55 | A = A_org.copy() 56 | for s in idx: 57 | A[s] = 1-A[s] 58 | val = calculate_Fc(A, XW, U, activation) + np.sum(L*A) 59 | if val < minimum: 60 | minimum = val 61 | A_final = A 62 | 63 | else: 64 | all_possible_adjacency_matrices = possible_matrix_with_delta_l(A_org, delta_l) 65 | print('# matrice satisfing delta_l: ', len(all_possible_adjacency_matrices)) 66 | 67 | XWU = XW @ U 68 | minimum = np.inf 69 | for possible_matrix in all_possible_adjacency_matrices: 70 | possible_matrix = np.asarray(possible_matrix) 71 | 72 | symmetry = np.allclose(possible_matrix, possible_matrix.T) if check_symmetry else True 73 | if symmetry and np.sum(np.abs(A_org-possible_matrix)) <= delta_g: 74 | val = calculate_Fc(possible_matrix, XW, U, activation) + np.sum(L*possible_matrix) 75 | if val < minimum: 76 | minimum = val 77 | A_final = possible_matrix 78 | 79 | sol = { 80 | 'opt_A': A_final, 81 | 'opt_f': minimum 82 | } 83 | return sol 84 | 85 | def sanity_check_doubleL_relax(A_org, Q, p, L, delta_l, delta_g, check_symmetry=True): 86 | """ 87 | Sanity approach for solving min_{A_G^{1+2+3}} F_c(A) + np.sum(A.*L) 88 | 89 | param: 90 | A_org: original adjacency matrix 91 | Q: Q 92 | p: p 93 | L: L 94 | delta_l: row budgets 95 | delta_g: global budgets 96 | check_symmetry: If True, optA is symmtric 97 | activation: 'linear' or 'relu' 98 | 99 | return a dict with keywords: 100 | opt_A: optimal perturbed matrix 101 | opt_f: optimal dual objective 102 | """ 103 | 104 | nG = A_org.shape[0] 105 | if nG > 6: 106 | print("Sanity check only support nG < 7, return None!") 107 | else: 108 | all_possible_adjacency_matrices = possible_matrix_with_delta_l(A_org, delta_l) 109 | print('# matrice satisfing delta_l: ', len(all_possible_adjacency_matrices)) 110 | 111 | minimum = np.inf 112 | for possible_matrix in all_possible_adjacency_matrices: 113 | possible_matrix = np.asarray(possible_matrix) 114 | 115 | symmetry = np.allclose(possible_matrix, possible_matrix.T) if check_symmetry else True 116 | if symmetry and np.sum(np.abs(A_org-possible_matrix)) <= delta_g: 117 | val = calculate_doubleL_Fc(possible_matrix, Q, p) + np.sum(L*possible_matrix) 118 | if val < minimum: 119 | minimum = val 120 | A_final = possible_matrix 121 | 122 | sol = { 123 | 'opt_A': A_final, 124 | 'opt_f': minimum 125 | } 126 | return sol 127 | 128 | 129 | def sanity_check_polar_operator(A_from_dp, A_org, R, delta_l, delta_g): 130 | nG = A_org.shape[0] 131 | all_possible_adjacency_matrices = possible_matrix_with_delta_l(A_org, delta_l) 132 | print('# matrice satisfing delta_l: ', len(all_possible_adjacency_matrices)) 133 | 134 | A_final = np.array([]) 135 | m = -float('inf') 136 | for possible_matrix in all_possible_adjacency_matrices: 137 | possible_matrix = np.array([np.array(i) for i in possible_matrix]) 138 | 139 | # if np.allclose(possible_matrix,possible_matrix.T) and check_global_budget(possible_matrix, A_org, delta_l, delta_g): 140 | if check_global_budget(possible_matrix, A_org, delta_l, delta_g): 141 | if np.array_equal(possible_matrix,A_from_dp): 142 | print('--- Match Found ---') 143 | val = np.sum(R*possible_matrix) 144 | if val > m: 145 | m = val 146 | A_final = possible_matrix.copy() 147 | 148 | print("---DP Solution---") 149 | print(A_from_dp) 150 | print(calculate_budget(A_from_dp, A_org, delta_l, delta_g)) 151 | print(np.sum(R*A_from_dp)) 152 | 153 | print('---Brute Force Solution----') 154 | print(A_final) 155 | print(calculate_budget(A_final, A_org, delta_l, delta_g)) 156 | print(m) 157 | print('-------------------------------------------------------') -------------------------------------------------------------------------------- /robograph/attack/genetic_attack.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from robograph.attack.utils import calculate_Fc, check_budgets_and_symmetry 3 | from copy import deepcopy 4 | import random 5 | 6 | 7 | np.random.seed(0) 8 | 9 | population_size = 100 #Population Size for Each Generation 10 | cross_rate = 0.2 #Generally from 0.1 to 0.5 11 | mutate_rate = 0.4 #Generally from 0.1 to 0.5 12 | # rounds = 10 #Number of Rounds of evolution 13 | 14 | def calculate_budget(A_org, A_perturbed): 15 | """ 16 | Returns the total pertubations count 17 | """ 18 | return int(np.sum(abs(A_org - A_perturbed))) 19 | 20 | def check_local(A_org_row, A_perturbed_row): 21 | """ 22 | Returns the total pertubations in a row 23 | """ 24 | return int(np.sum(abs(A_org_row - A_perturbed_row))) 25 | 26 | class Genetic_Attack(object): 27 | """ 28 | Genetic algorithm (upper bound) for solving min_{A_G^{1+2+3}} F_c(A) 29 | 30 | param: 31 | A_org: original adjacency matrix 32 | XW: XW 33 | U: (u_y-u_c)/nG 34 | delta_l: row budgets (vector) 35 | delta_g: global budget (scalar) 36 | activation: 'linear' or 'relu' 37 | """ 38 | 39 | def __init__(self, A_org, XW, U, delta_l, delta_g, activation): 40 | self.A_org = A_org 41 | self.XW, self.U = XW, U 42 | self.XWU = XW @ U 43 | self.delta_l = delta_l 44 | self.delta_g = delta_g 45 | self.activation = activation 46 | self.nG = A_org.shape[0] 47 | self.fc = calculate_Fc(A_org, self.XW, self.U,self.activation) 48 | self.population = [] 49 | self.solution = None 50 | self.sol = None 51 | for k in range(population_size): 52 | L = np.zeros(self.nG,dtype=int) #Array Containing Local Budgets 53 | F = np.zeros((self.nG,self.nG),dtype=int) 54 | for i in range(self.nG): 55 | while True: 56 | l_temp = np.random.randint(0,int(self.delta_l[0])+1,size=1) 57 | if sum(L) + l_temp <= self.delta_g: 58 | break 59 | L[i] = l_temp 60 | indices_flip = np.random.choice(self.nG,l_temp, replace = False) 61 | F[i,indices_flip] = 1 62 | A_new = np.multiply(1-2*A_org, F) + A_org 63 | np.fill_diagonal(A_new, 0) 64 | A_new = (A_new + A_new.T)//2 65 | self.population.append(A_new) 66 | 67 | 68 | def fitness(self): 69 | fitness_scores = np.zeros(len(self.population)) 70 | if self.solution is None: 71 | for i in range(len(self.population)): 72 | fitness_scores[i] = calculate_Fc(self.population[i], self.XW, self.U, self.activation) 73 | if fitness_scores[i] < self.fc: 74 | self.fc = fitness_scores[i] 75 | self.sol = { 76 | 'opt_A': self.population[i], 77 | 'opt_f': fitness_scores[i] 78 | } 79 | if fitness_scores[i] < 0: 80 | self.solution = { 81 | 'opt_A': self.population[i], 82 | 'opt_f': fitness_scores[i] 83 | } 84 | break 85 | return fitness_scores 86 | 87 | 88 | def select(self, fitness): 89 | scores = np.exp(fitness) 90 | min_args = np.argsort(scores) 91 | 92 | result = [] 93 | for i in range(population_size - population_size // 2): 94 | result.append(deepcopy(self.population[min_args[i]])) 95 | 96 | idx = np.random.choice(np.arange(population_size), 97 | size=population_size // 2, 98 | replace=True, 99 | p=scores/scores.sum()) 100 | 101 | for i in idx: 102 | result.append(deepcopy(self.population[i])) 103 | 104 | return result 105 | 106 | 107 | def crossover(self, parent, pop): 108 | if np.random.rand() < cross_rate: 109 | another = pop[np.random.randint(len(pop))] 110 | lll = np.random.rand() 111 | if lll <= 0.25: 112 | return np.copy(another) 113 | elif lll>0.25 and lll<=0.5: 114 | return np.copy(parent) 115 | else: 116 | tem = None 117 | count = 0 118 | for k in range(3): 119 | tem = np.zeros((self.nG, self.nG),dtype=int) 120 | for i in range(self.nG): 121 | if np.random.rand() < 0.5: 122 | tem[i,i:] = parent[i,i:] 123 | tem[i:,i] = parent[i:,i] 124 | else: 125 | tem[i,i:] = another[i,i:] 126 | tem[i:,i] = another[i:,i] 127 | if check_local(tem[i],self.A_org) > int(self.delta_l[0]): 128 | tem = None 129 | break 130 | if tem!=None and calculate_budget(tem, self.A_org) <= self.delta_g: 131 | break 132 | if tem!=None: 133 | return tem 134 | else: 135 | return np.copy(another) 136 | else: 137 | return np.copy(parent) 138 | 139 | 140 | def mutate(self, child): 141 | if calculate_budget(self.A_org,child) == self.delta_g: 142 | return child 143 | mutated = [] 144 | for i in range(self.nG): 145 | if np.random.rand() < mutate_rate: 146 | for k in range(int(self.delta_l[0])//2): 147 | indices = np.random.choice(self.nG, 2, replace = False) 148 | for j in indices: 149 | if i in mutated or j in mutated: 150 | continue 151 | child[i,j], child[j,i] = 1 - child[i,j], 1 - child[j,i] 152 | mutated.append(i) 153 | mutated.append(j) 154 | if check_local(self.A_org[i],child[i]) <= int(self.delta_l[0]) and check_local(self.A_org[j],child[j]) <= int(self.delta_l[0]) and \ 155 | calculate_budget(self.A_org,child) <= self.delta_g: 156 | break 157 | else: 158 | child[i,j], child[j,i] = 1 - child[i,j], 1 - child[j,i] 159 | np.fill_diagonal(child, 0) 160 | return child 161 | 162 | 163 | def evolve(self): 164 | fitness = self.fitness() 165 | if self.solution is not None: 166 | return 167 | pop = self.select(fitness) 168 | 169 | new_pop_list = [] 170 | for parent in pop: 171 | child = self.crossover(parent, pop) 172 | child = self.mutate(child) 173 | new_pop_list.append(child) 174 | 175 | self.population = new_pop_list 176 | 177 | def attack(self, rounds): 178 | for i in range(rounds): 179 | self.evolve() 180 | if self.solution!=None: 181 | return self.solution 182 | else: 183 | return self.sol 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /robograph/model/gnn.py: -------------------------------------------------------------------------------- 1 | import os.path as osp 2 | import torch 3 | import torch.nn.functional as F 4 | 5 | from torch.nn import Module, Linear, Dropout 6 | from torch.optim import Adam 7 | from torch_geometric.nn import global_mean_pool as gap 8 | from torch_geometric.nn import global_max_pool as gmp 9 | from torch_geometric.nn import GlobalAttention 10 | from torch_scatter import scatter_add 11 | from robograph.model.gcn_conv import GCNConv 12 | from robograph.attack.dual import dual_solver, dual_solver_doubleL 13 | from robograph.attack.greedy_attack import Greedy_Attack 14 | from robograph.utils import process_data 15 | import numpy as np 16 | 17 | 18 | class GC_NET(Module): 19 | """ Graph classification using single layer GCN 20 | """ 21 | 22 | def __init__(self, hidden, n_features, n_classes, act='relu', pool='avg', dropout=0.): 23 | """ Model init 24 | 25 | Parameters 26 | ---------- 27 | hidden: int 28 | Size of hidden layer 29 | n_features: int 30 | Size of feature dimension 31 | n_classes: int 32 | Number of classes 33 | act: str in ['relu', 'linear'] 34 | Default: 'relu' 35 | pool: str in ['avg', 'max', 'att_h', 'att_x'] 36 | Default: 'avg' 37 | dropout: float 38 | Dropout rate in training. Default: 0. 39 | """ 40 | super(GC_NET, self).__init__() 41 | 42 | self.hidden = hidden 43 | self.n_features = n_features 44 | self.n_classes = n_classes 45 | self.act = act 46 | self.pool = pool 47 | 48 | # GCN layer 49 | self.conv = GCNConv(self.n_features, self.hidden, bias=False) 50 | # pooling 51 | if self.pool == 'att_x': 52 | self.att_x = Linear(self.n_features, self.n_features, bias=False) 53 | elif self.pool == 'att_h': 54 | self.att_h = GlobalAttention(torch.nn.Linear(self.hidden, 1)) 55 | # linear output 56 | self.lin = Linear(self.hidden, self.n_classes, bias=False) 57 | # dropout 58 | self.dropout = Dropout(dropout) 59 | 60 | def forward(self, data): 61 | """ Forward computation of GC_NET model, computes the logits for each 62 | graph. 63 | 64 | Parameters 65 | ---------- 66 | data: ptg.Data 67 | 68 | Returns 69 | ------- 70 | logits: torch.tensor float32 [B, K] 71 | Logits of prediction for each label 72 | """ 73 | x, edge_index, batch = data.x, data.edge_index, data.batch 74 | 75 | if self.training: 76 | x = self.dropout(x) 77 | if self.pool == 'att_x': 78 | P = torch.matmul(self.att_x(x), x.T) 79 | P = torch.sigmoid(P) 80 | P = F.softmax(scatter_add(P, batch, dim=0), dim=-1) 81 | 82 | if self.act == 'relu': 83 | x = F.relu(self.conv(x, edge_index)) 84 | else: 85 | x = self.conv(x, edge_index) 86 | 87 | if self.training: 88 | x = self.dropout(x) 89 | 90 | if self.pool == 'avg': 91 | x = gap(x, batch) 92 | elif self.pool == 'max': 93 | x = gmp(x, batch) 94 | elif self.pool == 'att_h': 95 | x = self.att_h(x, batch) 96 | else: 97 | x = torch.matmul(P, x) 98 | 99 | logits = self.lin(x) 100 | return logits 101 | 102 | def predict(self, data): 103 | """ Predcit the classes of the input data 104 | 105 | Parameters 106 | ---------- 107 | data: ptg.Data 108 | 109 | Returns 110 | ------- 111 | labels: list 112 | Labels of the predicted data [B, ] 113 | """ 114 | return self.forward(data).argmax(1).detach() 115 | 116 | 117 | def train(model, loader, robust=False, adv_loader=None, lamb=0): 118 | """ Train GC_Net 119 | 120 | Parameters 121 | ---------- 122 | model: GC_NET instance 123 | loader: torch.util.data.DataLoader 124 | DataLoader with each data in torch.Data 125 | robust: bool 126 | Flag for robust training. Defualt: False 127 | 128 | Returns 129 | ------- 130 | loss: float 131 | Averaged loss on loader. 132 | """ 133 | model.train() 134 | _device = next(model.parameters()).device 135 | 136 | optimizer = Adam(model.parameters(), lr=0.001) 137 | loss_all = 0 138 | loader = loader if not robust else adv_loader 139 | 140 | for idx, data in enumerate(loader): 141 | data = data.to(_device) 142 | optimizer.zero_grad() 143 | output = model(data) 144 | loss = F.cross_entropy(output, data.y) 145 | 146 | if robust: 147 | ''' robust training with greedy attack ''' 148 | # _W = model.conv.weight.detach().cpu().numpy() 149 | # _U = model.lin.weight.detach().cpu().numpy() 150 | 151 | # for _ in range(20): 152 | # idx = np.random.randint(len(loader.dataset)) 153 | # _g_data = loader.dataset[idx] 154 | # A, X, y = process_data(_g_data) 155 | 156 | # deg = A.sum(1) 157 | # # local budget 158 | # delta_l = np.minimum(np.maximum(deg - np.max(deg) + 2, 159 | # 0), data.x.shape[0] - 1).astype(int) 160 | # # global budget 161 | # delta_g = 4 162 | 163 | # fc_vals_greedy = [] 164 | # for c in range(model.n_classes): 165 | # if c != y: 166 | # u = _U[y] - _U[c] 167 | # attack = Greedy_Attack(A, X@_W, u.T / data.x.shape[0], delta_l, delta_g, 168 | # activation=model.act) 169 | # greedy_sol = attack.attack(A) 170 | # fc_vals_greedy.append(-greedy_sol['opt_f']) 171 | # loss += max(max(fc_vals_greedy) + 1, 0) / 20 172 | # for adv in adv_loader: 173 | # adv = adv.to(_device) 174 | # output = model(adv) 175 | # loss = F.hinge_embedding_loss(output, torch.eye(output.shape[1])[data.y].to(_device), margin=0.5) 176 | # loss 177 | loss += lamb * F.hinge_embedding_loss(output, torch.eye(output.shape[1])[data.y].to(_device)) 178 | # loss = F.multilabel_margin_loss(output.argmax(1), data.y).to(_device) 179 | loss.backward() 180 | optimizer.step() 181 | loss_all += data.num_graphs * loss.item() 182 | 183 | return loss_all / len(loader.dataset) 184 | 185 | 186 | def eval(model, loader, testing=False, save_path=None, robust=False): 187 | """ Evaluate model with dataloader 188 | 189 | Parameters 190 | ---------- 191 | model: GC_NET instance 192 | loader: torch.util.data.DataLoader 193 | DataLoader with each data in torch.Data 194 | testing: bool 195 | Flag for testing. Default: False 196 | save_path: str 197 | Load model from saved path. Default: None 198 | robust: bool 199 | Flag for robust training. Defualt: False 200 | 201 | Returns 202 | ------- 203 | accuracy: float 204 | Accuracy with loader 205 | """ 206 | model.eval() 207 | _device = next(model.parameters()).device 208 | if testing: 209 | result_fn = 'result_robust.pk' if robust else 'result.pk' 210 | model.load_state_dict(torch.load(osp.join(save_path, result_fn))) 211 | 212 | correct = 0 213 | for data in loader: 214 | data = data.to(_device) 215 | output = model(data) 216 | pred = output.argmax(dim=1) 217 | correct += pred.eq(data.y).sum().item() 218 | return correct / len(loader.dataset) 219 | -------------------------------------------------------------------------------- /robograph/attack/dp_bak.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | from numba import njit, prange 4 | 5 | @njit(parallel=True, fastmath=True, cache=True) 6 | # @njit(parallel=True, fastmath=True) 7 | # @njit 8 | def local_solver_linear_term(A_org, XWU, delta_l, L): 9 | """ 10 | solver of equation 8&11 of the paper when activation is identity, max_margin loss and average pooling 11 | """ 12 | nG = A_org.shape[0] 13 | a = np.zeros((nG+1, delta_l+1)) # matrix a for described in equation 6 14 | add_edge_matrix = np.zeros((nG+1, delta_l+1)) 15 | for i in prange(1, nG+1): # looping each row of A 16 | A_i = A_org[i-1,:] 17 | A_i_edges = int(np.sum(A_i)) 18 | L_i = L[:, i-1] 19 | max_edges = min(A_i_edges + delta_l + 1, nG) 20 | min_edges = max(A_i_edges - delta_l + 1, 1) 21 | possible_denomi = max_edges - min_edges + 1 22 | chunk_edges_mtx, chunk_no_edges_mtx = np.zeros((possible_denomi,delta_l+1)), np.zeros((possible_denomi,delta_l+1)) 23 | for x in range(min_edges, max_edges+1): # looping all possible (1'A_i + 1) 24 | V_L = XWU + L_i.T*x 25 | indices = np.argsort(V_L) 26 | chunk_edges, chunk_no_edges = [0.0]*(delta_l+1), [0.0]*(delta_l+1) 27 | temp_idx = 1 28 | for y in indices: 29 | if temp_idx > delta_l: break 30 | if y == i-1: continue # excluding self edge 31 | if A_i[y] == 0: 32 | chunk_no_edges[temp_idx] = V_L[y] + chunk_no_edges[temp_idx-1] 33 | temp_idx += 1 34 | 35 | temp_idx = 1 36 | for y in indices[::-1]: 37 | if temp_idx > delta_l: break 38 | if y == i-1: continue # excluding self edge 39 | if A_i[y] == 1: 40 | chunk_edges[temp_idx] = V_L[y] + chunk_edges[temp_idx-1] 41 | temp_idx += 1 42 | 43 | chunk_edges_mtx[x - min_edges] = chunk_edges 44 | chunk_no_edges_mtx[x - min_edges] = chunk_no_edges 45 | 46 | 47 | A_V_i = np.dot(A_i, np.ascontiguousarray(XWU)) + XWU[i-1] 48 | A_L_i = np.dot(A_i, np.ascontiguousarray(L_i)) 49 | a[i,0] = A_V_i/(A_i_edges+1) + A_L_i 50 | for j in range(1,delta_l+1): # looping each possible local constraint 51 | min_f = np.inf 52 | for k in range(j+1): # looping different combinations of adding/removing 53 | add_edges, remove_edges = k, j-k 54 | if A_i_edges+add_edges > nG-1 or A_i_edges-remove_edges < 0: 55 | continue 56 | 57 | new_edges = A_i_edges+add_edges-remove_edges + 1 58 | f = A_V_i + A_L_i*new_edges 59 | 60 | # adding k edges from chunk of A_i=0 in ascent order 61 | if add_edges > 0: 62 | # print(chunk_no_edges_mtx[new_edges][add_edges]) 63 | f += chunk_no_edges_mtx[new_edges - min_edges][add_edges] 64 | 65 | # removing j-k edges from chunk of A_i=1 in descent order 66 | if remove_edges > 0: 67 | # print(chunk_edges_mtx[new_edges][remove_edges]) 68 | f -= chunk_edges_mtx[new_edges - min_edges][remove_edges] 69 | 70 | final_f = f/new_edges 71 | if final_f < min_f: 72 | min_f = final_f 73 | sol = (min_f, add_edges) 74 | a[i,j], add_edge_matrix[i,j] = sol 75 | return a, add_edge_matrix 76 | 77 | @njit(cache=True) 78 | def get_A_opt(XWU, A_i, L_i, nG, delta_l, i, j, add_edges): 79 | A_i_edges = np.sum(A_i) 80 | remove_edges = j - add_edges 81 | new_edges = A_i_edges+add_edges-remove_edges + 1 82 | V_L = XWU + L_i.T*new_edges 83 | indices = np.argsort(V_L) 84 | 85 | A_new_i = A_i.copy() 86 | added_edges = 0 87 | for y in indices: 88 | if added_edges == add_edges: break 89 | if y == i-1: continue # excluding self edge 90 | if A_i[y] == 0: 91 | A_new_i[y] = 1 92 | added_edges += 1 93 | 94 | removed_edges = 0 95 | for y in indices[::-1]: 96 | if removed_edges == remove_edges: break 97 | if y == i-1: continue # excluding self edge 98 | if A_i[y] == 1: 99 | A_new_i[y] = 0 100 | removed_edges += 1 101 | 102 | return A_new_i 103 | 104 | @njit(cache=True) 105 | def first_loop(a, delta_l, delta_g): 106 | nG = a.shape[0] - 1 107 | c = np.array([delta_l*x if delta_l*x < delta_g else delta_g for x in range(nG+1)]) 108 | # s = [np.array([0.0]*(i+1)) for i in c] 109 | s = np.zeros((nG+1, min(nG*delta_l, delta_g)+1)) 110 | for t in range(1, nG+1): 111 | st_1, st, at = s[t-1], s[t], a[t] 112 | for j in range(0,c[t]+1): 113 | m = np.inf 114 | for k in range(max(0, j-c[t-1]), min(j, delta_l)+1): 115 | m = min(st_1[j-k]+at[k], m) # accessing s seems costly 116 | st[j] = m 117 | return c, s 118 | 119 | @njit(cache=True) 120 | def second_loop(A_org, XWU, delta_l, linear_term, s, c, a, add_edge_matrix): 121 | nG = A_org.shape[0] 122 | A_pert = np.zeros((nG,nG)) 123 | j = np.argmin(s[nG]) # this sort takes nG*delta_l log(nG*delta_l) 124 | opt_val = s[nG][j] 125 | unpert_val = s[nG][0] 126 | for t in range(nG,0,-1): 127 | temp = np.ones(delta_l+1)*np.inf 128 | st_1, at = s[t-1], a[t] 129 | for k in range(max(0, j-c[t-1]), min(j, delta_l)+1): 130 | temp[k] = st_1[j-k] + at[k] 131 | kt = np.argmin(temp) 132 | j = j - kt 133 | A_pert[t-1,:] = get_A_opt(XWU, A_org[t-1,:], linear_term[:, t-1], nG, delta_l,\ 134 | t, kt, add_edge_matrix[t][kt]) 135 | 136 | return (unpert_val, opt_val, A_pert) 137 | 138 | 139 | def dp_solver_1(A_org, XWU, delta_l, delta_g, linear_term): 140 | """ 141 | DP for min_{A_G^{1+2}} F_c(A) (Algorithm 1) 142 | # 1. Precomputing matrix a 143 | # 2. DP to get matrix s 144 | # 3. Tracing back 145 | Complexity: nG^2*delta_l*log(nG) + nG*delta_l^2 + nG^2*delta_l^2 146 | 147 | param: 148 | A_org: original adjacency matrix 149 | XWU: XW*(u_y-u_c)/nG 150 | delta_l: row budgets 151 | delta_g: global budgets 152 | linear_term: linear term on A 153 | 154 | return: 155 | unpert_val: function value under A_org 156 | opt_val: function value under A_pert 157 | A_pert: optimal attacking adjacency matrix 158 | """ 159 | 160 | # start = time.time() 161 | a, add_edge_matrix = local_solver_linear_term(A_org, XWU, delta_l, linear_term) 162 | # print(f'Precomputation of matrix a: {time.time() - start}') 163 | 164 | # start = time.time() 165 | c, s = first_loop(a, delta_l, delta_g) 166 | # print(f'First loop in DP : {time.time() - start}') 167 | 168 | # start = time.time() 169 | sol = second_loop(A_org, XWU, delta_l, linear_term, s, c, a, add_edge_matrix) 170 | # print(f'Second loop in DP : {time.time() - start}') 171 | 172 | return sol 173 | 174 | 175 | def po_dp_solver(A_org, R, delta_l, delta_g): 176 | nG = A_org.shape[0] 177 | 178 | # precomputing a matrix 179 | J = R*(-2*A_org + 1) 180 | a = po_local_solver(J, nG, delta_l) 181 | 182 | A_pert = np.zeros((nG,nG)) 183 | V_pert = np.zeros((nG,nG)) 184 | c, s = first_loop(a, delta_l, delta_g) 185 | j = np.argmin(s[nG]) 186 | unpert_val = s[nG][0] 187 | opt_val = s[nG][j] 188 | for t in range(nG,0,-1): 189 | temp = np.ones(delta_l+1)*np.inf 190 | st_1, at = s[t-1], a[t] 191 | for k in range(max(0, j-c[t-1]), min(j, delta_l)+1): 192 | temp[k] = st_1[j-k] + at[k] 193 | kt = np.argmin(temp) 194 | j = j - kt 195 | V_pert[t-1,:] = optVt_from_a_tj(J[t-1, :], t, kt, delta_l) 196 | A_pert[t-1,:] = ((2*A_org[t-1, :] - 1)*(-2*V_pert[t-1,:]+1)+1)/2 197 | 198 | return A_pert 199 | 200 | def po_local_solver(J, nG, delta_l): 201 | a = np.zeros((nG+1, delta_l+1)) 202 | 203 | for i in range(1, nG+1): # looping each row of A 204 | J_i = J[i-1, :].copy() 205 | J_i = -np.delete(J_i, i-1) 206 | indices = np.argsort(J_i) 207 | 208 | for j in range(1,delta_l+1): # looping each possible local constraints 209 | a[i,j] = J_i[indices[j-1]] + a[i,j-1] 210 | return a 211 | 212 | def optVt_from_a_tj(J_t, t, j, delta_l): 213 | V = np.zeros(J_t.shape) 214 | indices = np.argsort(-J_t) 215 | changed_edges = 0 216 | for i in range(j+1): 217 | if indices[i] == t-1: continue 218 | V[indices[i]] = 1 219 | changed_edges += 1 220 | if changed_edges >= j: break 221 | return V 222 | -------------------------------------------------------------------------------- /robograph/attack/dp_old_version.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def local_solver(A_org, XWU, nG, delta_l): 4 | """ 5 | solver of equation 6 of the paper when activation is identity, max_margin loss and average pooling 6 | 7 | #Parameters: 8 | 9 | #Returns: 10 | # a: matrix of local problem 11 | # A 12 | """ 13 | a = np.zeros((nG+1, delta_l+1)) # matrix a for described in equation 6 14 | A_opt = np.zeros((nG+1, delta_l+1, nG)) 15 | 16 | indices = np.argsort(XWU) 17 | for i in range(1, nG+1): # looping each row of A 18 | A_i = A_org[i-1,:] 19 | A_i_edges = np.sum(A_i) 20 | canonical = np.zeros(nG); canonical[i-1] = 1.0 21 | chunk_edges, chunk_no_edges = [], [] 22 | for x in indices: 23 | if x == i-1: continue # excluding self edge 24 | if A_i[x] == 1: 25 | chunk_edges.append((XWU[x], x)) 26 | else: 27 | chunk_no_edges.append((XWU[x], x)) 28 | chunk_edges.reverse() 29 | 30 | for j in range(0,delta_l+1): # looping each possible local constraint 31 | if j == 0: 32 | A_opt[i, j] = A_i 33 | a[i,j] = np.dot(A_i + canonical, XWU)/(A_i_edges+1) 34 | else: 35 | min_f = float('inf') 36 | temp = [] 37 | for k in range(j+1): # looping different combinations of adding/removing 38 | add_edges, remove_edges = k, j-k 39 | if A_i_edges+add_edges > nG-1 or A_i_edges-remove_edges < 0: 40 | continue 41 | 42 | f = np.dot(A_i+canonical, XWU) 43 | # adding k edges from chunk of A_i=0 by ascent order 44 | add_edge_idx = [] 45 | if add_edges > 0: 46 | edge_val, add_edge_idx = zip(*chunk_no_edges[0:add_edges]) 47 | f += np.sum(edge_val) 48 | 49 | # removing j-k edges from chunk of A_i=1 by descent order 50 | remove_edge_idx = [] 51 | if remove_edges > 0: 52 | edge_val, remove_edge_idx = zip(*chunk_edges[0:remove_edges]) 53 | f -= np.sum(edge_val) 54 | 55 | final_f = f/(A_i_edges+add_edges-remove_edges + 1) 56 | temp.append( (final_f, add_edge_idx, remove_edge_idx) ) 57 | if final_f < min_f: 58 | min_f = final_f 59 | sol = (final_f, add_edge_idx, remove_edge_idx) 60 | 61 | final_f, add_edge_idx, remove_edge_idx = sol 62 | A_new_i = A_i.copy() 63 | A_new_i[list(add_edge_idx)] = 1 64 | A_new_i[list(remove_edge_idx)] = 0 65 | A_opt[i, j] = A_new_i 66 | f_1 = final_f 67 | # f_2 = np.dot(A_new_i+canonical,XWU)/(A_i_edges+len(add_edge_idx)-len(remove_edge_idx) + 1) 68 | a[i,j] = f_1 69 | return a, A_opt 70 | 71 | 72 | def local_solver_linear_term(A_org, XWU, nG, delta_l, linear_matrix): 73 | """ 74 | solver of equation 8&11 of the paper when activation is identity, max_margin loss and average pooling 75 | """ 76 | a = np.zeros((nG+1, delta_l+1)) # matrix a for described in equation 6 77 | A_opt = np.zeros((nG+1, delta_l+1, nG)) 78 | # complexity: nG^2 delta_l log(nG) + nG*delta_l^2 79 | # 50^2*10*5 + 50*100 = 130000 / 10^7 = 1/100 80 | L = linear_matrix 81 | for i in range(1, nG+1): # looping each row of A 82 | A_i = A_org[i-1,:] 83 | A_i_edges = np.sum(A_i) 84 | L_i = L[:, i-1] 85 | canonical = np.zeros(nG); canonical[i-1] = 1.0 86 | # precomuting chunk_edges and chunk_no_edges 87 | # need to loop possible values of (1'A_i+1) since it is in the numerator 88 | # ( A_i@V + I_i@V+ A_i @ L_i*(1'A_i + 1) )/(1'A_i + 1) 89 | chunk_edges_mtx, chunk_no_edges_mtx = [None]*(nG+1), [None]*(nG+1) 90 | max_edges = min(A_i_edges + delta_l + 1, nG) 91 | min_edges = max(A_i_edges - delta_l + 1, 1) 92 | for x in range(min_edges, max_edges+1): # looping all possible (1'A_i + 1) 93 | V_L = XWU + L_i.T*x 94 | indices = np.argsort(V_L) 95 | edge_temp, no_edge_temp = [], [] 96 | for y in indices: 97 | if y == i-1: continue # excluding self edge 98 | if A_i[y] == 1: 99 | edge_temp.append((V_L[y], y)) 100 | else: 101 | no_edge_temp.append((V_L[y], y)) 102 | edge_temp.reverse() 103 | 104 | # edge_temp = [[V_L[y], y] for y in reversed(indices) if A_i[y]==1 and y!=(i-1)] 105 | # no_edge_temp = [[V_L[y], y] for y in indices if A_i[y]==0 and y!=(i-1)] 106 | # for s1 in range(1, len(edge_temp)): 107 | # edge_temp[s1][0] += edge_temp[s1-1][0] 108 | # for s1 in range(1, len(no_edge_temp)): 109 | # no_edge_temp[s1][0] += no_edge_temp[s1-1][0] 110 | 111 | chunk_edges_mtx[x] = edge_temp 112 | chunk_no_edges_mtx[x] = no_edge_temp 113 | 114 | # chunk_edges_mtx, chunk_no_edges_mtx = [], [] 115 | # for x in range(nG+1): 116 | # V_L = XWU + L_i.T*x 117 | # indices = np.argsort(V_L) 118 | # edge_temp, no_edge_temp = [], [] 119 | # for y in indices: 120 | # if y == i-1: continue # excluding self edge 121 | # if A_i[y] == 1: 122 | # edge_temp.append((V_L[y], y)) 123 | # else: 124 | # no_edge_temp.append((V_L[y], y)) 125 | # edge_temp.reverse() 126 | # chunk_edges_mtx.append(edge_temp) 127 | # chunk_no_edges_mtx.append(no_edge_temp) 128 | 129 | A_V_i = np.dot(A_i + canonical, XWU) 130 | A_L_i = np.dot(A_i, L_i) 131 | for j in range(0,delta_l+1): # looping each possible local constraint 132 | if j == 0: 133 | A_opt[i, j] = A_i 134 | a[i,j] = A_V_i/(A_i_edges+1) + A_L_i 135 | else: 136 | min_f = float('inf') 137 | temp = [] 138 | for k in range(j+1): # looping different combinations of adding/removing 139 | add_edges, remove_edges = k, j-k 140 | if A_i_edges+add_edges > nG-1 or A_i_edges-remove_edges < 0: 141 | continue 142 | 143 | new_edges = A_i_edges+add_edges-remove_edges + 1 144 | f = A_V_i + A_L_i*new_edges 145 | 146 | # adding k edges from chunk of A_i=0 by ascent order 147 | add_edge_idx = [] 148 | if add_edges > 0: 149 | chunk_no_edges = chunk_no_edges_mtx[new_edges] 150 | edge_val, add_edge_idx = zip(*chunk_no_edges[0:add_edges]) 151 | f += np.sum(edge_val) 152 | # edge_val, add_edge_idx = zip(*chunk_no_edges[0:add_edges]) 153 | # f += edge_val[-1] 154 | 155 | # removing j-k edges from chunk of A_i=1 by descent order 156 | remove_edge_idx = [] 157 | if remove_edges > 0: 158 | chunk_edges = chunk_edges_mtx[new_edges] 159 | edge_val, remove_edge_idx = zip(*chunk_edges[0:remove_edges]) 160 | f -= np.sum(edge_val) 161 | # edge_val, remove_edge_idx = zip(*chunk_edges[0:remove_edges]) 162 | # f -= edge_val[-1] 163 | 164 | final_f = f/new_edges 165 | temp.append( (final_f, add_edge_idx, remove_edge_idx) ) 166 | if final_f < min_f: 167 | min_f = final_f 168 | sol = (final_f, add_edge_idx, remove_edge_idx) 169 | 170 | final_f, add_edge_idx, remove_edge_idx = sol 171 | A_new_i = A_i.copy() 172 | A_new_i[list(add_edge_idx)] = 1 173 | A_new_i[list(remove_edge_idx)] = 0 174 | A_opt[i, j] = A_new_i 175 | f_1 = final_f 176 | # f_2 = np.dot(A_new_i+canonical,XWU)/(A_i_edges+len(add_edge_idx)-len(remove_edge_idx) + 1) 177 | a[i,j] = f_1 178 | return a, A_opt 179 | 180 | 181 | def dp_solver(A_org, XWU, nG, delta_l, delta_g, linear_term=[]): 182 | """ 183 | DP for min_{A_G^{1+2}} F_c(A) (Algorithm 1) 184 | 185 | #Parameters: 186 | 187 | #Returns: 188 | # A_pert: optimal attack adjacency matrix 189 | """ 190 | # precomputing a matrix 191 | if len(linear_term) > 0: 192 | a, A_opt = local_solver_linear_term(A_org, XWU, nG, delta_l, linear_term) 193 | else: 194 | a, A_opt = local_solver(A_org, XWU, nG, delta_l) 195 | 196 | A_pert = np.zeros((nG,nG)) 197 | s = np.ones((nG+1, min(nG*delta_l, delta_g)+1))*float('inf') 198 | s[0,0] = 0 199 | c = np.zeros(nG+1,dtype=int) 200 | # nG*min(nG*delta_l, delta_g)*delta_l = 50*500*10 = 250000 201 | for t in range(1, nG+1): 202 | c[t] = int(min(c[t-1] + delta_l, delta_g)) 203 | for j in range(0,c[t]+1): 204 | m = float('inf') 205 | for k in range(0, delta_l+1): 206 | if j-k>=0 and j-k<=c[t-1]: 207 | s[t,j] = min(s[t-1,j-k]+a[t,k], m) 208 | m = s[t,j] 209 | J = np.array([0]*(nG+1)) 210 | J[nG] = np.argmin(s[nG,1:])+1 211 | # print(s) 212 | # print(J[nG]) 213 | K = np.zeros(nG+1,dtype=int) 214 | for t in range(nG,0,-1): 215 | temp = np.ones(delta_l+1)*float('inf') 216 | for k in range(0,delta_l+1): 217 | if (J[t] - k)>=0 and (J[t]-k)<= c[t-1]: 218 | temp[k] = s[t-1,J[t]-k] + a[t,k] 219 | K[t] = int(np.argmin(temp)) 220 | J[t-1] = J[t] - K[t] 221 | A_pert[t-1,:] = A_opt[t,K[t]] 222 | 223 | unpert_val = s[nG, 0] 224 | opt_val = s[nG, J[nG]] 225 | return (unpert_val, opt_val, A_pert) 226 | 227 | if __name__ == "__main__": 228 | np.random.seed(1) 229 | 230 | delta_l,delta_g = 2, 7 #Setting the delta_l and delta_g values 231 | nG = 4 # Setting theNumber of Nodes 232 | 233 | h = 7 # dimension of hidden representation 234 | XW = np.random.randn(nG, h) 235 | U = np.random.randn(h) 236 | XWU = XW @ U 237 | 238 | A_org = np.random.randint(2, size = (nG,nG)) # Random Adjacency Matrix 239 | for i in range(nG): 240 | A_org[i, i] = 0 # Adjacency Matrix must be zero diagonal 241 | 242 | lam = np.random.randn(nG, nG)*5 243 | linear_term = np.transpose(lam) - lam 244 | sol = dp_solver(A_org, XWU, nG, delta_l, delta_g, linear_term) 245 | unpert_val, opt_val, A_pert = sol 246 | print(A_org, unpert_val) 247 | print(A_pert, opt_val) 248 | print(np.sum(abs(A_org-A_pert), axis=1)) 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | -------------------------------------------------------------------------------- /robograph/attack/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | import scipy.optimize as optim 4 | from docplex.mp.model import Model 5 | from qpsolvers import solve_qp 6 | from scipy.sparse import identity 7 | 8 | 9 | def projection_coA1(A, A_org, delta_l): 10 | if np.all((A >= 0) & (A <= 1)) and np.all(np.abs(A-A_org).sum(1) <= delta_l): 11 | return A 12 | proj_Z = np.zeros(A.shape) 13 | for i in range(A.shape[0]): 14 | proj_Z[i] = projection_coA2(A[i], A_org[i], delta_l[i]) 15 | 16 | eps = 1e-5 17 | assert np.all((proj_Z >= (-eps)) & (proj_Z <= (1+eps))) and np.all(np.abs(proj_Z-A_org).sum(1) <= (delta_l+eps)) 18 | return proj_Z 19 | 20 | 21 | def projection_coA2(A, A_org, delta_g): 22 | if delta_g == 0: 23 | return A_org 24 | if np.all((A >= 0) & (A <= 1)) and np.sum(np.abs(A-A_org)) <= delta_g: 25 | return A 26 | # Dual approach 27 | # start = time.time() 28 | # lamb, iters = 0.1, 10 29 | # bounds = [(0, np.inf)] 30 | # def objective(lamb): 31 | # opt_z = A - lamb*(-2*A_org+1) 32 | # opt_z[opt_z>1] = 1 33 | # opt_z[opt_z<0] = 0 34 | # s = np.sum(np.abs(opt_z - A_org)) 35 | # f = np.sum((opt_z-A)**2)/2 + lamb*( s - delta_g) 36 | # grad_on_lamb = -delta_g + s 37 | # return -f, -np.array([grad_on_lamb]) 38 | # # print(optim.check_grad(lambda x: objective(x)[0], lambda x: objective(x)[1], np.asarray([lamb]))) 39 | # def callback(x): 40 | # print(objective(x)[0]) 41 | 42 | # res = optim.fmin_l_bfgs_b(objective, lamb, bounds=bounds, maxiter=iters, callback=None) 43 | # opt_z = A - res[0]*(-2*A_org+1) 44 | # opt_z[opt_z>1] = 1 45 | # opt_z[opt_z<0] = 0 46 | # f = np.sum((opt_z-A_org)**2)/2 47 | # print(f, np.sum(np.abs(opt_z-A_org))) 48 | # print(f'bfgs cputime: {time.time() - start}') 49 | 50 | # Quadractic Programming (quadprog) 51 | start = time.time() 52 | n = len(A_org) 53 | P = np.eye(n) 54 | q = -A 55 | G = -2*A_org + 1 56 | h = np.array([delta_g - np.sum(A_org)]) 57 | lb = np.zeros(n) 58 | ub = np.ones(n) 59 | opt_z_1 = solve_qp(P, q, G, h, lb=lb, ub=ub) 60 | # f_1 = np.sum((opt_z_1-A_org)**2)/2 61 | # print(f_1, np.sum(np.abs(opt_z_1-A_org))) 62 | # print(f'qp cputime: {time.time() - start}') 63 | 64 | return opt_z_1 65 | 66 | 67 | def projection_coPi_and_affine(Ai, Ai_org, delta_l, alpha): 68 | if np.all((Ai >= 0) & (Ai <= 1)) and np.sum(np.abs(Ai-Ai_org)) <= delta_l \ 69 | and np.sum(Ai) == alpha: 70 | return Ai 71 | lamb, u = 0.1, 0.1 72 | for i in range(10): 73 | opt_z = Ai - lamb*(1-2*Ai_org) - u 74 | opt_z[opt_z > 1] = 1 75 | opt_z[opt_z < 0] = 0 76 | grad_on_lamb = -delta_l + np.sum(np.abs(opt_z - Ai_org)) 77 | grad_on_u = -alpha + np.sum(opt_z) 78 | lamb = max(0, lamb+0.01*grad_on_lamb) 79 | u += 0.01*grad_on_u 80 | # print(lamb, u, np.sum((opt_z-Ai)**2)) 81 | # print('final slackness: ', np.sum(opt_z), alpha, np.sum(np.abs(opt_z-Ai)), delta_l) 82 | # assert np.sum(np.abs(opt_z-Ai)) <= delta_l+0.1 83 | return opt_z 84 | 85 | 86 | def projection_A3(A): 87 | return (1/2) * (A + A.T) 88 | 89 | 90 | def projection_A123(A, A_org, delta_l, delta_g): 91 | n = A.shape[0] 92 | for i in range(20): 93 | A_begin = A.copy() 94 | A_proj = projection_A3(A_begin) 95 | A_proj = projection_coA1(delete_diagonal(A_proj), delete_diagonal(A_org), delta_l) 96 | A = fill_diagonal(A_proj) 97 | A_proj = projection_coA2(delete_diagonal(A).flatten(), delete_diagonal(A_org).flatten(), delta_g) 98 | A = fill_diagonal(A_proj.reshape(n, n-1)) 99 | # A = projection_coA1(A_begin, A_org, delta_l) 100 | # A = projection_coA2(A, A_org, delta_g) 101 | 102 | changes = np.max(np.abs(A-A_begin)) 103 | # print(changes) 104 | if changes < 1e-5: 105 | break 106 | return A 107 | 108 | 109 | def delete_diagonal(A): 110 | return A[~np.eye(A.shape[0], dtype=bool)].reshape(A.shape[0], -1) 111 | 112 | 113 | def fill_diagonal(A, value=0): 114 | n = A.shape[0] 115 | S = np.ones((n, n))*value 116 | S[:, :-1] += np.tril(A, -1) 117 | S[:, 1:] += np.triu(A, 0) 118 | return S 119 | 120 | 121 | def check_budgets_and_symmetry(A_new, A, delta_l, delta_g, check_symmetry=True): 122 | """ 123 | Check if A_new satisfies: local budgets & global budgets & symmetry 124 | """ 125 | nG = A.shape[0] 126 | local_budgets, global_budgets = np.zeros(nG), 0 127 | for i in range(nG): 128 | if A_new[i, i]: 129 | print('Violate zero diagonals!') 130 | return False 131 | local = np.sum(abs(A[i]-A_new[i])) 132 | if local > delta_l[i]: 133 | print('Violate local budgets!') 134 | return False 135 | local_budgets[i] = local 136 | global_budgets += local 137 | if global_budgets > delta_g: 138 | print('Violate global budgets!') 139 | return False 140 | 141 | if check_symmetry: 142 | if not np.array_equal(A_new, A_new.T): 143 | print('A_pert is non-symmetric!') 144 | return False 145 | return True 146 | 147 | 148 | def calculate_Fc(A, XW, U, activation='linear'): 149 | """ 150 | Calculate F_c 151 | """ 152 | nG = A.shape[0] 153 | total_val = 0 154 | if activation == 'linear': 155 | XWU = XW@U 156 | for i in range(nG): 157 | total_val += (A[i] @ XWU + XWU[i]) / (np.sum(A[i]) + 1) 158 | else: 159 | for i in range(nG): 160 | total_val += (np.maximum(A[i] @ XW + XW[i], 0) @ U) / (np.sum(A[i]) + 1) 161 | return total_val 162 | 163 | 164 | def calculate_doubleL_Fc(A, Q, p): 165 | """ 166 | Calculate F_c 167 | """ 168 | total_val = 0 169 | nG = A.shape[0] 170 | f = np.sum((A + np.eye(nG)) * Q, axis=1) + p 171 | de = 1+np.sum(A, axis=1) 172 | total_val = np.sum(f/de) 173 | return total_val 174 | 175 | 176 | def display(A_org, XW, U, delta_l, delta_g, solutions): 177 | print('') 178 | # print('original A:\n', A_org) 179 | print('original_Fc: ', calculate_Fc(A_org, XW, U)) 180 | 181 | iters = 100 182 | x_axis = list(range(1, iters+1)) 183 | fig, ax = plt.subplots() 184 | 185 | if 'brute_sol' in solutions: 186 | brute_sol = solutions['brute_sol'] 187 | brute_opt_A = brute_sol['opt_A'] 188 | brute_opt_f = brute_sol['opt_f'] 189 | # print(check_budgets_and_symmetry(brute_opt_A, A_org, delta_l, delta_g)) 190 | print('brute_opt_f: ', brute_opt_f) 191 | ax.plot(x_axis, [brute_opt_f]*iters, 'g-', label='brute-force') 192 | 193 | if 'admm_sol' in solutions: 194 | admm_sol = solutions['admm_sol'] 195 | admm_opt_A = admm_sol['opt_A'] 196 | admm_opt_f = admm_sol['opt_f'] 197 | if check_budgets_and_symmetry(admm_opt_A, A_org, delta_l, delta_g): 198 | symmtric = 'symmetric' 199 | else: 200 | symmtric = 'non_symmetric' 201 | print('admm_opt_f: ', admm_opt_f, '(A_admm is', symmtric, ')') 202 | ax.plot(x_axis, [admm_opt_f]*iters, 'r-', label='admm') 203 | 204 | if 'admm_g_sol' in solutions: 205 | admm_g_sol = solutions['admm_g_sol'] 206 | admm_g_opt_A = admm_g_sol['opt_A'] 207 | admm_g_opt_f = admm_g_sol['opt_f'] 208 | # print(check_budgets_and_symmetry(admm_g_opt_A, A_org, delta_l, delta_g)) 209 | print('admm_g_opt_f: ', admm_g_opt_f) 210 | ax.plot(x_axis, [admm_g_opt_f]*iters, 'c-', label='admm_greedy') 211 | 212 | if 'greedy_sol' in solutions: 213 | greedy_sol = solutions['greedy_sol'] 214 | greedy_opt_A = greedy_sol['opt_A'] 215 | greedy_opt_f = greedy_sol['opt_f'] 216 | # print('greedy opt A:\n', greedy_opt_A) 217 | # print(check_budgets_and_symmetry(greedy_opt_A, A_org, delta_l, delta_g)) 218 | print('greedy_opt_f: ', greedy_opt_f) 219 | ax.plot(x_axis, [greedy_opt_f]*iters, 'm--', label='greedy') 220 | 221 | if 'dual_sol' in solutions: 222 | brute_sol = solutions['dual_sol'] 223 | dual_opt_A = dual_sol['opt_A'] 224 | dual_opt_f = dual_sol['opt_f'] 225 | # print(check_budgets_and_symmetry(dual_opt_A, A_org, delta_l, delta_g)) 226 | print('dual_opt_f: ', dual_opt_f) 227 | ax.plot(x_axis, [dual_opt_f]*iters, 'b-.', label='dual') 228 | 229 | if 'cvx_env_sol' in solutions: 230 | cvx_env_sol = solutions['cvx_env_sol'] 231 | cvx_env_opt_A = cvx_env_sol['opt_A'] 232 | cvx_env_opt_f = cvx_env_sol['opt_f'] 233 | print('cvx_env_opt: ', cvx_env_opt_f) 234 | ax.plot(x_axis, [cvx_env_opt_f] * iters, 'k-', label='cvx_env') 235 | 236 | if 'cvx_pers_sol' in solutions: 237 | cvx_pers_sol = solutions['cvx_pers_sol'] 238 | cvx_pers_opt_A = cvx_pers_sol['opt_A'] 239 | cvx_pers_opt_f = cvx_pers_sol['opt_f'] 240 | print('cvx_pers_opt: ', cvx_pers_opt_f) 241 | ax.plot(x_axis, [cvx_pers_opt_f] * iters, 'y-', label='cvx_pers') 242 | 243 | legend = ax.legend(loc='upper right', fontsize='large') 244 | plt.ylabel('f_val', fontsize=16) 245 | plt.xlabel('iter', fontsize=16) 246 | plt.savefig('robograph/tests/bound_linear.png') 247 | -------------------------------------------------------------------------------- /robograph/attack/SPG.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import sys 3 | # NOT IMPLEMENTED: 4 | # Cubic line search is not implemented (Currently only halving) 5 | 6 | # Options: 7 | # verbose: level of verbosity (0: no output, 1: final, 2: iter (default), 3:debug) 8 | # optTol: tolerance used to check for optimality 9 | # progTol: tolerance used to check for lack of progress 10 | # maxIter: maximum number of calls to funObj 11 | # suffDec: sufficient decrease parameter in Armijo condition 12 | # curvilinear: backtrack along projection arc 13 | # memory: number of steps to look back in non-monotone Armijo condition 14 | # bbType: type of Barzilai-Borwein step 15 | # interp: 0=none, 2=cubic (for the most part.. see below) 16 | # numDiff: compute derivatives numerically (0: use user-supplied derivatives (default), 1: use finite differences) 17 | 18 | # V0.2 Feb 15th 2014 19 | # Python code by: Tomer Levinboim (first.last at usc.edu) 20 | # Original matlab code by Mark Schmidt: 21 | # http://www.di.ens.fr/~mschmidt/Software/minConf.html 22 | class SPGOptions(): 23 | pass 24 | 25 | default_options = SPGOptions() 26 | default_options.maxIter = 500 27 | default_options.verbose = 2 28 | default_options.suffDec = 1e-4 29 | default_options.progTol = 1e-9 30 | default_options.optTol = 1e-5 31 | default_options.curvilinear = False 32 | default_options.memory = 10 33 | default_options.useSpectral = True 34 | default_options.bbType = 1 35 | default_options.interp = 2 # cubic 36 | default_options.numdiff = 0 37 | default_options.testOpt = True 38 | 39 | 40 | def log(options, level, msg): 41 | if options.verbose >= level: 42 | print(msg, file=sys.stderr) 43 | 44 | 45 | def assertVector(v): 46 | assert len(v.shape) == 1 47 | 48 | 49 | def SPG(funObj0, funProj, x, options=default_options): 50 | x = funProj(x) 51 | i = 1 # iteration 52 | 53 | funEvalMultiplier = 1 54 | if options.numdiff == 1: 55 | funObj = lambda x: auto_grad(x, funObj0, options) 56 | funEvalMultiplier = len(x)+1 57 | else: 58 | funObj = funObj0 59 | 60 | f, g = funObj(x) 61 | projects = 1 62 | funEvals = 1 63 | 64 | if options.verbose >= 2: 65 | if options.testOpt: 66 | print('%10s %10s %10s %15s %15s %15s' % ('Iteration', 'FunEvals', 'Projections', 'Step Length', 'Function Val', 'Opt Cond')) 67 | else: 68 | print('%10s %10s %10s %15s %15s' % ('Iteration', 'FunEvals', 'Projections', 'Step Length', 'Function Val')) 69 | 70 | while funEvals <= options.maxIter: 71 | if i == 1 or not options.useSpectral: 72 | alpha = 1.0 73 | else: 74 | y = g - g_old 75 | s = x - x_old 76 | assertVector(y) 77 | assertVector(s) 78 | 79 | # type of BB step 80 | if options.bbType == 1: 81 | alpha = np.dot(s.T, s) / np.dot(s.T, y) 82 | else: 83 | alpha = np.dot(s.T, y) / np.dot(y.T, y) 84 | 85 | if alpha <= 1e-10 or alpha > 1e10: 86 | alpha = 1.0 87 | 88 | d = -alpha * g 89 | f_old = f 90 | x_old = x 91 | g_old = g 92 | 93 | if not options.curvilinear: 94 | d = funProj(x + d) - x 95 | projects += 1 96 | 97 | gtd = np.dot(g, d) 98 | 99 | if gtd > -options.progTol: 100 | log(options, 1, 'Directional Derivative below progTol') 101 | break 102 | 103 | if i == 1: 104 | t = min([1, 1.0 / np.sum(np.absolute(g))]) 105 | else: 106 | t = 1.0 107 | 108 | if options.memory == 1: 109 | funRef = f 110 | else: 111 | if i == 1: 112 | old_fvals = np.tile(-np.inf, (options.memory, 1)) 113 | 114 | if i <= options.memory: 115 | old_fvals[i - 1] = f 116 | else: 117 | old_fvals = np.vstack([old_fvals[1:], f]) 118 | 119 | funRef = np.max(old_fvals) 120 | 121 | if options.curvilinear: 122 | x_new = funProj(x + t * d) 123 | projects += 1 124 | else: 125 | x_new = x + t * d 126 | 127 | f_new, g_new = funObj(x_new) 128 | funEvals += 1 129 | lineSearchIters = 1 130 | while f_new > funRef + options.suffDec * np.dot(g.T, (x_new - x)) or not isLegal(f_new): 131 | temp = t 132 | # Halfing step size 133 | if options.interp == 2 and isLegal(g_new): 134 | log(options, 3, 'Cubic Backtracking') 135 | gtd_new = np.dot(g_new, d) 136 | t = polyinterp2(np.array([[0, f, gtd], [t, f_new, gtd_new]])) 137 | elif options.interp == 0 or ~isLegal(f_new): 138 | log(options, 3, 'Halving Step Size') 139 | t /= 2.0 140 | elif lineSearchIters < 2 or ~isLegal(f_prev): 141 | log(options, 3, 'Quadratic Backtracking') 142 | t = polyinterp2(np.array([[0, f, gtd], [t, f_new, 1j]])).real 143 | else: 144 | # t = polyinterp([0 f gtd; t f_new sqrt(-1);t_prev f_prev sqrt(-1)]); 145 | # not implemented. 146 | # fallback on halving. 147 | t /= 2.0 148 | 149 | if t < temp * 1e-3: 150 | log(options, 3, 'Interpolated value too small, Adjusting: ' + str(t)) 151 | t = temp * 1e-3 152 | elif t > temp * 0.6: 153 | log(options, 3, 'Interpolated value too large, Adjusting: ' + str(t)) 154 | t = temp * 0.6 155 | # Check whether step has become too small 156 | if np.max(np.absolute(t * d)) < options.progTol or t == 0: 157 | log(options, 3, 'Line Search failed') 158 | t = 0.0 159 | f_new = f 160 | g_new = g 161 | break 162 | 163 | # Evaluate New Point 164 | f_prev = f_new 165 | t_prev = temp 166 | 167 | if options.curvilinear: 168 | x_new = funProj(x + t * d) 169 | projects += 1 170 | else: 171 | x_new = x + t * d 172 | 173 | f_new, g_new = funObj(x_new) 174 | funEvals += 1 175 | lineSearchIters += 1 176 | 177 | # Take Step 178 | x = x_new 179 | f = f_new 180 | g = g_new 181 | 182 | if options.testOpt: 183 | optCond = np.max(np.absolute(funProj(x - g) - x)) 184 | projects += 1 185 | 186 | # Output Log 187 | if options.verbose >= 2: 188 | if options.testOpt: 189 | print('{:10d} {:10d} {:10d} {:15.5e} {:15.5e} {:15.5e}'.format(i, funEvals * funEvalMultiplier, 190 | projects, t, f, optCond)) 191 | else: 192 | print('{:10d} {:10d} {:10d} {:15.5e} {:15.5e}'.format(i, funEvals * funEvalMultiplier, projects, t, f)) 193 | 194 | # Check optimality 195 | if options.testOpt: 196 | if optCond < options.optTol: 197 | log(options, 1, 'First-Order Optimality Conditions Below optTol') 198 | break 199 | 200 | if np.max(np.absolute(t * d)) < options.progTol: 201 | log(options, 1, 'Step size below progTol') 202 | break 203 | 204 | if np.absolute(f - f_old) < options.progTol: 205 | log(options, 1, 'Function value changing by less than progTol') 206 | break 207 | 208 | if funEvals * funEvalMultiplier > options.maxIter: 209 | log(options, 1, 'Function Evaluations exceeds maxIter') 210 | break 211 | 212 | i += 1 213 | 214 | return x, f 215 | 216 | 217 | def isLegal(v): 218 | no_complex = v.imag.any().sum() == 0 219 | no_nan = np.isnan(v).sum() == 0 220 | no_inf = np.isinf(v).sum() == 0 221 | return no_complex and no_nan and no_inf 222 | 223 | 224 | def polyinterp2(points): 225 | # Code for most common case: 226 | # - cubic interpolation of 2 points w/ function and derivative values for both 227 | # - no xminBound/xmaxBound 228 | # Solution in this case (where x2 is the farthest point): 229 | # d1 = g1 + g2 - 3*(f1-f2)/(x1-x2); 230 | # d2 = sqrt(d1^2 - g1*g2); 231 | # minPos = x2 - (x2 - x1)*((g2 + d2 - d1)/(g2 - g1 + 2*d2)); 232 | # t_new = min(max(minPos,x1),x2); 233 | minPos = np.argmin(points[:, 0]) 234 | # minVal = points[minPos, 0] 235 | notMinPos = -minPos + 1 236 | d1 = points[minPos, 2] + points[notMinPos, 2] - 3*(points[minPos, 1]-points[notMinPos, 1])/(points[minPos, 0] - points[notMinPos, 0]) 237 | d2 = np.sqrt(d1**2 - points[minPos, 2] * points[notMinPos,2]) 238 | if np.isreal(d2): 239 | t = points[notMinPos, 0] - (points[notMinPos, 0] - points[minPos, 0])*((points[notMinPos, 2] + d2 - d1) / (points[notMinPos, 2] - points[minPos, 2] + 2*d2)) 240 | minPos = min([max([t, points[minPos, 0]]), points[notMinPos, 0]]) 241 | else: 242 | minPos = np.mean(points[:, 0]) 243 | return minPos 244 | 245 | 246 | def auto_grad(x, funObj, options): 247 | # notice the funObj should return a single value here - the objective (i.e., no gradient) 248 | p = len(x) 249 | f = funObj(x) 250 | if type(f) == type(()): 251 | f = f[0] 252 | 253 | mu = 2*np.sqrt(1e-12)*(1+np.linalg.norm(x))/np.linalg.norm(p) 254 | diff = np.zeros((p,)) 255 | for j in xrange(p): 256 | e_j = np.zeros((p,)) 257 | e_j[j] = 1 258 | # this is somewhat wrong, since we also need to project, 259 | # but practically (and locally) it doesn't seem to matter. 260 | v = funObj(x + mu*e_j) 261 | if type(v) == type(()): 262 | diff[j] = v[0] 263 | else: 264 | diff[j] = v 265 | 266 | g = (diff-f)/mu 267 | 268 | return f, g 269 | -------------------------------------------------------------------------------- /robograph/attack/dp.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | from numba import njit, prange 4 | 5 | def exact_solver_wrapper(A_org, Q, p, L, delta_l, delta_g, constr='1'): 6 | """ 7 | Exact attacks for A^1, A^2 or A^{1+2} 8 | 9 | param: 10 | A_org: original adjacency matrix 11 | Q: matrix, Q_i = Q[i] 12 | p: vector 13 | L: matrix, L_i = L[i] 14 | delta_l: row budgets. If it is a scalar, expand to list with same value 15 | delta_g: global budgets 16 | constr: '1' (local budget solver) or '1+2' (local+global budget solver) or '2' 17 | 18 | return: 19 | unpert_val: function value under A_org (if constr='1', this is a vector) 20 | opt_val: function value under A_pert (if constr='1', this is a vector) 21 | A_pert: optimal attacking adjacency matrix 22 | """ 23 | 24 | if constr == '1': 25 | # Exact attacks for A^1 26 | return local_budget_solver(A_org, Q, p, L, delta_l, delta_g) 27 | elif constr == '1+2': 28 | # Exact attacks for A^{1+2} 29 | return dp_solver(A_org, Q, p, L, delta_l, delta_g) 30 | elif constr == '2': 31 | # Exact attacks for A^2 32 | raise NotImplementedError('Exact attacks for A^2 is not implemented!') 33 | 34 | 35 | @njit("(float64[:, :], float64[:, :], float64[:], float64[:, :], int64)", parallel=False, fastmath=True, cache=True) 36 | # # @njit(parallel=True, fastmath=True) 37 | # # @njit 38 | def local_budget_precompute(A_org, Q, p, L, delta_l): 39 | """ 40 | solver of equation 8&11 of the paper when activation is identity, max_margin loss and average pooling 41 | """ 42 | nG = A_org.shape[0] 43 | a = np.zeros((nG+1, delta_l+1)) # matrix a for described in equation 6 44 | add_edge_matrix = np.zeros((nG+1, delta_l+1)) 45 | for i in range(1, nG+1): # looping each row of A 46 | A_i = A_org[i-1,:] 47 | A_i_edges = int(np.sum(A_i)) 48 | Q_i = Q[i-1] 49 | L_i = L[i-1] 50 | max_edges = min(A_i_edges + delta_l + 1, nG) 51 | min_edges = max(A_i_edges - delta_l + 1, 1) 52 | possible_denomi = max_edges - min_edges + 1 53 | chunk_edges_mtx, chunk_no_edges_mtx = np.zeros((possible_denomi,delta_l+1)), np.zeros((possible_denomi,delta_l+1)) 54 | for x in range(min_edges, max_edges+1): # looping all possible (1'A_i + 1) 55 | V_L = Q_i + L_i*x 56 | indices = np.argsort(V_L) 57 | chunk_edges, chunk_no_edges = [0.0]*(delta_l+1), [0.0]*(delta_l+1) 58 | temp_idx = 1 59 | for y in indices: 60 | if temp_idx > delta_l: break 61 | if y == i-1: continue # excluding self edge 62 | if A_i[y] == 0: 63 | chunk_no_edges[temp_idx] = V_L[y] + chunk_no_edges[temp_idx-1] 64 | temp_idx += 1 65 | 66 | temp_idx = 1 67 | for y in indices[::-1]: 68 | if temp_idx > delta_l: break 69 | if y == i-1: continue # excluding self edge 70 | if A_i[y] == 1: 71 | chunk_edges[temp_idx] = V_L[y] + chunk_edges[temp_idx-1] 72 | temp_idx += 1 73 | 74 | chunk_edges_mtx[x - min_edges] = chunk_edges 75 | chunk_no_edges_mtx[x - min_edges] = chunk_no_edges 76 | 77 | 78 | A_V_i = np.dot(A_i, Q_i) + Q_i[i-1] + p[i-1] 79 | A_L_i = np.dot(A_i, L_i) 80 | a[i,0] = A_V_i/(A_i_edges+1) + A_L_i 81 | for j in range(1,delta_l+1): # looping each possible local constraint 82 | min_f = np.inf 83 | for k in range(j+1): # looping different combinations of adding/removing 84 | add_edges, remove_edges = k, j-k 85 | if A_i_edges+add_edges > nG-1 or A_i_edges-remove_edges < 0: 86 | continue 87 | 88 | new_edges = A_i_edges+add_edges-remove_edges + 1 89 | f = A_V_i + A_L_i*new_edges 90 | 91 | # adding k edges from chunk of A_i=0 in ascent order 92 | if add_edges > 0: 93 | # print(chunk_no_edges_mtx[new_edges][add_edges]) 94 | f += chunk_no_edges_mtx[new_edges - min_edges][add_edges] 95 | 96 | # removing j-k edges from chunk of A_i=1 in descent order 97 | if remove_edges > 0: 98 | # print(chunk_edges_mtx[new_edges][remove_edges]) 99 | f -= chunk_edges_mtx[new_edges - min_edges][remove_edges] 100 | 101 | final_f = f/new_edges 102 | if final_f < min_f: 103 | min_f = final_f 104 | sol = (min_f, add_edges) 105 | a[i,j], add_edge_matrix[i,j] = sol 106 | return a, add_edge_matrix 107 | 108 | @njit("(float64[:], float64[:], float64[:], int64, int64, int64)", cache=True) 109 | def get_A_opt(Q_i, A_i, L_i, i, j, add_edges): 110 | A_i_edges = np.sum(A_i) 111 | remove_edges = j - add_edges 112 | new_edges = A_i_edges+add_edges-remove_edges + 1 113 | V_L = Q_i + L_i.T*new_edges 114 | indices = np.argsort(V_L) 115 | 116 | A_new_i = A_i.copy() 117 | added_edges = 0 118 | for y in indices: 119 | if added_edges == add_edges: break 120 | if y == i-1: continue # excluding self edge 121 | if A_i[y] == 0: 122 | A_new_i[y] = 1 123 | added_edges += 1 124 | 125 | removed_edges = 0 126 | for y in indices[::-1]: 127 | if removed_edges == remove_edges: break 128 | if y == i-1: continue # excluding self edge 129 | if A_i[y] == 1: 130 | A_new_i[y] = 0 131 | removed_edges += 1 132 | 133 | return A_new_i 134 | 135 | 136 | @njit("(float64[:,:], float64[:,:], float64[:], float64[:,:], int64[:], int64)", cache=True) 137 | def dp_solver(A_org, Q, p, L, delta_l, delta_g): 138 | """ 139 | DP for solving min_{A_G^{1+2}} \sum_i [(A_i+e_i)@Q_i + p_i]/(1'A_i + 1) + A_i@L_i] 140 | 141 | Algorithm 1: 142 | 1. Precomputing matrix a 143 | 2. DP to get matrix s 144 | 3. Tracing back 145 | 146 | Complexity: nG^2*delta_l*log(nG) + nG*delta_l^2 + nG^2*delta_l^2 147 | 148 | param: 149 | A_org: original adjacency matrix 150 | Q: matrix, Q_i = Q[i] 151 | p: vector 152 | L: matrix, L_i = L[i] 153 | delta_l: row budgets 154 | delta_g: global budgets 155 | 156 | """ 157 | 158 | # start = time.time() 159 | max_delta_l = max(delta_l) 160 | a, add_edge_matrix = local_budget_precompute(A_org, Q, p, L, max_delta_l) 161 | # print(f'Precomputation of matrix a: {time.time() - start}') 162 | 163 | 164 | # ---------------------FIRST LOOP--------------------- 165 | nG = A_org.shape[0] 166 | c = [0]*(nG+1) 167 | for t in range(1, nG+1): 168 | c[t] = min(c[t-1]+delta_l[t-1], delta_g) 169 | s = [np.array([0.0]*(i+1)) for i in c] 170 | # s = np.zeros((nG+1, min(nG*np.max(delta_l), delta_g)+1)) 171 | for t in range(1, nG+1): 172 | st_1, st, at = s[t-1], s[t], a[t] 173 | for j in range(0,c[t]+1): 174 | m = np.inf 175 | for k in range(max(0, j-c[t-1]), min(j, delta_l[t-1])+1): 176 | m = min(st_1[j-k]+at[k], m) # accessing s seems costly 177 | st[j] = m 178 | 179 | 180 | # ---------------------SECOND LOOP--------------------- 181 | A_pert = np.zeros((nG,nG)) 182 | j = np.argmin(s[nG]) # this sort takes nG*delta_l log(nG*delta_l) 183 | opt_val = s[nG][j] 184 | unpert_val = s[nG][0] 185 | for t in range(nG,0,-1): 186 | temp = np.ones(delta_l[t-1]+1)*np.inf 187 | st_1, at = s[t-1], a[t] 188 | for k in range(max(0, j-c[t-1]), min(j, delta_l[t-1])+1): 189 | temp[k] = st_1[j-k] + at[k] 190 | kt = np.argmin(temp) 191 | j = j - kt 192 | A_pert[t-1,:] = get_A_opt(Q[t-1], A_org[t-1], L[t-1], \ 193 | t, kt, add_edge_matrix[t][kt]) 194 | sol = (unpert_val, opt_val, A_pert) 195 | 196 | return sol 197 | 198 | 199 | @njit("(float64[:,:], float64[:,:], float64[:], float64[:,:], int64[:], int64)", cache=True) 200 | def local_budget_solver(A_org, Q, p, L, delta_l, delta_g): 201 | max_delta_l = max(delta_l) 202 | a, add_edge_matrix = local_budget_precompute(A_org, Q, p, L, max_delta_l) 203 | 204 | nG = A_org.shape[0] 205 | A_pert = np.zeros((nG,nG)) 206 | opt_fvals = np.zeros(nG) 207 | for i in range(nG): 208 | delta_l_i = delta_l[i] 209 | best_delta_l = np.argmin(a[i+1][0:(delta_l_i+1)]) 210 | A_pert[i] = get_A_opt(Q[i], A_org[i], L[i], i+1, best_delta_l, \ 211 | add_edge_matrix[i+1][best_delta_l]) 212 | opt_fvals[i] = a[i+1][best_delta_l] 213 | sol = (a[:, 0], opt_fvals, A_pert) 214 | 215 | return sol 216 | 217 | 218 | 219 | 220 | 221 | 222 | def po_dp_solver(A_org, R, delta_l, delta_g): 223 | nG = A_org.shape[0] 224 | 225 | # precomputing a matrix 226 | J = R*(-2*A_org + 1) 227 | a = po_local_solver(J, nG, delta_l) 228 | 229 | A_pert = np.zeros((nG,nG)) 230 | V_pert = np.zeros((nG,nG)) 231 | c, s = first_loop(a, delta_l, delta_g) 232 | j = np.argmin(s[nG]) 233 | unpert_val = s[nG][0] 234 | opt_val = s[nG][j] 235 | for t in range(nG,0,-1): 236 | temp = np.ones(delta_l+1)*np.inf 237 | st_1, at = s[t-1], a[t] 238 | for k in range(max(0, j-c[t-1]), min(j, delta_l)+1): 239 | temp[k] = st_1[j-k] + at[k] 240 | kt = np.argmin(temp) 241 | j = j - kt 242 | V_pert[t-1,:] = optVt_from_a_tj(J[t-1, :], t, kt, delta_l) 243 | A_pert[t-1,:] = ((2*A_org[t-1, :] - 1)*(-2*V_pert[t-1,:]+1)+1)/2 244 | 245 | return A_pert 246 | 247 | def po_local_solver(J, nG, delta_l): 248 | a = np.zeros((nG+1, delta_l+1)) 249 | 250 | for i in range(1, nG+1): # looping each row of A 251 | J_i = J[i-1, :].copy() 252 | J_i = -np.delete(J_i, i-1) 253 | indices = np.argsort(J_i) 254 | 255 | for j in range(1,delta_l+1): # looping each possible local constraints 256 | a[i,j] = J_i[indices[j-1]] + a[i,j-1] 257 | return a 258 | 259 | def optVt_from_a_tj(J_t, t, j, delta_l): 260 | V = np.zeros(J_t.shape) 261 | indices = np.argsort(-J_t) 262 | changed_edges = 0 263 | for i in range(j+1): 264 | if indices[i] == t-1: continue 265 | V[indices[i]] = 1 266 | changed_edges += 1 267 | if changed_edges >= j: break 268 | return V 269 | -------------------------------------------------------------------------------- /robograph/attack/cvx_env_solver.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | 4 | import scipy.optimize as optim 5 | from robograph.attack.convex_relaxation import ConvexRelaxation 6 | from robograph.attack.dp import exact_solver_wrapper 7 | from robograph.attack.greedy_attack import Greedy_Attack 8 | from robograph.attack.frank_wolfe import polar_operator 9 | from robograph.attack.SPG import * 10 | from robograph.attack.utils import projection_A123 11 | from nsopy.methods.subgradient import SubgradientMethod 12 | from nsopy.methods.quasi_monotone import SGMDoubleSimpleAveraging, SGMTripleAveraging 13 | from nsopy.loggers import GenericMethodLogger 14 | 15 | # import matlab.engine 16 | from docplex.mp.model import Model 17 | 18 | def cvx_env_solver(A_org, XW, U, delta_l, delta_g, **params): 19 | """ 20 | Solver for min_{Z\in co(A)} F_\circ(Z), where F_\circ(Z) is an convex relaxation of F(Z) via convex envelop. 21 | 22 | param: 23 | A_org: original adjacency matrix 24 | XW: XW 25 | U: (u_y-u_c)/nG 26 | delta_l: row budgets 27 | delta_g: global budgets 28 | params: 'algo': could be 'swapping' or 'pqn' 29 | 'nonsmooth_init': could be 'random' or 'subgrad' 30 | 1. random initialization 31 | 2. initilize LBFGS by solution of Quasi-Monotone Methods 32 | 33 | return a dict with keywords: 34 | opt_A: original A 35 | opt_f: optimal objective value 36 | """ 37 | 38 | if params['algo'] == 'swapping': 39 | return swapping_solver(A_org, XW, U, delta_l, delta_g, **params) 40 | elif params['algo'] == 'pqn': 41 | return projected_lbfgs(A_org, XW, U, delta_l, delta_g, **params) 42 | 43 | 44 | def swapping_solver(A_org, XW, U, delta_l, delta_g, **params): 45 | """ 46 | Solving the convexified problem: 47 | min_{Z\in co(A)} F_\circ(Z) = min_{Z\in co(A)} max_R min_W ... 48 | = max_R min_{Z\in co(A)} min_W ... 49 | where the inner min_{Z\in co(A)} can be exactly solved by CPLEX LP. 50 | """ 51 | 52 | cvx_relaxation = ConvexRelaxation(A_org, XW, U, delta_l, delta_g, params['activation'], 'envelop', \ 53 | relu_relaxation=params.get('relu_bound')) 54 | if params.get('relu_bound') == 'singleL': 55 | doubleL_relax = ConvexRelaxation(A_org, XW, U, delta_l, delta_g, params['activation'], 'envelop', \ 56 | relu_relaxation='doubleL') 57 | nG = A_org.shape[0] 58 | XWU = XW @ U 59 | 60 | # Setup SPG solver for min_W 61 | spg_options = default_options 62 | spg_options.verbose = 0 63 | spg_options.maxIter = 20 64 | 65 | # Setup CPLEX solver for min_Z 66 | mdl = Model("LP") 67 | n = nG 68 | V = -2*A_org + 1 69 | ub = np.ones(A_org.shape) 70 | np.fill_diagonal(ub, 0) 71 | x_vars = mdl.continuous_var_matrix(n, n, ub= ub.ravel()) 72 | mdl.add_constraints_( mdl.sum(V[i, j] * x_vars[i, j] + A_org[i, j] for j in range(n)) <= delta_l[i] 73 | for i in range(n) ) 74 | mdl.add_constraint_( mdl.sum(V[i, j] * x_vars[i, j] + A_org[i, j] for i in range(n) for j in range(n)) <= delta_g) 75 | mdl.add_constraints_( x_vars[i, j] - x_vars[j, i] == 0 for i in range(n) for j in range(i+1, n) ) 76 | 77 | tighest_value = -np.inf 78 | for i in range(params.get('luiter', 1)): 79 | iters, eps = params['iter'], 0.0 80 | def objective(x): 81 | R = x.reshape(nG, nG) 82 | # Given R, find optimal W 83 | if params['activation'] == 'relu': 84 | if params['relu_bound'] == 'doubleL': 85 | local_budget_sol = exact_solver_wrapper(A_org, cvx_relaxation.Q, cvx_relaxation.p, -R, delta_l, delta_g, '1+2') 86 | _, opt_f_W, opt_W = local_budget_sol 87 | opt_f_W = np.sum(opt_f_W) 88 | 89 | elif params['relu_bound'] == 'singleL': 90 | local_budget_sol = exact_solver_wrapper(A_org, doubleL_relax.Q, doubleL_relax.p, -R, delta_l, delta_g, '1+2') 91 | _, opt_f_W_1, opt_W_1 = local_budget_sol 92 | opt_f_W_1 = np.sum(opt_f_W_1) 93 | # cvx_relaxation.warm_start_W = opt_W_1 94 | # opt_W, opt_f_W = cvx_relaxation.get_opt_W(R, spg_options) 95 | opt_W = opt_W_1 96 | opt_f_W = np.sum(cvx_relaxation.g_hat(opt_W)[0]/(1+np.sum(opt_W, axis=1))) - np.sum(R*opt_W) 97 | else: 98 | # local_budget_sol = exact_solver_wrapper(A_org, np.tile(XWU, (nG, 1)), np.zeros(nG), -R, delta_l, delta_g, '1') 99 | # _, opt_f_W, opt_W = local_budget_sol 100 | # opt_f_W = np.sum(opt_f_W) 101 | 102 | dp_sol = exact_solver_wrapper(A_org, np.tile(XWU, (nG, 1)), np.zeros(nG), -R, delta_l, delta_g, '1+2') 103 | _, opt_f_W, opt_W = dp_sol 104 | 105 | # Given R, solve min_{Z \in co(A1) \cap co(A2) \cap A3} tr(R.T*Z) 106 | opt_Z = RZ_solver(x_vars, mdl, A_org, delta_l, delta_g, R) 107 | 108 | fval = opt_f_W + np.sum(R*opt_Z) - eps*np.sum(R**2)/2 109 | grad = opt_Z - opt_W - eps*R 110 | return -fval, -grad.flatten() 111 | # print(optim.check_grad(lambda x: objective(x)[0], lambda x: objective(x)[1], init_R.flatten())) 112 | 113 | def callback(x): 114 | print(-objective(x)[0]) 115 | 116 | if params['nonsmooth_init'] == 'subgrad': 117 | # Subgradient Method to optimize the non-smooth objective 118 | # method = SubgradientMethod(lambda x: (0, -objective(x)[0], -objective(x)[1]), lambda x: x, dimension=nG*nG,\ 119 | # stepsize_0=0.01, stepsize_rule='constant', sense='max') 120 | method = SGMTripleAveraging(lambda x: (0, -objective(x)[0], -objective(x)[1]), lambda x: x, dimension=nG*nG,\ 121 | gamma=3, sense='max') 122 | logger = GenericMethodLogger(method) 123 | for iteration in range(50): 124 | method.step() 125 | init_R = logger.x_k_iterates[-1].reshape(nG, nG) 126 | fval_sub = logger.f_k_iterates[-1] 127 | elif params['nonsmooth_init'] == 'random': 128 | init_R = np.random.randn(nG, nG)*0.01 129 | 130 | 131 | if params['verbose']: 132 | res = optim.fmin_l_bfgs_b(objective, init_R.flatten(), maxiter=iters, m=20, maxls=20, callback=callback) 133 | res_status = res[2] 134 | print('warnflag: %d, iters: %d, funcalls: %d' % (res_status['warnflag'], res_status['nit'], res_status['funcalls']) ) 135 | print(res_status['task']) 136 | else: 137 | res = optim.fmin_l_bfgs_b(objective, init_R.flatten(), maxiter=iters, m=20, maxls=20, callback=None) 138 | 139 | fval, opt_R = -res[1], res[0].reshape(nG, nG) 140 | # print(fval) 141 | 142 | # if params['relu_bound'] == 'doubleL': 143 | # flag = update_lb_ub(opt_R, cvx_relaxation) 144 | # elif params['relu_bound'] == 'singleL': 145 | # flag = update_lb_ub(opt_R, doubleL_relax, cvx_relaxation) 146 | 147 | # if flag and fval > tighest_value: 148 | # tighest_value = fval 149 | 150 | sol = { 151 | 'opt_A': A_org, 152 | 'opt_f': fval 153 | } 154 | return sol 155 | 156 | def update_lb_ub(opt_R, doubleL, singleL=None): 157 | local_budget_sol = exact_solver_wrapper(doubleL.A_org, doubleL.Q, doubleL.p, -opt_R, \ 158 | doubleL.delta_l, doubleL.delta_g, '1+2') 159 | opt_W = local_budget_sol[-1] 160 | 161 | # update lb and ub 162 | before_relu = opt_W @ doubleL.XW + doubleL.XW 163 | 164 | # case i 165 | i_idx = (before_relu > doubleL.ub) 166 | # case ii 167 | ii_idx = (before_relu < doubleL.lb) 168 | # within [lb, ub] 169 | idx = ~(i_idx ^ ii_idx) 170 | # idx = (before_relu <= doubleL.ub) & (before_relu >= doubleL.lb) 171 | # case iii 172 | iii_idx = idx & (before_relu > 0) 173 | # case iv 174 | iv_idx = idx & (before_relu < 0) 175 | 176 | if np.all(idx): 177 | flag = True 178 | # print('pre_relu in [lb, ub]!') 179 | else: 180 | flag = False 181 | # print('pre_relu NOT in [lb, ub]!') 182 | 183 | doubleL.ub[i_idx] = before_relu[i_idx] 184 | doubleL.lb[ii_idx] = before_relu[ii_idx] 185 | # doubleL.ub[iii_idx] = (before_relu[iii_idx] + doubleL.ub[iii_idx])/2 186 | doubleL.ub[iii_idx] = before_relu[iii_idx] 187 | # doubleL.lb[iv_idx] = (before_relu[iv_idx] + doubleL.lb[iv_idx])/2 188 | doubleL.lb[iv_idx] = before_relu[iv_idx] 189 | 190 | doubleL.S_minus = (doubleL.lb*doubleL.ub < 0) & np.tile((doubleL.U < 0), (doubleL.nG, 1)) 191 | doubleL.I_plus = (doubleL.lb > 0) 192 | doubleL.I_mix = (doubleL.lb*doubleL.ub < 0) 193 | doubleL.Q, doubleL.p = doubleL.doubleL_lb_coefficient() 194 | 195 | if singleL != None: 196 | singleL.ub = doubleL.ub 197 | singleL.lb = singleL.lb 198 | 199 | singleL.S_minus = (singleL.lb*singleL.ub < 0) & np.tile((singleL.U < 0), (singleL.nG, 1)) 200 | singleL.S_others = ~(singleL.S_minus) 201 | 202 | return flag 203 | 204 | def projected_lbfgs(A_org, XW, U, delta_l, delta_g, **params): 205 | """ 206 | Solving the convexified problem: 207 | min_{Z\in co(A)} F_\circ(Z) = min_{Z\in co(A)} max_R min_W ... 208 | """ 209 | cvx_relaxation = ConvexRelaxation(A_org, XW, U, delta_l, delta_g, params['activation'], 'envelop', \ 210 | relu_relaxation=params.get('relu_bound')) 211 | n = A_org.shape[0] 212 | 213 | def objective(x): 214 | X = x.reshape(n, n) 215 | f, G = cvx_relaxation.convex_F(X) 216 | return f, G.flatten() 217 | def proj(x): 218 | X = x.reshape(n, n) 219 | projected_value = projection_A123(X, A_org, delta_l, delta_g) 220 | return projected_value.flatten() 221 | 222 | spg_options = default_options 223 | spg_options.curvilinear = 0 224 | spg_options.interp = 2 225 | spg_options.numdiff = 0 # 0 to use gradients, 1 for numerical diff 226 | spg_options.verbose = 2 if params['verbose'] else 0 227 | spg_options.maxIter = params['iter'] 228 | 229 | # 1. init Z as A_org 230 | init_x = A_org.copy() 231 | # 2. init by optimal R of (39), given Z* from greedy attack. 232 | # greedy_attack = Greedy_Attack(A_org, XW, U, delta_l, delta_g, 'relu') 233 | # greedy_sol = greedy_attack.attack(A_org) 234 | # init_x = greedy_sol['opt_A'] 235 | 236 | x, f = SPG(objective, proj, init_x.flatten(), spg_options) 237 | sol = { 238 | 'opt_A': A_org, 239 | 'opt_f': fval 240 | } 241 | return sol 242 | 243 | 244 | def RZ_solver(x_vars, mdl, A_org, delta_l, delta_g, R): 245 | # PO on co(A^{1+2+3}) is equivalent to PO on A^{1+2+3} 246 | # opt_Z, _ = polar_operator(A_org, -R, delta_l, delta_g, '1+2+3') 247 | 248 | n = A_org.shape[0] 249 | mdl.minimize( mdl.sum(R[i, j] * x_vars[i, j] for i in range(n) for j in range(n)) ) 250 | msol = mdl.solve() 251 | if msol: 252 | opt_Z = np.array([x_vars[(i, j)].solution_value for i in range(n) for j in range(n)]).reshape(n, n) 253 | else: 254 | raise Exception("cplex failed!") 255 | 256 | return opt_Z 257 | -------------------------------------------------------------------------------- /robograph/attack/greedy_attack.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from robograph.attack.utils import calculate_Fc 3 | 4 | class Greedy_Attack(object): 5 | """ 6 | Greedy algorithm (upper bound) for solving min_{A_G^{1+2+3}} F_c(A) 7 | 8 | param: 9 | A_org: original adjacency matrix 10 | XW: XW 11 | U: (u_y-u_c)/nG 12 | delta_l: row budgets (vector) 13 | delta_g: global budget (scalar) 14 | activation: 'linear' or 'relu' 15 | """ 16 | 17 | def __init__(self, A_org, XW, U, delta_l, delta_g, activation='linear'): 18 | self.A_org = A_org 19 | self.XW, self.U = XW, U 20 | self.XWU = XW @ U 21 | self.delta_l = delta_l 22 | self.delta_g = delta_g 23 | self.activation = activation 24 | self.nG = A_org.shape[0] 25 | 26 | def attack(self, A_pert): 27 | """ 28 | Greedy searching minimal F_c(A) (upper bound) 29 | 30 | param: 31 | A_pert: Starting point for greedy search 32 | 33 | return a dict with keywords: 34 | opt_A: optimal perturbed matrix 35 | opt_f: optimal objective value 36 | """ 37 | # Precompution based on initial A 38 | self.A_pert = A_pert 39 | self.edge_coors, self.no_edge_coors = [], [] 40 | self.Flip = np.logical_xor(self.A_org, self.A_pert) 41 | self.local_budget = np.sum(self.Flip, axis=1) 42 | self.global_budget = np.sum(self.local_budget) 43 | self.Denominator, self.Numerator = np.zeros(self.nG), np.zeros(self.nG) 44 | for i in range(self.nG): 45 | self.Denominator[i] = np.sum(A_pert[i]) + 1 46 | if self.activation == 'linear': 47 | self.Numerator[i] = A_pert[i] @ self.XWU + self.XWU[i] 48 | else: 49 | self.Numerator[i] = np.maximum(A_pert[i] @ self.XW + self.XW[i], 0) @ self.U 50 | for j in range(i+1, self.nG): 51 | if A_pert[i][j]: self.edge_coors.append((i, j)) 52 | else: self.no_edge_coors.append((i, j)) 53 | 54 | # Prepruning if not satisfing local/global budget 55 | if self.global_budget > self.delta_g or np.any(self.local_budget > self.delta_l): 56 | self.preprocess(self.A_pert) 57 | 58 | # Attack 59 | A_start = self.A_pert.copy() 60 | while True: 61 | A_end = self.pruning(A_start) 62 | A_end = self.adding(A_end) 63 | if np.array_equal(A_start, A_end): break 64 | else: A_start = A_end 65 | sol = { 66 | 'opt_A': A_end, 67 | 'opt_f': calculate_Fc(A_end, self.XW, self.U, self.activation) 68 | } 69 | return sol 70 | 71 | 72 | 73 | def preprocess(self, A_pert): 74 | while np.any(self.local_budget > self.delta_l): 75 | max_decrement, change_coordinate = np.inf, None 76 | for i in range(self.nG): 77 | for j in range(i+1, self.nG): 78 | if self.Flip[i][j] and (self.local_budget[i] > self.delta_l[i] or self.local_budget[j] > self.delta_l[j]): 79 | if A_pert[i][j]: 80 | nume_i = self.Numerator[i] - self.XWU[j] 81 | deno_i = self.Denominator[i] - 1 82 | nume_j = self.Numerator[j] - self.XWU[i] 83 | deno_j = self.Denominator[j] - 1 84 | else: 85 | nume_i = self.Numerator[i] + self.XWU[j] 86 | deno_i = self.Denominator[i] + 1 87 | nume_j = self.Numerator[j] + self.XWU[i] 88 | deno_j = self.Denominator[j] + 1 89 | decrement = nume_i/deno_i - self.Numerator[i]/self.Denominator[i] 90 | decrement += nume_j/deno_j - self.Numerator[j]/self.Denominator[j] 91 | if decrement < max_decrement: 92 | max_decrement = decrement 93 | change_coordinate = (i, j) 94 | 95 | if change_coordinate: 96 | i, j = change_coordinate 97 | A_pert[i][j], A_pert[j][i] = self.A_org[i][j], self.A_org[j][i] 98 | self.local_budget[i] -= 1 99 | self.local_budget[j] -= 1 100 | self.global_budget -= 2 101 | self.Flip[i][j], self.Flip[j][i] = 0, 0 102 | if A_pert[i][j]: 103 | self.Numerator[i] += self.XWU[j] 104 | self.Numerator[j] += self.XWU[i] 105 | self.Denominator[i] += 1 106 | self.Denominator[j] += 1 107 | else: 108 | self.Numerator[i] -= self.XWU[j] 109 | self.Numerator[j] -= self.XWU[i] 110 | self.Denominator[i] -= 1 111 | self.Denominator[j] -= 1 112 | if np.any(self.Denominator == 0): 113 | aa = 1 114 | 115 | while self.global_budget > self.delta_g: 116 | max_decrement, change_coordinate = np.inf, None 117 | for i in range(self.nG): 118 | for j in range(i+1, self.nG): 119 | if self.Flip[i][j]: 120 | if A_pert[i][j]: 121 | nume_i = self.Numerator[i] - self.XWU[j] 122 | deno_i = self.Denominator[i] - 1 123 | nume_j = self.Numerator[j] - self.XWU[i] 124 | deno_j = self.Denominator[j] - 1 125 | else: 126 | nume_i = self.Numerator[i] + self.XWU[j] 127 | deno_i = self.Denominator[i] + 1 128 | nume_j = self.Numerator[j] + self.XWU[i] 129 | deno_j = self.Denominator[j] + 1 130 | decrement = nume_i/deno_i - self.Numerator[i]/self.Denominator[i] 131 | decrement += nume_j/deno_j - self.Numerator[j]/self.Denominator[j] 132 | if decrement < max_decrement: 133 | max_decrement = decrement 134 | change_coordinate = (i, j) 135 | 136 | if change_coordinate: 137 | i, j = change_coordinate 138 | A_pert[i][j], A_pert[j][i] = self.A_org[i][j], self.A_org[j][i] 139 | self.local_budget[i] -= 1 140 | self.local_budget[j] -= 1 141 | self.global_budget -= 2 142 | self.Flip[i][j], self.Flip[j][i] = 0, 0 143 | if A_pert[i][j]: 144 | self.Numerator[i] += self.XWU[j] 145 | self.Numerator[j] += self.XWU[i] 146 | self.Denominator[i] += 1 147 | self.Denominator[j] += 1 148 | else: 149 | self.Numerator[i] -= self.XWU[j] 150 | self.Numerator[j] -= self.XWU[i] 151 | self.Denominator[i] -= 1 152 | self.Denominator[j] -= 1 153 | if np.any(self.Denominator == 0): 154 | aa = 1 155 | if self.global_budget > self.delta_g or np.any(self.local_budget > self.delta_l): 156 | raise AssertionError('still does not satisfy budgets after prepruning') 157 | 158 | self.edge_coors, self.no_edge_coors = [], [] 159 | self.Flip = np.logical_xor(self.A_org, self.A_pert) 160 | self.local_budget = np.sum(self.Flip, axis=1) 161 | self.global_budget = np.sum(self.local_budget) 162 | self.Denominator, self.Numerator = np.zeros(self.nG), np.zeros(self.nG) 163 | for i in range(self.nG): 164 | self.Denominator[i] = np.sum(A_pert[i]) + 1 165 | self.Numerator[i] = np.dot(A_pert[i], self.XWU) + self.XWU[i] 166 | for j in range(i+1, self.nG): 167 | if A_pert[i][j]: self.edge_coors.append((i, j)) 168 | else: self.no_edge_coors.append((i, j)) 169 | 170 | def pruning(self, A): 171 | max_decrement = 0 172 | for idx, coodinate in enumerate(self.edge_coors): 173 | i, j = coodinate 174 | local_budget_i = self.local_budget[i] + 2*~(self.Flip[i][j]) - 1 175 | local_budget_j = self.local_budget[j] + 2*~(self.Flip[j][i]) - 1 176 | global_budget_ij = self.global_budget + 2*~(self.Flip[i][j]) - 1 + 2*~(self.Flip[j][i]) - 1 177 | if local_budget_i <= self.delta_l[i] and local_budget_j <= self.delta_l[j] \ 178 | and global_budget_ij <= self.delta_g: 179 | # if satisfy local/global budgets, removing edge 180 | if self.activation == 'linear': 181 | nume_i = self.Numerator[i] - self.XWU[j] 182 | nume_j = self.Numerator[j] - self.XWU[i] 183 | else: 184 | nume_i = np.maximum(A[i] @ self.XW + self.XW[i] - self.XW[j], 0) @ self.U 185 | nume_j = np.maximum(A[j] @ self.XW + self.XW[j] - self.XW[i], 0) @ self.U 186 | deno_i = self.Denominator[i] - 1 187 | deno_j = self.Denominator[j] - 1 188 | decrement = nume_i/deno_i - self.Numerator[i]/self.Denominator[i] 189 | decrement += nume_j/deno_j - self.Numerator[j]/self.Denominator[j] 190 | if decrement < max_decrement: 191 | max_decrement = decrement 192 | pruning_coordinate = coodinate 193 | pruning_idx = idx 194 | 195 | # update and return 196 | A_opt = A.copy() 197 | if max_decrement < 0: 198 | i, j = pruning_coordinate 199 | A_opt[i][j], A_opt[j][i] = 0, 0 200 | self.local_budget[i] += 2*~(self.Flip[i][j]) - 1 201 | self.local_budget[j] += 2*~(self.Flip[j][i]) - 1 202 | self.global_budget += 2*~(self.Flip[i][j]) - 1 + 2*~(self.Flip[j][i]) - 1 203 | self.Flip[i][j], self.Flip[j][i] = ~(self.Flip[i][j]), ~(self.Flip[j][i]) 204 | if self.activation == 'linear': 205 | self.Numerator[i] -= self.XWU[j] 206 | self.Numerator[j] -= self.XWU[i] 207 | else: 208 | self.Numerator[i] = np.maximum(A_opt[i] @ self.XW + self.XW[i], 0) @ self.U 209 | self.Numerator[j] = np.maximum(A_opt[j] @ self.XW + self.XW[j], 0) @ self.U 210 | self.Denominator[i] -= 1 211 | self.Denominator[j] -= 1 212 | self.edge_coors.pop(pruning_idx) 213 | self.no_edge_coors.append(pruning_coordinate) 214 | if np.any(self.Denominator == 0): 215 | aa = 1 216 | return A_opt 217 | 218 | def adding(self, A): 219 | max_decrement = 0 220 | for idx, coordinate in enumerate(self.no_edge_coors): 221 | i, j = coordinate 222 | local_budget_i = self.local_budget[i] + 2*~(self.Flip[i][j]) - 1 223 | local_budget_j = self.local_budget[j] + 2*~(self.Flip[j][i]) - 1 224 | global_budget_ij = self.global_budget + 2*~(self.Flip[i][j]) - 1 + 2*~(self.Flip[j][i]) - 1 225 | if local_budget_i <= self.delta_l[i] and local_budget_j <= self.delta_l[j] \ 226 | and global_budget_ij <= self.delta_g: 227 | # if satisfy local/global budgets, adding edge 228 | if self.activation == 'linear': 229 | nume_i = self.Numerator[i] + self.XWU[j] 230 | nume_j = self.Numerator[j] + self.XWU[i] 231 | else: 232 | nume_i = np.maximum(A[i] @ self.XW + self.XW[i] + self.XW[j], 0) @ self.U 233 | nume_j = np.maximum(A[j] @ self.XW + self.XW[j] + self.XW[i], 0) @ self.U 234 | deno_i = self.Denominator[i] + 1 235 | deno_j = self.Denominator[j] + 1 236 | decrement = nume_i/deno_i - self.Numerator[i]/self.Denominator[i] 237 | decrement += nume_j/deno_j - self.Numerator[j]/self.Denominator[j] 238 | if decrement < max_decrement: 239 | max_decrement = decrement 240 | removing_coordinate = coordinate 241 | removing_idx = idx 242 | 243 | # update and return 244 | A_opt = A.copy() 245 | if max_decrement < 0: 246 | i, j = removing_coordinate 247 | A_opt[i][j], A_opt[j][i] = 1, 1 248 | self.local_budget[i] += 2*~(self.Flip[i][j]) - 1 249 | self.local_budget[j] += 2*~(self.Flip[j][i]) - 1 250 | self.global_budget += 2*~(self.Flip[i][j]) - 1 + 2*~(self.Flip[j][i]) - 1 251 | self.Flip[i][j], self.Flip[j][i] = ~(self.Flip[i][j]), ~(self.Flip[j][i]) 252 | if self.activation == 'linear': 253 | self.Numerator[i] += self.XWU[j] 254 | self.Numerator[j] += self.XWU[i] 255 | else: 256 | self.Numerator[i] = np.maximum(A_opt[i] @ self.XW + self.XW[i], 0) @ self.U 257 | self.Numerator[j] = np.maximum(A_opt[j] @ self.XW + self.XW[j], 0) @ self.U 258 | self.Denominator[i] += 1 259 | self.Denominator[j] += 1 260 | self.no_edge_coors.pop(removing_idx) 261 | self.edge_coors.append(removing_coordinate) 262 | if np.any(self.Denominator == 0): 263 | aa = 1 264 | return A_opt 265 | -------------------------------------------------------------------------------- /demo_linear.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# RoboGraph\n", 8 | "This is the demo for the submission of paper\n", 9 | "> __Certified Robustness of Graph Convolution Networks for Graph Classification under Topological Attacks__\n", 10 | "\n", 11 | "Before running the demo, please make sure all the required packages are installed.\n", 12 | "\n", 13 | "A detailed instruction is provided in [README.md](./README.md)." 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": null, 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "import torch\n", 23 | "import numpy as np\n", 24 | "import os.path as osp\n", 25 | "import tempfile\n", 26 | "from torch_geometric.datasets import TUDataset\n", 27 | "from torch_geometric.data import DataLoader\n", 28 | "from torch_geometric.data.makedirs import makedirs\n", 29 | "from robograph.model.gnn import GC_NET, train, eval\n", 30 | "from tqdm.notebook import tqdm\n", 31 | "from robograph.utils import process_data, cal_logits\n", 32 | "\n", 33 | "from robograph.attack.admm import admm_solver\n", 34 | "from robograph.attack.cvx_env_solver import cvx_env_solver\n", 35 | "from robograph.attack.dual import dual_solver\n", 36 | "from robograph.attack.greedy_attack import Greedy_Attack\n", 37 | "from robograph.attack.utils import calculate_Fc" 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "metadata": {}, 43 | "source": [ 44 | "## Graph classification with linear activation function" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "torch.manual_seed(0)\n", 54 | "np.random.seed(0)\n", 55 | "\n", 56 | "# prepare dataset\n", 57 | "ds_name = 'ENZYMES'\n", 58 | "path = osp.join(tempfile.gettempdir(), 'data', ds_name)\n", 59 | "save_path = osp.join(tempfile.gettempdir(), 'data', ds_name, 'saved')\n", 60 | "if not osp.isdir(save_path):\n", 61 | " makedirs(save_path)\n", 62 | "dataset = TUDataset(path, name=ds_name, use_node_attr=True)\n", 63 | "dataset = dataset.shuffle()\n", 64 | "train_size = len(dataset) // 10 * 3\n", 65 | "val_size = len(dataset) // 10 * 2\n", 66 | "train_dataset = dataset[:train_size]\n", 67 | "val_dataset = dataset[train_size: train_size + val_size]\n", 68 | "test_dataset = dataset[train_size + val_size:]\n", 69 | "\n", 70 | "# prepare dataloader\n", 71 | "train_loader = DataLoader(train_dataset, batch_size=20)\n", 72 | "val_loader = DataLoader(val_dataset, batch_size=20)\n", 73 | "test_loader = DataLoader(test_dataset, batch_size=20)\n", 74 | "\n", 75 | "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", 76 | "\n", 77 | "# create model\n", 78 | "model = GC_NET(hidden=64,\n", 79 | " n_features=dataset.num_features,\n", 80 | " n_classes=dataset.num_classes,\n", 81 | " act='linear',\n", 82 | " pool='avg',\n", 83 | " dropout=0.).to(device)" 84 | ] 85 | }, 86 | { 87 | "cell_type": "markdown", 88 | "metadata": {}, 89 | "source": [ 90 | "## Training a vanilla model" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": null, 96 | "metadata": { 97 | "scrolled": false 98 | }, 99 | "outputs": [], 100 | "source": [ 101 | "best=0\n", 102 | "for epoch in tqdm(range(200)):\n", 103 | " loss_all = train(model, train_loader)\n", 104 | " train_acc = eval(model, train_loader)\n", 105 | " val_acc = eval(model, val_loader)\n", 106 | " if val_acc >= best:\n", 107 | " best = val_acc\n", 108 | " torch.save(model.state_dict(), osp.join(save_path, \"result.pk\"))\n", 109 | " \n", 110 | " tqdm.write(\"epoch {:03d} \".format(epoch+1) + \n", 111 | " \"train_loss {:.4f} \".format(loss_all) +\n", 112 | " \"train_acc {:.4f} \".format(train_acc) +\n", 113 | " \"val_acc {:.4f} \".format(val_acc))\n", 114 | "test_acc = eval(model, test_loader, testing=True, save_path=save_path)\n", 115 | "print(\"test_acc {:.4f}\".format(test_acc))" 116 | ] 117 | }, 118 | { 119 | "cell_type": "markdown", 120 | "metadata": {}, 121 | "source": [ 122 | "## Robustness certificate" 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": null, 128 | "metadata": {}, 129 | "outputs": [], 130 | "source": [ 131 | "W = model.conv.weight.detach().cpu().numpy().astype(np.float64)\n", 132 | "U = model.lin.weight.detach().cpu().numpy().astype(np.float64)\n", 133 | "\n", 134 | "k = dataset.num_classes\n", 135 | "\n", 136 | "# counter of certifiably robust and vulnerable \n", 137 | "robust_dual = 0\n", 138 | "robust_cvx = 0\n", 139 | "vul_admm = 0\n", 140 | "vul_admm_g = 0\n", 141 | "vul_greedy = 0\n", 142 | "\n", 143 | "# counter of correct classification\n", 144 | "correct = 0\n", 145 | "\n", 146 | "# attacker settings\n", 147 | "strength = 3\n", 148 | "delta_g = 10\n", 149 | "\n", 150 | "# setting for solvers\n", 151 | "dual_params = dict(iter=200, nonsmooth_init='random')\n", 152 | "cvx_params = dict(iter=400, lr=0.3, verbose=0, constr='1+2+3', \n", 153 | " activation='linear', algo='swapping', nonsmooth_init='subgrad')\n", 154 | "admm_params = dict(iter=200, mu=1)\n", 155 | "\n", 156 | "for data in tqdm(test_dataset, desc='across graphs'):\n", 157 | " A, X, y = process_data(data)\n", 158 | " deg = A.sum(1)\n", 159 | " n_nodes = A.shape[0]\n", 160 | " n_edges = np.count_nonzero(A) // 2\n", 161 | " \n", 162 | " delta_l = np.minimum(np.maximum(deg - np.max(deg) + strength, 0), n_nodes - 1).astype(int)\n", 163 | " # delta_g\n", 164 | " \n", 165 | " logits = cal_logits(A, X@W, U, act='linear')\n", 166 | " c_pred = logits.argmax()\n", 167 | " \n", 168 | " if c_pred != y:\n", 169 | " continue\n", 170 | " correct += 1\n", 171 | " fc_vals_orig = [0] * k\n", 172 | " fc_vals_dual = [0] * k\n", 173 | " fc_vals_cvx = [0] * k\n", 174 | " fc_vals_admm = [0] * k\n", 175 | " fc_vals_admm_g = [0] * k\n", 176 | " fc_vals_greedy = [0] * k\n", 177 | " \n", 178 | " \n", 179 | " for c in tqdm(range(k), desc='across labels', leave=False):\n", 180 | " if c == y:\n", 181 | " continue\n", 182 | " u = U[y] - U[c]\n", 183 | " XW = X@W\n", 184 | " \n", 185 | " # fc_val_orig\n", 186 | " fc_vals_orig[c] = calculate_Fc(A, XW, u / n_nodes)\n", 187 | " \n", 188 | " # fc_val_dual\n", 189 | " dual_sol = dual_solver(A, XW, u / n_nodes, delta_l=delta_l, delta_g=delta_g, **dual_params)\n", 190 | " fc_vals_dual[c] = dual_sol['opt_f']\n", 191 | " \n", 192 | " # fc_val_cvx\n", 193 | " cvx_sol =cvx_env_solver(A, XW, u / n_nodes, delta_l=delta_l, delta_g=delta_g, **cvx_params)\n", 194 | " fc_vals_cvx[c] = cvx_sol['opt_f']\n", 195 | " \n", 196 | " # fc_val_admm\n", 197 | " admm_params['init_B'] = dual_sol['opt_A']\n", 198 | " admm_sol = admm_solver(A, XW, u / n_nodes, delta_l=delta_l, delta_g=delta_g, **admm_params)\n", 199 | " fc_vals_admm[c] = admm_sol['opt_f']\n", 200 | " \n", 201 | " # fc_val_admm_g: admm + greedy\n", 202 | " attack = Greedy_Attack(A, XW, u / n_nodes, delta_l=delta_l, delta_g=delta_g)\n", 203 | " if np.array_equal(admm_sol['opt_A'], admm_sol['opt_A'].T):\n", 204 | " admm_A = admm_sol['opt_A']\n", 205 | " else:\n", 206 | " admm_A = np.minimum(admm_sol['opt_A'], admm_sol['opt_A'].T)\n", 207 | " admm_g_sol = attack.attack(admm_A) # init from admm\n", 208 | " fc_vals_admm_g[c] = admm_g_sol['opt_f']\n", 209 | " \n", 210 | " # fc_val_greedy\n", 211 | " attack = Greedy_Attack(A, XW, u / n_nodes, delta_l=delta_l, delta_g=delta_g)\n", 212 | " greedy_sol = attack.attack(A) # init from A\n", 213 | " fc_vals_greedy[c] = greedy_sol['opt_f']\n", 214 | " \n", 215 | " if np.min(fc_vals_dual) >= 0:\n", 216 | " robust_dual += 1\n", 217 | " if np.min(fc_vals_cvx) >= 0:\n", 218 | " robust_cvx += 1\n", 219 | " if np.min(fc_vals_admm) < 0:\n", 220 | " vul_admm += 1\n", 221 | " if np.min(fc_vals_admm_g) < 0:\n", 222 | " vul_admm_g += 1\n", 223 | " if np.min(fc_vals_greedy) < 0:\n", 224 | " vul_greedy += 1" 225 | ] 226 | }, 227 | { 228 | "cell_type": "code", 229 | "execution_count": null, 230 | "metadata": {}, 231 | "outputs": [], 232 | "source": [ 233 | "print('dataset {}'.format(ds_name),\n", 234 | " 'strength {:02d}'.format(strength),\n", 235 | " 'delta_g {:02d}'.format(delta_g),\n", 236 | " 'dual {:.2f}'.format(robust_dual / correct),\n", 237 | " 'cvx {:.2f}'.format(robust_cvx / correct),\n", 238 | " 'admm rate {:.2f}'.format(vul_admm / correct),\n", 239 | " 'admm_g rate {:.2f}'.format(vul_admm_g / correct),\n", 240 | " 'greedy rate {:.2f}'.format(vul_greedy / correct),)" 241 | ] 242 | }, 243 | { 244 | "cell_type": "markdown", 245 | "metadata": {}, 246 | "source": [ 247 | "## Warm start from adversarial sample by greedy method" 248 | ] 249 | }, 250 | { 251 | "cell_type": "code", 252 | "execution_count": null, 253 | "metadata": {}, 254 | "outputs": [], 255 | "source": [ 256 | "strength = 3\n", 257 | "for idx, data in tqdm(enumerate(train_dataset), desc='adverarial examples'):\n", 258 | " A, X, y = process_data(data)\n", 259 | " deg = A.sum(1)\n", 260 | " n_nodes = A.shape[0]\n", 261 | " delta_l = np.minimum(np.maximum(deg - np.max(deg) + strength, 0), n_nodes - 1).astype(int)\n", 262 | " delta_g = n_nodes * np.max(delta_l)\n", 263 | " logits = cal_logits(A, X@W, U, act='linear')\n", 264 | " c_pred = logits.argmax()\n", 265 | " \n", 266 | " fc_vals_greedy = [0] * k\n", 267 | " fc_A_greedy = [A] * k\n", 268 | " for c in range(k):\n", 269 | " u = U[y] - U[c]\n", 270 | " XW = X@W\n", 271 | " ''' greedy attack '''\n", 272 | " attack = Greedy_Attack(A, XW, u / n_nodes, delta_l=delta_l, delta_g=delta_g)\n", 273 | " greedy_sol = attack.attack(A) # init from A\n", 274 | " fc_vals_greedy[c] = greedy_sol['opt_f']\n", 275 | " fc_A_greedy[c] = greedy_sol['opt_A']\n", 276 | " pick_idx = np.argmin(fc_vals_greedy)\n", 277 | " train_dataset[idx].edge_index = torch.tensor(fc_A_greedy[pick_idx].nonzero())\n", 278 | "torch.save(train_dataset, osp.join(save_path, 'adv_set.pk'))" 279 | ] 280 | }, 281 | { 282 | "cell_type": "markdown", 283 | "metadata": {}, 284 | "source": [ 285 | "## Robust linear model" 286 | ] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": null, 291 | "metadata": {}, 292 | "outputs": [], 293 | "source": [ 294 | "model = GC_NET(hidden=64,\n", 295 | " n_features=dataset.num_features,\n", 296 | " n_classes=dataset.num_classes,\n", 297 | " act='linear',\n", 298 | " pool='avg',\n", 299 | " dropout=0.).to(device)\n", 300 | "adv = torch.load(osp.join(save_path, 'adv_set.pk'))\n", 301 | "adv_loader = DataLoader(adv + train_dataset, batch_size=20)\n", 302 | "\n", 303 | "best = 0\n", 304 | "for epoch in tqdm(range(200), desc='epoch'):\n", 305 | " loss_all = train(model, train_loader, robust=True, adv_loader=adv_loader, lamb=0.5)\n", 306 | " train_acc = eval(model, train_loader, robust=True)\n", 307 | " val_acc = eval(model, val_loader, robust=True)\n", 308 | " \n", 309 | " if val_acc >= best:\n", 310 | " best = val_acc\n", 311 | " torch.save(model.state_dict(), osp.join(save_path, 'result_robust.pk'))\n", 312 | " # tqdm.write(\"epoch {:03d} \".format(epoch+1) + \n", 313 | " # \"train_loss {:.4f} \".format(loss_all) +\n", 314 | " # \"train_acc {:.4f} \".format(train_acc) +\n", 315 | " # \"val_acc {:.4f} \".format(val_acc))\n", 316 | "\n", 317 | "test_acc = eval(model, test_loader, testing=True, save_path=save_path, robust=True)\n", 318 | "print(\"test_acc {:.4f}\".format(test_acc))" 319 | ] 320 | }, 321 | { 322 | "cell_type": "markdown", 323 | "metadata": {}, 324 | "source": [ 325 | "## Robustness certificate with robust model" 326 | ] 327 | }, 328 | { 329 | "cell_type": "code", 330 | "execution_count": null, 331 | "metadata": {}, 332 | "outputs": [], 333 | "source": [ 334 | "W = model.conv.weight.detach().cpu().numpy().astype(np.float64)\n", 335 | "U = model.lin.weight.detach().cpu().numpy().astype(np.float64)\n", 336 | "\n", 337 | "k = dataset.num_classes\n", 338 | "\n", 339 | "# counter of certifiably robust and vulnerable \n", 340 | "robust_dual = 0\n", 341 | "robust_cvx = 0\n", 342 | "vul_admm = 0\n", 343 | "vul_admm_g = 0\n", 344 | "vul_greedy = 0\n", 345 | "\n", 346 | "# counter of correct classification\n", 347 | "correct = 0\n", 348 | "\n", 349 | "# attacker settings\n", 350 | "strength = 3\n", 351 | "delta_g = 10\n", 352 | "\n", 353 | "# setting for solvers\n", 354 | "dual_params = dict(iter=200, nonsmooth_init='random')\n", 355 | "cvx_params = dict(iter=400, lr=0.3, verbose=0, constr='1+2+3', \n", 356 | " activation='linear', algo='swapping', nonsmooth_init='subgrad')\n", 357 | "admm_params = dict(iter=200, mu=1)\n", 358 | "\n", 359 | "for data in tqdm(test_dataset, desc='across graphs'):\n", 360 | " A, X, y = process_data(data)\n", 361 | " deg = A.sum(1)\n", 362 | " n_nodes = A.shape[0]\n", 363 | " n_edges = np.count_nonzero(A) // 2\n", 364 | " \n", 365 | " delta_l = np.minimum(np.maximum(deg - np.max(deg) + strength, 0), n_nodes - 1).astype(int)\n", 366 | " # delta_g\n", 367 | " \n", 368 | " logits = cal_logits(A, X@W, U, act='linear')\n", 369 | " c_pred = logits.argmax()\n", 370 | " \n", 371 | " if c_pred != y:\n", 372 | " continue\n", 373 | " correct += 1\n", 374 | " fc_vals_orig = [0] * k\n", 375 | " fc_vals_dual = [0] * k\n", 376 | " fc_vals_cvx = [0] * k\n", 377 | " fc_vals_admm = [0] * k\n", 378 | " fc_vals_admm_g = [0] * k\n", 379 | " fc_vals_greedy = [0] * k\n", 380 | " \n", 381 | " \n", 382 | " for c in tqdm(range(k), desc='across labels', leave=False):\n", 383 | " if c == y:\n", 384 | " continue\n", 385 | " u = U[y] - U[c]\n", 386 | " XW = X@W\n", 387 | " \n", 388 | " # fc_val_orig\n", 389 | " fc_vals_orig[c] = calculate_Fc(A, XW, u / n_nodes)\n", 390 | " \n", 391 | " # fc_val_dual\n", 392 | " dual_sol = dual_solver(A, XW, u / n_nodes, delta_l=delta_l, delta_g=delta_g, **dual_params)\n", 393 | " fc_vals_dual[c] = dual_sol['opt_f']\n", 394 | " \n", 395 | " # fc_val_cvx\n", 396 | " cvx_sol =cvx_env_solver(A, XW, u / n_nodes, delta_l=delta_l, delta_g=delta_g, **cvx_params)\n", 397 | " fc_vals_cvx[c] = cvx_sol['opt_f']\n", 398 | " \n", 399 | " # fc_val_admm\n", 400 | " admm_params['init_B'] = dual_sol['opt_A']\n", 401 | " admm_sol = admm_solver(A, XW, u / n_nodes, delta_l=delta_l, delta_g=delta_g, **admm_params)\n", 402 | " fc_vals_admm[c] = admm_sol['opt_f']\n", 403 | " \n", 404 | " # fc_val_admm_g: admm + greedy\n", 405 | " attack = Greedy_Attack(A, XW, u / n_nodes, delta_l=delta_l, delta_g=delta_g)\n", 406 | " if np.array_equal(admm_sol['opt_A'], admm_sol['opt_A'].T):\n", 407 | " admm_A = admm_sol['opt_A']\n", 408 | " else:\n", 409 | " admm_A = np.minimum(admm_sol['opt_A'], admm_sol['opt_A'].T)\n", 410 | " admm_g_sol = attack.attack(admm_A) # init from admm\n", 411 | " fc_vals_admm_g[c] = admm_g_sol['opt_f']\n", 412 | " \n", 413 | " # fc_val_greedy\n", 414 | " attack = Greedy_Attack(A, XW, u / n_nodes, delta_l=delta_l, delta_g=delta_g)\n", 415 | " greedy_sol = attack.attack(A) # init from A\n", 416 | " fc_vals_greedy[c] = greedy_sol['opt_f']\n", 417 | " \n", 418 | " if np.min(fc_vals_dual) >= 0:\n", 419 | " robust_dual += 1\n", 420 | " if np.min(fc_vals_cvx) >= 0:\n", 421 | " robust_cvx += 1\n", 422 | " if np.min(fc_vals_admm) < 0:\n", 423 | " vul_admm += 1\n", 424 | " if np.min(fc_vals_admm_g) < 0:\n", 425 | " vul_admm_g += 1\n", 426 | " if np.min(fc_vals_greedy) < 0:\n", 427 | " vul_greedy += 1" 428 | ] 429 | }, 430 | { 431 | "cell_type": "code", 432 | "execution_count": null, 433 | "metadata": {}, 434 | "outputs": [], 435 | "source": [ 436 | "print('dataset {}'.format(ds_name),\n", 437 | " 'strength {:02d}'.format(strength),\n", 438 | " 'delta_g {:02d}'.format(delta_g),\n", 439 | " 'dual {:.2f}'.format(robust_dual / correct),\n", 440 | " 'cvx {:.2f}'.format(robust_cvx / correct),\n", 441 | " 'admm rate {:.2f}'.format(vul_admm / correct),\n", 442 | " 'admm_g rate {:.2f}'.format(vul_admm_g / correct),\n", 443 | " 'greedy rate {:.2f}'.format(vul_greedy / correct),)" 444 | ] 445 | } 446 | ], 447 | "metadata": { 448 | "kernelspec": { 449 | "display_name": "Python 3", 450 | "language": "python", 451 | "name": "python3" 452 | }, 453 | "language_info": { 454 | "codemirror_mode": { 455 | "name": "ipython", 456 | "version": 3 457 | }, 458 | "file_extension": ".py", 459 | "mimetype": "text/x-python", 460 | "name": "python", 461 | "nbconvert_exporter": "python", 462 | "pygments_lexer": "ipython3", 463 | "version": "3.7.6" 464 | } 465 | }, 466 | "nbformat": 4, 467 | "nbformat_minor": 4 468 | } 469 | -------------------------------------------------------------------------------- /robograph/attack/convex_relaxation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | from robograph.attack.dp import exact_solver_wrapper 4 | import robograph.attack.utils as utils 5 | import scipy.optimize as optim 6 | from robograph.attack.SPG import * 7 | from docplex.mp.model import Model 8 | # import matlab.engine 9 | 10 | 11 | class ConvexRelaxation(object): 12 | """ 13 | Convex relaxation of F_c(A) 14 | 15 | param: 16 | A_org: original adjacency matrix 17 | XW: XW 18 | U: (u_y-u_c)/nG 19 | delta_l: row budgets 20 | delta_g: global budgets 21 | activation: 'linear' or 'relu' 22 | relaxation: 'envelop' or 'perspective' 23 | relu_relaxation: 'singleL' or 'doubleL' 24 | """ 25 | 26 | def __init__(self, A_org, XW, U, delta_l, delta_g, activation='linear', relaxation='envelop', relu_relaxation='singleL'): 27 | self.A_org = A_org 28 | self.XW, self.U = XW, U 29 | self.XWU = XW @ U 30 | self.delta_l, self.delta_g = delta_l, delta_g 31 | self.activation = activation 32 | self.relaxation = relaxation 33 | self.nG = A_org.shape[0] 34 | self.warm_start_R = np.random.randn(self.nG, self.nG) 35 | self.warm_start_r = np.random.randn(self.nG) 36 | self.warm_start_W = A_org.copy() 37 | if activation == 'relu': 38 | self.relu_relaxation = relu_relaxation 39 | self.hidden_dim = len(U) 40 | self.lb, self.ub = self.relu_bounds() 41 | self.S_minus = (self.lb*self.ub < 0) & np.tile((self.U < 0), (self.nG, 1)) 42 | if relu_relaxation == 'doubleL': 43 | self.I_plus = (self.lb > 0) 44 | self.I_mix = (self.lb*self.ub < 0) 45 | self.Q, self.p = self.doubleL_lb_coefficient() 46 | elif relu_relaxation == 'singleL': 47 | self.S_others = ~(self.S_minus) 48 | 49 | def convex_F(self, A_pert): 50 | if self.activation == 'relu': 51 | return self.convex_F_relu(A_pert) 52 | elif self.activation == 'linear': 53 | return self.convex_F_linear(A_pert) 54 | 55 | def convex_F_linear(self, A_pert): 56 | if self.relaxation == 'perspective': 57 | # Perspective Relaxation for linear activation 58 | fval, G = 0, np.zeros((self.nG, self.nG)) 59 | M = max(0, -min(self.XWU)) 60 | b = self.XWU + M 61 | for i in range(self.nG): 62 | x = A_pert[i, :] 63 | Dx = b*x 64 | dom = sum(x)+1 65 | fval += (x@Dx+b[i])/dom - M 66 | G[i, :] = (2*Dx*dom - (x@Dx+b[i])*np.ones(x.shape))/(dom**2) 67 | return fval, G 68 | 69 | elif self.relaxation == 'envelop': 70 | # Convex Envelop Relaxation for linear activation 71 | R = np.zeros((self.nG, self.nG)) 72 | iters, eps = 100, 0.0 73 | 74 | def objective(x): 75 | R = x.reshape(self.nG, self.nG) 76 | dp_sol = exact_solver_wrapper(self.A_org, np.tile(self.XWU, (self.nG, 1)), 77 | np.zeros(self.nG), -R, self.delta_l, self.delta_g, '1') 78 | _, opt_val, opt_W = dp_sol 79 | fval = opt_val.sum() + np.sum(R*A_pert) - eps*np.sum(R**2)/2 80 | grad = A_pert - opt_W - eps*R 81 | return -fval, -grad.flatten() 82 | # print(optim.check_grad(lambda x: objective(x)[0], lambda x: objective(x)[1], R.flatten())) 83 | 84 | def callback(x): 85 | print(-objective(x)[0]) 86 | 87 | res = optim.fmin_l_bfgs_b(objective, R.flatten(), maxiter=iters, m=30, callback=None) 88 | F, G = -res[1], res[0].reshape(self.nG, self.nG) 89 | return F, G 90 | 91 | def convex_F_relu(self, A_pert): 92 | # Perspective Relaxation for ReLU (double linear) 93 | if self.relaxation == 'perspective': 94 | if self.relu_relaxation == 'doubleL': 95 | fval, G = 0, np.zeros((self.nG, self.nG)) 96 | for i in range(self.nG): 97 | q = self.Q[i] 98 | M = max(0, -min(q)) 99 | b = q + M 100 | x = A_pert[i] 101 | Dx = b*x 102 | dom = np.sum(x)+1 103 | fval += (x@Dx + b[i] + self.p[i])/dom - M 104 | G[i] = (2*Dx*dom - (x@Dx+q[i]+b[i]+self.p[i])*np.ones(x.shape))/(dom**2) 105 | return fval, G 106 | else: 107 | raise Exception("Perspective Relaxation is only for 'doubleL' ReLU!") 108 | 109 | elif self.relaxation == 'envelop': 110 | # Convex Envelop Relaxation for ReLU 111 | 112 | # Convex envelop (38) by solving R row-wise 113 | spg_options = default_options 114 | spg_options.curvilinear = 1 115 | spg_options.interp = 2 116 | spg_options.numdiff = 0 # 0 to use gradients, 1 for numerical diff 117 | spg_options.verbose = 0 118 | spg_options.maxIter = 20 119 | 120 | F, G = 0, np.zeros((self.nG, self.nG)) 121 | init_W = self.warm_start_W 122 | for i in range(self.nG): 123 | z = A_pert[i] 124 | 125 | def objective(r): 126 | opt_w_i, opt_f_i = self.get_opt_W_i(i, r, init_W[i], spg_options) 127 | f = opt_f_i + r@z 128 | return -f, opt_w_i-z 129 | # print(optim.check_grad(lambda x: objective(x)[0], lambda x: objective(x)[1], np.random.randn(self.nG))) 130 | 131 | def callback(x): 132 | print(-objective(x)[0]) 133 | 134 | init_r_i = np.random.randn(self.nG)*0.01 135 | res = optim.fmin_l_bfgs_b(objective, init_r_i, maxiter=20, callback=None) 136 | fval, opt_r = -res[1], res[0] 137 | F += fval 138 | G[i] = opt_r 139 | 140 | # Convex envelop (38) by solving R as a matrix 141 | # R = np.zeros((self.nG, self.nG)) 142 | # iters = 100 143 | # def objective(x): 144 | # R = x.reshape(self.nG, self.nG) 145 | # dp_sol = exact_solver_wrapper(self.A_org, self.Q.T, self.p, -R.T, self.delta_l, self.delta_g, '1') 146 | # _, opt_val, opt_W = dp_sol 147 | # fval = opt_val.sum() + np.sum(R*A_pert) 148 | # grad = A_pert - opt_W 149 | # return -fval, -grad.flatten() 150 | # def callback(x): 151 | # print(-objective(x)[0]) 152 | 153 | # res = optim.fmin_l_bfgs_b(objective, R.flatten(), maxiter=iters, m=20, callback=callback) 154 | # F, G = -res[1], res[0].reshape(self.nG, self.nG) 155 | 156 | return F, G 157 | 158 | def get_opt_W_i(self, i, r, init_w, spg_options): 159 | # # ------------------ Solving W_i by Projected Gradient Method ------------------ 160 | def func(w): 161 | f, g = self.g_i_hat(w, i) 162 | de = 1+np.sum(w) 163 | return f/de - r @ w, (g*de-f)/de**2 - r 164 | # print(optim.check_grad(lambda x: func(x)[0], lambda x: func(x)[1], init_w)) 165 | 166 | # Projected Gradient Method 167 | def proj(w): 168 | proj_w = utils.projection_coA2(np.delete(w, i), np.delete(self.A_org[i], i), self.delta_l[i]) 169 | return np.insert(proj_w, i, 0) 170 | opt_w_i, opt_f_i = SPG(func, proj, init_w, spg_options) 171 | 172 | # # ------------------ Solving W_i as a Linear Constrained Non-convex Problem ------------------ 173 | # lb = np.zeros(self.nG) 174 | # ub = np.ones(self.nG) 175 | # ub[i] = 0 176 | # bounds = optim.Bounds(lb, ub) 177 | # v = -2*self.A_org[i] + 1 178 | # linear_constraint = optim.LinearConstraint(v, -np.inf, self.delta_l[i] - np.sum(self.A_org[i])) 179 | # res = optim.minimize(lambda x: func(x)[0], init_w, method='trust-constr', jac=lambda x: func(x)[1], 180 | # constraints=linear_constraint, options={'verbose': 0}, bounds=bounds) 181 | # # print(res.message) 182 | # opt_w_i, opt_f_i = res.x, res.fun 183 | 184 | return opt_w_i, opt_f_i 185 | 186 | def get_opt_W(self, R, spg_options): 187 | # # ------------------ Solving W as a Linear Constrained Non-convex Problem ------------------ 188 | # start = time.time() 189 | # init_W = self.warm_start_W 190 | # def func(W): 191 | # mat_W = W.reshape(self.nG, self.nG) 192 | # kappa, G = self.g_hat(mat_W) 193 | # de = 1+np.sum(mat_W, axis=1) 194 | # f = np.sum(kappa/de) - np.sum(R*mat_W) 195 | # grad = (G*de.reshape(-1, 1)-kappa.reshape(-1, 1))/(de**2).reshape(-1, 1) - R 196 | # return f, grad.ravel() 197 | # # print(optim.check_grad(lambda x: func(x)[0], lambda x: func(x)[1], self.A_org.copy().ravel())) 198 | # def proj(W): 199 | # mat_W = W.reshape(self.nG, self.nG) 200 | # proj_W = utils.projection_coA1(utils.delete_diagonal(mat_W), utils.delete_diagonal(self.A_org), self.delta_l) 201 | # return utils.fill_diagonal(proj_W).ravel() 202 | # opt_W_1, F_1 = SPG(func, proj, init_W.ravel(), spg_options) 203 | # opt_W_1 = opt_W_1.reshape(self.nG, self.nG) 204 | # # self.warm_start_W = opt_W 205 | # print(f'opt on W cputime: {time.time() - start}') 206 | 207 | # # ------------------ Solving W Row-wise ------------------ 208 | start = time.time() 209 | init_W = self.warm_start_W 210 | opt_F, opt_W = 0, np.zeros((self.nG, self.nG)) 211 | for i in range(self.nG): 212 | r_i = R[i] 213 | opt_w_i, opt_f_i = self.get_opt_W_i(i, r_i, init_W[i], spg_options) 214 | opt_W[i] = opt_w_i 215 | opt_F += opt_f_i 216 | # self.warm_start_W = opt_W 217 | # print(f'row-wise W cputime: {time.time() - start}') 218 | return opt_W, opt_F 219 | 220 | def g_hat(self, A_pert): 221 | if self.relu_relaxation == 'doubleL': 222 | f = np.sum((A_pert + np.eye(self.nG)) * self.Q, axis=1) + self.p 223 | grad_Z = self.Q 224 | else: 225 | A_hat_XW = A_pert @ self.XW + self.XW 226 | rep_U = np.tile(self.U, (self.nG, 1)) 227 | 228 | # second term in eq (36) 229 | a, b = self.ub, self.ub - self.lb 230 | tmp1 = np.divide(a, b, out=np.zeros_like(a), where=b != 0) 231 | term2 = np.sum(rep_U * ((A_hat_XW - self.lb)*tmp1) * self.S_minus, axis=1) 232 | g_on_term2 = (rep_U*tmp1*self.S_minus) @ self.XW.T 233 | 234 | # first term in eq (36) 235 | before_relu = A_hat_XW 236 | after_relu = np.maximum(before_relu, 0) 237 | term1 = np.sum(rep_U * after_relu * self.S_others, axis=1) 238 | g_on_term1 = (rep_U*(before_relu > 0)*self.S_others) @ self.XW.T 239 | 240 | f = term1+term2 241 | grad_Z = g_on_term1+g_on_term2 242 | 243 | return f, grad_Z 244 | 245 | def g_i_hat(self, z, i): 246 | if self.relu_relaxation == 'doubleL': 247 | f = z @ self.Q[i] + self.Q[i, i] + self.p[i] 248 | grad_z_i = self.Q[i] 249 | else: 250 | S_i_minus = self.S_minus[i] 251 | A_hat_XW = z @ self.XW + self.XW[i] 252 | ub_i, lb_i = self.ub[i], self.lb[i] 253 | 254 | # second term in eq (36) 255 | tmp1 = ub_i[S_i_minus]/(ub_i[S_i_minus] - lb_i[S_i_minus]) 256 | term2 = self.U[S_i_minus] @ ((A_hat_XW[S_i_minus] - lb_i[S_i_minus])*tmp1) 257 | g_on_term2 = self.XW[:, S_i_minus] @ (self.U[S_i_minus]*tmp1) 258 | 259 | # second term under vanilla relu 260 | # before_relu = A_hat_XW[S_i_minus] 261 | # after_relu = np.maximum(before_relu, 0) 262 | # term2_vanilla = self.U[S_i_minus] @ after_relu 263 | 264 | # first term in eq (36) 265 | S_i_others = ~(S_i_minus) 266 | before_relu = A_hat_XW[S_i_others] 267 | after_relu = np.maximum(before_relu, 0) 268 | term1 = self.U[S_i_others] @ after_relu 269 | g_on_term1 = self.XW[:, S_i_others] @ (self.U[S_i_others]*(before_relu > 0)) 270 | 271 | f = term1+term2 272 | grad_z_i = g_on_term1+g_on_term2 273 | 274 | return f, grad_z_i 275 | 276 | def relu_bounds(self): 277 | # update lb and ub matrix 278 | # lb_{ij} = min_{|| A_{i:} - A_{i:}^ori||_1 \le delta_l} A_{i:} @ (XW)_{:j} + (XW)_{i,j} 279 | # = min_{1'v \le delta_l} [(2*A_{i:}^ori-1)\circ(-2v+1) + 1]/2 @ (XW)_{:j} + (XW)_{i,j} 280 | # = min_{1'v \le delta_l} v.T @ [(1 - 2*A_{i:}^ori) \circ (XW)_{:j}] + A_{i:}^ori @ (XW)_{:j} + (XW)_{i,j} 281 | # Also need to consider delta_g 282 | lb = np.zeros((self.nG, self.hidden_dim)) 283 | ub = np.zeros((self.nG, self.hidden_dim)) 284 | for i in range(self.nG): 285 | A_i = self.A_org[i] 286 | const = A_i @ self.XW 287 | L = np.expand_dims((1-2*A_i), axis=1) * self.XW 288 | for j in range(self.hidden_dim): 289 | L_j = L[:, j] 290 | indices = np.argsort(L_j) 291 | k, flip, minimum = 0, 0, 0 292 | while k < self.nG and L_j[indices[k]] < 0 and flip < self.delta_l[i] and flip < self.delta_g: 293 | if indices[k] == i: 294 | k += 1 295 | continue 296 | minimum += L_j[indices[k]] 297 | flip += 1 298 | k += 1 299 | 300 | k, flip, maximum = self.nG-1, 0, 0 301 | while k > -1 and L_j[indices[k]] > 0 and flip < self.delta_l[i] and flip < self.delta_g: 302 | if indices[k] == i: 303 | k -= 1 304 | continue 305 | maximum += L_j[indices[k]] 306 | flip += 1 307 | k -= 1 308 | lb[i, j] = minimum + const[j] + self.XW[i, j] 309 | ub[i, j] = maximum + const[j] + self.XW[i, j] 310 | return lb, ub 311 | 312 | def doubleL_lb_coefficient(self): 313 | # g_i_hat = (A_i+e_i)*Q_i + p_i 314 | rep_U = np.tile(self.U, (self.nG, 1)) 315 | 316 | # linear term in I_plus: (A_pert + I) .* Q1 317 | Q1 = (rep_U * self.I_plus) @ self.XW.T 318 | 319 | # linear term in I_mix: (A_pert + I) .* Q2 320 | a, b = self.ub, self.ub - self.lb 321 | tmp1 = np.divide(a, b, out=np.zeros_like(a), where=b != 0) 322 | Q2 = (rep_U * self.I_mix * tmp1) @ self.XW.T 323 | 324 | # remainder term in S_minus 325 | p = -np.sum(rep_U * self.S_minus * self.lb * tmp1, axis=1) 326 | return Q1+Q2, p 327 | 328 | def doubleL_ub_coefficient(self): 329 | # g_i_hat = (A_i+e_i)*Q_i + p_i 330 | rep_U = np.tile(self.U, (self.nG, 1)) 331 | 332 | # linear term in I_plus: (A_pert + I) .* Q1 333 | Q1 = (rep_U * self.I_plus) @ self.XW.T 334 | 335 | # linear term in I_mix: (A_pert + I) .* Q2 336 | a, b = self.ub, self.ub - self.lb 337 | tmp1 = np.divide(a, b, out=np.zeros_like(a), where=b != 0) 338 | Q2 = (rep_U * self.I_mix * tmp1) @ self.XW.T 339 | 340 | # remainder term in S_plus 341 | S_plus = (self.lb*self.ub < 0) & np.tile((self.U > 0), (self.nG, 1)) 342 | p = -np.sum(rep_U * S_plus * self.lb * tmp1, axis=1) 343 | return Q1+Q2, p 344 | 345 | # def F_relaxation_2(self, A_pert): 346 | # # Convex envelop (38) by solving eq (39) element-wise 347 | # F, G = 0, np.zeros((self.nG, self.nG)) 348 | # for i in range(self.nG): 349 | # f, g = self.f_i_relaxation_2(A_pert[i], i) 350 | # F += f 351 | # G[i] = g 352 | # return F, G 353 | 354 | # def f_i_relaxation_2(self, z, i): 355 | # r = np.zeros(self.nG) 356 | # r_iters = 10 357 | # f_r = [0]*r_iters 358 | # for j in range(r_iters): 359 | # pass 360 | # # alpha_iters = 10 361 | # # f_alpha = [0]*alpha_iters 362 | # # alpha = np.sum(A_org[i]) 363 | # # alpha_min, alpha_max = max(0, alpha-delta_l), min(self.nG, alpha+delta_l) 364 | # # for k in range(alpha_iters): 365 | # # w_iters = 10 366 | # # f_w = [0]*w_iters 367 | # # # given alpha, find optimal w 368 | # # w = A_org[i].copy() 369 | # # for q in range(w_iters): 370 | # # f, g = g_i_hat(w, i) 371 | # # f = f/(1+alpha) - r @ w 372 | # # g = g/(1+alpha) - r 373 | # # f_w[q] = f 374 | # # w = w - 0.01*g 375 | # # w = projection_coPi_and_affine(np.delete(w, i), np.delete(A_org[i], i), delta_l, alpha) 376 | # # w = np.insert(w, i, 0) 377 | # # # w = projection_coPi_and_affine(w - 0.1*g, A_org[i], delta_l, alpha) 378 | # # # print(f_w) 379 | 380 | # # # given optimal w, take the gradient over alpha via eq (45) 381 | # # kappa, grad_kappa = g_i_hat(w, i) 382 | # # f_alpha[k] = kappa/(1+alpha) - r@w 383 | # # theta = np.abs(w - A_org[i]) 384 | # # beta = (1-2*A_org[i]) 385 | # # gamma = r*beta 386 | # # grad_kappa = grad_kappa * beta 387 | # # slack_idx = np.argwhere((theta<0.99) & (theta>0.01)) 388 | # # if len(slack_idx) > 1: 389 | # # if np.sum(theta) < delta_l: 390 | # # si = slack_idx[0] 391 | # # mu = -(grad_kappa[si]/(1+alpha) - gamma[si])/beta[si] 392 | # # else: 393 | # # si, sj = slack_idx[0], slack_idx[1] 394 | # # if beta[si] == beta[sj]: 395 | # # mu = -(grad_kappa[si]/(1+alpha) - gamma[si])/beta[si] 396 | # # else: 397 | # # mu = ((grad_kappa[si]-grad_kappa[sj])/(1+alpha) + (gamma[sj]-gamma[si]))/(beta[sj]-beta[si]) 398 | # # else: 399 | # # print('less than 2 indices satify theta_j \in (0, 1)!') 400 | # # grad_on_alpha = -kappa/(1+alpha)**2 - mu 401 | 402 | # # # update alpha 403 | # # alpha -= 0.1*grad_on_alpha.item() 404 | # # alpha = max(alpha_min, alpha) 405 | # # alpha = min(alpha_max, alpha) 406 | # # # print(f_alpha) 407 | 408 | # # f_r[j] = kappa/(1+alpha) + r@(z-w) 409 | 410 | # def f_i(self, z, i): 411 | # before_relu = z @ self.XW + self.XW[i] 412 | # after_relu = np.maximum(before_relu, 0) 413 | # term1 = self.U @ after_relu 414 | # g_on_term1 = self.XW @ ( self.U*(after_relu > 0) ) 415 | 416 | # return term1, g_on_term1 417 | -------------------------------------------------------------------------------- /demo-relu.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# RoboGraph\n", 8 | "This is the demo for the submission of paper\n", 9 | "> __Certified Robustness of Graph Convolution Networks for Graph Classification under Topological Attacks__\n", 10 | "\n", 11 | "Before running the demo, please make sure all the required packages are installed.\n", 12 | "\n", 13 | "A detailed instruction is provided in [README.md](./README.md)." 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": null, 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "import torch\n", 23 | "import numpy as np\n", 24 | "import os.path as osp\n", 25 | "import tempfile\n", 26 | "from torch_geometric.datasets import TUDataset\n", 27 | "from torch_geometric.data import DataLoader\n", 28 | "from torch_geometric.data.makedirs import makedirs\n", 29 | "from robograph.model.gnn import GC_NET, train, eval\n", 30 | "from tqdm.notebook import tqdm\n", 31 | "from robograph.utils import process_data, cal_logits\n", 32 | "\n", 33 | "from robograph.attack.admm import admm_solver_doubleL\n", 34 | "from robograph.attack.cvx_env_solver import cvx_env_solver\n", 35 | "from robograph.attack.dual import dual_solver_doubleL\n", 36 | "from robograph.attack.greedy_attack import Greedy_Attack\n", 37 | "from robograph.attack.utils import calculate_Fc\n", 38 | "from robograph.attack.convex_relaxation import ConvexRelaxation" 39 | ] 40 | }, 41 | { 42 | "cell_type": "markdown", 43 | "metadata": {}, 44 | "source": [ 45 | "## Graph classification with _ReLU_ activation function" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": null, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "torch.manual_seed(0)\n", 55 | "np.random.seed(0)\n", 56 | "\n", 57 | "# prepare dataset\n", 58 | "ds_name = 'ENZYMES'\n", 59 | "path = osp.join(tempfile.gettempdir(), 'data', ds_name)\n", 60 | "save_path = osp.join(tempfile.gettempdir(), 'data', ds_name, 'saved')\n", 61 | "if not osp.isdir(save_path):\n", 62 | " makedirs(save_path)\n", 63 | "dataset = TUDataset(path, name=ds_name, use_node_attr=True)\n", 64 | "dataset = dataset.shuffle()\n", 65 | "train_size = len(dataset) // 10 * 3\n", 66 | "val_size = len(dataset) // 10 * 2\n", 67 | "train_dataset = dataset[:train_size]\n", 68 | "val_dataset = dataset[train_size: train_size + val_size]\n", 69 | "test_dataset = dataset[train_size + val_size:]\n", 70 | "\n", 71 | "# prepare dataloader\n", 72 | "train_loader = DataLoader(train_dataset, batch_size=20)\n", 73 | "val_loader = DataLoader(val_dataset, batch_size=20)\n", 74 | "test_loader = DataLoader(test_dataset, batch_size=20)\n", 75 | "\n", 76 | "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", 77 | "\n", 78 | "# create model\n", 79 | "model = GC_NET(hidden=64,\n", 80 | " n_features=dataset.num_features,\n", 81 | " n_classes=dataset.num_classes,\n", 82 | " act='relu',\n", 83 | " pool='avg',\n", 84 | " dropout=0.).to(device)" 85 | ] 86 | }, 87 | { 88 | "cell_type": "markdown", 89 | "metadata": {}, 90 | "source": [ 91 | "## Training a vanilla model" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": null, 97 | "metadata": { 98 | "scrolled": false 99 | }, 100 | "outputs": [], 101 | "source": [ 102 | "best=0\n", 103 | "for epoch in tqdm(range(200)):\n", 104 | " loss_all = train(model, train_loader)\n", 105 | " train_acc = eval(model, train_loader)\n", 106 | " val_acc = eval(model, val_loader)\n", 107 | " if val_acc >= best:\n", 108 | " best = val_acc\n", 109 | " torch.save(model.state_dict(), osp.join(save_path, \"result.pk\"))\n", 110 | " \n", 111 | " tqdm.write(\"epoch {:03d} \".format(epoch+1) + \n", 112 | " \"train_loss {:.4f} \".format(loss_all) +\n", 113 | " \"train_acc {:.4f} \".format(train_acc) +\n", 114 | " \"val_acc {:.4f} \".format(val_acc))\n", 115 | "test_acc = eval(model, test_loader, testing=True, save_path=save_path)\n", 116 | "print(\"test_acc {:.4f}\".format(test_acc))" 117 | ] 118 | }, 119 | { 120 | "cell_type": "markdown", 121 | "metadata": {}, 122 | "source": [ 123 | "## Robustness certificate" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": null, 129 | "metadata": {}, 130 | "outputs": [], 131 | "source": [ 132 | "W = model.conv.weight.detach().cpu().numpy().astype(np.float64)\n", 133 | "U = model.lin.weight.detach().cpu().numpy().astype(np.float64)\n", 134 | "\n", 135 | "k = dataset.num_classes\n", 136 | "\n", 137 | "# counter of certifiably robust and vulnerable \n", 138 | "robust_dual = 0\n", 139 | "robust_cvx_DL = 0\n", 140 | "robust_cvx_SL = 0\n", 141 | "vul_admm_g = 0\n", 142 | "vul_greedy = 0\n", 143 | "\n", 144 | "# counter of correct classification\n", 145 | "correct = 0\n", 146 | "\n", 147 | "# attacker settings\n", 148 | "strength = 3\n", 149 | "delta_g = 10\n", 150 | "\n", 151 | "# setting for solvers\n", 152 | "dual_params = dict(iter=100, verbose=0, nonsmooth_init='random')\n", 153 | "cvx_params = dict(iter=400, lr=0.3, verbose=0, constr='1+2+3', \n", 154 | " activation='relu', algo='swapping', nonsmooth_init='subgrad')\n", 155 | "admm_params = dict(iter=200, mu=1)\n", 156 | "\n", 157 | "for data in tqdm(test_dataset, desc='across graphs'):\n", 158 | " A, X, y = process_data(data)\n", 159 | " deg = A.sum(1)\n", 160 | " n_nodes = A.shape[0]\n", 161 | " n_edges = np.count_nonzero(A) // 2\n", 162 | " \n", 163 | " delta_l = np.minimum(np.maximum(deg - np.max(deg) + strength, 0), n_nodes - 1).astype(int)\n", 164 | " # delta_g\n", 165 | " \n", 166 | " logits = cal_logits(A, X@W, U, act='relu')\n", 167 | " c_pred = logits.argmax()\n", 168 | " \n", 169 | " if c_pred != y:\n", 170 | " continue\n", 171 | " correct += 1\n", 172 | " fc_vals_orig = [0] * k\n", 173 | " fc_vals_dual = [0] * k\n", 174 | " fc_vals_cvx_DL = [0] * k\n", 175 | " fc_vals_cvx_SL = [0] * k\n", 176 | " fc_vals_admm = [0] * k\n", 177 | " fc_vals_admm_g = [0] * k\n", 178 | " fc_vals_greedy = [0] * k\n", 179 | " \n", 180 | " \n", 181 | " for c in tqdm(range(k), desc='across labels', leave=False):\n", 182 | " if c == y:\n", 183 | " continue\n", 184 | " u = U[y] - U[c]\n", 185 | " XW = X@W\n", 186 | " \n", 187 | " # fc_val_orig\n", 188 | " fc_vals_orig[c] = calculate_Fc(A, XW, u / n_nodes, activation='relu')\n", 189 | " \n", 190 | " # fc_val_dual\n", 191 | " cvx_relax = ConvexRelaxation(A, XW, u / n_nodes, delta_l, delta_g, 'relu', 'envelop', 'doubleL')\n", 192 | " dual_sol = dual_solver_doubleL(A, cvx_relax.Q, cvx_relax.p, \n", 193 | " delta_l=delta_l, delta_g=delta_g, **dual_params)\n", 194 | " fc_vals_dual[c] = dual_sol['opt_f']\n", 195 | " \n", 196 | " # fc_val_cvx_DL\n", 197 | " cvx_env_params = dict(iter=100, lr=1, verbose=0, constr='1+2+3', nonsmooth_init='subgrad',\n", 198 | " activation='relu', algo='swapping', relu_bound='doubleL')\n", 199 | " cvx_env_doubleL_sol = cvx_env_solver(A, XW, u / n_nodes, delta_l, delta_g, **cvx_env_params)\n", 200 | " fc_vals_cvx_DL[c] = cvx_env_doubleL_sol['opt_f']\n", 201 | " \n", 202 | " # fc_val_cvx_SL\n", 203 | " cvx_env_params = dict(iter=100, lr=1, verbose=0, constr='1+2+3', nonsmooth_init='subgrad',\n", 204 | " activation='relu', algo='swapping', relu_bound='singleL')\n", 205 | " cvx_env_singleL_sol = cvx_env_solver(A, XW, u / n_nodes, delta_l, delta_g, **cvx_env_params)\n", 206 | " fc_vals_cvx_SL[c] = cvx_env_singleL_sol['opt_f']\n", 207 | " \n", 208 | " # fc_val_admm\n", 209 | " Q, p = cvx_relax.doubleL_ub_coefficient()\n", 210 | " admm_params['init_B'] = dual_sol['opt_A'].copy()\n", 211 | " admm_sol = admm_solver_doubleL(A, Q, p, delta_l, delta_g, **admm_params)\n", 212 | " fc_vals_admm[c] = admm_sol['opt_f']\n", 213 | " \n", 214 | " # fc_val_admm_g: admm + greedy\n", 215 | " admm_g_attack = Greedy_Attack(A, XW, u / n_nodes, delta_l=delta_l, delta_g=delta_g, activation='relu')\n", 216 | " if np.array_equal(admm_sol['opt_A'], admm_sol['opt_A'].T):\n", 217 | " admm_A = admm_sol['opt_A']\n", 218 | " else:\n", 219 | " admm_A = np.minimum(admm_sol['opt_A'], admm_sol['opt_A'].T)\n", 220 | " admm_g_sol = admm_g_attack.attack(admm_A) # init from A\n", 221 | " fc_vals_admm_g[c] = admm_g_sol['opt_f']\n", 222 | " \n", 223 | " # fc_val_greedy\n", 224 | " attack = Greedy_Attack(A, XW, u / n_nodes, delta_l=delta_l, delta_g=delta_g, activation='relu')\n", 225 | " greedy_sol = attack.attack(A) # init from A\n", 226 | " fc_vals_greedy[c] = greedy_sol['opt_f']\n", 227 | " \n", 228 | " if np.min(fc_vals_dual) >= 0:\n", 229 | " robust_dual += 1\n", 230 | " if np.min(fc_vals_cvx_DL) >= 0:\n", 231 | " robust_cvx_DL += 1\n", 232 | " if np.min(fc_vals_cvx_SL) >= 0:\n", 233 | " robust_cvx_SL += 1\n", 234 | " if np.min(fc_vals_admm_g) < 0:\n", 235 | " vul_admm_g += 1\n", 236 | " if np.min(fc_vals_greedy) < 0:\n", 237 | " vul_greedy += 1" 238 | ] 239 | }, 240 | { 241 | "cell_type": "code", 242 | "execution_count": null, 243 | "metadata": {}, 244 | "outputs": [], 245 | "source": [ 246 | "print('dataset {}'.format(ds_name),\n", 247 | " 'strength {:02d}'.format(strength),\n", 248 | " 'delta_g {:02d}'.format(delta_g),\n", 249 | " 'dual {:.3f}'.format(robust_dual / correct),\n", 250 | " 'cvx_DL {:.3f}'.format(robust_cvx_DL / correct),\n", 251 | " 'cvx_SL {:.3f}'.format(robust_cvx_SL / correct),\n", 252 | " 'admm_g {:.3f}'.format(vul_admm_g / correct),\n", 253 | " 'greedy {:.3f}'.format(vul_greedy / correct),\n", 254 | " )" 255 | ] 256 | }, 257 | { 258 | "cell_type": "markdown", 259 | "metadata": {}, 260 | "source": [ 261 | "## Warm start from adversarial sample by greedy method" 262 | ] 263 | }, 264 | { 265 | "cell_type": "code", 266 | "execution_count": null, 267 | "metadata": {}, 268 | "outputs": [], 269 | "source": [ 270 | "strength = 3\n", 271 | "for idx, data in tqdm(enumerate(train_dataset), desc='adverarial examples'):\n", 272 | " A, X, y = process_data(data)\n", 273 | " deg = A.sum(1)\n", 274 | " n_nodes = A.shape[0]\n", 275 | " delta_l = np.minimum(np.maximum(deg - np.max(deg) + strength, 0), n_nodes - 1).astype(int)\n", 276 | " delta_g = n_nodes * np.max(delta_l)\n", 277 | " logits = cal_logits(A, X@W, U, act='relu')\n", 278 | " c_pred = logits.argmax()\n", 279 | " \n", 280 | " fc_vals_greedy = [0] * k\n", 281 | " fc_A_greedy = [A] * k\n", 282 | " for c in range(k):\n", 283 | " u = U[y] - U[c]\n", 284 | " XW = X@W\n", 285 | " ''' greedy attack '''\n", 286 | " attack = Greedy_Attack(A, XW, u / n_nodes, delta_l=delta_l, delta_g=delta_g)\n", 287 | " greedy_sol = attack.attack(A) # init from A\n", 288 | " fc_vals_greedy[c] = greedy_sol['opt_f']\n", 289 | " fc_A_greedy[c] = greedy_sol['opt_A']\n", 290 | " pick_idx = np.argmin(fc_vals_greedy)\n", 291 | " train_dataset[idx].edge_index = torch.tensor(fc_A_greedy[pick_idx].nonzero())\n", 292 | "torch.save(train_dataset, osp.join(save_path, 'adv_set.pk'))" 293 | ] 294 | }, 295 | { 296 | "cell_type": "markdown", 297 | "metadata": {}, 298 | "source": [ 299 | "## Robust ReLU model" 300 | ] 301 | }, 302 | { 303 | "cell_type": "code", 304 | "execution_count": null, 305 | "metadata": {}, 306 | "outputs": [], 307 | "source": [ 308 | "model = GC_NET(hidden=64,\n", 309 | " n_features=dataset.num_features,\n", 310 | " n_classes=dataset.num_classes,\n", 311 | " act='relu',\n", 312 | " pool='avg',\n", 313 | " dropout=0.).to(device)\n", 314 | "adv = torch.load(osp.join(save_path, 'adv_set.pk'))\n", 315 | "adv_loader = DataLoader(adv + train_dataset, batch_size=20)\n", 316 | "\n", 317 | "best = 0\n", 318 | "for epoch in tqdm(range(200), desc='epoch'):\n", 319 | " loss_all = train(model, train_loader, robust=True, adv_loader=adv_loader, lamb=0.5)\n", 320 | " train_acc = eval(model, train_loader, robust=True)\n", 321 | " val_acc = eval(model, val_loader, robust=True)\n", 322 | " \n", 323 | " if val_acc >= best:\n", 324 | " best = val_acc\n", 325 | " torch.save(model.state_dict(), osp.join(save_path, 'result_robust.pk'))\n", 326 | " # tqdm.write(\"epoch {:03d} \".format(epoch+1) + \n", 327 | " # \"train_loss {:.4f} \".format(loss_all) +\n", 328 | " # \"train_acc {:.4f} \".format(train_acc) +\n", 329 | " # \"val_acc {:.4f} \".format(val_acc))\n", 330 | "\n", 331 | "test_acc = eval(model, test_loader, testing=True, save_path=save_path, robust=True)\n", 332 | "print(\"test_acc {:.4f}\".format(test_acc))" 333 | ] 334 | }, 335 | { 336 | "cell_type": "markdown", 337 | "metadata": {}, 338 | "source": [ 339 | "## Robustness certificate with robust model" 340 | ] 341 | }, 342 | { 343 | "cell_type": "code", 344 | "execution_count": null, 345 | "metadata": {}, 346 | "outputs": [], 347 | "source": [ 348 | "W = model.conv.weight.detach().cpu().numpy().astype(np.float64)\n", 349 | "U = model.lin.weight.detach().cpu().numpy().astype(np.float64)\n", 350 | "\n", 351 | "k = dataset.num_classes\n", 352 | "\n", 353 | "# counter of certifiably robust and vulnerable \n", 354 | "robust_dual = 0\n", 355 | "robust_cvx_DL = 0\n", 356 | "robust_cvx_SL = 0\n", 357 | "vul_admm_g = 0\n", 358 | "vul_greedy = 0\n", 359 | "\n", 360 | "# counter of correct classification\n", 361 | "correct = 0\n", 362 | "\n", 363 | "# attacker settings\n", 364 | "strength = 3\n", 365 | "delta_g = 10\n", 366 | "\n", 367 | "# setting for solvers\n", 368 | "dual_params = dict(iter=100, verbose=0, nonsmooth_init='random')\n", 369 | "cvx_params = dict(iter=400, lr=0.3, verbose=0, constr='1+2+3', \n", 370 | " activation='relu', algo='swapping', nonsmooth_init='subgrad')\n", 371 | "admm_params = dict(iter=200, mu=1)\n", 372 | "\n", 373 | "for data in tqdm(test_dataset, desc='across graphs'):\n", 374 | " A, X, y = process_data(data)\n", 375 | " deg = A.sum(1)\n", 376 | " n_nodes = A.shape[0]\n", 377 | " n_edges = np.count_nonzero(A) // 2\n", 378 | " \n", 379 | " delta_l = np.minimum(np.maximum(deg - np.max(deg) + strength, 0), n_nodes - 1).astype(int)\n", 380 | " # delta_g\n", 381 | " \n", 382 | " logits = cal_logits(A, X@W, U, act='relu')\n", 383 | " c_pred = logits.argmax()\n", 384 | " \n", 385 | " if c_pred != y:\n", 386 | " continue\n", 387 | " correct += 1\n", 388 | " fc_vals_orig = [0] * k\n", 389 | " fc_vals_dual = [0] * k\n", 390 | " fc_vals_cvx_DL = [0] * k\n", 391 | " fc_vals_cvx_SL = [0] * k\n", 392 | " fc_vals_admm = [0] * k\n", 393 | " fc_vals_admm_g = [0] * k\n", 394 | " fc_vals_greedy = [0] * k\n", 395 | " \n", 396 | " \n", 397 | " for c in tqdm(range(k), desc='across labels', leave=False):\n", 398 | " if c == y:\n", 399 | " continue\n", 400 | " u = U[y] - U[c]\n", 401 | " XW = X@W\n", 402 | " \n", 403 | " # fc_val_orig\n", 404 | " fc_vals_orig[c] = calculate_Fc(A, XW, u / n_nodes, activation='relu')\n", 405 | " \n", 406 | " # fc_val_dual\n", 407 | " cvx_relax = ConvexRelaxation(A, XW, u / n_nodes, delta_l, delta_g, 'relu', 'envelop', 'doubleL')\n", 408 | " dual_sol = dual_solver_doubleL(A, cvx_relax.Q, cvx_relax.p, \n", 409 | " delta_l=delta_l, delta_g=delta_g, **dual_params)\n", 410 | " fc_vals_dual[c] = dual_sol['opt_f']\n", 411 | " \n", 412 | " # fc_val_cvx_DL\n", 413 | " cvx_env_params = dict(iter=100, lr=1, verbose=0, constr='1+2+3', nonsmooth_init='subgrad',\n", 414 | " activation='relu', algo='swapping', relu_bound='doubleL')\n", 415 | " cvx_env_doubleL_sol = cvx_env_solver(A, XW, u / n_nodes, delta_l, delta_g, **cvx_env_params)\n", 416 | " fc_vals_cvx_DL[c] = cvx_env_doubleL_sol['opt_f']\n", 417 | " \n", 418 | " # fc_val_cvx_SL\n", 419 | " cvx_env_params = dict(iter=100, lr=1, verbose=0, constr='1+2+3', nonsmooth_init='subgrad',\n", 420 | " activation='relu', algo='swapping', relu_bound='singleL')\n", 421 | " cvx_env_singleL_sol = cvx_env_solver(A, XW, u / n_nodes, delta_l, delta_g, **cvx_env_params)\n", 422 | " fc_vals_cvx_SL[c] = cvx_env_singleL_sol['opt_f']\n", 423 | " \n", 424 | " # fc_val_admm\n", 425 | " Q, p = cvx_relax.doubleL_ub_coefficient()\n", 426 | " admm_params['init_B'] = dual_sol['opt_A'].copy()\n", 427 | " admm_sol = admm_solver_doubleL(A, Q, p, delta_l, delta_g, **admm_params)\n", 428 | " fc_vals_admm[c] = admm_sol['opt_f']\n", 429 | " \n", 430 | " # fc_val_admm_g: admm + greedy\n", 431 | " admm_g_attack = Greedy_Attack(A, XW, u / n_nodes, delta_l=delta_l, delta_g=delta_g, activation='relu')\n", 432 | " if np.array_equal(admm_sol['opt_A'], admm_sol['opt_A'].T):\n", 433 | " admm_A = admm_sol['opt_A']\n", 434 | " else:\n", 435 | " admm_A = np.minimum(admm_sol['opt_A'], admm_sol['opt_A'].T)\n", 436 | " admm_g_sol = admm_g_attack.attack(admm_A) # init from A\n", 437 | " fc_vals_admm_g[c] = admm_g_sol['opt_f']\n", 438 | " \n", 439 | " # fc_val_greedy\n", 440 | " attack = Greedy_Attack(A, XW, u / n_nodes, delta_l=delta_l, delta_g=delta_g, activation='relu')\n", 441 | " greedy_sol = attack.attack(A) # init from A\n", 442 | " fc_vals_greedy[c] = greedy_sol['opt_f']\n", 443 | " \n", 444 | " if np.min(fc_vals_dual) >= 0:\n", 445 | " robust_dual += 1\n", 446 | " if np.min(fc_vals_cvx_DL) >= 0:\n", 447 | " robust_cvx_DL += 1\n", 448 | " if np.min(fc_vals_cvx_SL) >= 0:\n", 449 | " robust_cvx_SL += 1\n", 450 | " if np.min(fc_vals_admm_g) < 0:\n", 451 | " vul_admm_g += 1\n", 452 | " if np.min(fc_vals_greedy) < 0:\n", 453 | " vul_greedy += 1" 454 | ] 455 | }, 456 | { 457 | "cell_type": "code", 458 | "execution_count": null, 459 | "metadata": {}, 460 | "outputs": [], 461 | "source": [ 462 | "print('dataset {}'.format(ds_name),\n", 463 | " 'strength {:02d}'.format(strength),\n", 464 | " 'delta_g {:02d}'.format(delta_g),\n", 465 | " 'dual {:.3f}'.format(robust_dual / correct),\n", 466 | " 'cvx_DL {:.3f}'.format(robust_cvx_DL / correct),\n", 467 | " 'cvx_SL {:.3f}'.format(robust_cvx_SL / correct),\n", 468 | " 'admm_g {:.3f}'.format(vul_admm_g / correct),\n", 469 | " 'greedy {:.3f}'.format(vul_greedy / correct),\n", 470 | " )" 471 | ] 472 | } 473 | ], 474 | "metadata": { 475 | "kernelspec": { 476 | "display_name": "Python 3", 477 | "language": "python", 478 | "name": "python3" 479 | }, 480 | "language_info": { 481 | "codemirror_mode": { 482 | "name": "ipython", 483 | "version": 3 484 | }, 485 | "file_extension": ".py", 486 | "mimetype": "text/x-python", 487 | "name": "python", 488 | "nbconvert_exporter": "python", 489 | "pygments_lexer": "ipython3", 490 | "version": "3.7.6" 491 | } 492 | }, 493 | "nbformat": 4, 494 | "nbformat_minor": 4 495 | } 496 | --------------------------------------------------------------------------------